Current section
Files
Jump to
Current section
Files
lib/harnais/error/error.ex
defmodule Harnais.Error do
@moduledoc ~S"""
The Exception for the Harnais Package Family.
Many of the functions in the Harnais packages return
either `{:ok, any}` or `{error, error}` where `error` will be an
`Exception`.
Many of the errors will be instances of `Harnais.Error` `struct`.
## Exporting the Exception
As well as supporting the usual `Exception` callbacks, the package
support "exporting" the exception using `Harnais.Error.export/1`.
See the exception fields `:export_function` and `:export_config` for
how to contol what the export does.
## Exception State
The exception `struct` has a number of fields
| Field | Aliases |
| :---------------- | -------------------: |
| `:message` | *:m, :msg* |
| `:reason` | *:r* |
| `:type` | *:t* |
| `:location` | *:l, :loc, :ndx, :index, :i* |
| `:value0` | *:v, :v0, :value, :e, :error* |
| `:value1` | *:v1* |
| `:value2` | *:v2* |
| `:message_function` | |
| `:message_config` | |
| `:export_function` | |
| `:export_config` | |
### Exception Field: `:message`
The explanatory message, normally a string.
### Exception Field: `:reason`
The reason for the error; normally an `Atom`.
### Exception Field: `:type`
An aribtrary term defining the type of the error e.g. `:arg`, `:key`, `:value`, etc
### Exception Field: `:location`
An aribtrary term defining where the error happened.
For example this could be a key, index in a list, whatever.
### Exception Field: `:value0`, `:value1`, `:value2`
Arbitrary terms identifying the cause of the error.
Three fields are useful when, for example, `:value0` holds the name of
a key and `:value1` and `:value2` hold the values of the key that
were expected to be equal.
Sometimes `:value0` holds an instance of `Harnais.Error.Status`.
### Exception Field: `:message_function`
The module implements its own `Exception.message/1` callback but
this can be overridden by this field.
If supplied, it must be an arity 1 function that is passed the
`struct` and must return a string.
### Exception Field: `:message_config`
This field is used by the default `Exception.message/1` formatter to
hold the fields to be included in the message. It can be overridden
to set the wanted fields.
If an explicit `:message_function` function is supplied, this field
can also be used to hold configuration for it.
### Exception Field: `:export_function`
The exception can be "exported" using `Harnais.Error.export/1` which
is passed the exception struct.
The default exporter creates a `Keyword` of selected fields where
the keys are the *shortest* alias for the field (e.g. `:m` for
`:message`).
A custom export function can be provided using this field.
### Exception Field: `:export_config`
The default exporter using this field to hold the fields to be
included in the export.
If a custom export function is provided, this field can be used to
provide configuration for it.
## Standard API
Unless otherwise specified, functions return either `{:ok, status}`
or `{:error, error}` when `error` will be an Exception..
Many functions have peer *bang* functions that returns either the `value`
in `{:ok, value}` or raise the `error` in `{:error, error}`.
"""
require Logger
require Plymio.Codi, as: CODI
require Plymio.Fontais.Option
alias Harnais.Utility, as: HUU
alias Harnais.Error.Status, as: HES
use Plymio.Fontais.Attribute
use Plymio.Codi.Attribute
use Harnais.Attribute
use Harnais.Error.Attribute
@codi_opts [
{@plymio_codi_key_vekil, Plymio.Vekil.Codi.__vekil__()}
]
import Plymio.Fontais.Guard,
only: [
is_value_set: 1,
is_value_unset: 1
]
import Plymio.Fontais.Utility,
only: [
list_wrap_flat_just: 1
]
import Plymio.Fontais.Option,
only: [
opts_create_aliases_dict: 1,
opts_canonical_keys: 2,
canonical_key: 2
]
import Plymio.Fontais.Error.Utility,
only: [
reduce_errors_default_function: 1
]
@harnais_error_kvs_aliases [
@harnais_error_field_alias_message,
@harnais_error_field_alias_reason,
@harnais_error_field_alias_type,
@harnais_error_field_alias_location,
@harnais_error_field_alias_value0,
@harnais_error_field_alias_value1,
@harnais_error_field_alias_value2,
@harnais_error_field_alias_message_function,
@harnais_error_field_alias_export_function,
@harnais_error_field_alias_message_config,
@harnais_error_field_alias_export_config
]
@harnais_error_dict_aliases @harnais_error_kvs_aliases
|> opts_create_aliases_dict
@harnais_error_kvs_short_aliases @harnais_error_kvs_aliases
|> Enum.map(fn {canon_key, alias_keys} ->
alias_keys = alias_keys |> list_wrap_flat_just
all_keys = [canon_key | alias_keys]
alias_shortest =
[canon_key | alias_keys]
|> Enum.sort_by(fn key ->
key |> to_string |> String.length()
end)
|> List.first()
{alias_shortest, all_keys}
end)
@harnais_error_dict_short_aliases @harnais_error_kvs_short_aliases
|> opts_create_aliases_dict
@doc false
def update_canonical_opts(opts, dict \\ @harnais_error_dict_aliases) do
opts |> opts_canonical_keys(dict)
end
@doc false
def opts_short_keys(opts, dict \\ @harnais_error_dict_short_aliases) do
opts |> opts_canonical_keys(dict)
end
@doc false
def short_key(key, dict \\ @harnais_error_dict_short_aliases) do
key |> canonical_key(dict)
end
@harnais_error_defstruct @harnais_error_kvs_aliases
|> Enum.map(fn {k, _v} -> {k, @plymio_fontais_the_unset_value} end)
|> Kernel.++([
{@harnais_error_field_message_config,
@harnais_error_default_message_config},
{@harnais_error_field_export_config,
@harnais_error_default_export_config}
])
|> Keyword.new()
defexception @harnais_error_defstruct
@type t :: %__MODULE__{}
@type opts :: Harnais.opts()
@type kv :: {any, any}
@type error :: Harnais.error()
@doc_new ~S"""
`new/1` creates an instance of the error module's struct `t`.
If the `opts` are not empty, it calls `update/2` with `t` and the `opts`.
Either `{:ok, t}` or `{:error, error}` is returned where `error` is
an exception generated during the creation.
## Examples
iex> {:ok, t} = new()
...> match?(%Harnais.Error{}, t)
true
iex> {:ok, t} = new(m: "failed again", r: :usual_cause, v: 42)
...> t |> Exception.message
"failed again, reason=:usual_cause, got: 42"
"""
[
# update the doc, etc proxies
{@plymio_codi_pattern_proxy_put,
[
state_def_new_since: quote(do: @since("0.1.0")),
state_def_new_since!: quote(do: @since("0.1.0")),
state_def_update_since: quote(do: @since("0.1.0")),
state_def_update_since!: quote(do: @since("0.1.0")),
state_def_new_doc: quote(do: @doc(unquote(@doc_new))),
state_def_update_doc:
quote do
@doc ~S"""
`update/2` takes an `instance` of the module's `struct` and an optional *opts*.
The *opts* are normalised by calling the module's `update_canonical_opts/1`
and then reduced with `update_field/2`:
opts |> Enum.reduce(instance, fn {k,v}, s -> s |> update_field({k,v}) end)
`{:ok, instance}` is returned.
"""
end,
defexception_def_message_doc:
quote do
@doc ~S"""
`message/1` is the standard `Exception` callback.
"""
end,
defexception_def_message_since: quote(do: @since("0.1.0")),
defexception_def_message_header: quote(do: def(message(t))),
defexception_def_message_spec: quote(do: @spec(message(t) :: String.t())),
# this is set to nil in fontais v0.2.0 so function is visible
defexception_def_format_error_message_doc: :doc_false,
defexception_def_format_error_message_since: quote(do: @since("0.1.0")),
# this is set to nil in fontais v0.2.0 so function is visible
defexception_def_new_result_doc: :doc_false,
defexception_def_new_result_since: quote(do: @since("0.1.0"))
]},
{@plymio_codi_pattern_proxy_put,
[
state_defp_update_field_passthru:
quote do
defp update_field(%__MODULE__{} = state, {k, v}) do
state
|> Map.has_key?(k)
|> case do
true ->
{:ok, state |> struct!([{k, v}])}
_ ->
new_error_result(m: "field invalid", v: k)
end
end
end
]},
{@plymio_codi_pattern_proxy_fetch,
[
:defexception_redtape,
:defexception_def_new_result,
:doc_false,
:defexception_def_new_error_result_defdelegate_new_result,
:defexception_def_message_doc,
:defexception_def_message_since,
:defexception_def_message_spec,
:defexception_def_message_header,
:defexception_def_message_clause_user_transform,
:defexception_def_message_clause_default,
:defexception_def_format_error_message,
:state_base_package,
:state_defp_update_field_header
]},
{@plymio_codi_pattern_proxy_fetch,
[
{@plymio_codi_key_proxy_name, :state_defp_update_proxy_field_fun1},
{@plymio_fontais_key_forms_edit,
[{@plymio_fontais_key_rename_atoms, [proxy_field: @harnais_error_field_message_function]}]}
]},
{@plymio_codi_pattern_proxy_fetch,
[
{@plymio_codi_key_proxy_name, :state_defp_update_proxy_field_fun1},
{@plymio_fontais_key_forms_edit,
[{@plymio_fontais_key_rename_atoms, [proxy_field: @harnais_error_field_export_function]}]}
]},
{@plymio_codi_pattern_proxy_fetch, [:state_defp_update_field_passthru]}
]
|> CODI.reify_codi(@codi_opts)
@doc false
@since "0.1.0"
def new_errors_result(errors) do
with {:ok, error} <- errors |> reduce_errors_default_function do
{:error, error}
else
{:error, %{__exception__: true}} = result -> result
end
end
@since "0.1.0"
def exception(opts \\ [])
def exception(%__MODULE__{} = error) do
error
end
def exception(%HES{} = status) do
exception(value: status, message: "status error", reason: :status)
end
def exception(opts) when is_list(opts) do
opts
|> new!
end
@doc false
@since "0.1.0"
def new_from_status(opts \\ [])
def new_from_status(%HES{} = status) do
{:ok, %__MODULE__{@harnais_error_field_value0 => status}}
end
def new_from_status(opts) when is_list(opts) do
with {:ok, %HES{} = status} <- opts |> HES.new() do
{:ok, %__MODULE__{@harnais_error_field_value0 => status}}
else
{:error, %{__exception__: true}} = result -> result
end
end
@doc false
def new_error_result_from_status(value) do
with {:ok, error} <- value |> new_from_status do
{:error, error}
else
{:error, %{__exception__: true}} = result -> result
end
end
@doc false
@since "0.1.0"
def normalise_result(value)
def normalise_result({:ok, _} = result) do
result
end
def normalise_result({:error, %HES{} = status}) do
{:error, exception(status)}
end
def normalise_result(%HES{} = status) do
case status |> HES.has_errors?() do
# return an exception
true ->
{:error, exception(status)}
# if no errors, return the last ok result
_ ->
status |> HES.get_last_ok_result()
end
end
def normalise_result({:error, errors}) when is_list(errors) do
with {:ok, %HES{} = status} <- [add_errors: errors] |> HES.new() do
{:error, exception(status)}
else
{:error, %{__exception__: true}} = result -> result
end
end
def normalise_result(errors) when is_list(errors) do
{:error, errors} |> normalise_result
end
def normalise_result({:error, %__MODULE__{}} = r) do
r
end
def normalise_result(%__MODULE__{} = error) do
{:error, error}
end
@doc false
@since "0.1.0"
# this is used in doctests
def harnais_error_export_result(value)
def harnais_error_export_result(%__MODULE__{} = error) do
error |> export
end
def harnais_error_export_result(%HES{} = status) do
status |> HES.harnais_status_export_result()
end
def harnais_error_export_result(
{:error, %__MODULE__{@harnais_error_field_value0 => %HES{} = status}}
) do
status |> HES.harnais_status_export_result()
end
def harnais_error_export_result({:error, %__MODULE__{} = error}) do
error |> harnais_error_export_result
end
def harnais_error_export_result(%__MODULE__{@harnais_error_field_value0 => %HES{} = status}) do
status |> HES.harnais_status_export_result()
end
defp default_export_function(result)
defp default_export_function(
%__MODULE__{@harnais_error_field_export_config => format_order} = state
) do
with export <- state |> Map.from_struct(),
export <- export |> Map.take(format_order),
export <- export |> Map.to_list(),
export <- export |> HUU.opts_sort_keys(format_order),
export <- export |> Enum.filter(fn {_k, v} -> v |> is_value_set end),
{:ok, export} <- export |> opts_short_keys,
true <- true do
{:ok, [error: [export]]}
else
{:error, %{__exception__: true}} = result -> result
end
end
@doc ~S"""
`export/1` takes an instance of the module's `struct` and exports it.
A custom arity one export function can be given in the field `:export_function`.
Otherwise the default export function is used which creates `Keyword`
with one key (`:error`) whose value is a list, currently with only
one entry which is itself a `Keyword` of the set fields in the
*error* where the keys are the short form of the full field name (e.g. `:m` for `:message`)
If the export works, `{:ok, export}` is returned.
## Examples
iex> {:ok, error} = [message: "something broke", type: :arg, value: 42] |> new
...> error |> export
{:ok, [error: [[m: "something broke", t: :arg, v: 42]]]}
iex> export_fun = fn error -> {:ok, error.value} end
...> {:ok, error} = [export_function: export_fun, message: "something broke", type: :arg, value: 42] |> new
...> error |> export
{:ok, 42}
"""
@since "0.1.0"
@spec export(t) :: {:ok, any} | {:error, error}
def export(t)
def export(%__MODULE__{@harnais_error_field_export_function => fun_export} = error) do
fun_export
|> case do
fun when is_value_set(fun) ->
error |> transform(fun)
_ ->
error |> transform(&default_export_function/1)
end
end
@doc false
@since "0.1.0"
def transform(t, transform)
def transform(%__MODULE__{} = error, fun_transform)
when is_function(fun_transform, 1) do
error
|> fun_transform.()
end
end
defimpl Inspect, for: Harnais.Error do
use Harnais.Attribute
use Harnais.Error.Attribute
import Plymio.Fontais.Guard,
only: [
is_value_unset: 1
]
def inspect(
%Harnais.Error{
@harnais_error_field_message => message,
@harnais_error_field_reason => reason,
@harnais_error_field_type => type,
@harnais_error_field_location => location,
@harnais_error_field_value0 => value0,
@harnais_error_field_value1 => value1,
@harnais_error_field_value2 => value2
},
_opts
) do
message_telltale =
message
|> case do
x when is_value_unset(x) -> nil
_x -> "M=+"
end
reason_telltale =
reason
|> case do
x when is_value_unset(x) -> nil
x -> "R=#{inspect(x)}"
end
type_telltale =
type
|> case do
x when is_value_unset(x) -> nil
x -> "T=#{inspect(x)}"
end
location_telltale =
location
|> case do
x when is_value_unset(x) -> nil
x -> "L=#{inspect(x)}"
end
value0_telltale =
value0
|> case do
x when is_value_unset(x) -> nil
x -> "V0=#{inspect(x)}"
end
value1_telltale =
value1
|> case do
x when is_value_unset(x) -> nil
x -> "V1=#{inspect(x)}"
end
value2_telltale =
value2
|> case do
x when is_value_unset(x) -> nil
x -> "V2=#{inspect(x)}"
end
error_telltale =
[
message_telltale,
reason_telltale,
type_telltale,
location_telltale,
value0_telltale,
value1_telltale,
value2_telltale
]
|> Enum.reject(&is_nil/1)
|> Enum.join("; ")
"HEE(#{error_telltale})"
end
end