Skip to content

Add config option for detailed message for nested errors #77

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
322 changes: 180 additions & 142 deletions lib/ex_json_schema/validator/error/string_formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,212 +8,250 @@ defmodule ExJsonSchema.Validator.Error.StringFormatter do
end)
end

defimpl String.Chars, for: Error.AdditionalItems do
def to_string(%Error.AdditionalItems{}) do
"Schema does not allow additional items."
end
def message(error) do
message(error, detailed?())
end

defimpl String.Chars, for: Error.AdditionalProperties do
def to_string(%Error.AdditionalProperties{}) do
"Schema does not allow additional properties."
end
def message(%Error.AdditionalItems{}, _) do
"Schema does not allow additional items."
end

defimpl String.Chars, for: Error.AllOf do
def to_string(%Error.AllOf{invalid: invalid}) do
"Expected all of the schemata to match, but the schemata at the following indexes did not: #{
Enum.map_join(invalid, ", ", & &1.index)
}."
end
def message(%Error.AdditionalProperties{}, _) do
"Schema does not allow additional properties."
end

defimpl String.Chars, for: Error.AnyOf do
def to_string(%Error.AnyOf{}) do
"Expected any of the schemata to match but none did."
end
def message(%Error.AllOf{invalid: invalid}, false) do
"Expected all of the schemata to match, but the schemata at the following indexes did not: #{Enum.map_join(invalid, ", ", & &1.index)}."
end

defimpl String.Chars, for: Error.Const do
def to_string(%Error.Const{expected: expected}) do
"Expected data to be #{inspect(expected)}."
end
def message(%Error.AllOf{} = error, true) do
"""
#{message(error, false)}

#{nest_errors(error)}
"""
end

defimpl String.Chars, for: Error.Contains do
def to_string(%Error.Contains{}) do
"Expected any of the items to match the schema but none did."
end
def message(%Error.AnyOf{}, false) do
"Expected any of the schemata to match but none did."
end

defimpl String.Chars, for: Error.ContentEncoding do
def to_string(%Error.ContentEncoding{expected: expected}) do
"Expected the content to be #{expected}-encoded."
end
def message(%Error.AnyOf{} = error, true) do
"""
#{message(error, false)}

#{nest_errors(error)}
"""
end

defimpl String.Chars, for: Error.ContentMediaType do
def to_string(%Error.ContentMediaType{expected: expected}) do
"Expected the content to be of media type #{expected}."
end
def message(%Error.Const{expected: expected}, _) do
"Expected data to be #{inspect(expected)}."
end

defimpl String.Chars, for: Error.Dependencies do
def to_string(%Error.Dependencies{property: property, missing: [missing]}) do
"Property #{property} depends on property #{missing} to be present but it was not."
end
def message(%Error.Contains{}, false) do
"Expected any of the items to match the schema but none did."
end

def to_string(%Error.Dependencies{property: property, missing: missing}) do
"Property #{property} depends on properties #{Enum.join(missing, ", ")} to be present but they were not."
end
def message(%Error.Contains{} = error, true) do
"""
#{message(error, false)}

#{nest_errors(error)}
"""
end

defimpl String.Chars, for: Error.Enum do
def to_string(%Error.Enum{}) do
"Value is not allowed in enum."
end
def message(%Error.ContentEncoding{expected: expected}, _) do
"Expected the content to be #{expected}-encoded."
end

defimpl String.Chars, for: Error.False do
def to_string(%Error.False{}) do
"False schema never matches."
end
def message(%Error.ContentMediaType{expected: expected}, _) do
"Expected the content to be of media type #{expected}."
end

defimpl String.Chars, for: Error.Format do
def to_string(%Error.Format{expected: expected}) do
"Expected to be a valid #{format_name(expected)}."
end
def message(%Error.Dependencies{property: property, missing: [missing]}, _) do
"Property #{property} depends on property #{missing} to be present but it was not."
end

defp format_name("date-time"), do: "ISO 8601 date-time"
defp format_name("ipv4"), do: "IPv4 address"
defp format_name("ipv6"), do: "IPv6 address"
defp format_name(expected), do: expected
def message(%Error.Dependencies{property: property, missing: missing}, _) do
"Property #{property} depends on properties #{Enum.join(missing, ", ")} to be present but they were not."
end

defimpl String.Chars, for: Error.IfThenElse do
def to_string(%Error.IfThenElse{branch: branch}) do
"Expected the schema in the #{branch} branch to match but it did not."
end
def message(%Error.Enum{}, _) do
"Value is not allowed in enum."
end

defimpl String.Chars, for: Error.ItemsNotAllowed do
def to_string(%Error.ItemsNotAllowed{}) do
"Items are not allowed."
end
def message(%Error.False{}, _) do
"False schema never matches."
end

defimpl String.Chars, for: Error.MaxItems do
def to_string(%Error.MaxItems{expected: expected, actual: actual}) do
"Expected a maximum of #{expected} items but got #{actual}."
end
def message(%Error.Format{expected: expected}, _) do
"Expected to be a valid #{format_name(expected)}."
end

defimpl String.Chars, for: Error.MaxLength do
def to_string(%Error.MaxLength{expected: expected, actual: actual}) do
"Expected value to have a maximum length of #{expected} but was #{actual}."
end
def message(%Error.IfThenElse{branch: branch}, _) do
"Expected the schema in the #{branch} branch to match but it did not."
end

defimpl String.Chars, for: Error.MaxProperties do
def to_string(%Error.MaxProperties{expected: expected, actual: actual}) do
"Expected a maximum of #{expected} properties but got #{actual}"
end
def message(%Error.ItemsNotAllowed{}, _) do
"Items are not allowed."
end

defimpl String.Chars, for: Error.Maximum do
def to_string(%Error.Maximum{expected: expected, exclusive?: exclusive?}) do
"Expected the value to be #{if exclusive?, do: "<", else: "<="} #{expected}"
end
def message(%Error.MaxItems{expected: expected, actual: actual}, _) do
"Expected a maximum of #{expected} items but got #{actual}."
end

defimpl String.Chars, for: Error.MinItems do
def to_string(%Error.MinItems{expected: expected, actual: actual}) do
"Expected a minimum of #{expected} items but got #{actual}."
end
def message(%Error.MaxLength{expected: expected, actual: actual}, _) do
"Expected value to have a maximum length of #{expected} but was #{actual}."
end

defimpl String.Chars, for: Error.MinLength do
def to_string(%Error.MinLength{expected: expected, actual: actual}) do
"Expected value to have a minimum length of #{expected} but was #{actual}."
end
def message(%Error.MaxProperties{expected: expected, actual: actual}, _) do
"Expected a maximum of #{expected} properties but got #{actual}"
end

defimpl String.Chars, for: Error.MinProperties do
def to_string(%Error.MinProperties{expected: expected, actual: actual}) do
"Expected a minimum of #{expected} properties but got #{actual}"
end
def message(%Error.Maximum{expected: expected, exclusive?: exclusive?}, _) do
"Expected the value to be #{if exclusive?, do: "<", else: "<="} #{expected}"
end

defimpl String.Chars, for: Error.Minimum do
def to_string(%Error.Minimum{expected: expected, exclusive?: exclusive?}) do
"Expected the value to be #{if exclusive?, do: ">", else: ">="} #{expected}"
end
def message(%Error.MinItems{expected: expected, actual: actual}, _) do
"Expected a minimum of #{expected} items but got #{actual}."
end

defimpl String.Chars, for: Error.MultipleOf do
def to_string(%Error.MultipleOf{expected: expected}) do
"Expected value to be a multiple of #{expected}."
end
def message(%Error.MinLength{expected: expected, actual: actual}, _) do
"Expected value to have a minimum length of #{expected} but was #{actual}."
end

defimpl String.Chars, for: Error.Not do
def to_string(%Error.Not{}) do
"Expected schema not to match but it did."
end
def message(%Error.MinProperties{expected: expected, actual: actual}, _) do
"Expected a minimum of #{expected} properties but got #{actual}"
end

defimpl String.Chars, for: Error.OneOf do
def to_string(%Error.OneOf{valid_indices: valid_indices}) do
if length(valid_indices) > 1 do
"Expected exactly one of the schemata to match, but the schemata at the following indexes did: " <>
Enum.join(valid_indices, ", ") <> "."
else
"Expected exactly one of the schemata to match, but none of them did."
end
end
def message(%Error.Minimum{expected: expected, exclusive?: exclusive?}, _) do
"Expected the value to be #{if exclusive?, do: ">", else: ">="} #{expected}"
end

defimpl String.Chars, for: Error.Pattern do
def to_string(%Error.Pattern{expected: expected}) do
"Does not match pattern #{inspect(expected)}."
end
def message(%Error.MultipleOf{expected: expected}, _) do
"Expected value to be a multiple of #{expected}."
end

defimpl String.Chars, for: Error.PropertyNames do
def to_string(%Error.PropertyNames{invalid: invalid}) do
"Expected the property names to be valid but the following were not: #{
invalid |> Map.keys() |> Enum.sort() |> Enum.join(", ")
}."
end
def message(%Error.Not{}, _) do
"Expected schema not to match but it did."
end

defimpl String.Chars, for: Error.Required do
def to_string(%Error.Required{missing: [missing]}) do
"Required property #{missing} was not present."
def message(%Error.OneOf{valid_indices: valid_indices}, false) do
if length(valid_indices) > 1 do
"Expected exactly one of the schemata to match, but the schemata at the following indexes did: " <>
Enum.join(valid_indices, ", ") <> "."
else
"Expected exactly one of the schemata to match, but none of them did."
end
end

def to_string(%Error.Required{missing: missing}) do
"Required properties #{Enum.join(missing, ", ")} were not present."
def message(%Error.OneOf{valid_indices: valid_indices} = error, true) do
message = message(error)

if length(valid_indices) > 1 do
message
else
"""
#{message}

#{nest_errors(error)}
"""
end
end

defimpl String.Chars, for: Error.Type do
def to_string(%Error.Type{expected: expected, actual: actual}) do
"Type mismatch. Expected #{type_names(expected)} but got #{type_names(actual)}."
end
def message(%Error.Pattern{expected: expected}, _) do
"Does not match pattern #{inspect(expected)}."
end

defp type_names(types) do
types
|> List.wrap()
|> Enum.map(&String.capitalize/1)
|> Enum.join(", ")
end
def message(%Error.PropertyNames{invalid: invalid}, _) do
"Expected the property names to be valid but the following were not: #{invalid |> Map.keys() |> Enum.sort() |> Enum.join(", ")}."
end

def message(%Error.Required{missing: [missing]}, _) do
"Required property #{missing} was not present."
end

def message(%Error.Required{missing: missing}, _) do
"Required properties #{Enum.join(missing, ", ")} were not present."
end

def message(%Error.Type{expected: expected, actual: actual}, _) do
"Type mismatch. Expected #{type_names(expected)} but got #{type_names(actual)}."
end

def message(%Error.UniqueItems{}, _) do
"Expected items to be unique but they were not."
end

defp format_name("date-time"), do: "ISO 8601 date-time"
defp format_name("ipv4"), do: "IPv4 address"
defp format_name("ipv6"), do: "IPv6 address"
defp format_name(expected), do: expected

defp type_names(types) do
types
|> List.wrap()
|> Enum.map(&String.capitalize/1)
|> Enum.join(", ")
end

defp nest_errors(%{invalid: invalid}) do
error_messages =
Enum.map_join(invalid, "\n", fn invalid ->
"#{invalid.index}: " <>
Enum.map_join(invalid.errors, "\n", fn %Error{error: error} -> to_string(error) end)
end)
|> String.replace("\n", "\n ")

"""
The following errors were found:
#{error_messages}
"""
|> String.trim()
end

def detailed?() do
Application.get_env(:ex_json_schema, :detailed_errors, false)
end

defimpl String.Chars, for: Error.UniqueItems do
def to_string(%Error.UniqueItems{}) do
"Expected items to be unique but they were not."
@error_types [
Error.AdditionalItems,
Error.AdditionalProperties,
Error.AllOf,
Error.AnyOf,
Error.Const,
Error.Contains,
Error.ContentEncoding,
Error.ContentMediaType,
Error.Dependencies,
Error.Enum,
Error.False,
Error.Format,
Error.IfThenElse,
Error.InvalidAtIndex,
Error.ItemsNotAllowed,
Error.MaxItems,
Error.MaxLength,
Error.MaxProperties,
Error.Maximum,
Error.MinItems,
Error.MinLength,
Error.MinProperties,
Error.Minimum,
Error.MultipleOf,
Error.Not,
Error.OneOf,
Error.Pattern,
Error.PropertyNames,
Error.Required,
Error.Type,
Error.UniqueItems
]

for error_type <- @error_types do
defimpl String.Chars, for: error_type do
def to_string(error),
do: ExJsonSchema.Validator.Error.StringFormatter.message(error)
end
end
end