Packages
igniter
0.4.5
0.8.2
0.8.1
0.8.0
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.30
0.6.29
0.6.28
0.6.27
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.52
0.5.51
0.5.50
0.5.49
0.5.48
0.5.47
0.5.46
0.5.45
0.5.44
0.5.43
0.5.42
0.5.41
0.5.40
0.5.39
0.5.38
0.5.37
0.5.36
0.5.35
0.5.34
0.5.33
0.5.32
0.5.31
0.5.30
0.5.29
0.5.28
0.5.27
0.5.26
0.5.25
0.5.24
0.5.23
0.5.22
0.5.21
0.5.20
0.5.19
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.78
0.3.77
0.3.76
0.3.75
0.3.74
0.3.73
0.3.72
0.3.71
0.3.70
0.3.69
0.3.68
0.3.67
0.3.66
0.3.65
0.3.64
0.3.63
0.3.62
0.3.61
0.3.60
0.3.59
0.3.58
0.3.57
0.3.56
0.3.55
0.3.54
0.3.53
0.3.52
0.3.51
0.3.50
0.3.49
0.3.48
0.3.47
0.3.46
0.3.45
0.3.44
0.3.43
0.3.42
0.3.41
0.3.40
0.3.39
0.3.38
0.3.37
0.3.36
0.3.35
0.3.34
0.3.33
0.3.32
0.3.31
0.3.30
0.3.29
0.3.28
0.3.27
0.3.26
0.3.25
0.3.24
0.3.23
0.3.22
0.3.21
0.3.20
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
A code generation and project patching framework
Current section
Files
Jump to
Current section
Files
lib/mix/task.ex
defmodule Igniter.Mix.Task do
@moduledoc """
A behaviour for implementing a Mix task that is enriched to be composable with other Igniter tasks.
> ### Note {: .info}
>
> A default `run/1` is implemented so you can directly run the task. Igniter never uses this function, so it is overridable.
>
> This enables your library to make use of the task for its own purposes if needed. An example would be if you wanted to implement an Igniter installer, but also have an `install` task for end-user consumption (e.g. `mix tailwind.install`).
## Options and Arguments
Command line args are automatically parsed into `igniter.args` using the configuration returned
from `c:info/2`. See `Igniter.Mix.Task.Info` for more.
"""
alias Igniter.Mix.Task.Args
alias Igniter.Mix.Task.Info
require Logger
@doc """
Whether or not it supports being run in the root of an umbrella project
At the moment, this is still experimental and we suggest not turning it on.
"""
@callback supports_umbrella?() :: boolean()
@doc "Main entrypoint for tasks. This callback accepts and returns an `Igniter` struct."
@callback igniter(igniter :: Igniter.t()) :: Igniter.t()
@doc "All the generator behavior happens here, you take an igniter and task arguments, and return an igniter."
@doc deprecated: "Use igniter/1 instead"
@callback igniter(igniter :: Igniter.t(), argv :: list(String.t())) :: Igniter.t()
@doc """
Returns an `Igniter.Mix.Task.Info` struct, with information used when running the igniter task.
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
* Each task still must parse its own argv in `igniter/2` and *must* ignore any unknown options.
To accomplish this, use the automatically imported `options!(argv)` macro, which uses the `info/2`
callback to validate args and return options
* You cannot use `composes` to list tasks unless they are in your library or in direct dependencies of your library.
To validate their options, you must include their options in your own option schema.
"""
@callback info(argv :: list(String.t()), composing_task :: nil | String.t()) ::
Info.t()
@doc """
Returns an `Igniter.Mix.Task.Args` struct.
This callback can be implemented to private custom parsing and validation behavior for
command line arguments. By default, the options specified in `c:info/2` will be used
to inject a default implementation.
"""
@callback parse_argv(argv :: list(String.t())) :: Args.t()
@callback installer?() :: boolean()
@optional_callbacks [igniter: 1, igniter: 2]
defmacro __using__(_opts) do
quote do
use Mix.Task
import Igniter.Mix.Task, only: [options!: 1, positional_args!: 1]
@behaviour Igniter.Mix.Task
@after_compile Igniter.Mix.Task
@before_compile Igniter.Mix.Task
@impl Mix.Task
def run(argv) do
if !supports_umbrella?() && Mix.Project.umbrella?() do
raise """
Cannot run #{inspect(__MODULE__)} in an umbrella project.
"""
end
if Mix.Task.task_name(__MODULE__) != "igniter.upgrade" do
Mix.Task.run("compile")
end
Application.ensure_all_started(:rewrite)
global_options = Info.global_options()
info =
argv
|> info(nil)
|> Map.update!(:schema, &Keyword.merge(&1, global_options[:switches]))
{opts, _} =
Igniter.Util.Info.validate!(argv, info, Mix.Task.task_name(__MODULE__))
Igniter.new()
|> Igniter.Mix.Task.configure_and_run(__MODULE__, argv)
|> Igniter.do_or_dry_run(opts)
end
defoverridable run: 1
@impl true
def installer?, do: __MODULE__ |> Mix.Task.task_name() |> String.ends_with?(".install")
@impl Igniter.Mix.Task
def supports_umbrella?, do: false
@impl Igniter.Mix.Task
def info(argv, source) do
%Info{extra_args?: true}
end
@impl Igniter.Mix.Task
def parse_argv(argv) do
{positional, argv_flags} = positional_args!(argv)
options = options!(argv_flags)
%Args{positional: positional, options: options, argv: argv, argv_flags: argv_flags}
end
defoverridable supports_umbrella?: 0, info: 2, installer?: 0
end
end
def __after_compile__(env, _bytecode) do
igniter1_defined? = function_exported?(env.module, :igniter, 1)
igniter2_defined? = function_exported?(env.module, :igniter, 2)
if not (igniter1_defined? or igniter2_defined?) do
raise CompileError,
description:
"#{inspect(env.module)} must define either igniter/1 or igniter/2 to implement the #{inspect(__MODULE__)} behaviour"
end
end
defmacro __before_compile__(_env) do
quote do
require Logger
if Module.defines?(__MODULE__, {:igniter, 1}, :def) and
Module.defines?(__MODULE__, {:igniter, 2}, :def) do
Logger.warning("""
#{inspect(__MODULE__)} (#{__ENV__.file})
Module defines both igniter/1 and igniter/2, but igniter/2 is deprecated and will never be called.
""")
end
if !Module.defines?(__MODULE__, {:igniter, 2}, :def) &&
Module.defines?(__MODULE__, {:igniter, 1}, :def) do
@doc false
@impl true
def igniter(igniter, _argv) do
igniter(igniter)
end
end
end
end
@doc false
def configure_and_run(igniter, task_module, argv) do
case task_module.parse_argv(argv) do
%Args{} = args ->
igniter = %{igniter | args: args}
if function_exported?(task_module, :igniter, 1) do
task_module.igniter(igniter)
else
task_module.igniter(igniter, argv)
end
other ->
raise """
Expected #{inspect(task_module)}.parse_argv/2 to return an Igniter.Mix.Task.Args struct,
but got: #{inspect(other)}
"""
end
end
@doc "Parses the options for the task based on its info."
@spec options!(argv :: term()) :: term() | no_return
defmacro options!(argv) do
quote do
argv = unquote(argv)
task_name = Mix.Task.task_name(__MODULE__)
info = info(argv, task_name)
argv = Igniter.Util.Info.args_for_group(argv, Igniter.Util.Info.group(info, task_name))
schema =
Enum.map(info.schema, fn
{k, :csv} ->
{k, :keep}
{k, v} ->
{k, v}
end)
{parsed, _} = OptionParser.parse!(argv, switches: schema, aliases: info.aliases)
parsed =
schema
|> Enum.filter(fn {_, type} ->
type == :keep
end)
|> Enum.reduce(parsed, fn {k, _}, parsed ->
parsed_without = Keyword.delete(parsed, k)
values =
parsed
|> Keyword.get_values(k)
|> List.wrap()
Keyword.put(parsed_without, k, values)
end)
parsed =
info.schema
|> Enum.reduce(parsed, fn
{k, :csv}, parsed ->
case Keyword.fetch(parsed, k) do
{:ok, value} ->
value
|> List.wrap()
|> Enum.flat_map(&String.split(&1, ",", trim: true))
|> then(fn v ->
Keyword.put(parsed, k, v)
end)
:error ->
Keyword.put(parsed, k, [])
end
{k, :keep}, parsed ->
Keyword.put_new(parsed, k, [])
_, parsed ->
parsed
end)
with_defaults = Keyword.merge(info.defaults, parsed)
Enum.each(info.required, fn option ->
if !with_defaults[option] do
Mix.shell().error(
"Missing required flag #{String.replace(to_string(option), "_", "-")} "
)
exit({:shutdown, 1})
end
end)
with_defaults
end
end
defmacro positional_args!(argv) do
quote do
argv = unquote(argv)
task_name = Mix.Task.task_name(__MODULE__)
info = info(argv, task_name)
argv = Igniter.Util.Info.args_for_group(argv, Igniter.Util.Info.group(info, task_name))
{argv, positional} = Installer.Lib.Private.SharedUtils.extract_positional_args(argv)
desired =
Enum.map(info.positional, fn
value when is_atom(value) ->
{value, []}
other ->
other
end)
{remaining_desired, got} =
Igniter.Mix.Task.consume_args(positional, desired)
case Enum.find(remaining_desired, fn {_arg, config} -> !config[:optional] end) do
{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, """
#{line}
Command: `#{Igniter.Mix.Task.call_structure(task_name, desired)}`
#{Igniter.Mix.Task.call_example(info)}
Run `mix help #{task_name}` for more information.
"""
_ ->
{Igniter.Mix.Task.add_default_values(Map.new(got), desired), argv}
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(_, [], got) do
{[], 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
"""
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} ->
with_optional =
if config[:optional] do
"[#{name}]"
else
to_string(name)
end
if config[:rest] do
with_optional <> "[...]"
else
with_optional
end
end)
"mix #{name} #{call}"
end
@doc false
def igniter_task?(task) when is_atom(task) do
mix_task? = function_exported?(task, :run, 1)
igniter_task? = function_exported?(task, :igniter, 1) or function_exported?(task, :igniter, 2)
mix_task? and igniter_task?
end
end