2020-05-01 18:21:46 +12:00
|
|
|
defmodule Ash.Error do
|
2020-06-02 17:47:25 +12:00
|
|
|
@moduledoc false
|
2020-05-01 18:21:46 +12:00
|
|
|
@type error_class() :: :invalid | :authorization | :framework | :unknown
|
|
|
|
|
|
|
|
# We use these error classes also to choose a single error
|
|
|
|
# to raise when multiple errors have occured. We raise them
|
|
|
|
# sorted by their error classes
|
|
|
|
@error_classes [
|
|
|
|
:forbidden,
|
|
|
|
:invalid,
|
|
|
|
:framework,
|
|
|
|
:unknown
|
|
|
|
]
|
|
|
|
|
2020-06-04 18:21:20 +12:00
|
|
|
alias Ash.Error.{Forbidden, Framework, Invalid, Unknown}
|
|
|
|
|
2020-05-01 18:21:46 +12:00
|
|
|
@error_modules [
|
2020-06-04 18:21:20 +12:00
|
|
|
forbidden: Forbidden,
|
|
|
|
invalid: Invalid,
|
|
|
|
framework: Framework,
|
|
|
|
unknown: Unknown
|
2020-05-01 18:21:46 +12:00
|
|
|
]
|
|
|
|
|
|
|
|
@error_class_indices @error_classes |> Enum.with_index() |> Enum.into(%{})
|
|
|
|
|
2020-08-28 10:35:31 +12:00
|
|
|
defmodule Stacktrace do
|
2020-08-28 13:14:19 +12:00
|
|
|
@moduledoc "A placeholder for a stacktrace so that we can avoid printing it everywhere"
|
2020-08-28 10:35:31 +12:00
|
|
|
defstruct [:stacktrace]
|
|
|
|
|
|
|
|
defimpl Inspect do
|
|
|
|
def inspect(_, _) do
|
|
|
|
"#Stacktrace<>"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-06-04 18:16:41 +12:00
|
|
|
def ash_error?(value) do
|
|
|
|
!!impl_for(value)
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_ash_error(values) when is_list(values) do
|
|
|
|
values =
|
2020-10-21 17:52:47 +13:00
|
|
|
values
|
|
|
|
|> Enum.uniq_by(&clear_stacktraces/1)
|
|
|
|
|> Enum.map(fn value ->
|
2020-06-04 18:16:41 +12:00
|
|
|
if ash_error?(value) do
|
|
|
|
value
|
|
|
|
else
|
2020-06-04 18:21:20 +12:00
|
|
|
Unknown.exception(error: values)
|
2020-06-04 18:16:41 +12:00
|
|
|
end
|
|
|
|
end)
|
2020-10-21 17:52:47 +13:00
|
|
|
|> Enum.uniq()
|
2020-06-04 18:16:41 +12:00
|
|
|
|
2020-06-04 18:21:20 +12:00
|
|
|
choose_error(values)
|
2020-06-04 18:16:41 +12:00
|
|
|
end
|
|
|
|
|
2020-10-21 17:57:27 +13:00
|
|
|
def to_ash_error(value) do
|
|
|
|
if ash_error?(value) do
|
|
|
|
value
|
|
|
|
else
|
|
|
|
to_ash_error([value])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-10-28 18:14:17 +13:00
|
|
|
def clear_stacktraces(%{stacktrace: stacktrace} = error) when not is_nil(stacktrace) do
|
2020-10-21 17:52:47 +13:00
|
|
|
clear_stacktraces(%{error | stacktrace: nil})
|
|
|
|
end
|
|
|
|
|
2020-10-28 18:14:17 +13:00
|
|
|
def clear_stacktraces(%{errors: errors} = exception) when is_list(errors) do
|
|
|
|
%{exception | errors: Enum.map(errors, &clear_stacktraces/1)}
|
2020-10-21 17:52:47 +13:00
|
|
|
end
|
|
|
|
|
2020-10-28 18:14:17 +13:00
|
|
|
def clear_stacktraces(error), do: error
|
2020-10-21 17:52:47 +13:00
|
|
|
|
2020-05-01 18:21:46 +12:00
|
|
|
def choose_error(errors) do
|
2020-06-19 14:59:30 +12:00
|
|
|
[error | other_errors] =
|
|
|
|
Enum.sort_by(errors, fn error ->
|
|
|
|
# the second element here sorts errors that are already parent errors
|
|
|
|
{Map.get(@error_class_indices, error.class),
|
|
|
|
@error_modules[error.class] != error.__struct__}
|
|
|
|
end)
|
2020-05-01 18:21:46 +12:00
|
|
|
|
|
|
|
parent_error_module = @error_modules[error.class]
|
|
|
|
|
2020-06-19 14:59:30 +12:00
|
|
|
if parent_error_module == error.__struct__ do
|
2020-06-19 15:43:06 +12:00
|
|
|
%{error | errors: (error.errors || []) ++ other_errors}
|
2020-06-19 14:59:30 +12:00
|
|
|
else
|
|
|
|
parent_error_module.exception(errors: errors)
|
|
|
|
end
|
2020-05-01 18:21:46 +12:00
|
|
|
end
|
|
|
|
|
2020-08-28 10:35:31 +12:00
|
|
|
def error_messages(errors, custom_message \\ nil, stacktraces? \\ false) do
|
2020-06-19 15:53:46 +12:00
|
|
|
generic_message =
|
|
|
|
errors
|
2020-06-22 15:26:47 +12:00
|
|
|
|> List.wrap()
|
2020-06-19 15:53:46 +12:00
|
|
|
|> Enum.group_by(& &1.class)
|
|
|
|
|> Enum.sort_by(fn {group, _} -> Map.get(@error_class_indices, group) end)
|
|
|
|
|> Enum.map_join("\n\n", fn {class, class_errors} ->
|
|
|
|
header = header(class) <> "\n\n"
|
|
|
|
|
2020-08-28 10:35:31 +12:00
|
|
|
if stacktraces? do
|
|
|
|
header <>
|
|
|
|
Enum.map_join(class_errors, "\n", fn
|
|
|
|
%{stacktrace: %Stacktrace{stacktrace: stacktrace}} = class_error ->
|
|
|
|
"* #{Exception.message(class_error)}\n" <>
|
|
|
|
Enum.map_join(stacktrace, "\n", fn stack_item ->
|
|
|
|
" " <> Exception.format_stacktrace_entry(stack_item)
|
|
|
|
end)
|
|
|
|
end)
|
|
|
|
else
|
|
|
|
header <>
|
|
|
|
Enum.map_join(class_errors, "\n", fn class_error ->
|
|
|
|
"* #{Exception.message(class_error)}"
|
|
|
|
end)
|
|
|
|
end
|
2020-06-19 15:53:46 +12:00
|
|
|
end)
|
2020-05-01 18:21:46 +12:00
|
|
|
|
2020-06-19 15:53:46 +12:00
|
|
|
if custom_message do
|
|
|
|
custom =
|
|
|
|
custom_message
|
|
|
|
|> List.wrap()
|
|
|
|
|> Enum.map_join("\n", &"* #{&1}")
|
|
|
|
|
|
|
|
"\n\n" <> custom <> generic_message
|
|
|
|
else
|
|
|
|
generic_message
|
|
|
|
end
|
2020-05-01 18:21:46 +12:00
|
|
|
end
|
|
|
|
|
|
|
|
def error_descriptions(errors) do
|
|
|
|
errors
|
|
|
|
|> Enum.group_by(& &1.class)
|
|
|
|
|> Enum.sort_by(fn {group, _} -> Map.get(@error_class_indices, group) end)
|
|
|
|
|> Enum.map_join("\n\n", fn {class, class_errors} ->
|
|
|
|
header = header(class) <> "\n\n"
|
|
|
|
|
2020-07-15 17:38:42 +12:00
|
|
|
header <> Enum.map_join(class_errors, "\n", &"* #{Ash.Error.message(&1)}")
|
2020-05-01 18:21:46 +12:00
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp header(:invalid), do: "Input Invalid"
|
|
|
|
defp header(:forbidden), do: "Forbidden"
|
|
|
|
defp header(:framework), do: "Framework Error"
|
|
|
|
defp header(:unknown), do: "Unknown Error"
|
|
|
|
|
|
|
|
defmacro __using__(_) do
|
|
|
|
quote do
|
|
|
|
import Ash.Error, only: [def_ash_error: 1, def_ash_error: 2]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defmacro def_ash_error(fields, opts \\ []) do
|
|
|
|
quote do
|
2020-08-28 10:35:31 +12:00
|
|
|
defexception unquote(fields) ++ [path: [], stacktrace: [], class: unquote(opts)[:class]]
|
2020-05-01 18:21:46 +12:00
|
|
|
|
|
|
|
@impl Exception
|
2020-10-21 06:11:21 +13:00
|
|
|
def message(%{message: {string, replacements}} = exception) do
|
|
|
|
string =
|
|
|
|
Enum.reduce(replacements, string, fn {key, value}, acc ->
|
|
|
|
String.replace(acc, "%{#{key}}", to_string(value))
|
|
|
|
end)
|
|
|
|
|
|
|
|
Ash.Error.message(%{exception | message: string})
|
|
|
|
end
|
|
|
|
|
|
|
|
def message(exception), do: Ash.Error.message(exception)
|
2020-08-28 10:35:31 +12:00
|
|
|
|
|
|
|
def exception(opts) do
|
|
|
|
case Process.info(self(), :current_stacktrace) do
|
|
|
|
{:current_stacktrace, [_, _ | stacktrace]} ->
|
|
|
|
super(
|
|
|
|
Keyword.put_new(opts, :stacktrace, %Ash.Error.Stacktrace{stacktrace: stacktrace})
|
|
|
|
)
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
super(opts)
|
|
|
|
end
|
|
|
|
end
|
2020-05-01 18:21:46 +12:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defdelegate id(error), to: Ash.ErrorKind
|
|
|
|
defdelegate code(error), to: Ash.ErrorKind
|
|
|
|
defdelegate message(error), to: Ash.ErrorKind
|
|
|
|
defdelegate impl_for(error), to: Ash.ErrorKind
|
|
|
|
end
|
|
|
|
|
|
|
|
defprotocol Ash.ErrorKind do
|
2020-06-03 15:51:39 +12:00
|
|
|
@moduledoc false
|
|
|
|
|
2020-05-01 18:21:46 +12:00
|
|
|
@spec id(t()) :: String.t()
|
|
|
|
def id(error)
|
|
|
|
|
|
|
|
@spec code(t()) :: String.t()
|
|
|
|
def code(error)
|
|
|
|
|
|
|
|
@spec message(t()) :: String.t()
|
|
|
|
def message(error)
|
|
|
|
end
|