Current section
Files
Jump to
Current section
Files
lib/hafnium.ex
defmodule Hafnium do
@external_resource readme = Path.join([__DIR__, "../README.md"])
@dialyzer {:nowarn_function, parse_result: 2}
@moduledoc readme
|> File.read!()
|> String.split("<!-- MDOC -->")
|> Enum.fetch!(2)
use MdDoctest.TransformModuleDoc
alias __MODULE__.{Compile, Config, Pipeline, Utils}
@type input :: any()
@typedoc """
* `:ok` — success
* `:error` — error
* `:noop` — like `ok` but do nothing
* `:fatal` — error and break
"""
@type state :: :noop | :error | :ok | :fatal
@type result :: {state(), any()}
@type invoke_result :: t() | any()
@type pipe_result :: t() | pipe_atom_result() | pipe_tuple_result()
@typep pipe_atom_result :: :noop | :ok
@typep pipe_result_code :: :ok | :error | :fatal
@typep pipe_tuple_result :: {code :: pipe_result_code(), result_or_reason :: any()}
@version Mix.Project.config()[:version]
@doc false
def version, do: @version
@typedoc "Base struct"
@type t :: %__MODULE__{
context: context :: keyword(),
result: result :: result(),
pipelines: pipelines :: [Pipeline.t()],
input: input(),
__pipeline__: Pipeline.t(),
__module__: current_module :: module()
}
@enforce_keys [:__module__]
defstruct [
:__pipeline__,
:__module__,
context: [],
result: {:ok, nil},
pipelines: [],
input: %{}
]
defmodule Error do
@moduledoc """
Error
"""
defexception message: nil, context: []
@type t :: %__MODULE__{message: binary(), context: keyword()}
end
defmacro __using__(_opts) do
quote location: :keep do
# @behaviour unquote(__MODULE__)
unless Module.get_attribute(__MODULE__, :moduledoc) do
@moduledoc """
See `#{unquote(__MODULE__)}`.
"""
end
import unquote(__MODULE__)
Module.register_attribute(__MODULE__, :__pipeline__, accumulate: true)
@spec call(%{}) :: unquote(__MODULE__).t()
def call(input \\ %{}), do: unquote(__MODULE__).invoke(__meta__(), input)
@before_compile unquote(__MODULE__)
end
end
defmacro __before_compile__(env) do
pipelines =
env.module
|> Module.get_attribute(:__pipeline__)
|> Enum.reverse()
|> (&Kernel.++(Config.before_pipelines(), &1)).()
|> Kernel.++(Config.after_pipelines())
|> Enum.map(&Compile.build_pipeline!/1)
|> Enum.map(&Macro.escape/1)
|> Enum.with_index()
|> Enum.map(fn {ast, i} -> quote do: Map.put(unquote(ast), :index, unquote(i)) end)
quote location: :keep do
@spec name :: atom()
def name, do: unquote(Utils.module_name(env.module))
@spec __pipelines__ :: [{module(), any()}]
@__pipelines__ unquote(pipelines)
def __pipelines__, do: @__pipelines__
@spec new :: unquote(__MODULE__).t()
def new, do: unquote(__MODULE__).new(__MODULE__)
@__meta__ %{__module__: __MODULE__, pipelines: @__pipelines__}
def __meta__, do: @__meta__
end
end
@spec new(atom()) :: %__MODULE__{}
def new(module) when is_atom(module) do
%__MODULE__{__module__: module, pipelines: module.__pipelines__()}
end
@doc """
Add pipeline
```elixir
defmodule ExampleApi do
use #{__MODULE__}
pipeline MyPipeline1, [foo: :bar]
pipeline MyPipeline2
end
```
"""
defmacro pipeline(module) do
quote bind_quoted: [module: module] do
@__pipeline__ module
end
end
defmacro pipeline(module, opts) do
quote bind_quoted: [module: module, opts: opts] do
@__pipeline__ {module, opts}
end
end
@spec invoke(t(), %{}) :: t()
def invoke(%__MODULE__{pipelines: pipelines} = o, %{} = input) do
o = %__MODULE__{
o
| input: Utils.atomize_keys(input),
result: {:ok, :noop}
}
Enum.reduce(pipelines, o, &reduce_invoke/2)
end
defp reduce_invoke(%Pipeline{} = pipeline, %__MODULE__{} = o) do
o
|> Map.put(:__pipeline__, pipeline)
|> inner_invoke()
|> parse_result(o)
end
defp inner_invoke(%__MODULE__{__pipeline__: %Pipeline{__module__: module}} = o) do
o |> module.before_call() |> module.call() |> module.after_call()
end
defp parse_result(%__MODULE__{} = o, _), do: o
@states [:ok, :error, :noop, :fatal]
defp parse_result({state, result}, o) when state in @states do
%__MODULE__{o | result: {state, result}}
end
end