defmodule Pluggable do @moduledoc """ The step specification. There are two kind of steps: function steps and module steps. #### Function steps A function step is any function that receives a token and a set of options and returns a token. Its type signature must be: (Pluggable.Token.t, Pluggable.opts) :: Pluggable.Token.t #### Module steps A module step is an extension of the function step. It is a module that must export: * a `c:call/2` function with the signature defined above * an `c:init/1` function which takes a set of options and initializes it. The result returned by `c:init/1` is passed as second argument to `c:call/2`. Note that `c:init/1` may be called during compilation and as such it must not return pids, ports or values that are specific to the runtime. The API expected by a module step is defined as a behaviour by the `Pluggable` module (this module). ## Examples Here's an example of a function step: def json_header_step(token, _opts) do My.Token.put_data(token, "some_data") end Here's an example of a module step: defmodule PutSomeData do def init(opts) do opts end def call(token, _opts) do My.Token.put_data(token, "some_data") end end ## The Pluggable Step pipeline The `Pluggable.StepBuilder` module provides conveniences for building pluggable step pipelines. """ @type opts :: binary | tuple | atom | integer | float | [opts] | %{optional(opts) => opts} | MapSet.t() @callback init(opts) :: opts @callback call(token :: Pluggable.Token.t(), opts) :: Pluggable.Token.t() require Logger @doc """ Run a series of pluggable steps at runtime. The steps given here can be either a tuple, representing a module step and their options, or a simple function that receives a token and returns a token. If any of the steps halt, the remaining steps are not invoked. If the given token was already halted, none of the steps are invoked either. While `Pluggable.StepBuilder` works at compile-time, this is a straight-forward alternative that works at runtime. ## Examples Pluggable.run(token, [{My.Step, []}, &IO.inspect/1]) ## Options * `:log_on_halt` - a log level to be used if a pipeline halts """ @spec run( Pluggable.Token.t(), [{module, opts} | (Pluggable.Token.t() -> Pluggable.Token.t())], Keyword.t() ) :: Pluggable.Token.t() def run(token, steps, opts \\ []) do if Pluggable.Token.halted?(token), do: token, else: do_run(token, steps, Keyword.get(opts, :log_on_halt)) end defp do_run(token, [{mod, opts} | steps], level) when is_atom(mod) do next_token = mod.call(token, mod.init(opts)) if !Pluggable.Token.impl_for(next_token), do: raise("expected #{inspect(mod)} to return Pluggable.Token, got: #{inspect(next_token)}") if Pluggable.Token.halted?(next_token) do level && Logger.log(level, "Pluggable pipeline halted in #{inspect(mod)}.call/2") next_token else do_run(next_token, steps, level) end end defp do_run(token, [fun | steps], level) when is_function(fun, 1) do next_token = fun.(token) if !Pluggable.Token.impl_for(next_token), do: raise("expected #{inspect(fun)} to return Pluggable.Token, got: #{inspect(next_token)}") if Pluggable.Token.halted?(next_token) do level && Logger.log(level, "Pluggable pipeline halted in #{inspect(fun)}") next_token else do_run(next_token, steps, level) end end defp do_run(token, [], _level), do: token end