defmodule JsonSchema.Parser.ErrorUtil do
@moduledoc """
Contains helper functions for reporting parser errors.
"""
alias Jason.DecodeError
alias JsonSchema.{Parser, Types}
alias Parser.{ParserError, Util}
@spec could_not_read_file(Path.t()) :: ParserError.t()
def could_not_read_file(schema_path) do
error_msg = """
Failed to read file at #{to_string(schema_path)}. Are you sure the file path is correct?
"""
ParserError.new(to_string(schema_path), :could_not_read_file, error_msg)
end
@spec invalid_json(Path.t(), DecodeError.t()) :: ParserError.t()
def invalid_json(schema_path, decode_error) do
error_msg = """
Failed to parse file at #{to_string(schema_path)} as JSON.
#{DecodeError.message(decode_error)}
"""
ParserError.new(to_string(schema_path), :invalid_json, error_msg)
end
@spec unsupported_schema_version(String.t(), [String.t()]) :: ParserError.t()
def unsupported_schema_version(supplied_value, supported_versions) do
root_path = URI.parse("#")
stringified_value = sanitize_value(supplied_value)
# TODO: Add a config/option argument for `json_schema` that can be used to
# determine whether to return a human readable error description or a
# machine readable error.
error_msg = """
Unsupported JSON schema version found at '#'.
"$schema": #{stringified_value}
#{error_markings(stringified_value)}
Was expecting one of the following types:
#{inspect(supported_versions)}
Hint: See the specification section 7. "The '$schema' keyword"
<https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-01#section-7>
"""
ParserError.new(root_path, :unsupported_schema_version, error_msg)
end
@spec missing_property(Types.typeIdentifier(), String.t()) :: ParserError.t()
def missing_property(identifier, property) do
full_identifier = print_identifier(identifier)
error_msg = """
Could not find property '#{property}' at '#{full_identifier}'
"""
ParserError.new(identifier, :missing_property, error_msg)
end
@spec invalid_enum(Types.typeIdentifier(), String.t(), [String.t()], Types.json_value()) ::
ParserError.t()
def invalid_enum(identifier, property, expected_values, actual_value) do
stringified_value = sanitize_value(actual_value)
full_identifier = print_identifier(identifier)
padding = whitespace(property)
error_msg = """
Expected value of property '#{property}' at '#{full_identifier}'
to be in #{inspect(expected_values)} but found the value #{stringified_value}
"#{property}": #{stringified_value}
#{padding} #{error_markings(stringified_value)}
"""
ParserError.new(identifier, :unexpected_value, error_msg)
end
@spec invalid_type(Types.typeIdentifier(), String.t(), String.t(), Types.json_value()) ::
ParserError.t()
def invalid_type(identifier, property, expected_type, actual_value) do
actual_type = Util.get_type(actual_value)
stringified_value = sanitize_value(actual_value)
full_identifier = print_identifier(identifier)
padding = whitespace(property)
error_msg = """
Expected value of property '#{property}' at '#{full_identifier}'
to be of type '#{expected_type}' but found a value of type '#{actual_type}'
"#{property}": #{stringified_value}
#{padding} #{error_markings(stringified_value)}
"""
ParserError.new(identifier, :unexpected_type, error_msg)
end
@spec schema_name_collision(Types.typeIdentifier()) :: ParserError.t()
def schema_name_collision(identifier) do
full_identifier = print_identifier(identifier)
error_msg = """
Found more than one schema with id: '#{full_identifier}'
"""
ParserError.new(identifier, :name_collision, error_msg)
end
@spec name_collision(Types.typeIdentifier()) :: ParserError.t()
def name_collision(identifier) do
full_identifier = print_identifier(identifier)
error_msg = """
Found more than one property with identifier '#{full_identifier}'
"""
ParserError.new(identifier, :name_collision, error_msg)
end
@spec name_not_a_regex(Types.typeIdentifier(), String.t()) :: ParserError.t()
def name_not_a_regex(identifier, property) do
full_identifier = print_identifier(identifier)
error_msg = """
Could not parse pattern '#{property}' at '#{full_identifier}' into a valid Regular Expression.
Hint: See specification section 6.5.5 "patternProperties"
<https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6.5.5>
"""
ParserError.new(identifier, :name_not_a_regex, error_msg)
end
@spec invalid_uri(Types.typeIdentifier(), String.t(), String.t()) ::
ParserError.t()
def invalid_uri(identifier, property, actual) do
full_identifier = print_identifier(identifier)
stringified_value = sanitize_value(actual)
error_msg = """
Could not parse property '#{property}' at '#{full_identifier}' into a valid URI.
"id": #{stringified_value}
#{error_markings(stringified_value)}
Hint: See URI specification section 3. "Syntax Components"
<https://datatracker.ietf.org/doc/html/rfc3986#section-3>
"""
ParserError.new(identifier, :invalid_uri, error_msg)
end
@spec unresolved_reference(
Types.typeIdentifier(),
URI.t()
) :: ParserError.t()
def unresolved_reference(identifier, parent) do
printed_path = to_string(parent)
stringified_value = sanitize_value(identifier)
error_msg = """
The following reference at `#{printed_path}` could not be resolved
"$ref": #{stringified_value}
#{error_markings(stringified_value)}
Hint: See the specification section 8.2 "Base URI and Dereferencing"
<https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-01#section-8>
"""
ParserError.new(parent, :unresolved_reference, error_msg)
end
@spec unknown_type(String.t()) :: ParserError.t()
def unknown_type(type_name) do
error_msg = "Could not find parser for type: '#{type_name}'"
ParserError.new(type_name, :unknown_type, error_msg)
end
@spec unexpected_type(Types.typeIdentifier(), String.t()) :: ParserError.t()
def unexpected_type(identifier, error_msg) do
ParserError.new(identifier, :unexpected_type, error_msg)
end
@spec unknown_union_type(Types.typeIdentifier(), String.t()) ::
ParserError.t()
def unknown_union_type(identifier, type_name) do
printed_path = to_string(identifier)
error_msg = """
Encountered unknown union type at `#{printed_path}`
"type": [#{type_name}]
#{error_markings(type_name)}
Hint: See the specification section 6. "Validation Keywords"
<https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6.1.1>
"""
ParserError.new(identifier, :unknown_union_type, error_msg)
end
@spec unknown_enum_type(String.t()) :: ParserError.t()
def unknown_enum_type(type_name) do
error_msg = "Unknown or unsupported enum type: '#{type_name}'"
ParserError.new(type_name, :unknown_enum_type, error_msg)
end
@spec unknown_primitive_type(String.t()) :: ParserError.t()
def unknown_primitive_type(type_name) do
error_msg = "Unknown or unsupported primitive type: '#{type_name}'"
ParserError.new(type_name, :unknown_primitive_type, error_msg)
end
@spec unknown_node_type(
URI.t(),
String.t() | :anonymous,
Types.schemaNode()
) :: ParserError.t()
def unknown_node_type(identifier, name, schema_node) do
full_identifier =
if name == :anonymous do
identifier |> to_string()
else
identifier
|> Util.add_fragment_child(name)
|> to_string()
end
stringified_value = sanitize_value(schema_node)
error_msg = """
The value of "type" at '#{full_identifier}' did not match a known node type
"type": #{stringified_value}
#{error_markings(stringified_value)}
Was expecting one of the following types
["null", "boolean", "object", "array", "number", "integer", "string"]
Hint: See the specification section 6.25. "Validation keywords - type"
<https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.1>
"""
ParserError.new(full_identifier, :unknown_node_type, error_msg)
end
@spec print_identifier(Types.typeIdentifier()) :: String.t()
defp print_identifier(identifier) do
to_string(identifier)
end
@spec sanitize_value(value :: any) :: String.t()
defp sanitize_value(%URI{} = value), do: URI.to_string(value)
defp sanitize_value(value) when is_list(value), do: Jason.encode!(value)
defp sanitize_value(value) when is_map(value), do: Jason.encode!(value)
defp sanitize_value(value), do: inspect(value)
@spec error_markings(String.t()) :: [String.t()]
defp error_markings(value) do
red(String.duplicate("^", String.length(value)))
end
@spec whitespace(String.t()) :: String.t()
defp whitespace(value) do
String.duplicate(" ", String.length(value))
end
@spec red(String.t()) :: [String.t()]
defp red(str) do
IO.ANSI.format([:red, str])
end
end