View Source Resourceful.Error (Resourceful v0.1.2)

Errors in Resourceful follow a few conventions. This module contains functions to help work with those conventions. Client-facing errors are loosely inspired by and should be easily converted to JSON:API-style errors, however they should also be suitable when JSON:API isn't used at all.

error-structure

Error Structure

All errors returned in Resourceful are expected to be two element tuples where the first element is :error. This is a common Elixir and Erlang convention, but it's strict for the purposes of this library. Resourceful generates and expects one of two kinds of errors:

basic-errors

Basic Errors

Basic errors are always a two element tuple where the second element is an atom that should related to specific kind of error returned. These are meant for situations where context is either easily inferred, unnecessary, or obvious in some other manner.

Example: {:error, :basic_err}

Basic errors that require context are often transformed into contextual errors by higher level functions as they have context that lower level functions do not.

contextual-errors

Contextual Errors

Contextual errors are always a two element tuple where the second element is another two element tuple in which the first element is an atom and the second element is a map containing contextual information such as user input, data being validated, and/or the source of the error. The map's keys must be atoms.

Example: {:error, {:context_err, %{input: "XYZ", source: ["email_address"]}}

Errors outside of this format are not expected to work with these functions.

Sources

While not strictly required by contextual errors the :source key is used to indicate an element in a complex data structure that is responsible for a particular error. It must be a list of atoms, strings, or integers used to navigate a tree of data. Non-nested values should still be in the form of a list as prefixing sources is common. It's common for functions generating errors to be ignorant of their full context and higher level functions to prepend errors with their current location.

Other Common Conventions

There are a few other keys that appear in errors regularly:

  • :detail: A human friendly description about the error. If there is information related to resolving the error, it belongs here.
  • :input: A text representation of the actual input given by the client. It should be as close as possible to the original. (In general, IO.inspect/1 is used.)
  • :key: Not to be confused with :source, a :key key should always be present when a lookup of data is done by some sort of key and there is a failure.
  • :value: Not to be confused with :input, a :value key

Note: all of these keys have convenience functions as it is a very common convention to create a new error with one of these keys or add one to an existing error.

error-types

Error Types

Both basic and contextual errors will contain an atom describing the specific type of error. These should be thought of as error codes and should be unique to the kinds of errors. For example, :attribute_not_found means that in some context, an attribute for a given resource doesn't exist. This could be an attempt to filter or sort. Either way, the error type remains the same. In both contexts, this error means the same thing.

Contextual errors of a particular type should always contain at least some expected keys in their context maps. For example, :attribute_not_found should always contain a :name and may contain other information. More is usually better when it comes to errors.

Keys should remain consistent in their use. :invalid_filter_operator, for example, should always contain an :attribute key implying that the failure was on a valid attribute whereas :attribute_not_found contains a :key key implying it was never resolved to an actual attribute.

Link to this section Summary

Functions

Mostly a convenience function to use instead of list/1 with the option to auto source errors as well. Additionally it will take a non-collection value and convert it to a list.

Recursively checks arbitrary data structures for any basic or contextual errors.

Transverses an arbitrary data structure that may contain errors and prepends :source data given the error's position in the data structure using either an index from a list or a key from a map.

Extracts the context map from an error.

Deletes key from an error's context map if present.

Converts errors from an Ecto Changeset into Resourceful errors. The type is inferred from the :validation key as Resourceful tends to use longer names. Rather than relying on separate input params, the :input is inserted from data, and :source is also inferred.

Many error types should, at a minimum, have an associated :title. If there are regular context values, it should also include a :detail value as well. Both of these keys provide extra information about the nature of the error and can help the client understand the particulars of the provided context. While it might not be readily obvious what :key means in an error, if it is used in :detail it will help the client understand the significance.

Transforms an arbitrary data structure that may contain errors into a single, flat list of those errors. Non-error values are removed. Collections are checked recursively.

Replaces context bindings in a message with atom keys in a context map.

Recursively transforms arbitrary data structures containing :ok tuples with just values. Values which are not in :ok tuples are left untouched.

Checks an arbitrary data structure for errors and returns either the errors or valid data.

Adds or prepends source context to an error. A common pattern when dealing with sources in nested data structures being transversed recursively is for the child structure to have no knowledge of the parent structure. Once the child errors are resolved the parent can then prepend its location in the structure to the child's errors.

Adds a context map to an error if it lacks one, converting a basic error to a contextual error. It may also take a single atom to prevent error generating code from having to constantly wrap errors in an :error tuple.

Adds the specified context as an error's context. If the error already has a context map the new context is merged.

Adds a context map to an error if it lacks on and then puts the key and value into that map.

Convenience function to create or modify an existing error with :input context.

Convenience function to create or modify an existing error with :key context.

Adds source context to an error and replaces :source if present.

Link to this section Types

@type basic() :: {:error, atom()}
@type contextual() :: {:error, {atom(), map()}}
@type or_type() :: atom() | t()
@type t() :: basic() | contextual()

Link to this section Functions

@spec all(
  any(),
  keyword()
) :: list()

Mostly a convenience function to use instead of list/1 with the option to auto source errors as well. Additionally it will take a non-collection value and convert it to a list.

Returns a list of errors.

@spec any?(any()) :: boolean()

Recursively checks arbitrary data structures for any basic or contextual errors.

Link to this function

auto_source(error_or_enum, prefix \\ [])

View Source
@spec auto_source(t() | map() | list(), list()) :: any()

Transverses an arbitrary data structure that may contain errors and prepends :source data given the error's position in the data structure using either an index from a list or a key from a map.

Note: This will not work for keyword lists. In order to infer source information they must first be converted to maps.

Returns input data structure with errors modified to contain :source in their context.

@spec context(t() | map()) :: map()

Extracts the context map from an error.

Returns a context map.

Link to this function

delete_context_key(error, key)

View Source
@spec delete_context_key(t(), atom()) :: t()

Deletes key from an error's context map if present.

Returns an error tuple.

Link to this function

from_changeset(changeset)

View Source
@spec from_changeset(%Ecto.Changeset{
  action: term(),
  changes: term(),
  constraints: term(),
  data: term(),
  empty_values: term(),
  errors: term(),
  filters: term(),
  params: term(),
  prepare: term(),
  repo: term(),
  repo_opts: term(),
  required: term(),
  types: term(),
  valid?: term(),
  validations: term()
}) :: [contextual()]

Converts errors from an Ecto Changeset into Resourceful errors. The type is inferred from the :validation key as Resourceful tends to use longer names. Rather than relying on separate input params, the :input is inserted from data, and :source is also inferred.

Link to this function

humanize(error, opts \\ [])

View Source
@spec humanize(
  t() | [t()],
  keyword()
) :: contextual() | [contextual()]

Many error types should, at a minimum, have an associated :title. If there are regular context values, it should also include a :detail value as well. Both of these keys provide extra information about the nature of the error and can help the client understand the particulars of the provided context. While it might not be readily obvious what :key means in an error, if it is used in :detail it will help the client understand the significance.

Unlike the error's type itself--which realistically should serve as an error code of sorts--the title should should be more human readable and able to be localized, although it should be consistent. Similarly, detail should be able to be localized although it can change depending on the specifics of the error or values in the context map.

This function handles injecting default :title and :detail items into the context map if they are available for an error type and replacing context- related bindings in messages. (See message_with_context/2 for details.)

In the future, this is also where localization should happen.

@spec list(list() | map()) :: [t()]

Transforms an arbitrary data structure that may contain errors into a single, flat list of those errors. Non-error values are removed. Collections are checked recursively.

Maps are given special treatment in that their values are checked but their keys are discarded.

This format is meant to keep reading errors fairly simple and consistent at the edge. Clients can rely on reading a single list of errors regardless of whether transversing nested validation failures or handling more simple single fault situations.

This function is also designed with another convention in mind: mixing successes and failures in a single payload. A design goal of error use in this library is to wait until as late as possible to return errors. That way, a single request can return the totality of its failure to the client. This way, many different paths can be evaluated and if there are any errors along the way, those errors can be returned in full.

Link to this function

message_with_context(message, context)

View Source
@spec message_with_context(String.t(), map()) :: String.t()

Replaces context bindings in a message with atom keys in a context map.

A message of "Invalid input %{input}." would have%{input}replaced with the value in the context map of:input`.

@spec ok_value(any()) :: any()

Recursively transforms arbitrary data structures containing :ok tuples with just values. Values which are not in :ok tuples are left untouched.

It does not check for errors. If errors are included, they will remain in the structure untouched. This function is designed to work on error free data and is unlikely to be used on its own but rather with or_ok/1.

Keyword lists where :ok may be included with other keys won't be returned as probably intended. Keep this limitation in mind.

Returns the input data structure with all instances of {:ok, value} replaced with value.

Link to this function

or_ok(value, opts \\ [])

View Source

Checks an arbitrary data structure for errors and returns either the errors or valid data.

See all/1, any?/1, and ok_value/1 for specific details as this function combines the three into a common pattern. Return the errors if there are any or the validated data.

Returns either a list of errors wrapped in an :error tuple or valid data wrapped in an :ok tuple.

Link to this function

prepend_source(errors, prefix)

View Source
@spec prepend_source(or_type() | [or_type()], any()) :: contextual() | [contextual()]

Adds or prepends source context to an error. A common pattern when dealing with sources in nested data structures being transversed recursively is for the child structure to have no knowledge of the parent structure. Once the child errors are resolved the parent can then prepend its location in the structure to the child's errors.

@spec with_context(or_type()) :: contextual()

Adds a context map to an error if it lacks one, converting a basic error to a contextual error. It may also take a single atom to prevent error generating code from having to constantly wrap errors in an :error tuple.

Link to this function

with_context(error_or_type, context)

View Source
@spec with_context(or_type(), map()) :: contextual()

Adds the specified context as an error's context. If the error already has a context map the new context is merged.

Link to this function

with_context(error_or_type, key, value)

View Source
@spec with_context(or_type(), atom(), any()) :: contextual()

Adds a context map to an error if it lacks on and then puts the key and value into that map.

Link to this function

with_input(error_or_type, input)

View Source
@spec with_input(or_type(), any()) :: contextual()

Convenience function to create or modify an existing error with :input context.

Link to this function

with_key(error_or_type, key)

View Source
@spec with_key(or_type(), any()) :: contextual()

Convenience function to create or modify an existing error with :key context.

Link to this function

with_source(error_or_type, source, context \\ %{})

View Source
@spec with_source(or_type(), any(), map()) :: contextual()

Adds source context to an error and replaces :source if present.