defmodule Rustic.Result do
@moduledoc """
Documentation for `RusticResult`.
"""
defmodule UnhandledError do
@moduledoc "Error raised when trying to unwrap an Err result"
defexception [:reason]
@doc "Convert error to string"
@spec message(%__MODULE__{}) :: String.t()
def message(e) do
"Expected an Ok result, \"#{inspect(e.reason)}\" given."
end
end
defmodule MissingError do
@moduledoc "Error raised when trying to unwrap an Ok value"
defexception [:value]
@doc "Convert error to string"
@spec message(%__MODULE__{}) :: String.t()
def message(e) do
"Expected an Err result, \"#{inspect(e.value)}\" given."
end
end
@typedoc "Describe an Ok value"
@type ok :: :ok | {:ok, any()}
@typedoc "Describe an Err value"
@type err :: {:error, term()}
@typedoc "Describe a Result type"
@type t :: ok() | err()
@typedoc "A function that maps a value to a result"
@type f :: (any() -> t())
@doc "Wraps a value into an Ok result"
@spec ok(any()) :: ok()
def ok(v), do: {:ok, v}
@doc "Wraps a value into an Err result"
@spec err(term()) :: err()
def err(reason), do: {:error, reason}
@doc "Returns true if the Result is an Ok value"
@spec is_ok?(t()) :: boolean()
def is_ok?(:ok), do: true
def is_ok?({:ok, _}), do: true
def is_ok?({:error, _}), do: false
@doc "Returns true if the Result is an Err value"
@spec is_err?(t()) :: boolean()
def is_err?(:ok), do: false
def is_err?({:ok, _}), do: false
def is_err?({:error, _}), do: true
@doc "Is valid if and only if an Ok result is supplied"
defguard is_ok_result(val) when
val == :ok
or (is_tuple(val) and elem(val, 0) == :ok)
@doc "Is valid if and only if an Err result is supplied"
defguard is_err_result(val) when
is_tuple(val) and elem(val, 0) == :error
@doc "Unwrap an Ok result, or raise an exception"
@spec unwrap!(t()) :: any()
def unwrap!(:ok), do: nil
def unwrap!({:ok, val}), do: val
def unwrap!({:error, reason}), do: raise UnhandledError, reason: reason
@doc "Unwrap an Err result, or raise an exception"
@spec unwrap_err!(t()) :: term()
def unwrap_err!(:ok), do: raise MissingError, value: nil
def unwrap_err!({:ok, val}), do: raise MissingError, value: val
def unwrap_err!({:error, reason}), do: reason
@doc "Unwrap an Ok result, or return a default value"
@spec unwrap_or(t(), any()) :: any()
def unwrap_or(:ok, _default), do: nil
def unwrap_or({:ok, val}, _default), do: val
def unwrap_or({:error, _reason}, default), do: default
@doc """
Apply a function to the value contained in an Ok result, or propagates the
error.
"""
@spec map(t(), (any() -> any())) :: t()
def map(:ok, func), do: {:ok, func.(nil)}
def map({:ok, val}, func), do: {:ok, func.(val)}
def map(err, _func), do: err
@doc """
Apply a function to the value contained in an Err result, or propagates the
Ok result.
"""
@spec map_err(t(), (any() -> any())) :: t()
def map_err(:ok, _func), do: :ok
def map_err({:ok, val}, _func), do: {:ok, val}
def map_err({:error, reason}, func), do: {:error, func.(reason)}
@doc """
Apply a function which returns a result to an Ok result, or propagates the
error.
"""
@spec and_then(t(), f()) :: t()
def and_then(:ok, func), do: func.(nil)
def and_then({:ok, val}, func), do: func.(val)
def and_then(err, _func), do: err
@doc """
Apply a function which returns a result to an Err result, or propagates the
Ok value.
"""
@spec or_else(t(), f()) :: t()
def or_else({:error, reason}, func), do: func.(reason)
def or_else(ok, _func), do: ok
@doc """
Flatten a result containing another result.
"""
@spec flatten(t()) :: t()
def flatten(:ok), do: :ok
def flatten({:ok, :ok}), do: :ok
def flatten({:ok, {:ok, val}}), do: {:ok, val}
def flatten({:ok, {:error, reason}}), do: {:error, reason}
def flatten({:ok, val}), do: {:ok, val}
def flatten({:error, :ok}), do: :ok
def flatten({:error, {:ok, val}}), do: {:ok, val}
def flatten({:error, {:error, reason}}), do: {:error, reason}
def flatten({:error, reason}), do: {:error, reason}
@doc """
Iterate over Results, will fail at the first Error result.
"""
@spec collect(Enumerable.t(t())) :: t()
def collect(enumerable) do
enumerable |> Enum.map(&unwrap!/1) |> ok()
rescue
err in UnhandledError ->
err(err.reason)
end
@doc """
Iterate over Results, will ignore failed items.
"""
@spec filter_collect(Enumerable.t(t())) :: ok()
def filter_collect(enumerable) do
enumerable |> Enum.filter(&is_ok?/1) |> collect()
end
@doc """
Iterate over Results, returns a tuple of:
- Ok result containing the list of Ok values
- Err result containing the list of Err reasons
"""
@spec partition_collect(Enumerable.t(t())) :: {ok(), err()}
def partition_collect(enumerable) do
{
enumerable |> filter_collect(),
enumerable |> Enum.filter(&is_err?/1) |> Enum.map(&unwrap_err!/1) |> err()
}
end
end