Packages
zoi
0.17.3
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.1
0.16.0
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.9.0-rc.1
0.9.0-rc.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Zoi is a schema validation library for Elixir, designed to provide a simple and flexible way to define and validate data.
Current section
Files
Jump to
Current section
Files
lib/zoi/error.ex
defmodule Zoi.Error do
@moduledoc """
Represents a validation error with detailed information.
## Fields
- `code`: Error code
- `issue`: A tuple with the message and keyword with error variables.
- `message`: Description of the error, formed from the issue message and variables.
- `path`: A list representing the path to the location of the error.
## Errors
All `Zoi` errors have a code that can be used to identify the type of error.
The following error codes are defined:
- `:invalid_type`
- `:invalid_literal`
- `:invalid_tuple`
- `:unrecognized_key`
- `:invalid_enum_value`
- `:not_in_values`
- `:required`
- `:less_than`
- `:greater_than`
- `:less_than_or_equal_to`
- `:greater_than_or_equal_to`
- `:invalid_length`
- `:invalid_format`
- `:multiple_of`
- `:custom`
## Example
The error struct follows this format:
%Zoi.Error{
code: :invalid_type,
issue: {"invalid type: expected string", [type: :string]},
message: "invalid type: expected string",
path: [:user, :name]
}
The `:message` field is generated by replacing the placeholders in the `:issue` message.
This allows for dynamic error messages that provide context about the validation failure and possibility for
localization using `Gettext` or similar libraries. Usually the `issue` and `message` will share the same content,
but the `issue` retains the original template and variables for further processing if needed.
This module is mostly used internally, but can be useful if need to use the built-in error types or create custom errors.
"""
@typedoc "The path to the location of the error."
@type path :: [atom() | binary() | integer()]
@type error_opts :: [
code: atom(),
issue: {binary(), keyword()} | nil,
message: binary(),
path: path()
]
@typedoc """
Error struct containing detailed information about a validation error.
"""
@type t :: %__MODULE__{
code: atom(),
issue: {binary(), keyword()} | nil,
message: binary(),
path: path()
}
defexception [:code, :issue, :message, path: []]
@doc """
Creates a new `Zoi.Error` struct.
"""
@spec new(error_opts() | map()) :: t()
def new(opts \\ [])
def new(opts) when is_map(opts) do
opts = Map.to_list(opts)
new(opts)
end
def new(opts) when is_list(opts) do
{msg, opts} = Keyword.pop(opts, :message)
if msg do
custom_error([{:issue, {msg, []}} | opts])
else
{issue, opts} = Keyword.pop(opts, :issue)
{message, issue} = render_message_from_issue(issue)
struct!(__MODULE__, [{:message, message}, {:issue, issue} | opts])
end
end
@impl true
def exception(opts) do
struct!(__MODULE__, opts)
end
@impl true
def message(%__MODULE__{message: message}) do
message
end
@doc """
Prepends a path to the error's existing path.
## Example
iex> error = Zoi.Error.invalid_type(:string, path: [:name])
iex> error = Zoi.Error.prepend_path(error, [:user])
iex> error.path
[:user, :name]
"""
@spec prepend_path(t(), any()) :: t()
def prepend_path(%__MODULE__{} = error, path) when is_list(path) do
%{error | path: path ++ error.path}
end
defp render_message_from_issue({issue, opts}) do
message =
Enum.reduce(opts, issue, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", parse_message_type(value))
end)
{message, {issue, opts}}
end
defp render_message_from_issue(nil) do
{nil, {nil, []}}
end
defp render_message_from_issue(issue) do
render_message_from_issue({issue, []})
end
defp parse_message_type(values) when is_list(values) do
Enum.map_join(values, ", ", &parse_message_type/1)
end
defp parse_message_type(%Regex{} = regex) do
Regex.source(regex)
end
defp parse_message_type(message) do
to_string(message)
end
## Error types
@doc """
Creates an invalid type error for the expected type.
## Example
iex> Zoi.Error.invalid_type(:string)
%Zoi.Error{
code: :invalid_type,
issue: {"invalid type: expected string", [type: :string]},
message: "invalid type: expected string"
}
"""
@spec invalid_type(atom()) :: t()
def invalid_type(type, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
if msg do
custom_error(issue: {msg, [type: type]})
else
{issue, opts} = Keyword.pop(opts, :issue)
new(
code: :invalid_type,
issue: {issue || "invalid type: expected #{type || "nil"}", [type: type]},
path: opts[:path] || []
)
end
end
@doc """
Creates an invalid literal error for the expected value.
## Example
iex> Zoi.Error.invalid_literal(42)
%Zoi.Error{
code: :invalid_literal,
issue: {"invalid literal: expected %{expected}", [expected: 42]},
message: "invalid literal: expected 42"
}
"""
@spec invalid_literal(any()) :: t()
def invalid_literal(value, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
if msg do
custom_error(issue: {msg, [expected: value]})
else
new([
{:code, :invalid_literal},
{:issue, {"invalid literal: expected %{expected}", [expected: value]}} | opts
])
end
end
@doc """
Creates an invalid enum value error for the given enum values.
## Example
iex> Zoi.Error.invalid_enum_value([{:a, "apple"}, {:b, "banana"}, {:c, "cherry"}])
%Zoi.Error{
code: :invalid_enum_value,
issue: {"invalid enum value: expected one of %{values}", [type: :enum, values: "apple, banana, cherry"]},
message: "invalid enum value: expected one of apple, banana, cherry"
}
"""
@spec invalid_enum_value([tuple()]) :: t()
def invalid_enum_value(enum, opts \\ []) when is_list(enum) do
{msg, _opts} = Keyword.pop(opts, :error)
expected = Enum.map_join(enum, ", ", fn {_key, value} -> value end)
if msg do
custom_error(issue: {msg, [expected: expected]})
else
new(
code: :invalid_enum_value,
issue: {"invalid enum value: expected one of %{values}", [type: :enum, values: expected]}
)
end
end
@doc ~S"""
Creates a not in values error for the given list of valid values.
## Example
iex> Zoi.Error.not_in_values(["red", "green", "blue"])
%Zoi.Error{
code: :not_in_values,
issue: {"invalid value: expected one of %{values}", [values: ["red", "green", "blue"]]},
message: "invalid value: expected one of red, green, blue",
path: []
}
"""
@spec not_in_values(list()) :: t()
def not_in_values(values, opts \\ []) when is_list(values) do
{msg, opts} = Keyword.pop(opts, :error)
if msg do
custom_error(issue: {msg, [values: values]})
else
new(
code: :not_in_values,
issue: {"invalid value: expected one of %{values}", [values: values]},
path: opts[:path] || []
)
end
end
@doc """
Creates an invalid tuple error for the expected and actual lengths.
## Example
iex> Zoi.Error.invalid_tuple(3, 5)
%Zoi.Error{
code: :invalid_tuple,
issue: {"invalid tuple: expected length %{expected_length}, got %{actual_length}", [expected_length: 3, actual_length: 5]},
message: "invalid tuple: expected length 3, got 5"
}
"""
@spec invalid_tuple(non_neg_integer(), non_neg_integer(), keyword()) :: t()
def invalid_tuple(expected_length, actual_length, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
default_opts = [expected_length: expected_length, actual_length: actual_length]
if msg do
custom_error(issue: {msg, default_opts})
else
new([
{:code, :invalid_tuple},
{:issue,
{
"invalid tuple: expected length %{expected_length}, got %{actual_length}",
default_opts
}}
| opts
])
end
end
@doc """
Creates an unrecognized key error for the given key.
## Example
iex> Zoi.Error.unrecognized_key(:foo)
%Zoi.Error{
code: :unrecognized_key,
issue: {"unrecognized key: %{key}", [key: :foo]},
message: "unrecognized key: foo"
}
"""
@spec unrecognized_key(atom() | binary() | integer()) :: t()
def unrecognized_key(key) do
new(
code: :unrecognized_key,
issue: {"unrecognized key: %{key}", [key: key]}
)
end
@doc """
Creates a required error for the given key.
## Example
iex> Zoi.Error.required(:name)
%Zoi.Error{
code: :required,
issue: {"is required", [key: :name]},
message: "is required"
}
"""
@spec required(atom() | binary() | integer()) :: t()
def required(key, opts \\ []) do
new([
{:code, :required},
{:issue, {"is required", [key: key]}} | opts
])
end
@doc """
Creates a less than or equal to error for the given type and maximum value.
## Example
iex> Zoi.Error.less_than_or_equal_to(:string, 10)
%Zoi.Error{
code: :less_than_or_equal_to,
issue: {"too big: must have at most %{count} character(s)", [count: 10]},
message: "too big: must have at most 10 character(s)"
}
"""
@spec less_than_or_equal_to(:string | :array | :number | :date, any(), keyword()) :: t()
def less_than_or_equal_to(type, max, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
if msg do
custom_error(issue: {msg, [count: max]})
else
message =
case type do
:string -> "too big: must have at most %{count} character(s)"
:array -> "too big: must have at most %{count} item(s)"
:number -> "too big: must be at most %{count}"
:date -> "too big: must be at most %{count}"
end
new(
code: :less_than_or_equal_to,
issue: {message, [count: max]},
path: opts[:path] || []
)
end
end
@doc """
Creates a greater than or equal to error for the given type and minimum value.
## Example
iex> Zoi.Error.greater_than_or_equal_to(:string, 3)
%Zoi.Error{
code: :greater_than_or_equal_to,
issue: {"too small: must have at least %{count} character(s)", [count: 3]},
message: "too small: must have at least 3 character(s)"
}
"""
@spec greater_than_or_equal_to(:string | :array | :number | :date, any(), keyword()) :: t()
def greater_than_or_equal_to(type, min, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
if msg do
custom_error(issue: {msg, [count: min]})
else
message =
case type do
:string -> "too small: must have at least %{count} character(s)"
:array -> "too small: must have at least %{count} item(s)"
:number -> "too small: must be at least %{count}"
:date -> "too small: must be at least %{count}"
end
new(
code: :greater_than_or_equal_to,
issue: {message, [count: min]},
path: opts[:path] || []
)
end
end
@doc """
Creates a greater than error for the given type and minimum value.
## Example
iex> Zoi.Error.greater_than(:number, 5)
%Zoi.Error{
code: :greater_than,
issue: {"too small: must be greater than %{count}", [count: 5]},
message: "too small: must be greater than 5"
}
"""
@spec greater_than(:number | :date, any(), keyword()) :: t()
def greater_than(type, min, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
if msg do
custom_error(issue: {msg, [count: min]})
else
message =
case type do
:number -> "too small: must be greater than %{count}"
:date -> "too small: must be greater than %{count}"
end
new(
code: :greater_than,
issue: {message, [count: min]},
path: opts[:path] || []
)
end
end
@doc """
Creates a less than error for the given type and maximum value.
## Example
iex> Zoi.Error.less_than(:number, 10)
%Zoi.Error{
code: :less_than,
issue: {"too big: must be less than %{count}", [count: 10]},
message: "too big: must be less than 10"
}
"""
@spec less_than(:number | :date, any(), keyword()) :: t()
def less_than(type, max, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
if msg do
custom_error(issue: {msg, [count: max]})
else
message =
case type do
:number -> "too big: must be less than %{count}"
:date -> "too big: must be less than %{count}"
end
new(
code: :less_than,
issue: {message, [count: max]},
path: opts[:path] || []
)
end
end
@doc """
Creates an invalid length error for the given type and length.
## Example
iex> Zoi.Error.invalid_length(:string, 5)
%Zoi.Error{
code: :invalid_length,
issue: {"invalid length: must have %{count} character(s)", [count: 5]},
message: "invalid length: must have 5 character(s)"
}
"""
@spec invalid_length(:string | :array, non_neg_integer(), keyword()) :: t()
def invalid_length(type, length, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
if msg do
custom_error(issue: {msg, [count: length]})
else
message =
case type do
:string -> "invalid length: must have %{count} character(s)"
:array -> "invalid length: must have %{count} item(s)"
end
new(
code: :invalid_length,
issue: {message, [count: length]},
path: opts[:path] || []
)
end
end
@doc """
Creates an invalid format error for the given starting string.
## Example
iex> Zoi.Error.invalid_starting_string("http")
%Zoi.Error{
code: :invalid_format,
issue: {"invalid format: must start with '%{value}'", [value: "http"]},
message: "invalid format: must start with 'http'"
}
"""
@spec invalid_starting_string(binary(), keyword()) :: t()
def invalid_starting_string(prefix, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
if msg do
custom_error(issue: {msg, [value: prefix]})
else
new(
code: :invalid_format,
issue: {"invalid format: must start with '%{value}'", [value: prefix]},
path: opts[:path] || []
)
end
end
@doc """
Creates an invalid format error for the given ending string.
## Example
iex> Zoi.Error.invalid_ending_string(".com")
%Zoi.Error{
code: :invalid_format,
issue: {"invalid format: must end with '%{value}'", [value: ".com"]},
message: "invalid format: must end with '.com'"
}
"""
@spec invalid_ending_string(binary(), keyword()) :: t()
def invalid_ending_string(suffix, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
if msg do
custom_error(issue: {msg, [value: suffix]})
else
new(
code: :invalid_format,
issue: {"invalid format: must end with '%{value}'", [value: suffix]},
path: opts[:path] || []
)
end
end
def invalid_url(url, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
if msg do
custom_error(issue: {msg, [value: url]})
else
new(
code: :invalid_format,
issue: {"invalid format: must be a valid URL", [value: url]},
path: opts[:path] || []
)
end
end
@doc ~S"""
Creates an invalid format error for the given regex pattern.
## Example
iex> %Zoi.Error{} = error = Zoi.Error.invalid_format(~r/^[^a-z]*$/, format: :upcase)
iex> error.code
:invalid_format
iex> {msg, opts} = error.issue
iex> msg
"invalid format: must match pattern %{pattern}"
iex> Regex.source(opts[:pattern])
"^[^a-z]*$"
iex> opts[:format]
:upcase
iex> error.message
"invalid format: must match pattern ^[^a-z]*$"
"""
@spec invalid_format(Regex.t(), keyword()) :: t()
def invalid_format(pattern, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
{format, opts} = Keyword.pop(opts, :format)
default_issue_opts = [pattern: pattern]
if msg do
custom_error(issue: {msg, default_issue_opts})
else
{message, opts} =
Keyword.pop(opts, :internal_message, "invalid format: must match pattern %{pattern}")
issue_opts =
case format do
nil -> default_issue_opts
_format -> [{:format, format} | default_issue_opts]
end
new(
code: :invalid_format,
issue: {message, issue_opts},
path: opts[:path] || []
)
end
end
@doc """
Creates a multiple_of error for the given value.
## Example
iex> Zoi.Error.multiple_of(5)
%Zoi.Error{
code: :multiple_of,
issue: {"must be a multiple of %{value}", [value: 5]},
message: "must be a multiple of 5"
}
"""
@spec multiple_of(number(), keyword()) :: t()
def multiple_of(value, opts \\ []) do
{msg, opts} = Keyword.pop(opts, :error)
if msg do
custom_error(issue: {msg, [value: value]})
else
new(
code: :multiple_of,
issue: {"must be a multiple of %{value}", [value: value]},
path: opts[:path] || []
)
end
end
@doc """
Creates a custom error with the given options.
## Example
iex> Zoi.Error.custom_error(issue: {"error %{num}", [num: 404]})
%Zoi.Error{
code: :custom,
issue: {"error %{num}", [num: 404]},
message: "error 404",
path: []
}
"""
@spec custom_error(opts :: error_opts()) :: t()
def custom_error(opts \\ []) do
new([{:code, :custom} | opts])
end
end
defmodule Zoi.ParseError do
@moduledoc false
defexception [:errors]
@impl true
def exception(opts) do
struct!(__MODULE__, opts)
end
@impl true
def message(%__MODULE__{errors: errors}) when is_list(errors) do
"""
Parsing error:
#{Zoi.prettify_errors(errors)}
"""
end
end