Packages

Tools to handle three common return values in Elixir: `:ok | {:ok, value} | {:error, reason}`

Current section

Files

Jump to
brex_result lib result helpers.ex
Raw

lib/result/helpers.ex

defmodule Brex.Result.Helpers do
@moduledoc """
Tools for modifying the reason in `error` tuples.
"""
import Brex.Result.Base
alias Brex.Result.Base
require Logger
@type s(x) :: Base.s(x)
@type t(x) :: Base.t(x)
@type p() :: Base.p()
@type s() :: Base.s()
@type t() :: Base.t()
@doc """
Wraps a naked `:error` atom in a tuple with the given reason.
Leaves success values and `error` tuples unchanged.
## Examples:
iex> :error
...> |> normalize_error(:parsing_failure)
{:error, :parsing_failure}
iex> {:ok, 2}
...> |> normalize_error()
{:ok, 2}
"""
@doc since: "0.3.0"
@spec normalize_error(any, any) :: t()
def normalize_error(x, reason \\ :normalized) do
case x do
:error -> {:error, reason}
{:error, _r} -> x
{:ok, _val} -> x
:ok -> :ok
end
end
@doc """
Lifts a value into a success tuple unless:
1) the value matches the second argument
2) when applied to the value, the second argument function returns `true`
In those cases an `error` tuple is returned with either
1) the third argument as the reason
2) the third argument function applied to the value, as the reason
`lift/3` is lazy, this means third argument will only be evaluated when necessary.
## Examples:
iex> nil
...> |> lift(nil, :not_found)
{:error, :not_found}
iex> 2
...> |> lift(nil, :not_found)
{:ok, 2}
iex> "test"
...> |> lift(&(&1 == "test"), fn x -> {:oops, x <> "ed"} end)
{:error, {:oops, "tested"}}
## Typespec:
lift(a | b, b | (a | b -> boolean), c | (a | b -> c)) :: s(a) when a: var, b: var, c: var
"""
@doc updated: "0.2.0"
@doc since: "0.1.0"
defmacro lift(val, p, f) do
quote do
p = unquote(p)
val = unquote(val)
# check if the value passes satisfies the predicate or matches the second argument.
match = if is_function(p), do: p.(val), else: val == p
if match do
f = unquote(f)
# lift to error tuple on match
{:error, if(is_function(f), do: f.(val), else: f)}
else
# ok tuple otherwise
{:ok, val}
end
end
end
@doc """
Applies the function to the reason in an `error` tuple.
Leaves success unchanged.
## Example:
iex> account_name = "test"
...> {:error, :not_found}
...> |> map_error(fn r -> {r, account_name} end)
{:error, {:not_found, "test"}}
"""
@doc since: "0.1.0"
@spec map_error(t(a), (any -> any)) :: t(a) when a: var
def map_error({:error, r}, f), do: error(f.(r))
def map_error({:ok, _val} = ma, _), do: ma
def map_error(:ok, _), do: :ok
@doc """
Replaces the reason in an `error` tuple.
Leaves success unchanged.
Lazy. Only evaluates the second argument if necessary.
## Example:
iex> account_name = "test"
...> {:error, :not_found}
...> |> mask_error({:nonexistent_account, account_name})
{:error, {:nonexistent_account, "test"}}
## Typespec:
mask_error(t(a), any) :: t(a) when a: var
"""
@doc updated: "0.2.0"
@doc since: "0.1.0"
defmacro mask_error(ma, term) do
quote do
case unquote(ma) do
{:error, _} -> {:error, unquote(term)}
{:ok, val} -> {:ok, val}
:ok -> :ok
end
end
end
@doc """
Logs on `error`, does nothing on success.
## Example:
{:error, :not_found}
|> log_error("There was an error", level: :info, metadata: "some meta")
"""
@doc updated: "0.2.0"
@doc since: "0.1.0"
# TODO: refine the type of second argument
@spec log_error(t(a), String.t() | (any -> any), Keyword.t()) :: t(a) when a: var
def log_error(ma, chardata_or_fun, opts \\ [])
def log_error({:error, r} = ma, chardata_or_fun, opts) when is_binary(chardata_or_fun) do
# default to :error level
{level, metadata} = Keyword.pop(opts, :level, :error)
log_fn =
case level do
:debug -> &Logger.debug/2
:info -> &Logger.info/2
:warn -> &Logger.warn/2
:error -> &Logger.error/2
end
log_fn.(chardata_or_fun, [reason: "#{r}"] ++ metadata)
ma
end
def log_error({:ok, _val} = ma, _, _), do: ma
def log_error(:ok, _, _), do: :ok
@doc """
Converts a matching error to `:ok`
An error matches if the reason is equal to the supplied atom or the reason passes the predicate.
Leaves success and other errors unchanged.
## Examples:
iex> {:error, :already_completed}
...> |> convert_error(:already_completed)
:ok
iex> {:error, :already_completed}
...> |> convert_error(fn r -> r == :already_completed end)
:ok
"""
@doc updated: "0.2.0"
@doc since: "0.1.0"
@spec convert_error(t(a), (any -> boolean) | any) :: t(a) when a: var
def convert_error({:error, r} = ma, p) when is_function(p) do
if p.(r), do: :ok, else: ma
end
def convert_error({:error, r} = ma, term) do
if r == term, do: :ok, else: ma
end
def convert_error({:ok, _val} = ma, _p), do: ma
def convert_error(:ok, _p), do: :ok
@doc """
Converts a matching error to a success with the given value or function.
An error matches if the reason is equal to the supplied atom or the reason passes the predicate.
Leaves success and other errors unchanged.
Lazy. Only evaluates the second argument if necessary.
## Examples:
iex> {:error, :already_completed}
...> |> convert_error(:already_completed, "submitted")
{:ok, "submitted"}
iex> {:error, "test"}
...> |> convert_error(&(&1 == "test"), fn r -> {:ok, r <> "ed"} end)
{:ok, "tested"}
## Typespec:
convert_error(t(), (any -> boolean) | any, b | (any -> t(b))) :: t(b) when b: var
"""
@doc updated: "0.2.0"
@doc since: "0.1.0"
defmacro convert_error(ma, p, f) do
quote do
ma = unquote(ma)
p = unquote(p)
case ma do
{:error, r} ->
match = if is_function(p), do: p.(r), else: r == p
if match do
f = unquote(f)
# convert to ok tuple with new value.
if is_function(f), do: f.(r), else: {:ok, f}
else
ma
end
{:ok, _v} ->
ma
:ok ->
ma
end
end
end
end