defmodule DataSchema.Errors do
@moduledoc """
When we create a struct we either return the struct we were creating or we return this error.
The error/errors that happened during struct creation are collected into this struct
"""
@typedoc """
An error is the struct key that caused the error and either an error message or a
DataSchema.Errors struct in the case of nested error.
"""
@type t :: %__MODULE__{errors: [{atom, String.t() | __MODULE__.t()}]}
defstruct errors: []
@doc """
Adds an error to the given errors struct. The error is prepended to the list of current errors.
"""
def add_error(%__MODULE__{} = errors, error) do
%{errors | errors: [error | errors.errors]}
end
@doc false
def new({field, error}) do
DataSchema.Errors.add_error(%__MODULE__{}, {field, error})
end
@default_error_message "There was an error!"
@doc false
def default_error(field) do
DataSchema.Errors.add_error(%DataSchema.Errors{}, {field, @default_error_message})
end
@doc false
@non_null_error_message "Field was marked as not null but was found to be null."
def null_error(field) do
DataSchema.Errors.add_error(%DataSchema.Errors{}, {field, @non_null_error_message})
end
@doc false
@empty_required_value_error_message "Field was required but value supplied is considered empty"
def empty_required_value_error(field) do
DataSchema.Errors.add_error(
%DataSchema.Errors{},
{field, @empty_required_value_error_message}
)
end
@doc """
Turns the DataSchema.Errors struct into a flattened error tuple of path to field and
error message
### Examples
iex> error = %DataSchema.Errors{
...> errors: [
...> comments: %DataSchema.Errors{
...> errors: [author:
...> %DataSchema.Errors{
...> errors: [name: "There was an error!"]
...> }
...> ]
...> }
...> ]
...> }
...> DataSchema.Errors.to_error_tuple(error)
{:error, {[:comments, :author, :name], "There was an error!"}}
"""
@type path_to_error :: [atom()]
@type error_message :: String.t()
@spec to_error_tuple(__MODULE__.t()) :: {:error, {path_to_error, error_message}}
def to_error_tuple(%__MODULE__{} = error) do
{:error, flatten_errors(error)}
end
@doc """
Returns an error tuple of the path to the problematic field and the error message.
Usually errors are returned as nested `DataSchema.Errors` structs. This was to help
cater for the possibility of collecting all errors, but right now we stop casting as
soon as we error on a casting function, so errors are a little confusing. This function
can be used to return a flattened error.
### Examples
iex> error = %DataSchema.Errors{
...> errors: [
...> comments: %DataSchema.Errors{
...> errors: [author:
...> %DataSchema.Errors{
...> errors: [name: "There was an error!"]
...> }
...> ]
...> }
...> ]
...> }
...> DataSchema.Errors.flatten_errors(error)
{[:comments, :author, :name], "There was an error!"}
"""
def flatten_errors(%__MODULE__{} = error) do
{path, error} = do_flatten_errors(error, {[], ""})
{Enum.reverse(path), error}
end
# Because we don't yet "collect" all errors we can ignore rest here and just to DFS.
defp do_flatten_errors(%__MODULE__{errors: [head | _rest]}, {path, msg}) do
case head do
{key, %DataSchema.Errors{} = error} -> do_flatten_errors(error, {[key | path], msg})
{key, error_message} when is_binary(error_message) -> {[key | path], error_message}
end
end
end