mirror of
https://github.com/ash-project/igniter.git
synced 2024-09-19 13:02:51 +12:00
improvement: clean up dependency compiling logic
fix: ensure igniter is compiled first fix: fetch deps after adding any nested installers improvement: optimize module finding w/ async_stream fix: various fixes & improvements to positional argument listing improvement: add `rest: true` option for positional args
This commit is contained in:
parent
279261fa31
commit
c52b226a1c
5 changed files with 156 additions and 42 deletions
|
@ -42,7 +42,7 @@ defmodule Igniter.Util.DepsCompile do
|
|||
deps = Mix.Dep.load_and_cache()
|
||||
|
||||
opts =
|
||||
[include_children: true, force: true]
|
||||
[include_children: true]
|
||||
|> Keyword.put(:recompile_igniter?, Keyword.get(opts, :recompile_igniter?))
|
||||
|
||||
compile(filter_available_and_local_deps(deps), opts)
|
||||
|
@ -54,12 +54,7 @@ defmodule Igniter.Util.DepsCompile do
|
|||
config = Mix.Project.deps_config()
|
||||
Mix.Task.run("deps.precompile")
|
||||
|
||||
igniter_needs_compiling? =
|
||||
if options[:recompile_igniter?] do
|
||||
true
|
||||
else
|
||||
not Code.ensure_loaded?(Igniter)
|
||||
end
|
||||
igniter_needs_compiling? = not Code.ensure_loaded?(Igniter)
|
||||
|
||||
compiled =
|
||||
deps
|
||||
|
@ -70,6 +65,9 @@ defmodule Igniter.Util.DepsCompile do
|
|||
Enum.reject(deps, &(&1.app == :igniter))
|
||||
end
|
||||
end)
|
||||
|> Enum.sort_by(fn %{app: app} ->
|
||||
app != :igniter
|
||||
end)
|
||||
|> Enum.map(fn %Mix.Dep{app: app, status: status, opts: opts, scm: scm} = dep ->
|
||||
check_unavailable!(app, scm, status)
|
||||
maybe_clean(dep, options)
|
||||
|
|
|
@ -125,18 +125,18 @@ defmodule Igniter.Code.Module do
|
|||
|
||||
igniter
|
||||
|> Map.get(:rewrite)
|
||||
|> Enum.find_value({:error, igniter}, fn source ->
|
||||
source
|
||||
|> Rewrite.Source.get(:quoted)
|
||||
|> Zipper.zip()
|
||||
|> move_to_defmodule(module_name)
|
||||
|> case do
|
||||
{:ok, zipper} ->
|
||||
{:ok, {igniter, source, zipper}}
|
||||
|> Task.async_stream(fn source ->
|
||||
{source
|
||||
|> Rewrite.Source.get(:quoted)
|
||||
|> Zipper.zip()
|
||||
|> move_to_defmodule(module_name), source}
|
||||
end)
|
||||
|> Enum.find_value({:error, igniter}, fn
|
||||
{:ok, {{:ok, zipper}, source}} ->
|
||||
{:ok, {igniter, source, zipper}}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
_other ->
|
||||
false
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -150,7 +150,7 @@ defmodule Igniter.Code.Module do
|
|||
matching_modules =
|
||||
igniter
|
||||
|> Map.get(:rewrite)
|
||||
|> Enum.flat_map(fn source ->
|
||||
|> Task.async_stream(fn source ->
|
||||
source
|
||||
|> Rewrite.Source.get(:quoted)
|
||||
|> Zipper.zip()
|
||||
|
@ -180,6 +180,9 @@ defmodule Igniter.Code.Module do
|
|||
end)
|
||||
|> elem(1)
|
||||
end)
|
||||
|> Enum.flat_map(fn {:ok, v} ->
|
||||
v
|
||||
end)
|
||||
|
||||
{igniter, matching_modules}
|
||||
end
|
||||
|
|
|
@ -19,7 +19,7 @@ defmodule Igniter.Util.Info do
|
|||
igniter =
|
||||
igniter
|
||||
|> add_deps(
|
||||
List.wrap(schema.adds_deps) ++ List.wrap(schema.installs),
|
||||
List.wrap(schema.adds_deps),
|
||||
opts
|
||||
)
|
||||
|> Igniter.apply_and_fetch_dependencies(opts)
|
||||
|
@ -31,6 +31,10 @@ defmodule Igniter.Util.Info do
|
|||
install_names = Keyword.keys(installs)
|
||||
|
||||
igniter
|
||||
|> add_deps(
|
||||
List.wrap(installs),
|
||||
opts
|
||||
)
|
||||
|> Igniter.apply_and_fetch_dependencies(opts)
|
||||
|> compose_install_and_validate!(
|
||||
argv,
|
||||
|
@ -38,7 +42,7 @@ defmodule Igniter.Util.Info do
|
|||
schema
|
||||
| composes: Enum.map(install_names, &"#{&1}.install"),
|
||||
installs: [],
|
||||
adds_deps: schema.adds_deps ++ installs
|
||||
adds_deps: schema.adds_deps
|
||||
},
|
||||
task_name,
|
||||
opts,
|
||||
|
|
106
lib/mix/task.ex
106
lib/mix/task.ex
|
@ -32,6 +32,9 @@ defmodule Igniter.Mix.Task do
|
|||
Each positional argument can provide the following options:
|
||||
|
||||
* `:optional` - Whether or not the argument is optional. Defaults to `false`.
|
||||
* `:rest` - Whether or not the argument consumes the rest of the positional arguments. Defaults to `false`.
|
||||
The value will be converted to a list automatically.
|
||||
|
||||
"""
|
||||
|
||||
@global_options [
|
||||
|
@ -57,7 +60,7 @@ defmodule Igniter.Mix.Task do
|
|||
schema: Keyword.t(),
|
||||
aliases: Keyword.t(),
|
||||
composes: [String.t()],
|
||||
positional: list(atom | {atom, [{:optional, boolean()}, {:example, String.t()}]}),
|
||||
positional: list(atom | {atom, [{:optional, boolean()}, {:rest, boolean()}]}),
|
||||
installs: [{atom(), String.t()}],
|
||||
adds_deps: [{atom(), String.t()}],
|
||||
example: String.t() | nil,
|
||||
|
@ -158,6 +161,8 @@ defmodule Igniter.Mix.Task do
|
|||
task_name = Mix.Task.task_name(__MODULE__)
|
||||
info = info(argv, task_name)
|
||||
|
||||
{argv, positional} = Igniter.Mix.Task.extract_positional_args(argv)
|
||||
|
||||
desired =
|
||||
Enum.map(info.positional, fn
|
||||
value when is_atom(value) ->
|
||||
|
@ -167,26 +172,20 @@ defmodule Igniter.Mix.Task do
|
|||
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)
|
||||
Igniter.Mix.Task.consume_args(positional, desired)
|
||||
|
||||
case Enum.find(remaining_desired, fn {_arg, config} -> !config[:optional] end) do
|
||||
{name, _config} ->
|
||||
{name, config} ->
|
||||
line =
|
||||
if config[:rest] do
|
||||
"Must provide one or more values for positional argument `#{name}`"
|
||||
else
|
||||
"Required positional argument `#{name}` was not supplied."
|
||||
end
|
||||
|
||||
raise ArgumentError, """
|
||||
Required positional argument `#{name}` was not supplied.
|
||||
#{line}
|
||||
|
||||
Command: `#{Igniter.Mix.Task.call_structure(task_name, desired)}`
|
||||
#{Igniter.Mix.Task.call_example(info)}
|
||||
|
@ -195,11 +194,48 @@ defmodule Igniter.Mix.Task do
|
|||
"""
|
||||
|
||||
_ ->
|
||||
{Map.new(got), rest}
|
||||
{Igniter.Mix.Task.add_default_values(Map.new(got), desired), positional}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_default_values(got, desired) do
|
||||
Enum.reduce(desired, got, fn {name, config}, acc ->
|
||||
if config[:optional] do
|
||||
if config[:rest] do
|
||||
Map.update(got, name, [], &List.wrap/1)
|
||||
else
|
||||
Map.put_new(got, name, nil)
|
||||
end
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def consume_args(positional, desired, got \\ [])
|
||||
|
||||
def consume_args([], desired, got) do
|
||||
{desired, got}
|
||||
end
|
||||
|
||||
def consume_args([arg | positional], desired, got) do
|
||||
{name, config} =
|
||||
Enum.find(desired, fn {_name, config} ->
|
||||
!config[:optional]
|
||||
end) || Enum.at(desired, 0)
|
||||
|
||||
desired = Keyword.delete(desired, name)
|
||||
|
||||
if config[:rest] do
|
||||
{desired, Keyword.put(got, name, [arg | positional])}
|
||||
else
|
||||
consume_args(positional, desired, Keyword.put(got, name, arg))
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def call_example(info) do
|
||||
if info.example do
|
||||
|
@ -212,6 +248,27 @@ defmodule Igniter.Mix.Task do
|
|||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def extract_positional_args(argv, argv \\ [], positional \\ [])
|
||||
def extract_positional_args([], argv, positional), do: {argv, positional}
|
||||
|
||||
def extract_positional_args(argv, got_argv, positional) do
|
||||
case OptionParser.next(argv, switches: []) do
|
||||
{:ok, key, value, rest} ->
|
||||
extract_positional_args(rest, got_argv ++ [{key, value}], positional)
|
||||
|
||||
{:invalid, key, value, rest} ->
|
||||
extract_positional_args(rest, got_argv ++ [{key, value}], positional)
|
||||
|
||||
{:undefined, key, value, rest} ->
|
||||
extract_positional_args(rest, got_argv ++ [{key, value}], positional)
|
||||
|
||||
{:error, rest} ->
|
||||
[first | rest] = rest
|
||||
extract_positional_args(rest, got_argv, positional ++ [first])
|
||||
end
|
||||
end
|
||||
|
||||
defp indent(example) do
|
||||
example
|
||||
|> String.split("\n")
|
||||
|
@ -222,10 +279,17 @@ defmodule Igniter.Mix.Task do
|
|||
def call_structure(name, desired) do
|
||||
call =
|
||||
Enum.map_join(desired, " ", fn {name, config} ->
|
||||
if config[:optional] do
|
||||
"[#{name}]"
|
||||
with_optional =
|
||||
if config[:optional] do
|
||||
"[#{name}]"
|
||||
else
|
||||
to_string(name)
|
||||
end
|
||||
|
||||
if config[:rest] do
|
||||
with_optional <> "[...]"
|
||||
else
|
||||
name
|
||||
with_optional
|
||||
end
|
||||
end)
|
||||
|
||||
|
|
45
test/mix/task_test.exs
Normal file
45
test/mix/task_test.exs
Normal file
|
@ -0,0 +1,45 @@
|
|||
defmodule Igniter.Mix.TaskTest do
|
||||
use ExUnit.Case
|
||||
|
||||
defmodule ExampleTask do
|
||||
use Igniter.Mix.Task
|
||||
|
||||
def info(_argv, _parent) do
|
||||
%Igniter.Mix.Task.Info{
|
||||
schema: [
|
||||
option: :string
|
||||
],
|
||||
positional: [
|
||||
:a,
|
||||
b: [
|
||||
optional: true,
|
||||
rest: true
|
||||
]
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def igniter(igniter, argv) do
|
||||
options = options!(argv)
|
||||
{args, _argv} = positional_args!(argv)
|
||||
|
||||
send(self(), {:args, args})
|
||||
send(self(), {:options, options})
|
||||
igniter
|
||||
end
|
||||
end
|
||||
|
||||
test "it parses options" do
|
||||
ExampleTask.igniter(Igniter.new(), ["foo", "--option", "foo"])
|
||||
assert_received {:options, options}
|
||||
assert options[:option] == "foo"
|
||||
assert_received {:args, %{a: "foo"}}
|
||||
end
|
||||
|
||||
test "it parses rest options" do
|
||||
ExampleTask.igniter(Igniter.new(), ["foo", "--option", "foo"])
|
||||
assert_received {:options, options}
|
||||
assert options[:option] == "foo"
|
||||
assert_received {:args, %{a: "foo"}}
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue