defmodule Diesel.Dsl do
@moduledoc """
Defines the syntax provided by a DSL.
Simple usage:
```elixir
defmodule MyApp.Fsm.Dsl do
use Diesel.Dsl,
root: :fsm,
tags: [
:state,
:on,
:action,
:next
]
end
```
Please check the documentation for more info on how to extend DSLs via `packages`
"""
alias Diesel.Tag
import Diesel.Naming
defmacro __using__(opts) do
dsl = __CALLER__.module
default_root_tag = dsl |> Module.split() |> Enum.drop(-1) |> List.last()
default_root_tag = Module.concat(dsl, default_root_tag)
root = opts |> Keyword.get(:root, default_root_tag) |> module_name()
tags = Keyword.get(opts, :tags, [])
if Enum.empty?(tags), do: raise("No tags defined in #{inspect(dsl)}")
tags = tags |> Enum.map(&module_name/1) |> Enum.uniq()
tags_by_name = Enum.reduce(tags, %{}, &Map.put(&2, Tag.name(&1), &1))
tag_names = Map.keys(tags_by_name)
root_name = Tag.name(root)
tag_names = tag_names -- [root_name]
tags_by_name = Map.put(tags_by_name, root_name, root)
quote do
@root unquote(root_name)
@tag_names unquote(tag_names)
@all_tags_names [@root | @tag_names]
@tags_by_name unquote(Macro.escape(tags_by_name))
@locals_without_parens for tag <- @all_tags_names, do: {tag, :*}
def root, do: @root
def tags, do: @all_tags_names
@doc """
Returns the list of locals without parens function signatures, so that they can be easily
included in .formatter.exs files
"""
@spec locals_without_parens() :: keyword()
def locals_without_parens, do: @locals_without_parens
def validate({tag, _, children} = node) do
with :ok <- validate(children), do: validate_node(node)
end
def validate(nodes) when is_list(nodes) do
Enum.reduce_while(nodes, :ok, fn node, _ ->
case validate(node) do
:ok -> {:cont, :ok}
error -> {:halt, error}
end
end)
end
def validate(_), do: :ok
defp validate_node({tag, _, _} = node) do
case Map.get(@tags_by_name, tag) do
nil ->
{:error, "Unsupported tag '#{inspect(tag)}'"}
tag ->
with {:error, reason} <- Tag.validate(tag, node) do
{:error, "in tag '#{Tag.name(tag)}'. #{reason}"}
end
end
end
defmacro unquote(root_name)(do: {:__block__, [], children}) do
quote do
@definition {unquote(@root), [], unquote(children)}
end
end
defmacro unquote(root_name)(do: child) do
quote do
@definition {unquote(@root), [], [unquote(child)]}
end
end
defmacro unquote(root_name)(attrs, do: {:__block__, [], children}) when is_list(attrs) do
quote do
@definition {unquote(@root), unquote(attrs), unquote(children)}
end
end
defmacro unquote(root_name)(name, do: {:__block__, [], children}) do
quote do
@definition {unquote(@root), [name: unquote(name)], unquote(children)}
end
end
defmacro unquote(root_name)(attrs, do: child) when is_list(attrs) do
quote do
@definition {unquote(@root), unquote(attrs), [unquote(child)]}
end
end
defmacro unquote(root_name)(name, do: child) do
quote do
@definition {unquote(@root), [name: unquote(name)], [unquote(child)]}
end
end
defmacro unquote(root_name)(attrs, child) when is_list(attrs) do
quote do
@definition {unquote(@root), unquote(attrs), [unquote(child)]}
end
end
defmacro unquote(root_name)(name, child) do
quote do
@definition {unquote(@root), [name: unquote(name)], [unquote(child)]}
end
end
defmacro unquote(root_name)(name, attrs, do: {:__block__, [], children}) do
quote do
@definition {unquote(@root), unquote(Keyword.put(attrs, :name, name)),
unquote(children)}
end
end
defmacro unquote(root_name)(name, attrs, do: child) do
quote do
@definition {unquote(@root), unquote(Keyword.put(attrs, :name, name)), [unquote(child)]}
end
end
unquote_splicing(
Enum.map(tag_names, fn tag ->
quote do
defmacro unquote(tag)(attrs, do: {:__block__, _, children}) when is_list(attrs) do
{:{}, [line: 1], [unquote(tag), attrs, children]}
end
defmacro unquote(tag)(attr, do: {:__block__, _, children}) do
{:{}, [line: 1], [unquote(tag), [name: attr], children]}
end
defmacro unquote(tag)(name, attrs, do: {:__block__, _, children}) do
{:{}, [line: 1], [unquote(tag), Keyword.put(attrs, :name, name), children]}
end
defmacro unquote(tag)(attrs, do: child) when is_list(attrs) do
{:{}, [line: 1], [unquote(tag), attrs, [child]]}
end
defmacro unquote(tag)(attr, do: child) do
{:{}, [line: 1], [unquote(tag), [name: attr], [child]]}
end
defmacro unquote(tag)(name, attrs, do: child) do
{:{}, [line: 1], [unquote(tag), Keyword.put(attrs, :name, name), [child]]}
end
defmacro unquote(tag)(do: {:__block__, _, children}) do
{:{}, [line: 1], [unquote(tag), [], children]}
end
defmacro unquote(tag)(do: child) do
{:{}, [line: 1], [unquote(tag), [], [child]]}
end
defmacro unquote(tag)(attrs) when is_list(attrs) do
if Keyword.keyword?(attrs) do
{:{}, [line: 1], [unquote(tag), attrs, []]}
else
{:{}, [line: 1], [unquote(tag), [], attrs]}
end
end
defmacro unquote(tag)(child) do
{:{}, [line: 1], [unquote(tag), [], [child]]}
end
defmacro unquote(tag)(name, attrs) when is_list(attrs) do
{:{}, [line: 1], [unquote(tag), Keyword.put(attrs, :name, name), []]}
end
defmacro unquote(tag)() do
{:{}, [line: 1], [unquote(tag), [], []]}
end
end
end)
)
end
end
@doc false
def validate!(dsl, definition) do
with {:error, reason} <- dsl.validate(definition) do
raise "invalid syntax #{reason}"
end
end
end