Current section
Files
Jump to
Current section
Files
lib/makina/command.ex
defmodule Makina.Command do
import Makina.Helpers
use Corsa, disabled: true
@moduledoc false
@typedoc """
This type contains the options to configure the commands of a model:
- `:extends` contains the model to be extended.
- `:defaults` whether or not to generate the default implementation of the callbacks.
- `:implemented_by` contains the module that contains the implementation under test.
- `:abstract` whether or not the model is an abstract model.
"""
@type option() ::
{:extends, Macro.t()}
| {:defaults, boolean()}
| {:implemented_by, module()}
| {:abstract, boolean()}
| {:debug, Macro.t()}
| {:debug_module, Macro.t()}
##################################################################################################
# Macros
##################################################################################################
@spec __using__([option()]) :: Macro.t()
@doc """
This macro configures the module to allow command defintions. Accepts global command options, this
means that options passed to `use` are passed to every command declared in the module. Supported
options are defined by the `#{inspect(__MODULE__)}.option()` type.
This macro registers the following module attributes:
| attribute | accumulate | initial value | description |
|---------------------|------------|---------------|-------------------------------------------|
| `:commands` | `true` | `[]` | names of the commands |
| `:commands_options` | `true` | `options` | global options for commands |
| `:commands_info` | `true` | `[]` | information about the command enviroments |
| `:commands_processed_options` | `true` | `[]` | information about processed options to avoid infinite recursion. |
## Examples
iex> defmodule Example do
...> use Makina.Command
...> end
iex> defmodule ExampleExtended do
...> use Makina.Command, extends: Example
...> end
iex> defmodule ExampleExtended do
...> use Makina.Command, abstract: true
...> end
iex> defmodule Example do
...> use Makina.Command, extends: "name"
...> end
** (Makina.Error) `"name"` is not a valid module name
iex> defmodule Example do
...> use Makina.Command, extends: A
...> end
** (Makina.Error) could not load `A`
iex> defmodule Example do
...> use Makina.Command, extends: Enum
...> end
** (Makina.Error) module `Enum` does not contain commands
iex> defmodule Example do
...> use Makina.Command, extends: Example
...> end
** (Makina.Error) `MakinaDoctestTest.Example` cannot extend itself
"""
defmacro __using__(options) do
options = Enum.map(options, fn {k, v} -> {k, Macro.expand(v, __CALLER__)} end)
Module.register_attribute(__CALLER__.module, :commands, accumulate: true)
Module.register_attribute(__CALLER__.module, :commands_options, accumulate: true)
Module.register_attribute(__CALLER__.module, :commands_info, accumulate: true)
Module.register_attribute(__CALLER__.module, :commands_processed_options, accumulate: true)
# Extracts and registers the global options passed to `use`.
register_options(
__CALLER__,
:commands_options,
[:defaults, :implemented_by, :extends, :abstract, :debug, :debug_module, :checks],
options
)
# Extracts checker which is needed for args generator in `extends`
checker = get_checker()
{implemented_by, options} = Keyword.pop(options, :implemented_by)
{defaults, options} = Keyword.pop(options, :defaults)
{extends, _options} = Keyword.pop(options, :extends)
quote do
use unquote(checker)
import unquote(__MODULE__)
# configure command extensions
use Makina.Command.Defaults, defaults: unquote(defaults)
import Makina.Command.Defaults, only: []
use Makina.Command.Extends, extends: unquote(extends)
import Makina.Command.Extends, only: []
use Makina.Command.ImplementedBy, implemented_by: unquote(implemented_by)
import Makina.Command.ImplementedBy, only: []
use Makina.Command.Base
import Makina.Command.Base, only: []
end
end
@spec command(Macro.t()) :: Macro.t()
@doc """
This macro allows to declare a command with no options and no body.
## Examples
iex> defmodule Impl do
...> def f(), do: :ok
...> end
iex> defmodule Example do
...> use Makina.Command, defaults: true, implemented_by: Impl
...> command f()
...> end
"""
defmacro command(decl), do: quote(do: command(unquote(decl), do: []))
@spec command(Macro.t(), [option] | Macro.t()) :: Macro.t()
@doc """
This macro allows to declare a command without options or without body.
This macro reads the module attribute `:commands_options`.
## Examples
iex> defmodule Example do
...> use Makina.Command, defaults: true
...> command f() do
...> call :ok
...> end
...> end
iex> defmodule A do
...> def f(), do: :ok
...> end
iex> defmodule Example do
...> use Makina.Command
...> command f(), defaults: true, implemented_by: A
...> end
"""
defmacro command(decl, block = [{:do, _block}]) do
context = __CALLER__.module
options = Module.get_attribute(context, :commands_options, [])
quote context: context do
command(unquote(decl), unquote(options), unquote(block))
end
end
defmacro command(decl, opts) do
context = __CALLER__.module
options = Module.get_attribute(context, :commands_options, [])
quote context: context do
command(unquote(decl), unquote(opts ++ options), do: [])
end
end
@spec command(Macro.t(), [option()], Macro.t()) :: Macro.t()
@doc """
This macro allows to declare a command with options and body.
This macro reads the module attributes `:commands_processed_options`, `:commands_options`.
This macro modifies the following module attributes:
| attribute | value |
|-------------------------------|----------|
| `:commands_processed_options` | [atom()] |
## Examples
iex> defmodule Example do
...> use Makina.Command
...> command f(), defaults: true do
...> call :ok
...> end
...> end
iex> defmodule ExampleExtended do
...> use Makina.Command, extends: Example
...> command f(), defaults: true
...> end
iex> defmodule Example do
...> use Makina.Command
...> command :f
...> end
** (Makina.Error) invalid syntax in command
"""
defmacro command(decl, opts, block) do
context = __CALLER__.module
processed = Module.get_attribute(context, :commands_processed_options, [])
options = Module.get_attribute(context, :commands_options, [])
options = (opts ++ options) |> Enum.filter(fn {k, _v} -> k not in processed end)
arguments = [decl, options, block]
cond do
options[:implemented_by] ->
Module.put_attribute(context, :commands_processed_options, :implemented_by)
quote context: context do
Makina.Command.ImplementedBy.command(unquote_splicing(arguments))
end
options[:extends] ->
Module.put_attribute(context, :commands_processed_options, :extends)
quote context: context do
Makina.Command.Extends.command(unquote_splicing(arguments))
end
options[:defaults] ->
Module.put_attribute(context, :commands_processed_options, :defaults)
quote context: context do
Makina.Command.Defaults.command(unquote_splicing(arguments))
end
true ->
Module.delete_attribute(context, :commands_processed_options)
Module.register_attribute(__CALLER__.module, :commands_processed_options, accumulate: true)
quote context: context do
Makina.Command.Base.command(unquote_splicing(arguments))
end
end
end
##################################################################################################
# Information functions
##################################################################################################
@type commands_info() :: %{commands: [atom()]}
@type command_info() :: %{
module: module(),
name: atom(),
arguments: Keyword.t(Macro.t()),
callbacks: [atom()],
result: Macro.t(),
env: Macro.Env.t()
}
@spec commands_info(module()) :: {:commands_info, commands_info()} | {:error, String.t()}
@doc """
This function extracts the information about commands from a model.
## Examples
iex> defmodule Example do
...> use Makina.Command, defaults: true
...> command f(x :: integer) :: :ok do
...> call :ok
...> end
...> end
iex> commands_info(Example)
{:commands_info, %{commands: [:f]}}
iex> commands_info(Enum)
{:error, "module `Enum` does not contain commands"}
iex> commands_info(A)
{:error, "could not load `A`"}
"""
def commands_info(module) do
with {:module, ^module} <- ensure_compiled(module),
commands_module = commands_module_name(module),
{:commands_module, {:module, ^commands_module}} <-
{:commands_module, ensure_compiled(commands_module)},
true <- {:__makina_info__, 0} in commands_module.__info__(:functions) do
{:commands_info, commands_module.__makina_info__()}
else
{:commands_module, _} ->
{:error, "module `#{inspect(module)}` does not contain commands"}
{:error, message} when is_bitstring(message) ->
{:error, message}
false ->
{:error, "could not load information about the commands module of model `#{module}`"}
unknown ->
{:error, "unknown error: `#{inspect(unknown)}`"}
end
end
@spec command_info(module(), atom()) :: {:command_info, command_info()} | {:error, String.t()}
@doc """
This function extracts information about a command from a model.
## Examples
iex> defmodule Example do
...> use Makina.Command, defaults: true
...> command f(x :: integer) :: :ok do
...> call :ok
...> end
...> end
iex> {:command_info, info} = command_info(Example, :f)
iex> Map.keys(info)
[:env, :module, :name, :callbacks, :arguments, :result]
iex> command_info(Enum, :f)
{:error, "module `Enum` does not contain commands"}
iex> command_info(A, :f)
{:error, "could not load `A`"}
"""
def command_info(module, command) when is_atom(module) and is_atom(command) do
with {:commands_info, commands} <- commands_info(module),
{:contained?, true} <- {:contained?, command in commands.commands},
command_module = command_module_name(command, module),
{:module, ^command_module} <- ensure_compiled(command_module),
true <- {:__makina_info__, 0} in command_module.__info__(:functions) do
{:command_info, command_module.__makina_info__()}
else
{:contained?, false} ->
{:error, "model `#{inspect(module)}` does not contain the command `#{inspect(command)}`"}
{:error, message} when is_bitstring(message) ->
{:error, message}
unknown ->
{:error, "unknown error: `#{inspect(unknown)}`"}
end
end
def command_info(module, atom) when is_atom(module),
do: {:error, "`#{inspect(atom)}` is not a valid command name"}
def command_info(module, _atom),
do: {:error, "`#{inspect(module)}` is not a valid module name"}
@spec command_info(module()) :: {:command_info, [command_info()]} | {:error, String.t()}
@doc """
This function extracts information about all commands from a model.
## Examples
iex> defmodule Example do
...> use Makina.Command, defaults: true
...> command f(x :: integer) :: :ok do
...> call :ok
...> end
...> end
iex> {:command_info, infos} = command_info(Example)
iex> 1 = length(infos)
iex> command_info(Enum)
{:error, "module `Enum` does not contain commands"}
iex> command_info(A)
{:error, "could not load `A`"}
"""
def command_info(module) do
case commands_info(module) do
{:commands_info, %{commands: commands}} ->
Enum.map(commands, &command_info(module, &1))
|> Enum.split_with(&match?({:error, _}, &1))
|> then(fn
{[], commands} -> {:command_info, commands |> Keyword.values()}
{errors, _} -> concat_error_messages(errors)
end)
error ->
error
end
end
end