Packages

Makina is a DSL for writing PBT models for stateful systems.

Current section

Files

Jump to
makina lib makina import.ex
Raw

lib/makina/import.ex

defmodule Makina.Import do
import Makina.Helpers
import Makina.State, only: [attribute_info: 1]
import Makina.Invariants, only: [invariant_info: 1]
import Makina.Command, only: [command_info: 1]
alias Makina.Error
@moduledoc false
@typedoc """
This type represents the command options processed by this module:
- `:model` contains the model with the commands to export.
- `:where` contains keyword list with the commands to rename.
- `:hiding` contains a list with the commands to hide in the model.
"""
@type option() :: {:model, module()} | {:where, Keyword.t(atom())} | {:hiding, [atom()]}
##################################################################################################
# Macros
##################################################################################################
@doc """
This macro generates a Makina model that imports the state, invariants and commands.
## Examples
iex> defmodule Example do
...> use Makina
...> state attr1: 0 :: integer(), attr2: true
...> invariants invariant: true
...> command f() :: :ok do
...> call :ok
...> end
...> command g() :: :ok do
...> call :ok
...> end
...> end
iex> defmodule ImportExample do
...> use Makina.Import, model: Example, where: [f: :h], hiding: [:g]
...> end
iex> defmodule ImportExample do
...> use Makina.Import, model: Example, where: [f: :h], hiding: [:g]
...> end
** (Makina.Error) could not load the module `Example`
iex> defmodule ImportExample do
...> use Makina.Import, where: [f: :h], hiding: [:g]
...> end
** (Makina.Error) missing model to import
iex> defmodule Example do
...> use Makina
...> state attr1: 0 :: integer(), attr2: true
...> invariants invariant: true
...> command f() :: :ok do
...> call :ok
...> end
...> command g() :: :ok do
...> call :ok
...> end
...> end
iex> defmodule ImportExample do
...> use Makina.Import, model: Example, where: [f: :h], hiding: :g
...> end
** (Makina.Error) `:hiding` receives a list of command names
iex> defmodule Example do
...> use Makina
...> state attr1: 0 :: integer(), attr2: true
...> invariants invariant: true
...> command f() :: :ok do
...> call :ok
...> end
...> command g() :: :ok do
...> call :ok
...> end
...> end
iex> defmodule ImportExample do
...> use Makina.Import, model: Example, where: [a: :h]
...> end
** (Makina.Error) unknown commands `:a`
"""
@spec __using__([option()]) :: Macro.t()
defmacro __using__(options) do
options = Enum.map(options, fn opt -> Macro.prewalk(opt, &Macro.expand(&1, __CALLER__)) end)
with {:ok, module} <- Keyword.fetch(options, :model),
{:ok, state} <- import_state(module),
{:ok, invariants} <- import_invariants(module),
{:ok, commands} <- import_commands(module, options) do
quote do
use Makina
unquote(state)
unquote(invariants)
unquote_splicing(commands)
end
else
:error -> {:error, "missing model to import"}
{:error, error} -> {:error, error}
error -> {:error, "unknown error: `#{inspect(error)}`"}
end
|> then(fn
{:error, message} -> Error.throw_error(message, __CALLER__)
result -> result
end)
end
##################################################################################################
# Helpers
##################################################################################################
@doc """
This function generates an expression that imports the attributes from a given model.
## Examples
iex> defmodule Example do
...> use Makina.State
...> state attr1: 0 :: integer, attr2: true
...> end
iex> {:ok, state} = import_state(Example)
iex> result = state |> Macro.to_string()
iex> code = quote do
...> state attr2: MakinaDoctestTest.Example.State.Attribute.Attr2.init() :: any(),
...> attr1: MakinaDoctestTest.Example.State.Attribute.Attr1.init() :: integer
...> end |> Macro.to_string()
iex> assert result =~ code
iex> import Makina.Import
iex> {:error, _} = import_state("error")
"""
@spec import_state(module) :: {:ok, Macro.t()} | {:error, String.t()}
def import_state(module) do
case attribute_info(module) do
{:attribute_info, attributes} ->
for %{type: type, module: module, name: name} <- attributes do
expr = quote do: unquote(module).init() :: unquote(type)
{name, expr}
end
|> then(&{:ok, quote(do: state(unquote(&1)))})
{:error, error} ->
{:error, error}
end
end
@doc """
This function generates an expression that imports the invariants from a makina model.
## Examples
iex> defmodule Example do
...> use Makina.Invariants
...> invariants inv1: true
...> end
iex> {:ok, invariants} = import_invariants(Example)
iex> result = Macro.to_string(invariants)
iex> code = quote do
...> invariants inv1: MakinaDoctestTest.Example.Invariant.Inv1.check(state)
...> end |> Macro.to_string()
iex> assert result =~ code
"""
@spec import_invariants(module()) :: {:ok, Macro.t()} | {:error, String.t()}
def import_invariants(module) do
case invariant_info(module) do
{:invariant_info, invariants} ->
for %{name: name, module: module} <- invariants do
expr = quote do: unquote(module).check(unquote(Macro.var(:state, nil)))
{name, expr}
end
|> then(&{:ok, quote(do: invariants(unquote(&1)))})
error ->
error
end
end
@doc """
This function generates an expression that imports the commands from a makina model. It also hides
and renames commands passed in the options paramater.
## Examples
iex> defmodule Example do
...> use Makina.Command, defaults: true
...> command f(arg1 :: integer, arg2) :: :ok do
...> call :ok
...> end
...> end
iex> {:ok, commands} = import_commands(Example, [])
iex> result = commands |> Macro.to_string()
iex> code = quote do
...> [
...> command f(arg1 :: integer, arg2) :: :ok do
...> pre MakinaDoctestTest.Example.Command.F.pre(state)
...> args MakinaDoctestTest.Example.Command.F.args(state)
...> valid_args MakinaDoctestTest.Example.Command.F.valid_args(state, arguments)
...> call MakinaDoctestTest.Example.Command.F.call(arguments)
...> valid MakinaDoctestTest.Example.Command.F.valid(state, arguments)
...> next MakinaDoctestTest.Example.Command.F.next(state, arguments, result, valid)
...> post MakinaDoctestTest.Example.Command.F.post(state, arguments, result, valid)
...> weight MakinaDoctestTest.Example.Command.F.weight(state)
...> end
...> ]
...> end |> Macro.to_string()
iex> assert result =~ code
iex> {:ok, commands} = import_commands(Example, where: [f: :g])
iex> result = commands |> Macro.to_string()
iex> code = quote do
...> [
...> command g(arg1 :: integer, arg2) :: :ok do
...> pre MakinaDoctestTest.Example.Command.F.pre(state)
...> args MakinaDoctestTest.Example.Command.F.args(state)
...> valid_args MakinaDoctestTest.Example.Command.F.valid_args(state, arguments)
...> call MakinaDoctestTest.Example.Command.F.call(arguments)
...> valid MakinaDoctestTest.Example.Command.F.valid(state, arguments)
...> next MakinaDoctestTest.Example.Command.F.next(state, arguments, result, valid)
...> post MakinaDoctestTest.Example.Command.F.post(state, arguments, result, valid)
...> weight MakinaDoctestTest.Example.Command.F.weight(state)
...> end
...> ]
...> end |> Macro.to_string()
iex> assert result =~ code
iex> {:ok, []} = import_commands(Example, hiding: [:f])
"""
@spec import_commands(module(), [option()]) :: {:ok, Macro.t()} | {:error, String.t()}
def import_commands(module, options) do
with {:ok, hiding} <- validate_hiding(Keyword.get(options, :hiding, [])),
{:ok, rename} <- validate_where(Keyword.get(options, :where, [])),
{:command_info, commands} <- command_info(module),
names = Enum.map(commands, fn info -> info.name end),
{:names, []} <-
MapSet.difference(MapSet.new(hiding), MapSet.new(names))
|> MapSet.to_list()
|> then(&{:names, &1}),
{:names, []} <-
MapSet.difference(MapSet.new(Keyword.keys(rename)), MapSet.new(names))
|> MapSet.to_list()
|> then(&{:names, &1}) do
names = Enum.filter(names, &(&1 not in Keyword.keys(rename)))
names = Enum.zip(names, names)
for {old_name, new_name} <- names ++ rename, old_name not in hiding do
command = Enum.find(commands, fn info -> info.name == old_name end)
%{result: result_type, arguments: arguments, module: module} = command
args =
Enum.map(arguments, fn {arg, type} ->
if equivalent_type?(type, unknown_type()) do
quote do: unquote(Macro.var(arg, nil))
else
quote do: unquote(Macro.var(arg, nil)) :: unquote(type)
end
end)
state = Macro.var(:state, nil)
arguments = Macro.var(:arguments, nil)
result = Macro.var(:result, nil)
valid = Macro.var(:valid, nil)
quote do
command unquote(new_name)(unquote_splicing(args)) :: unquote(result_type) do
pre unquote(module).pre(unquote_splicing([state]))
args unquote(module).args(unquote_splicing([state]))
valid_args unquote(module).valid_args(unquote_splicing([state, arguments]))
call unquote(module).call(unquote_splicing([arguments]))
valid unquote(module).valid(unquote_splicing([state, arguments]))
next unquote(module).next(unquote_splicing([state, arguments, result, valid]))
post unquote(module).post(unquote_splicing([state, arguments, result, valid]))
weight unquote(module).weight(unquote_splicing([state]))
end
end
end
|> then(&{:ok, &1})
else
{:names, names} -> {:error, "unknown commands #{atoms_to_string(names)}"}
{:error, error} -> {:error, error}
error -> {:error, "unknown error: `#{inspect(error)}`"}
end
end
@doc """
This function validates the option `:where`.
## Examples
iex> validate_where(f: :g)
{:ok, [f: :g]}
iex> validate_where(:f)
{:error, "`:where` receives a keyword list"}
iex> validate_where(f: 1)
{:error, "`1` in `:where` are not valid command names"}
"""
@spec validate_where(Keyword.t(atom())) :: {:ok, Keyword.t(atom())} | {:error, String.t()}
def validate_where(where) do
with true <- Keyword.keyword?(where),
[] <- Keyword.values(where) |> Enum.filter(&(not is_atom(&1))) do
{:ok, where}
else
false ->
{:error, "`:where` receives a keyword list"}
values when is_list(values) ->
{:error, "#{atoms_to_string(values)} in `:where` are not valid command names"}
end
end
@doc """
This function validates the option `:hiding`
## Examples
iex> validate_hiding([:f, :g])
{:ok, [:f, :g]}
iex> validate_hiding(:f)
{:error, "`:hiding` receives a list of command names"}
"""
@spec validate_hiding([atom()]) :: {:ok, [atom()]} | {:error, String.t()}
def validate_hiding(hiding) do
with true <- is_list(hiding),
[] <- Enum.filter(hiding, &(not is_atom(&1))) do
{:ok, hiding}
else
false -> {:error, "`:hiding` receives a list of command names"}
values when is_list(values) -> {:error, "`:hiding` receives a list of command names"}
end
end
end