Packages

Convenience macros and functions for piping in Elixir

Current section

Files

Jump to
exordia lib exordia.ex
Raw

lib/exordia.ex

defmodule Exordia do
@moduledoc ~S"""
Exordia defines some convenience macros/functions.
Add `use Exordia` to modules in which you would like to use them.
"""
@doc false
defmacro __using__(args)
@spec __using__(any()) ::
{:__block__, [],
[{:alias, [any(), ...], [any(), ...]} | {:import, [any(), ...], [any(), ...]}, ...]}
defmacro __using__(_) do
quote do
import Exordia
alias Exordia.Error
end
end
@doc ~S"""
Pipe-like operator which converts `:error` or error tuples to `Exordia.Error`
structs, skips to the end of the chain on errors, and passes only `value`
when it encounters `{:ok, value}`.
An underscore can be used to pipe the argument to an arbitrary position.
## Examples
iex> 1 ~> Kernel.to_string()
"1"
iex> {:ok, 1} ~> Kernel.to_string(_)
"1"
iex> {:error, :something_bad} ~> Kernel.to_string()
%Exordia.Error{handled: false, meta: nil, reason: :something_bad}
"""
defmacro arg ~> ast
@spec any() ~> {atom(), [{atom(), any()}], [...] | nil} ::
{:case, [], [[{any(), any()}, ...] | {:exordize, [], [any(), ...]}, ...]}
defmacro v ~> {f, m, a} do
quote do
case exordize(unquote(v)) do
r = %Exordia.Error{} ->
r
r ->
unquote(pipex(quote(do: r), {f, m, a}))
end
end
end
@doc ~S"""
Similar to the `~>` operator but passes non-errors through and calls the
function on error values unless the `:handled` key of the `Exordia.Error`
struct is `true`.
## Examples
iex> 1 <~ Tuple.to_list()
1
iex> {:ok, 1} <~ Tuple.to_list(_)
1
iex> :error <~ Kernel.inspect()
"%Exordia.Error{handled: false, meta: nil, reason: nil}"
"""
defmacro arg <~ ast
@spec any() <~ {atom(), [{atom(), any()}], [...] | nil} ::
{:case, [], [[{any(), any()}, ...] | {:exordize, [], [any(), ...]}, ...]}
defmacro v <~ {f, m, a} do
quote do
case exordize(unquote(v)) do
r = %Exordia.Error{handled: false} ->
unquote(pipex(quote(do: r), {f, m, a}))
r ->
r
end
end
end
@doc """
Converts an `:error`, `{:error, r}`, or `{:error, r, d}` tuple into an
`Exordia.Error.t()` struct. Any othre value is passed through unmodified.
"""
def exordize(value)
@spec exordize(x | {:ok, x}) :: x | Exordia.Error.t() when x: any()
def exordize({:ok, r}) do
r
end
def exordize(:error) do
%Exordia.Error{}
end
def exordize({:error, r}) do
%Exordia.Error{reason: r}
end
def exordize({:error, r, d}) do
%Exordia.Error{reason: {r, d}}
end
def exordize(r) do
r
end
@doc """
Like `exordize/1` but pipes the value into the provided function.
"""
defmacro exordize(arg, fla)
@spec exordize(any(), {atom(), [{atom(), any()}], [...] | nil}) ::
{:exordize, [], [{any(), any(), [any(), ...]}, ...]}
defmacro exordize(v, {f, m, a}) do
pipe(quote(do: exordize(unquote(v))), {f, m, a})
end
@doc ~S"""
Converts `value` to `{:ok, value}`.
"""
defmacro ok_ify(arg)
@spec ok_ify(any()) :: {:ok, any()}
defmacro ok_ify(v) do
{:ok, v}
end
@doc ~S"""
Converts `value` to `{:ok, value}` and pipes this as an argument into the
provided function. Useful when you want to pipe an `:ok` tuple using `~>`
(which automatically unwraps these tuples).
"""
defmacro ok_ify(arg, fla)
@spec ok_ify(any(), {atom(), [{atom(), any()}], [...] | nil}) ::
{:exordize, [], [{any(), any(), [any(), ...]}, ...]}
defmacro ok_ify(v, {f, m, a}) do
pipex({:ok, v}, {f, m, a})
end
@doc ~S"""
Rescues exceptions, converting them into `Exordia.Error.t()` structs.
## Examples
iex> safe(div(10, 5))
2
iex> safe(div(10, 0))
%Exordia.Error{
handled: false,
meta: nil,
reason: %ArithmeticError{message: "bad argument in arithmetic expression"}
}
"""
defmacro safe(arg)
@spec safe(any()) :: {:try, [], [[{any(), any()}, ...], ...]}
defmacro safe(v) do
quote do
try do
unquote(v)
rescue
e ->
%Exordia.Error{reason: e}
end
end
end
@doc ~S"""
Rescues exceptions, converting them into `Exordia.Error.t()` structs. Passes
the first argument into the second argument's function call making it useful
for piping.
## Examples
iex> 10 |> safe(div(5))
2
iex> 10 |> safe(div(0))
%Exordia.Error{
handled: false,
meta: nil,
reason: %ArithmeticError{message: "bad argument in arithmetic expression"}
}
"""
defmacro safe(arg, ast)
@spec safe(any(), {atom(), [{atom(), any()}], [...] | nil}) ::
{:try, [], [[{any(), any()}, ...], ...]}
defmacro safe(v, {f, m, a}) do
quote do
try do
unquote(pipex(v, {f, m, a}))
rescue
e ->
%Exordia.Error{reason: e}
end
end
end
@doc """
Passes the argument to the provided function, ignoring the return value of the
function and returning the first argument, making it useful for piping.
"""
defmacro tee(arg, ast)
@spec tee(any(), {atom(), [{atom(), any()}], [...] | nil}) :: {:__block__, [], [any(), ...]}
defmacro tee(v, {f, m, a}) do
quote do
_ = unquote(pipex(v, {f, m, a}))
unquote(v)
end
end
@doc false
defmacro terror(arg)
@spec terror(any()) :: any()
defmacro terror(v) do
v
end
@doc """
Passes the argument to the provided function. Returns the first argument
unless the function returns an error in which case the error is returned,
making it useful for piping.
"""
defmacro terror(arg, ast)
@spec terror(any(), {atom(), [{atom(), any()}], [...] | nil}) ::
{:case, [], [[{any(), any()}, ...] | {:exordize, [], [any(), ...]}, ...]}
defmacro terror(v, {f, m, a}) do
quote do
case unquote(pipex(v, {f, m, a})) do
r = %Exordia.Error{} ->
r
r ->
unquote(v)
end
end
end
@doc """
Converts an `Exordia.Error.t()` struct into an `:error`, `{:error, r}`, or
`{:error, r, d}` tuple. Any other value is passed through unmodified.
"""
@spec un_exordize(any()) :: any()
def un_exordize(%Exordia.Error{reason: nil}) do
:error
end
def un_exordize(%Exordia.Error{reason: {r, d}}) do
{:error, r, d}
end
def un_exordize(%Exordia.Error{reason: r}) do
{:error, r}
end
def un_exordize(r) do
r
end
@doc """
Like `un_exordize/1` but pipes the value into the provided function.
"""
defmacro un_exordize(arg, fla)
@spec un_exordize(any(), {atom(), [{atom(), any()}], [...] | nil}) ::
{any(), any(), [any(), ...]}
defmacro un_exordize(v, {f, m, a}) do
pipe(quote(do: un_exordize(unquote(v))), {f, m, a})
end
@doc false
defp pipe(val, ast, pos)
@spec pipe(any(), {atom(), [{atom(), any()}], [...] | nil}, nil) :: any()
defp pipe(r, {f, m, a}, nil) do
quote do
unquote(Macro.pipe(r, {f, m, a}, 0))
end
end
defp pipe(r, {f, m, a}, p) do
quote do
unquote(Macro.pipe(r, {f, m, List.delete_at(a, p)}, p))
end
end
@doc false
defp pipe(val, ast)
@spec pipe(any(), {atom(), [{atom(), any()}], [...] | nil}) :: {any(), any(), [any(), ...]}
defp pipe(r, {f, m, a}) do
pipe(r, {f, m, a}, position(a))
end
@doc false
defp pipex(val, ast)
@spec pipex(any(), {atom(), [{atom(), any()}], [...] | nil}) ::
{:exordize, [], [{any(), any(), [any(), ...]}, ...]}
defp pipex(r, {f, m, a}) do
quote do
exordize(unquote(pipe(r, {f, m, a})))
end
end
@doc false
defp position(args)
@spec position(nil) :: nil
defp position(nil) do
nil
end
@spec position([...]) :: non_neg_integer() | nil
defp position(a) do
Enum.find_index(a, &underscore?/1)
end
@doc false
defp underscore?(identifier)
@spec underscore?({atom(), any(), any()}) :: boolean()
defp underscore?({:_, _, _}) do
true
end
defp underscore?(_) do
false
end
end