Packages
ash
2.14.7
3.29.3
3.29.2
3.29.1
3.29.0
3.28.0
3.27.8
3.27.7
3.27.6
3.27.5
3.27.4
3.27.3
3.27.2
3.27.1
3.27.0
3.26.0
3.25.2
3.25.1
3.25.0
retired
3.24.7
3.24.6
3.24.5
3.24.4
3.24.3
3.24.2
3.24.1
3.24.0
3.23.1
3.23.0
3.22.2
3.22.1
3.22.0
3.21.3
3.21.2
3.21.1
3.21.0
3.20.0
3.19.3
3.19.2
3.19.1
3.19.0
3.18.0
3.17.1
3.17.0
3.16.0
3.15.0
3.14.1
3.14.0
retired
3.13.2
3.13.1
3.13.0
3.12.0
3.11.3
3.11.2
3.11.1
3.11.0
3.10.1
3.10.0
3.9.0
3.8.0
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
retired
3.6.3
retired
3.6.2
3.6.1
3.6.0
3.5.43
3.5.42
3.5.41
3.5.40
3.5.39
3.5.38
3.5.37
3.5.36
3.5.35
3.5.34
3.5.33
3.5.32
3.5.31
3.5.30
3.5.29
3.5.28
3.5.27
3.5.26
3.5.25
3.5.24
3.5.23
3.5.22
3.5.21
3.5.20
3.5.19
3.5.18
3.5.17
3.5.16
3.5.15
3.5.14
3.5.13
3.5.12
3.5.11
3.5.10
3.5.9
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.4.74
retired
3.4.73
3.4.72
3.4.71
3.4.70
3.4.69
3.4.68
3.4.67
3.4.66
3.4.65
3.4.64
3.4.63
3.4.62
3.4.61
3.4.60
3.4.59
3.4.58
3.4.57
3.4.56
3.4.55
3.4.54
3.4.53
3.4.52
3.4.51
3.4.50
3.4.49
3.4.48
3.4.47
3.4.46
3.4.45
3.4.44
3.4.43
3.4.42
3.4.41
3.4.40
3.4.39
3.4.38
3.4.37
3.4.36
3.4.35
3.4.34
3.4.33
3.4.32
3.4.31
3.4.30
3.4.29
3.4.28
3.4.27
3.4.26
3.4.25
3.4.24
3.4.23
3.4.22
3.4.21
3.4.20
3.4.19
3.4.18
3.4.17
3.4.16
3.4.15
3.4.14
3.4.13
3.4.12
3.4.11
3.4.10
3.4.9
3.4.8
3.4.7
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.3
3.3.2
3.3.1
3.3.0
3.2.6
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.16
3.0.15
3.0.14
3.0.13
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.46
3.0.0-rc.45
3.0.0-rc.44
3.0.0-rc.43
3.0.0-rc.42
3.0.0-rc.41
3.0.0-rc.40
3.0.0-rc.39
3.0.0-rc.38
3.0.0-rc.37
3.0.0-rc.36
3.0.0-rc.35
3.0.0-rc.34
3.0.0-rc.33
3.0.0-rc.32
3.0.0-rc.31
3.0.0-rc.29
3.0.0-rc.28
3.0.0-rc.27
3.0.0-rc.26
3.0.0-rc.25
3.0.0-rc.24
3.0.0-rc.23
3.0.0-rc.22
3.0.0-rc.21
3.0.0-rc.20
3.0.0-rc.19
3.0.0-rc.18
3.0.0-rc.17
3.0.0-rc.16
3.0.0-rc.15
3.0.0-rc.14
3.0.0-rc.13
3.0.0-rc.12
3.0.0-rc.11
3.0.0-rc.10
3.0.0-rc.9
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.1
3.0.0-rc.0
2.21.15
2.21.14
2.21.13
2.21.12
2.21.11
2.21.10
2.21.9
2.21.8
2.21.7
2.21.6
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.3
2.20.2
2.20.1
2.20.0
2.19.14
2.19.13
2.19.12
2.19.11
2.19.10
2.19.9
2.19.8
2.19.7
2.19.6
2.19.5
2.19.4
2.19.3
retired
2.19.2
retired
2.19.1
retired
2.19.0
retired
2.18.2
2.18.1
2.18.0
2.17.24
2.17.23
2.17.22
2.17.21
2.17.20
2.17.19
2.17.18
2.17.17
2.17.16
2.17.15
2.17.14
2.17.13
2.17.12
2.17.11
2.17.10
2.17.9
2.17.8
2.17.7
2.17.6
2.17.5
2.17.4
2.17.3
2.17.2
2.17.1
2.17.0
2.16.1
2.16.0
2.15.20
2.15.19
2.15.18
2.15.17
2.15.16
2.15.15
2.15.14
2.15.13
2.15.12
2.15.11
2.15.10
2.15.9
2.15.8
2.15.7
2.15.6
2.15.5
2.15.4
2.15.2
2.15.1
2.15.0
2.14.21
2.14.20
2.14.19
2.14.18
2.14.17
2.14.16
2.14.15
2.14.14
2.14.13
2.14.12
2.14.11
2.14.10
2.14.9
2.14.8
2.14.7
2.14.6
2.14.5
2.14.4
2.14.3
2.14.2
2.14.1
2.14.0
2.13.4
retired
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.11
2.11.10
2.11.9
2.11.8
2.11.7
2.11.6
2.11.5
2.11.4
2.11.3
2.11.2
2.11.1
2.11.0
2.11.0-rc.3
2.11.0-rc.2
2.11.0-rc.1
2.11.0-rc.0
2.10.2
2.10.1
2.10.0
2.9.29
2.9.28
2.9.27
2.9.26
2.9.25
2.9.24
2.9.23
2.9.22
2.9.21
2.9.20
2.9.19
2.9.18
2.9.17
2.9.16
2.9.15
2.9.14
2.9.13
2.9.12
2.9.11
2.9.10
2.9.9
2.9.8
2.9.7
2.9.6
2.9.5
2.9.4
2.9.3
2.9.2
2.9.1
2.9.0
2.8.1
2.8.0
2.7.1
2.7.0
2.6.31
2.6.30
2.6.29
2.6.28
2.6.27
2.6.26
2.6.25
2.6.24
2.6.23
2.6.22
2.6.21
2.6.20
2.6.19
2.6.18
2.6.17
2.6.16
2.6.15
2.6.14
2.6.13
2.6.11
2.6.10
2.6.9
2.6.8
2.6.7
2.6.6
2.6.5
2.6.4
2.6.3
2.6.2
2.6.1
2.6.0
2.5.16
2.5.15
2.5.14
2.5.13
2.5.12
2.5.11
2.5.10
2.5.9
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.5.0-rc.6
2.5.0-rc.5
2.5.0-rc.4
2.5.0-rc.3
2.5.0-rc.2
2.5.0-rc.1
2.5.0-rc.0
2.4.30
2.4.29
2.4.28
2.4.27
2.4.26
2.4.25
2.4.24
2.4.23
2.4.22
2.4.21
2.4.20
2.4.19
2.4.18
2.4.17
2.4.16
2.4.15
2.4.14
2.4.13
2.4.12
2.4.11
2.4.10
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.0
2.0.0-rc.15
2.0.0-rc.14
2.0.0-rc.13
2.0.0-rc.12
2.0.0-rc.11
2.0.0-rc.10
2.0.0-rc.9
2.0.0-rc.8
2.0.0-rc.7
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-pre.8
2.0.0-pre.7
2.0.0-pre.6
2.0.0-pre.5
2.0.0-pre.4
2.0.0-pre.3
2.0.0-pre.2
2.0.0-pre.1
2.0.0-pre.0
1.53.3
1.53.2
1.53.0
1.52.0-rc.22
1.52.0-rc.21
1.52.0-rc.20
1.52.0-rc.19
1.52.0-rc.18
1.52.0-rc.17
1.52.0-rc.16
1.52.0-rc.15
1.52.0-rc.14
1.52.0-rc.13
1.52.0-rc.12
1.52.0-rc.11
1.52.0-rc.10
1.52.0-rc.9
1.52.0-rc.8
1.52.0-rc.7
1.52.0-rc.6
1.52.0-rc.5
1.52.0-rc.4
1.52.0-rc.3
1.52.0-rc.2
1.52.0-rc.1
1.52.0-rc.0
1.51.2
1.51.1
retired
1.51.0
1.50.21
1.50.20
1.50.19
1.50.18
1.50.17
1.50.16
1.50.15
1.50.14
1.50.13
1.50.12
1.50.11
1.50.10
1.50.9
1.50.8
1.50.7
1.50.6
1.50.5
1.50.4
1.50.3
1.50.2
1.50.1
1.50.0
1.49.0
1.48.0-rc.30
1.48.0-rc.29
1.48.0-rc.28
1.48.0-rc.27
1.48.0-rc.26
1.48.0-rc.25
1.48.0-rc.24
1.48.0-rc.23
1.48.0-rc.22
1.48.0-rc.21
1.48.0-rc.20
1.48.0-rc.19
1.48.0-rc.18
1.48.0-rc.17
1.48.0-rc.16
1.48.0-rc.15
1.48.0-rc.14
1.48.0-rc.13
1.48.0-rc.12
1.48.0-rc.11
1.48.0-rc.10
1.48.0-rc.9
1.48.0-rc.8
1.48.0-rc.7
1.48.0-rc.6
1.48.0-rc.5
1.48.0-rc.4
1.48.0-rc.3
1.48.0-rc.2
1.48.0-rc.1
1.48.0-rc.0
1.47.12
1.47.11
1.47.10
1.47.9
1.47.8
1.47.7
1.47.6
1.47.5
1.47.4
1.47.3
1.47.2
1.47.1
1.47.0
1.46.13
1.46.12
1.46.11
1.46.10
1.46.9
1.46.8
1.46.7
1.46.6
1.46.5
1.46.4
1.46.3
1.46.2
1.46.1
1.46.0
1.45.0-rc9
1.45.0-rc8
1.45.0-rc7
1.45.0-rc6
1.45.0-rc5
1.45.0-rc4
1.45.0-rc3
1.45.0-rc20
1.45.0-rc2
1.45.0-rc19
1.45.0-rc18
1.45.0-rc17
1.45.0-rc16
1.45.0-rc15
1.45.0-rc14
1.45.0-rc13
1.45.0-rc12
1.45.0-rc11
1.45.0-rc10
1.45.0-rc1
1.45.0-rc0
1.44.13
1.44.12
1.44.11
1.44.10
1.44.9
1.44.8
1.44.7
1.44.6
1.44.5
1.44.4
1.44.3
1.44.2
1.44.1
1.44.0
1.43.12
1.43.11
1.43.10
1.43.9
1.43.8
1.43.7
1.43.6
1.43.5
1.43.4
1.43.3
1.43.2
1.43.1
1.43.0
1.42.0
1.41.12
1.41.11
1.41.10
1.41.9
1.41.8
1.41.7
1.41.6
1.41.5
1.41.4
1.41.3
1.41.2
1.41.1
1.41.0
1.40.0
1.39.7
1.39.6
1.39.5
1.39.4
1.39.3
1.39.2
1.39.1
1.39.0
1.38.0
1.37.2
1.37.1
1.37.0
1.36.22
1.36.21
1.36.19
1.36.18
1.36.17
1.36.16
1.36.15
1.36.14
1.36.13
1.36.12
1.36.11
1.36.10
1.36.9
1.36.8
1.36.7
1.36.6
1.36.5
1.36.4
1.36.3
1.36.2
1.36.0
1.35.1
1.35.0
1.34.9
1.34.8
1.34.7
1.34.6
1.34.5
1.34.4
1.34.3
1.34.2
1.34.1
1.34.0
1.33.0
1.32.2
1.32.1
1.32.0
1.31.1
1.31.0
1.30.2
1.30.1
1.29.0-rc1
1.29.0-rc0
1.28.1
1.28.0
1.27.1
1.27.0
1.26.13
1.26.12
1.26.11
1.26.10
1.26.9
1.26.8
1.26.7
1.26.6
1.26.5
1.26.4
1.26.2
1.26.1
1.26.0
1.25.8
1.25.7
1.25.6
1.25.5
1.25.4
1.25.3
1.25.2
1.25.1
1.25.0
1.24.2
1.24.1
1.24.0
1.23.3
1.23.2
1.23.1
1.23.0
1.22.1
1.22.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.1
1.18.0
1.17.1
1.17.0
1.16.2
1.15.1
1.15.0
1.14.0
1.13.4
1.13.3
1.13.2
1.13.1
1.13.0
1.12.0
1.11.1
1.11.0
1.10.0
1.9.0
1.8.0
1.7.0
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.1
1.4.0
1.3.1
1.3.0
1.2.1
1.2.0
1.1.3
1.1.2
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
0.13.1
0.13.0
0.12.0
0.10.0
0.9.1
0.9.0
0.8.0
0.7.0
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.9
0.1.8
0.1.3
0.1.1
0.1.0
A declarative, extensible framework for building Elixir applications.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/ash/error/error.ex
defmodule Ash.Error do
@moduledoc """
Tools and utilities used by Ash to manage and conform errors
"""
alias Ash.Error.{Forbidden, Framework, Invalid, Stacktrace, Unknown}
alias Ash.Error.Unknown.UnknownError
@type error_class() :: :forbidden | :invalid | :framework | :unknown
@type t :: %{
required(:__struct__) => module,
required(:__exception__) => true,
required(:class) => error_class(),
required(:path) => [atom | integer],
required(:changeset) => Ash.Changeset.t() | nil,
required(:query) => Ash.Query.t() | nil,
required(:error_context) => list(String.t()),
required(:vars) => Keyword.t(),
required(:stacktrace) => Ash.Error.Stacktrace.t() | nil,
optional(atom) => any
}
# We use these error classes also to choose a single error
# to raise when multiple errors have occurred. We raise them
# sorted by their error classes
@error_classes [
:forbidden,
:invalid,
:framework,
:unknown
]
@error_modules [
forbidden: Forbidden,
invalid: Invalid,
framework: Framework,
unknown: Unknown
]
@error_class_indices @error_classes |> Enum.with_index() |> Enum.into(%{})
@doc false
def error_modules, do: Keyword.values(@error_modules)
@doc false
def set_path(errors, path) when is_list(errors) do
Enum.map(errors, &set_path(&1, path))
end
def set_path(error, path) when is_map(error) do
path = List.wrap(path)
error =
if Map.has_key?(error, :path) && is_list(error.path) do
%{error | path: path ++ error.path}
else
error
end
error =
if Map.has_key?(error, :changeset) && error.changeset do
%{error | changeset: %{error.changeset | errors: set_path(error.changeset.errors, path)}}
else
error
end
if Map.has_key?(error, :errors) && is_list(error.errors) do
%{error | errors: Enum.map(error.errors, &set_path(&1, path))}
else
error
end
end
def set_path(error, _), do: error
def ash_error?(value) do
!!Ash.ErrorKind.impl_for(value)
end
@doc """
Conforms a term into one of the built-in Ash [Error classes](handle-errors.html#error-classes).
The provided term would usually be an Ash Error or a list of Ash Errors.
If the term is:
- a map/struct/Ash Error with a key `:class` having a value `:special`,
- a list with a single map/struct/Ash Error element as above, or
- an `Ash.Error.Invalid` containing such a list in its `:errors` field
then the term is returned unchanged.
Example:
```elixir
iex(1)> Ash.Error.to_error_class("oops", changeset: Ash.Changeset.new(%Post{}), error_context: "some context")
%Ash.Error.Unknown{
changeset: #Ash.Changeset<
errors: [
%Ash.Error.Unknown.UnknownError{
changeset: nil,
class: :unknown,
error: "oops",
error_context: ["some context"],
field: nil,
path: [],
query: nil,
stacktrace: #Stacktrace<>,
vars: []
}
],
...
>,
class: :unknown,
error_context: ["some context"],
errors: [
%Ash.Error.Unknown.UnknownError{
changeset: nil,
class: :unknown,
error: "oops",
error_context: ["some context"],
field: nil,
path: [],
query: nil,
stacktrace: #Stacktrace<>,
vars: []
}
],
stacktrace: #Stacktrace<>,
stacktraces?: true,
vars: []
}
```
Example of nested errors:
```elixir
iex(1)> error1 = Ash.Error.to_ash_error("whoops!", nil, error_context: "some context")
iex(2)> error2 = Ash.Error.to_ash_error("whoops, again!!", nil, error_context: "some other context")
iex(3)> Ash.Error.to_error_class([error1, error2], error_context: "some higher context")
%Ash.Error.Unknown{
changeset: nil,
class: :unknown,
error_context: ["some higher context"],
errors: [
%Ash.Error.Unknown.UnknownError{
changeset: nil,
class: :unknown,
error: "whoops!",
error_context: ["some higher context", "some context"],
field: nil,
path: [],
query: nil,
stacktrace: #Stacktrace<>,
vars: []
},
%Ash.Error.Unknown.UnknownError{
changeset: nil,
class: :unknown,
error: "whoops, again!!",
error_context: ["some higher context", "some other context"],
field: nil,
path: [],
query: nil,
stacktrace: #Stacktrace<>,
vars: []
}
],
path: [],
query: nil,
stacktrace: #Stacktrace<>,
stacktraces?: true,
vars: []
}
```
Options:
- `changeset`: a changeset related to the error
- `query`: a query related to the error
- `error_context`: a sting message providing extra context around the error
"""
def to_error_class(values, opts \\ [])
def to_error_class(%{class: :special} = special, _opts) do
special
end
def to_error_class([%{class: :special} = special], _opts) do
special
end
def to_error_class(%Ash.Error.Invalid{errors: [%{class: :special} = special]}, _opts) do
special
end
def to_error_class(values, opts) when is_list(values) do
case values do
[%{class: :special} = exception] ->
exception
values ->
values =
values
|> flatten_preserving_keywords()
|> Enum.uniq_by(&clear_stacktraces/1)
|> Enum.map(fn value ->
if ash_error?(value) do
value
else
UnknownError.exception(error: value)
end
end)
|> Enum.uniq()
values
|> accumulate_error_context(opts[:error_context])
|> choose_error(opts[:changeset] || opts[:query])
|> add_error_context(opts[:error_context])
end
end
def to_error_class(value, opts) do
if ash_error?(value) && value.__struct__ in Keyword.values(@error_modules) do
value
|> add_changeset_or_query([value], opts[:changeset] || opts[:query])
|> Map.put(:error_context, [opts[:error_context] | value.error_context])
else
to_error_class([value], opts)
end
end
@doc """
Converts a term into an Ash Error.
The term could be a simple string, the second element in an `{:error, error}` tuple, an Ash Error, or a list of any of these.
In most cases the returned error is an Ash.Error.Unknown.UnknownError.
A stacktrace is added to the error, and any existing stacktrace (i.e. when the term is an Ash Error) is preserved.
`to_ash_error` converts string(s) into UnknownError(s):
```elixir
iex(1)> Ash.Error.to_ash_error("whoops!", nil, error_context: "some context")
%Ash.Error.Unknown.UnknownError{
changeset: nil,
class: :unknown,
error: "whoops!",
error_context: ["some context"],
field: nil,
path: [],
query: nil,
stacktrace: #Stacktrace<>,
vars: []
}
iex(2)> Ash.Error.to_ash_error(["whoops!", "whoops, again!!"], nil, error_context: "some context")
[
%Ash.Error.Unknown.UnknownError{
changeset: nil,
class: :unknown,
error: "whoops!",
error_context: ["some context"],
field: nil,
path: [],
query: nil,
stacktrace: #Stacktrace<>,
vars: []
},
%Ash.Error.Unknown.UnknownError{
changeset: nil,
class: :unknown,
error: "whoops, again!!",
error_context: ["some context"],
field: nil,
path: [],
query: nil,
stacktrace: #Stacktrace<>,
vars: []
}
]
```
`to_ash_error` can preserve error-like data from a keyword-list and accumulate context if called against an Ash Error:
```elixir
iex(1)> err = Ash.Error.to_ash_error([vars: [:some_var], message: "whoops!"], nil, error_context: " some context")
%Ash.Error.Unknown.UnknownError{
changeset: nil,
class: :unknown,
error: "whoops!",
error_context: ["some context"],
field: nil,
path: [],
query: nil,
stacktrace: #Stacktrace<>,
vars: [:some_var]
}
iex(2)> Ash.Error.to_ash_error(err, nil, error_context: "some higher context")
%Ash.Error.Unknown.UnknownError{
changeset: nil,
class: :unknown,
error: "whoops!",
error_context: ["some higher context", "some context"],
field: nil,
path: [],
query: nil,
stacktrace: #Stacktrace<>,
vars: [:some_var]
}
```
Options:
- `error_context`: a sting message providing extra context around the error
"""
def to_ash_error(list, stacktrace \\ nil, opts \\ [])
def to_ash_error(list, stacktrace, opts) when is_list(list) do
if Keyword.keyword?(list) do
list
|> Keyword.take([:error, :vars])
|> Keyword.put_new(:error, list[:message])
|> UnknownError.exception()
|> add_stacktrace(stacktrace)
|> add_error_context(opts[:error_context])
else
Enum.map(list, &to_ash_error(&1, stacktrace, opts))
end
end
def to_ash_error(error, stacktrace, opts) when is_binary(error) do
[error: error]
|> UnknownError.exception()
|> add_stacktrace(stacktrace)
|> add_error_context(opts[:error_context])
end
def to_ash_error(other, stacktrace, opts) do
cond do
ash_error?(other) ->
other
|> add_stacktrace(stacktrace)
|> add_error_context(opts[:error_context])
is_exception(other) ->
[error: Exception.format(:error, other)]
|> UnknownError.exception()
|> add_stacktrace(stacktrace)
|> add_error_context(opts[:error_context])
true ->
[error: "unknown error: #{inspect(other)}"]
|> UnknownError.exception()
|> add_stacktrace(stacktrace)
|> add_error_context(opts[:error_context])
end
end
defp add_stacktrace(error, stacktrace) do
stacktrace =
case stacktrace do
%Stacktrace{stacktrace: nil} ->
nil
nil ->
nil
stacktrace ->
%Stacktrace{stacktrace: stacktrace}
end
%{error | stacktrace: stacktrace || error.stacktrace || fake_stacktrace()}
end
defp fake_stacktrace do
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
%Stacktrace{stacktrace: Enum.drop(stacktrace, 2)}
end
defp add_error_context(error, error_context) when is_binary(error_context) do
%{error | error_context: [error_context | error.error_context]}
end
defp add_error_context(error, _) do
error
end
defp accumulate_error_context(%{errors: [_ | _] = errors} = error, error_context)
when is_binary(error_context) do
updated_errors = accumulate_error_context(errors, error_context)
%{error | errors: updated_errors}
end
defp accumulate_error_context(errors, error_context)
when is_list(errors) and is_binary(error_context) do
errors
|> Enum.map(fn err ->
err
|> add_error_context(error_context)
|> accumulate_error_context(error_context)
end)
end
defp accumulate_error_context(error, _) do
error
end
@doc "A utility to flatten a list, but preserve keyword list elements"
def flatten_preserving_keywords(list) do
if Keyword.keyword?(list) do
[list]
else
Enum.flat_map(list, fn item ->
cond do
Keyword.keyword?(item) ->
[item]
is_list(item) ->
item
true ->
[item]
end
end)
end
end
def clear_stacktraces(%{stacktrace: stacktrace} = error) when not is_nil(stacktrace) do
clear_stacktraces(%{error | stacktrace: nil})
end
def clear_stacktraces(%{errors: errors} = exception) when is_list(errors) do
%{exception | errors: Enum.map(errors, &clear_stacktraces/1)}
end
def clear_stacktraces(error), do: error
def choose_error(errors, changeset_or_query \\ nil)
def choose_error([], changeset_or_query) do
error = Ash.Error.Unknown.exception([])
add_changeset_or_query(error, [], changeset_or_query)
end
def choose_error(errors, changeset_or_query) do
errors = Enum.map(errors, &to_ash_error/1)
[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)
parent_error_module = @error_modules[error.class]
top_level_error =
if parent_error_module == error.__struct__ do
%{error | errors: (error.errors || []) ++ other_errors}
else
parent_error_module.exception(errors: errors)
end
add_changeset_or_query(top_level_error, errors, changeset_or_query)
end
defp add_changeset_or_query(error, errors, changeset_or_query) do
changeset = error.changeset || error.query || changeset_or_query
if changeset_or_query do
changeset_or_query = %{
changeset_or_query
| action_failed?: true,
errors: List.wrap(errors) ++ changeset.errors
}
case changeset_or_query do
%Ash.Changeset{} = changeset ->
%{error | changeset: %{changeset | errors: Enum.uniq(changeset.errors)}}
%Ash.Query{} = query ->
%{error | query: %{query | errors: Enum.uniq(query.errors)}}
end
else
error
end
end
def error_messages(errors, custom_message, stacktraces?) do
errors = Enum.map(errors, &to_ash_error/1)
generic_message =
errors
|> List.wrap()
|> 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"
if stacktraces? do
header <>
Enum.map_join(class_errors, "\n", fn
error when is_binary(error) ->
"* #{error}"
%{stacktrace: %Stacktrace{stacktrace: stacktrace}} = class_error ->
breadcrumb(class_error.error_context) <>
"* #{Exception.message(class_error)}\n" <>
path(class_error) <>
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 when is_binary(class_error) ->
"* #{class_error}"
class_error ->
breadcrumb(class_error.error_context) <>
"* #{Exception.message(class_error)}" <> path(class_error)
end)
end
end)
if custom_message do
custom =
custom_message
|> List.wrap()
|> Enum.map_join("\n", &"* #{&1}")
"\n\n" <> custom <> generic_message
else
generic_message
end
end
def error_descriptions(errors) do
errors
|> Kernel.||([])
|> 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"
header <> Enum.map_join(class_errors, "\n", &"* #{Exception.message(&1)}")
end)
end
defp path(%{path: path}) when path not in [[], nil] do
" at " <> to_path(path) <> "\n"
end
defp path(_), do: ""
defp to_path(path) do
Enum.map_join(path, ", ", fn item ->
if is_list(item) do
"[#{to_path(item)}]"
else
if is_binary(item) || is_atom(item) || is_number(item) do
item
else
inspect(item)
end
end
end)
end
defp header(:forbidden), do: "Forbidden"
defp header(:invalid), do: "Input Invalid"
defp header(:framework), do: "Framework Error"
defp header(:unknown), do: "Unknown Error"
@doc false
def breadcrumb(nil), do: ""
def breadcrumb([]), do: ""
def breadcrumb(error_context) do
case Enum.filter(error_context, & &1) do
[] ->
""
bread_crumbs ->
"Context: " <> Enum.join(bread_crumbs, " > ") <> "\n"
end
end
end