mirror of
https://github.com/ash-project/igniter.git
synced 2024-09-19 13:02:51 +12:00
improvement: add positional_args!/1
macro for use in tasks
fix: recompile igniter in `ingiter.install`
This commit is contained in:
parent
ef79a5c3ba
commit
29e492002a
6 changed files with 120 additions and 11 deletions
|
@ -37,11 +37,13 @@ defmodule Igniter.Util.DepsCompile do
|
||||||
# * `--skip-umbrella-children` - skips umbrella applications from compiling
|
# * `--skip-umbrella-children` - skips umbrella applications from compiling
|
||||||
# * `--skip-local-deps` - skips non-remote dependencies, such as path deps, from compiling
|
# * `--skip-local-deps` - skips non-remote dependencies, such as path deps, from compiling
|
||||||
|
|
||||||
def run do
|
def run(opts \\ []) do
|
||||||
Mix.Project.get!()
|
Mix.Project.get!()
|
||||||
deps = Mix.Dep.load_and_cache()
|
deps = Mix.Dep.load_and_cache()
|
||||||
|
|
||||||
opts = [include_children: true, force: true]
|
opts =
|
||||||
|
[include_children: true, force: true]
|
||||||
|
|> Keyword.put(:recompile_igniter?, Keyword.get(opts, :recompile_igniter?))
|
||||||
|
|
||||||
compile(filter_available_and_local_deps(deps), opts)
|
compile(filter_available_and_local_deps(deps), opts)
|
||||||
end
|
end
|
||||||
|
@ -53,7 +55,11 @@ defmodule Igniter.Util.DepsCompile do
|
||||||
Mix.Task.run("deps.precompile")
|
Mix.Task.run("deps.precompile")
|
||||||
|
|
||||||
igniter_needs_compiling? =
|
igniter_needs_compiling? =
|
||||||
|
if options[:recompile_igniter?] do
|
||||||
|
true
|
||||||
|
else
|
||||||
not Code.ensure_loaded?(Igniter)
|
not Code.ensure_loaded?(Igniter)
|
||||||
|
end
|
||||||
|
|
||||||
compiled =
|
compiled =
|
||||||
deps
|
deps
|
||||||
|
|
|
@ -53,8 +53,8 @@ defmodule Igniter.Project.Deps do
|
||||||
Mix.shell().yes?("""
|
Mix.shell().yes?("""
|
||||||
Dependency #{name} is already in mix.exs. Should we replace it?
|
Dependency #{name} is already in mix.exs. Should we replace it?
|
||||||
|
|
||||||
Desired: `#{inspect desired}`
|
Desired: `#{inspect(desired)}`
|
||||||
Found: `#{inspect current}`
|
Found: `#{inspect(current)}`
|
||||||
""") do
|
""") do
|
||||||
igniter
|
igniter
|
||||||
|> remove_dependency(name)
|
|> remove_dependency(name)
|
||||||
|
|
|
@ -221,7 +221,7 @@ defmodule Igniter.Util.Install do
|
||||||
|> File.read!()
|
|> File.read!()
|
||||||
|> Code.eval_string([], file: Path.expand("mix.exs"))
|
|> Code.eval_string([], file: Path.expand("mix.exs"))
|
||||||
|
|
||||||
Igniter.Util.DepsCompile.run()
|
Igniter.Util.DepsCompile.run(recompile_igniter?: true)
|
||||||
|
|
||||||
exit_code ->
|
exit_code ->
|
||||||
Mix.shell().info("""
|
Mix.shell().info("""
|
||||||
|
|
105
lib/mix/task.ex
105
lib/mix/task.ex
|
@ -16,13 +16,22 @@ defmodule Igniter.Mix.Task do
|
||||||
## Configurable Keys
|
## Configurable Keys
|
||||||
|
|
||||||
* `schema` - The option schema for this task, in the format given to `OptionParser`, i.e `[name: :string]`
|
* `schema` - The option schema for this task, in the format given to `OptionParser`, i.e `[name: :string]`
|
||||||
|
* `positional` - A list of positional arguments that this task accepts. A list of atoms, or a keyword list with the option and config.
|
||||||
|
See the positional arguments section for more.
|
||||||
* `aliases` - A map of aliases to the schema keys.
|
* `aliases` - A map of aliases to the schema keys.
|
||||||
* `composes` - A list of tasks that this task might compose.
|
* `composes` - A list of tasks that this task might compose.
|
||||||
* `installs` - A list of dependencies that should be installed before continuing.
|
* `installs` - A list of dependencies that should be installed before continuing.
|
||||||
* `adds_deps` - A list of dependencies that should be added to the `mix.exs`, but do not need to be installed before continuing.
|
* `adds_deps` - A list of dependencies that should be added to the `mix.exs`, but do not need to be installed before continuing.
|
||||||
* `extra_args?` - Whether or not to allow extra arguments. This forces all tasks that compose this task to allow extra args as well.
|
* `extra_args?` - Whether or not to allow extra arguments. This forces all tasks that compose this task to allow extra args as well.
|
||||||
|
* `example` - An example usage of the task. This is used in the help output.
|
||||||
|
|
||||||
Your task should *always* use `switches` and not `strict` to validate provided options!
|
Your task should *always* use `switches` and not `strict` to validate provided options!
|
||||||
|
|
||||||
|
## Positonal Arguments
|
||||||
|
|
||||||
|
Each positional argument can provide the following options:
|
||||||
|
|
||||||
|
* `:optional` - Whether or not the argument is optional. Defaults to `false`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@global_options [
|
@global_options [
|
||||||
|
@ -40,14 +49,18 @@ defmodule Igniter.Mix.Task do
|
||||||
composes: [],
|
composes: [],
|
||||||
installs: [],
|
installs: [],
|
||||||
adds_deps: [],
|
adds_deps: [],
|
||||||
|
positional: [],
|
||||||
|
example: nil,
|
||||||
extra_args?: false
|
extra_args?: false
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
schema: Keyword.t(),
|
schema: Keyword.t(),
|
||||||
aliases: Keyword.t(),
|
aliases: Keyword.t(),
|
||||||
composes: [String.t()],
|
composes: [String.t()],
|
||||||
|
positional: list(atom | {atom, [{:optional, boolean()}, {:example, String.t()}]}),
|
||||||
installs: [{atom(), String.t()}],
|
installs: [{atom(), String.t()}],
|
||||||
adds_deps: [{atom(), String.t()}],
|
adds_deps: [{atom(), String.t()}],
|
||||||
|
example: String.t() | nil,
|
||||||
extra_args?: boolean()
|
extra_args?: boolean()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +72,9 @@ defmodule Igniter.Mix.Task do
|
||||||
|
|
||||||
This info will be used to validate arguments in composed tasks.
|
This info will be used to validate arguments in composed tasks.
|
||||||
|
|
||||||
|
Use the `positional_args!(argv)` to get your positional arguments according to your `info.positional`, and the remaining unused args.
|
||||||
|
Use the `options!(argv)` macro to get your parsed options according to your `info.schema`.
|
||||||
|
|
||||||
## Important Limitations
|
## Important Limitations
|
||||||
|
|
||||||
* Each task still must parse its own argv in `igniter/2` and *must* ignore any unknown options.
|
* Each task still must parse its own argv in `igniter/2` and *must* ignore any unknown options.
|
||||||
|
@ -74,7 +90,7 @@ defmodule Igniter.Mix.Task do
|
||||||
quote do
|
quote do
|
||||||
use Mix.Task
|
use Mix.Task
|
||||||
@behaviour Igniter.Mix.Task
|
@behaviour Igniter.Mix.Task
|
||||||
import Igniter.Mix.Task, only: [options!: 1]
|
import Igniter.Mix.Task, only: [options!: 1, positional_args!: 1]
|
||||||
|
|
||||||
@impl Mix.Task
|
@impl Mix.Task
|
||||||
def run(argv) do
|
def run(argv) do
|
||||||
|
@ -122,14 +138,97 @@ defmodule Igniter.Mix.Task do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "Parses the options for the task based on its info."
|
||||||
@spec options!(argv :: term()) :: term() | no_return
|
@spec options!(argv :: term()) :: term() | no_return
|
||||||
defmacro options!(argv) do
|
defmacro options!(argv) do
|
||||||
quote do
|
quote do
|
||||||
argv = unquote(argv)
|
argv = unquote(argv)
|
||||||
info = info(argv, Mix.Task.task_name(__MODULE__))
|
|
||||||
{parsed, _} = OptionParser.parse!(argv, switches: info.schema, aliases: info.aliases)
|
|
||||||
|
|
||||||
|
task_name = Mix.Task.task_name(__MODULE__)
|
||||||
|
|
||||||
|
info = info(argv, task_name)
|
||||||
|
{parsed, _} = OptionParser.parse!(argv, switches: info.schema, aliases: info.aliases)
|
||||||
parsed
|
parsed
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defmacro positional_args!(argv) do
|
||||||
|
quote do
|
||||||
|
argv = unquote(argv)
|
||||||
|
task_name = Mix.Task.task_name(__MODULE__)
|
||||||
|
info = info(argv, task_name)
|
||||||
|
|
||||||
|
desired =
|
||||||
|
Enum.map(info.positional, fn
|
||||||
|
value when is_atom(value) ->
|
||||||
|
{value, []}
|
||||||
|
|
||||||
|
other ->
|
||||||
|
other
|
||||||
|
end)
|
||||||
|
|
||||||
|
{rest, positional} = Enum.split_with(argv, &String.starts_with?(&1, "-"))
|
||||||
|
|
||||||
|
{remaining_desired, got} =
|
||||||
|
Enum.reduce(positional, {desired, []}, fn
|
||||||
|
_arg, {[], got} ->
|
||||||
|
{[], got}
|
||||||
|
|
||||||
|
arg, {desired, got} ->
|
||||||
|
{name, _config} =
|
||||||
|
Enum.find(desired, fn {_name, config} ->
|
||||||
|
!config[:optional]
|
||||||
|
end) || Enum.at(desired, 0)
|
||||||
|
|
||||||
|
{Keyword.delete(desired, name), Keyword.put(got, name, arg)}
|
||||||
|
end)
|
||||||
|
|
||||||
|
case Enum.find(remaining_desired, fn {_arg, config} -> !config[:optional] end) do
|
||||||
|
{name, _config} ->
|
||||||
|
raise ArgumentError, """
|
||||||
|
Required positional argument `#{name}` was not supplied.
|
||||||
|
|
||||||
|
Command: `#{Igniter.Mix.Task.call_structure(task_name, desired)}`
|
||||||
|
#{Igniter.Mix.Task.call_example(info)}
|
||||||
|
|
||||||
|
Run `mix help #{task_name}` for more information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{Map.new(got), rest}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def call_example(info) do
|
||||||
|
if info.example do
|
||||||
|
"""
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
#{indent(info.example)}
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp indent(example) do
|
||||||
|
example
|
||||||
|
|> String.split("\n")
|
||||||
|
|> Enum.map_join("\n", &" #{&1}")
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def call_structure(name, desired) do
|
||||||
|
call =
|
||||||
|
Enum.map_join(desired, " ", fn {name, config} ->
|
||||||
|
if config[:optional] do
|
||||||
|
"[#{name}]"
|
||||||
|
else
|
||||||
|
name
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
"mix #{name} #{call}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,7 +30,11 @@ defmodule Igniter.Mix.Tasks.InstallTest do
|
||||||
test "rerunning the same installer lets you know the dependency was not changed" do
|
test "rerunning the same installer lets you know the dependency was not changed" do
|
||||||
_ = cmd!("mix", ["igniter.install", "jason", "--yes"], cd: "test_project")
|
_ = cmd!("mix", ["igniter.install", "jason", "--yes"], cd: "test_project")
|
||||||
output = cmd!("mix", ["igniter.install", "jason", "--yes"], cd: "test_project")
|
output = cmd!("mix", ["igniter.install", "jason", "--yes"], cd: "test_project")
|
||||||
assert String.contains?(output, "Dependency jason is already in mix.exs with the desired version. Skipping.")
|
|
||||||
|
assert String.contains?(
|
||||||
|
output,
|
||||||
|
"Dependency jason is already in mix.exs with the desired version. Skipping."
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue