Packages
jsv
0.19.1
0.21.2
0.21.1
0.21.0
0.20.0
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.3
0.18.2
0.18.1
0.18.0
0.17.1
0.17.0
0.16.0
0.15.2
0.15.1
0.15.0
0.14.0
0.13.1
0.13.0
0.12.0
0.11.5
0.11.4
0.11.3
retired
0.11.2
0.11.1
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.0
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
A JSON Schema Validator with complete support for the latest specifications.
Current section
Files
Jump to
Current section
Files
lib/jsv/error_formatter.ex
defmodule JSV.ErrorFormatter do
alias JSV.Key
alias JSV.Normalizer
alias JSV.Ref
alias JSV.ValidationError
alias JSV.Validator
alias JSV.Validator.Error
@moduledoc """
Error formatting helpers.
Errors are grouped by:
* Instance location: the bit of data that was invalidated
* Schema location: the part of the schema that invalidated it
* Evaluation path: the path followed from the root to this schema location
"""
@type error_unit :: %{
required(:valid) => boolean,
required(:instanceLocation) => binary,
required(:evaluationPath) => binary,
required(:schemaLocation) => binary,
optional(:errors) => [keyword_error]
}
@type keyword_error :: %{
required(:kind) => atom,
required(:message) => String.t(),
optional(:details) => [error_unit]
}
@type raw_path :: [raw_path] | binary | integer | atom
@doc false
@spec error_schema :: module
def error_schema do
__MODULE__.ValidationErrorSchema
end
@normalize_opts_schema NimbleOptions.new!(
sort: [
type: {:in, [:asc, :desc]},
default: :desc,
doc: """
Controls the sort direction. Errors are sorted by `instanceLocation`.
"""
],
keys: [
type: {:in, [:atoms, :strings]},
doc: """
Define the type of the keys in the normalized errors maps.
While truly "normalized" JSON data should not have atom keys,
this option defaults to :atoms for backward compatibility reasons.
"""
]
)
@type normalize_opt :: unquote(NimbleOptions.option_typespec(@normalize_opts_schema))
@doc """
Returns a JSON-able version of the errors contained in the ValidationError.
This is generatlly useful to generate HTTP API responses or message broker
responses.
### Options
#{NimbleOptions.docs(@normalize_opts_schema)}
"""
@spec normalize_error(ValidationError.t(), keyword) :: map()
def normalize_error(%ValidationError{} = e, opts \\ []) do
opts =
opts
|> NimbleOptions.validate!(@normalize_opts_schema)
|> Keyword.put_new(:keys, :atoms)
top = %{valid: false, details: normalize_errors(e.errors, opts)}
normalize_keys(top, opts)
end
defp normalize_keys(with_atoms, opts) do
case opts[:keys] do
:atoms -> with_atoms
:strings -> Normalizer.normalize(with_atoms)
end
end
defp normalize_errors(errors, formatter \\ nil, opts) do
errors
|> Enum.group_by(fn
%Error{data_path: dp, eval_path: ep, schema_path: sp} ->
{dp, ep, sp}
%{valid: _, instanceLocation: _, evaluationPath: _, schemaLocation: _} = already_normalized ->
already_normalized
other when formatter != nil ->
raise ArgumentError,
"invalid annotation returned by #{inspect(formatter)}, expected #{inspect(Error)} struct or raw annotation, got: #{other}"
other ->
raise ArgumentError, "invalid annotation, expected #{inspect(Error)} struct or raw annotation, got: #{other}"
end)
|> Enum.map(fn
{{data_path, eval_path, schema_path}, errors} -> error_annot(data_path, eval_path, schema_path, errors, opts)
{already_normalized, [already_normalized]} -> already_normalized
end)
|> sort_errors(opts[:sort])
end
defp sort_errors(errors, order) do
Enum.sort_by(errors, & &1.instanceLocation, order)
end
defp error_annot(rev_data_path, rev_eval_path, rev_schema_path, errors, opts) do
errors_fmt = Enum.map(errors, &build_error(&1, opts))
%{
valid: false,
errors: errors_fmt,
instanceLocation: format_data_path(rev_data_path),
evaluationPath: format_eval_path(rev_eval_path),
schemaLocation: format_schema_path(rev_schema_path)
}
end
defp build_error(error, opts) do
%Error{kind: kind, data: data, formatter: formatter, args: args} =
error
args_map = Map.new(args)
case formatter.format_error(kind, args_map, data) do
message when is_binary(message) ->
%{message: message, kind: kind}
tuple when is_tuple(tuple) ->
cast_deprecated_error_format(tuple, kind, opts, formatter)
%{message: message} = map when is_binary(message) ->
build_error(Map.delete(map, :message), %{message: message, kind: kind}, formatter, opts)
end
end
defp build_error(map, acc, formatter, opts) do
Enum.reduce(map, acc, fn
{:annots, annots}, acc when is_list(annots) ->
Map.put(acc, :details, normalize_errors(annots, formatter, opts))
{:kind, kind}, acc when is_binary(kind) when is_atom(kind) ->
Map.put(acc, :kind, kind)
{k, v}, _ when k in [:annots, :kind] ->
raise "invalid format_error value for key #{inspect(k)} from formatter #{inspect(formatter)}: #{inspect(v)}"
{k, v}, _ ->
IO.warn("unknown format_error value with key #{inspect(k)} from formatter #{inspect(formatter)}: #{inspect(v)}")
acc
end)
end
defp cast_deprecated_error_format(tuple, kind, opts, formatter) do
:ok = warn_deprecated_format_error(formatter, tuple)
case tuple do
{new_kind, message} when is_atom(new_kind) and is_binary(message) ->
%{message: message, kind: new_kind}
{message, sub_errors} when is_binary(message) and is_list(sub_errors) ->
%{message: message, kind: kind, details: normalize_errors(sub_errors, formatter, opts)}
{new_kind, message, sub_errors} when is_atom(new_kind) and is_binary(message) and is_list(sub_errors) ->
%{message: message, kind: new_kind, details: normalize_errors(sub_errors, formatter, opts)}
end
end
if Mix.env() == :test do
@spec warn_deprecated_format_error(module, term) :: :ok
defp warn_deprecated_format_error(formatter, value) do
if Process.get({__ENV__.file, __ENV__.line}) == 1 do
:ok
else
raise """
deprecated format_error return value from #{inspect(formatter)}
Expected message or map but got:
#{inspect(value, pretty: true)}
"""
end
end
else
defp warn_deprecated_format_error(formatter, _) do
IO.warn("deprecated format_error return value from #{inspect(formatter)}")
end
end
@doc """
Returns an output unit with `valid: true` for the given
`#{inspect(Validator)}`. This can be substitued to an Error struct in the
nested details of an error. Mostly used to show multiple validated schemas
with `:oneOf`.
"""
@spec valid_annot(Validator.validator(), Validator.context()) :: error_unit
def valid_annot(subschema, vctx) do
evaluation_path = format_eval_path(vctx.eval_path)
%{
valid: true,
instanceLocation: format_data_path(vctx.data_path),
evaluationPath: evaluation_path,
schemaLocation: format_schema_path(subschema)
}
end
@spec format_data_path(raw_path) :: String.t()
def format_data_path([]) do
"#"
end
def format_data_path(rev_data_path) do
iodata =
List.foldl(rev_data_path, [], fn
index, acc when is_integer(index) -> [?/, Integer.to_string(index) | acc]
key, acc -> [?/, Ref.escape_json_pointer(key) | acc]
end)
IO.iodata_to_binary(["#" | iodata])
end
@doc false
@spec format_eval_path(list) :: binary
def format_eval_path([]) do
"#"
end
def format_eval_path(rev_eval_path) do
# map to string but also reverse the path
iodata = List.foldl(rev_eval_path, [], fn segment, acc -> [?/, format_eval_path_segment(segment) | acc] end)
IO.iodata_to_binary(["#" | iodata])
end
@doc false
@spec format_schema_path([term] | Validator.validator()) :: binary
def format_schema_path(rev_path)
def format_schema_path([]) do
"#"
end
def format_schema_path(rev_path) when is_list(rev_path) do
format_schema_path(rev_path, [])
end
def format_schema_path({:alias_of, key}) do
Key.to_iodata(key)
end
def format_schema_path(%{schema_path: sp}) do
format_schema_path(sp)
end
defp format_schema_path([segment, next | rev_path], acc) do
format_schema_path([next | rev_path], [?/, format_eval_path_segment(segment) | acc])
end
defp format_schema_path([last], acc) do
final_path =
case last do
:root -> IO.iodata_to_binary(["#" | acc])
id when is_binary(id) -> [id, "#" | acc]
end
IO.iodata_to_binary(final_path)
end
defp format_eval_path_segment(item) do
case item do
atom when is_atom(atom) -> Ref.escape_json_pointer(Atom.to_string(atom))
key when is_binary(key) -> Ref.escape_json_pointer(key)
index when is_integer(index) -> Integer.to_string(index)
{tag, key} when is_binary(key) -> [Atom.to_string(tag), ?/, Ref.escape_json_pointer(key)]
{tag, index} when is_integer(index) -> [Atom.to_string(tag), ?/, Integer.to_string(index)]
{_, :"$ref", _} -> "$ref"
{_, :"$dynamicRef", _} -> "$dynamicRef"
other -> raise "invalid eval path segment: #{inspect(other)}"
end
end
end