Extep (Extep v0.1.1)

View Source

A tiny and friendly step runner for Elixir pipelines.

Extep is a simple and dependency-free utility that helps you compose Elixir pipelines using a shared context. It's useful for building multi-step workflows that can gracefully halt or error along the way. Extep is an implementation of the Railway-oriented programming and was inspired by Ecto.Multi and Sage.

Example

iex> params = %{user_id: 1, plan: "super-power-plus"}
iex> Extep.new(%{params: params})
...> |> Extep.run(:params, &validate_params/1)
...> |> Extep.run(:user, &fetch_user/1)
...> |> Extep.run(:items, &fetch_items/1)
...> |> Extep.return(&create_subscription/1)
{:ok, %{id: 123, object: "subscription", user_id: 1, items: [%{code: "item1"}, %{code: "item2"}]}}

Summary

Functions

Creates a new Extep struct with an empty context.

Creates a new Extep struct with an initial context map.

Returns a final result from your pipeline.

Runs a checker function on the context without changing it.

Runs a mutator function that updates the context with the result under the given key.

Types

context()

@type context() :: map()

context_checker_fun()

@type context_checker_fun() :: (context() ->
                            :ok
                            | {:ok, any()}
                            | {:halt, any()}
                            | {:error, any()})

context_key()

@type context_key() :: atom()

context_mutator_fun()

@type context_mutator_fun() :: (context() ->
                            {:ok, any()} | {:halt, any()} | {:error, any()})

status()

@type status() :: :ok | :halted | :error

t()

@type t() :: %Extep{context: context(), message: any(), status: status()}

Functions

new()

@spec new() :: t()

Creates a new Extep struct with an empty context.

Examples

iex> Extep.new()
%Extep{status: :ok, context: %{}, message: nil}

new(context)

@spec new(map()) :: t()

Creates a new Extep struct with an initial context map.

Examples

iex> Extep.new(%{foo: "bar"})
%Extep{status: :ok, context: %{foo: "bar"}, message: nil}

return(extep, fun)

@spec return(t(), context_mutator_fun() | context_key()) :: any()

Returns a final result from your pipeline.

If your Extep status is :ok and you pass a function, it calls the function with the context and returns whatever it returns. If you pass a context key, it fetches the value for that key and returns it in an ok tuple.

If the pipeline was halted, it returns the content of the :message field of the Extep struct. If the pipeline errored, it returns {:error, message}.

Examples

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 1} end)
...> |> Extep.return(fn ctx -> {:ok, ctx.bar + 2} end)
{:ok, 4}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 1} end)
...> |> Extep.run(:baz, fn ctx -> {:ok, ctx.bar + 2} end)
...> |> Extep.return(:bar)
{:ok, 2}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 1} end)
...> |> Extep.return(&return_error_tuple/1)
{:error, %{return_error_tuple: "error message"}}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 1} end)
...> |> Extep.run(:baz, &return_error_tuple/1)
...> |> Extep.return(:bar)
{:error, %{return_error_tuple: "error message"}}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 1} end)
...> |> Extep.run(:baz, fn _ctx -> {:error, "error message"} end)
...> |> Extep.return(:bar)
{:error, %{baz: "error message"}}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 1} end)
...> |> Extep.run(fn _ctx -> {:error, "error message"} end)
...> |> Extep.return(:bar)
{:error, %{no_context_key: "error message"}}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 1} end)
...> |> Extep.return(fn _ctx -> {:halt, {:ok, "halt message"}} end)
{:ok, "halt message"}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 1} end)
...> |> Extep.return(fn _ctx -> {:halt, {:cancel, "cancel message"}} end)
{:cancel, "cancel message"}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 1} end)
...> |> Extep.run(:baz, fn _ctx -> {:halt, {:ok, "halt message"}} end)
...> |> Extep.return(:bar)
{:ok, "halt message"}

run(extep, fun)

@spec run(t(), context_checker_fun()) :: t()

Runs a checker function on the context without changing it.

If the function returns :ok or {:ok, "value"}, the pipeline continues. If it returns {:halt, "reason"} or {:error, "reason"}, the pipeline stops and the reason is set in the :message field of the Extep struct.

Examples

iex> Extep.new(%{foo: 1})
...> |> Extep.run(fn _ctx -> :ok end)
%Extep{status: :ok, context: %{foo: 1}, message: nil}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(fn _ctx -> {:ok, "is valid"} end)
%Extep{status: :ok, context: %{foo: 1}, message: nil}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(&return_error_tuple/1)
%Extep{status: :error, context: %{foo: 1}, message: %{return_error_tuple: "error message"}}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(fn _ctx -> {:error, "error message"} end)
%Extep{status: :error, context: %{foo: 1}, message: %{no_context_key: "error message"}}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:foo, fn _ctx -> {:halt, "halt message"} end)
%Extep{status: :halted, context: %{foo: 1}, message: "halt message"}

run(extep, context_key, fun)

@spec run(t(), context_key(), context_mutator_fun()) :: t()

Runs a mutator function that updates the context with the result under the given key.

If the function returns {:ok, "value"}, "value" is set to the given key and the pipeline continues. If it returns {:halt, "reason"} or {:error, "reason"}, the pipeline stops and the reason is set in the :message field of the Extep struct.

Examples

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:foo, fn ctx -> {:ok, ctx.foo + 1} end)
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 2} end)
%Extep{status: :ok, context: %{foo: 2, bar: 4}, message: nil}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:foo, &return_error_tuple/1)
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 2} end)
%Extep{status: :error, context: %{foo: 1}, message: %{return_error_tuple: "error message"}}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:foo, fn _ctx -> {:error, "error message"} end)
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 2} end)
%Extep{status: :error, context: %{foo: 1}, message: %{foo: "error message"}}

iex> Extep.new(%{foo: 1})
...> |> Extep.run(:foo, fn _ctx -> {:halt, {:ok, "halt message"}} end)
...> |> Extep.run(:bar, fn ctx -> {:ok, ctx.foo + 2} end)
%Extep{status: :halted, context: %{foo: 1}, message: {:ok, "halt message"}}