improvement!: message/1 instead of splode_message/1

improvement: add `use Splode.ErrorClass`
improvement: store the module that created an error in the `splode` key
This commit is contained in:
Zach Daniel 2024-03-18 15:52:45 -04:00
parent a59673a281
commit cd27664388
5 changed files with 141 additions and 60 deletions

View file

@ -17,29 +17,21 @@ end
# Error classes are splode errors with an `errors` key.
defmodule MyApp.Errors.Invalid do
use Splode.Error, fields: [:errors], class: :invalid
def splode_message(%{errors: errors}) do
Splode.ErrorClass.error_messages(errors)
end
use Splode.ErrorClass, class: :invalid
end
# You will want to define an unknown error class,
# otherwise splode will use its own
defmodule MyApp.Errors.Unknown do
use Splode.Error, fields: [:errors], class: :unknown
def splode_message(%{errors: errors}) do
Splode.ErrorClass.error_messages(errors)
end
use Splode.ErrorClass, class: :unknown
end
# This fallback exception will be used for unknown errors
defmodule MyApp.Errors.Unknown.Unknown do
use Splode.Error, fields: [:error], class: :unknown
use Splode.Error, class: :unknown
# your unknown message should have an `error` key
def splode_message(%{error: error}) do
def message(%{error: error}) do
if is_binary(error) do
to_string(error)
else
@ -53,7 +45,7 @@ end
defmodule MyApp.Errors.InvalidArgument do
use Splode.Error, fields: [:name, :message], class: :invalid
def splode_message(%{name: name, message: message}) do
def message(%{name: name, message: message}) do
"Invalid argument #{name}: #{message}"
end
end

View file

@ -139,10 +139,27 @@ defmodule Splode do
def splode_error?(_), do: false
def splode_error?(%struct{splode: splode}, splode) do
struct.splode_error?()
rescue
_ ->
false
end
def splode_error?(%struct{splode: nil}, _splode) do
struct.splode_error?()
rescue
_ ->
false
end
def splode_error?(_, _), do: false
@impl true
def to_class(value, opts \\ [])
def to_class(%struct{errors: [error]} = class, _opts) when struct in @class_modules do
def to_class(%struct{errors: [error]} = class, _opts)
when struct in @class_modules do
if error.class == :special do
error
else
@ -152,7 +169,7 @@ defmodule Splode do
def to_class(value, opts) when not is_list(value) do
if splode_error?(value) && value.class == :special do
value
Map.put(value, :splode, __MODULE__)
else
to_class([value], opts)
end
@ -174,14 +191,18 @@ defmodule Splode do
|> flatten_preserving_keywords()
|> Enum.uniq_by(&clear_stacktraces/1)
|> Enum.map(fn value ->
if splode_error?(value) do
value
if splode_error?(value, __MODULE__) do
Map.put(value, :splode, __MODULE__)
else
exception_opts =
if opts[:stacktrace] do
[error: value, stacktrace: %Splode.Stacktrace{stacktrace: opts[:stacktrace]}]
[
error: value,
stacktrace: %Splode.Stacktrace{stacktrace: opts[:stacktrace]},
splode: __MODULE__
]
else
[error: value]
[error: value, splode: __MODULE__]
end
@unknown_error.exception(exception_opts)
@ -189,11 +210,12 @@ defmodule Splode do
end)
|> choose_error()
|> accumulate_bread_crumbs(opts[:bread_crumbs])
|> Map.put(:splode, __MODULE__)
end
end
defp choose_error([]) do
@error_classes[:unknown].exception([])
@error_classes[:unknown].exception(splode: __MODULE__)
end
defp choose_error(errors) do
@ -211,7 +233,7 @@ defmodule Splode do
if parent_error_module == error.__struct__ do
%{error | errors: (error.errors || []) ++ other_errors}
else
parent_error_module.exception(errors: errors)
parent_error_module.exception(errors: errors, splode: __MODULE__)
end
end
@ -224,6 +246,7 @@ defmodule Splode do
|> Keyword.take([:error, :vars])
|> Keyword.put_new(:error, list[:message])
|> Keyword.put_new(:value, list)
|> Keyword.put(:splode, __MODULE__)
|> @unknown_error.exception()
|> add_stacktrace(opts[:stacktrace])
|> accumulate_bread_crumbs(opts[:bread_crumbs])
@ -239,7 +262,7 @@ defmodule Splode do
end
def to_error(error, opts) when is_binary(error) do
[error: error]
[error: error, splode: __MODULE__]
|> @unknown_error.exception()
|> Map.put(:stacktrace, nil)
|> add_stacktrace(opts[:stacktrace])
@ -248,20 +271,21 @@ defmodule Splode do
def to_error(other, opts) do
cond do
splode_error?(other) ->
splode_error?(other, __MODULE__) ->
other
|> Map.put(:splode, __MODULE__)
|> add_stacktrace(opts[:stacktrace])
|> accumulate_bread_crumbs(opts[:bread_crumbs])
is_exception(other) ->
[error: Exception.format(:error, other)]
[error: Exception.format(:error, other), splode: __MODULE__]
|> @unknown_error.exception()
|> Map.put(:stacktrace, nil)
|> add_stacktrace(opts[:stacktrace])
|> accumulate_bread_crumbs(opts[:bread_crumbs])
true ->
[error: "unknown error: #{inspect(other)}"]
[error: "unknown error: #{inspect(other)}", splode: __MODULE__]
|> @unknown_error.exception()
|> Map.put(:stacktrace, nil)
|> add_stacktrace(opts[:stacktrace])

View file

@ -8,7 +8,7 @@ defmodule Splode.Error do
defmodule MyApp.Errors.InvalidArgument do
use Splode.Error, fields: [:name, :message], class: :invalid
def splode_message(%{name: name, message: message}) do
def message(%{name: name, message: message}) do
"Invalid argument \#{name}: \#{message}"
end
end
@ -16,7 +16,7 @@ defmodule Splode.Error do
"""
@callback splode_error?() :: boolean()
@callback from_json(map) :: struct()
@callback splode_message(struct()) :: String.t()
@callback error_class?() :: boolean()
@type t :: Exception.t()
@doc false
@ -28,8 +28,9 @@ defmodule Splode.Error do
end
defmacro __using__(opts) do
quote generated: true, bind_quoted: [opts: opts] do
quote generated: true, bind_quoted: [opts: opts, mod: __MODULE__] do
@behaviour Splode.Error
@error_class !!opts[:error_class?]
if !opts[:class] do
raise "Must provide an error class for a splode error, i.e `use Splode.Error, class: :invalid`"
@ -37,6 +38,7 @@ defmodule Splode.Error do
defexception List.wrap(opts[:fields]) ++
[
splode: nil,
bread_crumbs: [],
vars: [],
path: [],
@ -44,43 +46,47 @@ defmodule Splode.Error do
class: opts[:class]
]
@before_compile mod
@impl Splode.Error
def splode_error?, do: true
@impl Exception
def message(%{vars: vars} = exception) do
string = splode_message(exception)
@impl Splode.Error
def error_class?, do: @error_class
string =
case Splode.ErrorClass.bread_crumb(exception.bread_crumbs) do
"" ->
string
context ->
context <> "\n" <> string
end
Enum.reduce(List.wrap(vars), string, fn {key, value}, acc ->
if String.contains?(acc, "%{#{key}}") do
String.replace(acc, "%{#{key}}", to_string(value))
else
acc
end
end)
end
def exception, do: exception([])
@impl Exception
def exception(opts) do
opts =
if is_nil(opts[:stacktrace]) do
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
if @error_class && match?([%error{class: :special} = special], opts[:errors]) do
special_error = Enum.at(opts[:errors], 0)
Keyword.put(opts, :stacktrace, %Splode.Stacktrace{stacktrace: stacktrace})
if special_error.__struct__.splode_error?() do
special_error
else
opts
end
opts =
if is_nil(opts[:stacktrace]) do
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
super(opts) |> Map.update(:vars, [], &Splode.Error.clean_vars/1)
Keyword.put(opts, :stacktrace, %Splode.Stacktrace{stacktrace: stacktrace})
else
opts
end
super(opts) |> Map.update(:vars, [], &Splode.Error.clean_vars/1)
end
else
opts =
if is_nil(opts[:stacktrace]) do
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
Keyword.put(opts, :stacktrace, %Splode.Stacktrace{stacktrace: stacktrace})
else
opts
end
super(opts) |> Map.update(:vars, [], &Splode.Error.clean_vars/1)
end
end
@impl Splode.Error
@ -97,6 +103,36 @@ defmodule Splode.Error do
end
end
defmacro __before_compile__(env) do
if Module.defines?(env.module, {:message, 1}, :def) do
quote generated: true do
defoverridable message: 1
@impl true
def message(%{vars: vars} = exception) do
string = super(exception)
string =
case Splode.ErrorClass.bread_crumb(exception.bread_crumbs) do
"" ->
string
context ->
context <> "\n" <> string
end
Enum.reduce(List.wrap(vars), string, fn {key, value}, acc ->
if String.contains?(acc, "%{#{key}}") do
String.replace(acc, "%{#{key}}", to_string(value))
else
acc
end
end)
end
end
end
end
@doc false
def clean_vars(vars) when is_map(vars) do
clean_vars(Map.to_list(vars))

View file

@ -1,6 +1,38 @@
defmodule Splode.ErrorClass do
@moduledoc "Tools for working with error classes"
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
opts =
Keyword.update(opts, :fields, [errors: []], fn fields ->
has_error_fields? =
Enum.any?(fields, fn
:errors ->
true
{:errors, _} ->
true
_ ->
false
end)
if has_error_fields? do
fields
else
fields ++ [errors: []]
end
end)
|> Keyword.put(:error_class?, true)
use Splode.Error, opts
def message(%{errors: errors}) do
Splode.ErrorClass.error_messages(errors)
end
end
end
@doc "Creates a long form composite error message for a list of errors"
def error_messages(errors, opts \\ []) do
custom_message = opts[:custom_message]

View file

@ -1,11 +1,8 @@
defmodule Splode.Error.Unknown do
@moduledoc "The default top level unknown error container"
use Splode.Error, fields: [:errors], class: :unknown
def splode_message(exception) do
Splode.ErrorClass.error_messages(exception.errors)
end
use Splode.ErrorClass, class: :unknown
@impl true
def exception(opts) do
if opts[:error] do
super(Keyword.update(opts, :errors, [opts[:error]], &[opts[:error] | &1]))