Current section
Files
Jump to
Current section
Files
lib/validate.ex
defmodule JTD.ValidationError do
@moduledoc """
ValidationError object which has instance path and schema path.
"""
@type t :: %__MODULE__{instance_path: [...], schema_path: [...]}
defstruct instance_path: [], schema_path: []
@doc """
Returns JTD.ValidationError struct generated from given map. This is for testing purposes only.
"""
def from_map(%{ "instancePath" => instance_path, "schemaPath" => schema_path }) do
%__MODULE__{instance_path: instance_path, schema_path: schema_path}
end
def from_map(%{ instancePath: instance_path, schemaPath: schema_path }) do
%__MODULE__{instance_path: instance_path, schema_path: schema_path}
end
end
defmodule JTD.MaxErrorsReachedError do
@moduledoc """
Exception that occurs when the option is given and the maximum number of errors has been reached.
"""
@type t :: %__MODULE__{message: String.t(), state: any}
defexception message: nil, state: nil
end
defmodule JTD.MaxDepthExceededError do
@moduledoc """
Exception that occurs when the option is given and map reaches the maximum depth.
"""
@type t :: %__MODULE__{message: String.t()}
defexception message: nil
end
defmodule JTD.ValidationState do
@moduledoc false
alias JTD.{ValidationError, MaxErrorsReachedError}
defstruct [:options, :root_schema, :instance_tokens, :schema_tokens, :errors]
def push_schema_token_stack(state, token) do
Map.update(state, :schema_tokens, [[]], fn current -> [token] ++ current end)
end
def pop_schema_token_stack(state) do
Map.update(state, :schema_tokens, [[]], fn current -> current |> List.pop_at(0) |> elem(1) end)
end
def push_schema_token(state, token) do
pushed = [token | List.first(state.schema_tokens)]
Map.update(state, :schema_tokens, [[]], fn current -> current |> List.replace_at(0, pushed) end)
end
def pop_schema_token(state) do
popped = state |> Map.get(:schema_tokens) |> List.first |> List.pop_at(0) |> elem(1)
Map.update(state, :schema_tokens, [[]], fn current -> current |> List.replace_at(0, popped) end)
end
def push_instance_token(state, token) do
pushed = [token | state.instance_tokens]
Map.update(state, :instance_tokens, [], fn _ -> pushed end)
end
def pop_instance_token(state) do
popped = state |> Map.get(:instance_tokens) |> List.pop_at(0) |> elem(1)
Map.update(state, :instance_tokens, [], fn _ -> popped end)
end
def push_error(state) do
error = %ValidationError{instance_path: state.instance_tokens |> Enum.reverse, schema_path: state.schema_tokens |> List.first |> Enum.reverse}
state = Map.update(state, :errors, [], fn current -> [error] ++ (current |> Enum.reverse) |> Enum.reverse end)
if length(state.errors) == state.options.max_errors do
raise MaxErrorsReachedError, message: nil, state: state
end
state
end
end
defmodule JTD.ValidationOptions do
@moduledoc """
Options for validation.
"""
@type t :: %__MODULE__{max_depth: integer, max_errors: integer}
defstruct max_depth: 0, max_errors: 0
end
defmodule JTD.Validator do
@moduledoc """
JSON Typedef validator.
"""
alias JTD.{MaxDepthExceededError, MaxErrorsReachedError}
@doc """
Perform validation to given instance.
"""
@spec validate(JTD.Schema.t(), Map.t(), JTD.ValidationOptions.t()) :: [JTD.ValidationError.t()]
def validate(schema, instance, options) do
state = %JTD.ValidationState{
options: options,
root_schema: schema,
instance_tokens: [],
schema_tokens: [[]],
errors: [],
}
try do
validate_with_state(state, schema, instance) |> Map.get(:errors)
rescue
# This is just a dummy error to immediately stop validation. We swallow
# the error here, and return the abridged set of errors.
e in MaxErrorsReachedError -> e.state |> Map.get(:errors)
end
end
defp validate_with_state(state, schema, instance, parent_tag \\ nil)
defp validate_with_state(state, schema, instance, _) when schema.nullable and is_nil(instance), do: state
defp validate_with_state(state, schema, instance, parent_tag) do
JTD.Schema.form(schema)
|> validate_form(state, schema, instance, parent_tag)
end
defp validate_form(:empty, state, _, _, _), do: state
defp validate_form(:ref, state, _, _, _) when length(state.schema_tokens) == state.options.max_depth do
raise MaxDepthExceededError, message: "max depth exceeded during JTD.validate/3"
end
defp validate_form(:ref, state, schema, instance, _) do
state
|> JTD.ValidationState.push_schema_token_stack([schema.ref, "definitions"])
|> validate_with_state(state.root_schema.definitions[schema.ref], instance)
|> JTD.ValidationState.pop_schema_token_stack
end
defp validate_form(:type, state, schema, instance, _) do
state = JTD.ValidationState.push_schema_token(state, "type")
schema
|> Map.get(:type)
|> String.to_atom
|> do_validate_type(state, instance, schema.nullable)
|> JTD.ValidationState.pop_schema_token
end
defp do_validate_type(_, state, nil, true), do: state
defp do_validate_type(:boolean, state, instance, _) when is_boolean(instance), do: state
defp do_validate_type(:float32, state, instance, _) when is_number(instance), do: state
defp do_validate_type(:float64, state, instance, _) when is_number(instance), do: state
defp do_validate_type(:int8, state, instance, _) when is_integer(instance) and (instance >= -128) and (instance <= 127), do: state
defp do_validate_type(:uint8, state, instance, _) when is_integer(instance) and (instance >= 0) and (instance <= 255), do: state
defp do_validate_type(:int16, state, instance, _) when is_integer(instance) and (instance >= -32_768) and (instance <= 32_767), do: state
defp do_validate_type(:uint16, state, instance, _) when is_integer(instance) and (instance >= 0) and (instance <= 65_535), do: state
defp do_validate_type(:int32, state, instance, _) when is_integer(instance) and (instance >= -2_147_483_648) and (instance <= 2_147_483_647), do: state
defp do_validate_type(:uint32, state, instance, _) when is_integer(instance) and (instance >= 0) and (instance <= 4_294_967_295), do: state
defp do_validate_type(:string, state, instance, _) when is_binary(instance), do: state
defp do_validate_type(:timestamp, state, instance, _) when is_binary(instance) do
case DateTime.from_iso8601(instance) do
{:ok, _, _} -> state
{:error, _} -> JTD.ValidationState.push_error(state)
end
end
defp do_validate_type(_, state, _, _), do: JTD.ValidationState.push_error(state)
defp validate_form(:enum, state, schema, instance, _) do
state = JTD.ValidationState.push_schema_token(state, "enum")
state = schema.enum
|> Enum.member?(instance)
|> unless do
JTD.ValidationState.push_error(state)
else
state
end
JTD.ValidationState.pop_schema_token(state)
end
defp validate_form(:elements, state, schema, instance, _) when is_list(instance) do
state = JTD.ValidationState.push_schema_token(state, "elements")
instance
|> Enum.with_index
|> Enum.reduce(state, fn {element, index}, s -> do_validate_element(s, schema, element, index) end)
|> JTD.ValidationState.pop_schema_token
end
defp validate_form(:elements, state, _, _, _) do
state
|> JTD.ValidationState.push_schema_token("elements")
|> JTD.ValidationState.push_error
|> JTD.ValidationState.pop_schema_token
end
defp do_validate_element(state, schema, sub_instance, index) do
state
|> JTD.ValidationState.push_instance_token(Integer.to_string(index))
|> validate_with_state(schema.elements, sub_instance)
|> JTD.ValidationState.pop_instance_token
end
defp validate_form(:properties, state, %{properties: properties}, instance, _) when not is_map(instance) do
case properties do
nil -> JTD.ValidationState.push_schema_token(state, "optionalProperties")
_properties -> JTD.ValidationState.push_schema_token(state, "properties")
end
|> JTD.ValidationState.push_error
|> JTD.ValidationState.pop_schema_token
end
defp validate_form(:properties, state, schema, instance, parent_tag) do
state
|> do_validate_properties(schema.properties, instance)
|> do_validate_optional_properties(schema.optional_properties, instance)
|> do_validate_additional_properties(schema, instance, parent_tag)
end
defp do_validate_properties(state, nil, _), do: state
defp do_validate_properties(state, properties, instance) do
state = JTD.ValidationState.push_schema_token(state, "properties")
properties
|> Enum.reduce(state, fn {property, sub_schema}, s ->
state = JTD.ValidationState.push_schema_token(s, property)
if is_map_key(instance, property) do
state
|> JTD.ValidationState.push_instance_token(property)
|> validate_with_state(sub_schema, instance[property])
|> JTD.ValidationState.pop_instance_token
else
state
|> JTD.ValidationState.push_error
end
|> JTD.ValidationState.pop_schema_token
end)
|> JTD.ValidationState.pop_schema_token
end
defp do_validate_optional_properties(state, nil, _), do: state
defp do_validate_optional_properties(state, optional_properties, instance) do
state = JTD.ValidationState.push_schema_token(state, "optionalProperties")
optional_properties
|> Enum.reduce(state, fn {property, sub_schema}, s ->
state = JTD.ValidationState.push_schema_token(s, property)
if is_map_key(instance, property) do
state
|> JTD.ValidationState.push_instance_token(property)
|> validate_with_state(sub_schema, instance[property])
|> JTD.ValidationState.pop_instance_token
else
state
end
|> JTD.ValidationState.pop_schema_token
end)
|> JTD.ValidationState.pop_schema_token
end
defp do_validate_additional_properties(state, %{additional_properties: true}, _, _), do: state
defp do_validate_additional_properties(state, schema, instance, parent_tag) do
properties = (schema.properties || %{}) |> Map.keys
optional_properties = (schema.optional_properties || %{}) |> Map.keys
instance_keys = instance |> Map.keys
parent_tags = [parent_tag]
instance_keys
|> Kernel.--(properties)
|> Kernel.--(optional_properties)
|> Kernel.--(parent_tags)
|> Enum.reduce(state, fn property, s ->
s
|> JTD.ValidationState.push_instance_token(property)
|> JTD.ValidationState.push_error
|> JTD.ValidationState.pop_instance_token
end)
end
defp validate_form(:values, state, schema, instance, _) when is_map(instance) do
state = JTD.ValidationState.push_schema_token(state, "values")
instance
|> Enum.reduce(state, fn {property, sub_instance}, s ->
s
|> JTD.ValidationState.push_instance_token(property)
|> validate_with_state(schema.values, sub_instance)
|> JTD.ValidationState.pop_instance_token
end)
|> JTD.ValidationState.pop_schema_token
end
defp validate_form(:values, state, _, _, _) do
state
|> JTD.ValidationState.push_schema_token("values")
|> JTD.ValidationState.push_error
|> JTD.ValidationState.pop_schema_token
end
defp validate_form(:discriminator, state, schema, instance, _) when is_map(instance) do
do_validate_form(state, schema, instance, instance[schema.discriminator])
end
defp validate_form(:discriminator, state, _, _, _) do
state
|> JTD.ValidationState.push_schema_token("discriminator")
|> JTD.ValidationState.push_error
|> JTD.ValidationState.pop_schema_token
end
defp do_validate_form(state, schema, instance, _) when (not is_map(instance)) or (not is_map_key(instance, schema.discriminator)) do
state
|> JTD.ValidationState.push_schema_token("discriminator")
|> JTD.ValidationState.push_error
|> JTD.ValidationState.pop_schema_token
end
defp do_validate_form(state, schema, _, instance_discriminator) when not is_binary(instance_discriminator) do
state
|> JTD.ValidationState.push_schema_token("discriminator")
|> JTD.ValidationState.push_instance_token(schema.discriminator)
|> JTD.ValidationState.push_error
|> JTD.ValidationState.pop_instance_token
|> JTD.ValidationState.pop_schema_token
end
defp do_validate_form(state, schema, _, instance_discriminator) when not is_map_key(schema.mapping, instance_discriminator) do
state
|> JTD.ValidationState.push_schema_token("mapping")
|> JTD.ValidationState.push_instance_token(schema.discriminator)
|> JTD.ValidationState.push_error
|> JTD.ValidationState.pop_instance_token
|> JTD.ValidationState.pop_schema_token
end
defp do_validate_form(state, schema, instance, instance_discriminator) do
sub_schema = schema.mapping[instance_discriminator]
state
|> JTD.ValidationState.push_schema_token("mapping")
|> JTD.ValidationState.push_schema_token(instance_discriminator)
|> validate_with_state(sub_schema, instance, schema.discriminator)
|> JTD.ValidationState.pop_schema_token
|> JTD.ValidationState.pop_schema_token
end
end