MishkaGervaz.Errors (MishkaGervaz v0.0.2)

Copy Markdown View Source

Splode-based error handling for MishkaGervaz.

Error Classes

  • :data - Data loading, query, and fetch errors
  • :action - Action execution errors (destroy, update, etc.)

Usage

# Raise an error
raise MishkaGervaz.Errors.Data.LoadFailed, resource: MyResource, reason: :timeout

# Create error without raising
error = MishkaGervaz.Errors.Data.LoadFailed.exception(resource: MyResource, reason: :timeout)

# Convert any value into a Splode error (unrecognized values become `Errors.Unknown`)
MishkaGervaz.Errors.to_error(error)

# Format error for flash message
MishkaGervaz.Errors.format_flash_message(error)

Translation

Every word these functions return goes through the Gettext backend MishkaGervaz.Messages.gettext_backend/0 names, in the locale of the calling process:

  • MishkaGervaz's own words and templates in the "mishka_gervaz" domain.
  • An error's own message, such as Ash's "is required", in the "errors" domain, with dngettext when its vars hold an integer :count. Its %{key} placeholders are filled from the error's vars after it is translated.
  • A field's name from the label the resource declares for it: its form field's ui label, else its table column's label. A field with neither is named by its humanized name in the "mishka_gervaz" domain.

A message whose last character ends a sentence (. ! ? … ؟ ۔ 。 and the like, before any closing quote or bracket) is shown as written. Any other message is joined to its field's name through the "%{field} %{message}" template.

A reason that is not a validation error is worded too: :not_found, and an Ash.Error.Query.NotFound with no primary key, as "This record is no longer here."; an Ash.Error.Forbidden by its policy's custom_message, else as "You are not allowed to do this."; any other exception by its own message.

Summary

Functions

Extracts a human-readable message from various error formats.

An error message about field, as a person reads it.

Formats an error into a human-readable flash message.

Translates an error's own message in the "errors" domain and fills its %{key} placeholders from vars.

Traverses errors, calling fun for each leaf error, and returns a nested map of results grouped by each error's path.

Raises an error if the result is an error, otherwise returns the result

Types

class()

@type class() :: %{
  :__struct__ => class_module(),
  :__exception__ => true,
  :errors => [t()],
  :class => error_class(),
  :bread_crumbs => [String.t()],
  :vars => Keyword.t(),
  :stacktrace => Splode.Stacktrace.t() | nil,
  :context => map(),
  optional(atom()) => any()
}

class_module()

@type class_module() ::
  MishkaGervaz.Errors.Action | MishkaGervaz.Errors.Data | Splode.Error.Unknown

error_class()

@type error_class() :: :action | :data | :unknown

t()

@type t() :: %{
  :__struct__ => module(),
  :__exception__ => true,
  :class => error_class(),
  :bread_crumbs => [String.t()],
  :vars => Keyword.t(),
  :stacktrace => Splode.Stacktrace.t() | nil,
  :context => map(),
  optional(atom()) => any()
}

Functions

extract_error_message(error, resource \\ nil)

@spec extract_error_message(any(), module() | nil) :: String.t()

Extracts a human-readable message from various error formats.

resource is the resource whose declared labels name the error's field. An Ash error AshPhoenix can show on a form (AshPhoenix.FormData.Error) is read the way the form reads it; one the form shows on no field is read by its own message. An error class holding several errors, such as Ash.Error.Invalid, joins the message of each, a repeated message once.

Examples

iex> MishkaGervaz.Errors.extract_error_message(%{message: "Invalid email"})
"Invalid email"

iex> MishkaGervaz.Errors.extract_error_message(%{field: :email, message: "is invalid"})
"Email is invalid"

iex> MishkaGervaz.Errors.extract_error_message(%{field: :tag_id, message: "That tag is archived."})
"That tag is archived."

iex> MishkaGervaz.Errors.extract_error_message(%{field: :title, message: "must be at least %{min}", vars: [min: 3]})
"Title must be at least 3"

field_message(message, vars, field, resource)

@spec field_message(
  String.t() | nil,
  keyword() | map() | nil,
  atom() | String.t() | nil,
  module() | nil
) :: String.t()

An error message about field, as a person reads it.

message is translated with translate_error/2. A translated message that ends a sentence is returned as written; any other is joined to the field's name through the "%{field} %{message}" template. field is named by the label resource declares for it, else by its humanized name. A nil field, or AshPhoenix's :_form for the whole form, returns the message alone.

format_flash_message(error, resource \\ nil)

@spec format_flash_message(any(), module() | nil) :: String.t()

Formats an error into a human-readable flash message.

Handles MishkaGervaz errors, Ash errors, and generic errors. resource names the fields of an error that has no resource of its own; an Action.Failed or Data.LoadFailed uses its own :resource when resource is nil.

An Action.Failed names its action by its :label (a string or a zero-arity function), else by its humanized :action.

Examples

iex> error = MishkaGervaz.Errors.Action.Failed.exception(action: :archive, reason: "forbidden")
iex> MishkaGervaz.Errors.format_flash_message(error)
"Archive failed: forbidden"

iex> error = MishkaGervaz.Errors.Action.Failed.exception(action: :archive, label: "Delete", reason: "forbidden")
iex> MishkaGervaz.Errors.format_flash_message(error)
"Delete failed: forbidden"

splode_error?(arg1, splode)

translate_error(message, vars)

@spec translate_error(String.t(), keyword() | map() | nil) :: String.t()

Translates an error's own message in the "errors" domain and fills its %{key} placeholders from vars.

Uses dngettext when vars holds an integer :count. A list var is joined with ", ", and a value String.Chars cannot print is inspected. A message with a %{key} placeholder vars does not fill is returned as written.

Examples

iex> MishkaGervaz.Errors.translate_error("must be at least %{min}", min: 3)
"must be at least 3"

traverse_errors(error_or_errors, fun)

Traverses errors, calling fun for each leaf error, and returns a nested map of results grouped by each error's path.

See Splode.traverse_errors/2 for full documentation.

Example

iex> Elixir.MishkaGervaz.Errors.traverse_errors(error, fn error ->
...>   Exception.message(error)
...> end)
%{name: ["name is required"]}

unwrap!(result, opts \\ nil)

Raises an error if the result is an error, otherwise returns the result

Alternatively, you can use the defsplode macro, which does this automatically.

Options

  • :error_opts - Options to pass to to_error/2 when converting the returned error
  • :unknown_error_opts - Options to pass to the unknown error if the function returns only :error. not necessary if your function always returns {:error, error}.

Examples

def function(arg) do

case do_something(arg) do
  :success -> :ok
  {:success, result} -> {:ok, result}
  {:error, error} -> {:error, error}
end

end

def function!(arg) do

YourErrors.unwrap!(function(arg))

end