Current section

Files

Jump to
nex_json_schema lib ex_json_schema validator.ex
Raw

lib/ex_json_schema/validator.ex

defmodule NExJsonSchema.Validator do
alias NExJsonSchema.Validator.Dependencies
alias NExJsonSchema.Validator.Format
alias NExJsonSchema.Validator.Items
alias NExJsonSchema.Validator.Properties
alias NExJsonSchema.Validator.Type
alias NExJsonSchema.Schema
alias NExJsonSchema.Schema.Root
@type errors :: [{String.t, String.t}] | []
@type errors_with_list_paths :: [{String.t, [String.t | integer]}] | []
@spec validate(Root.t, NExJsonSchema.data) :: :ok | {:error, errors}
def validate(root = %Root{}, data) do
errors = validate(root, root.schema, data, ["$"]) |> errors_with_string_paths
case Enum.empty?(errors) do
true -> :ok
false -> {:error, errors}
end
end
@spec validate(NExJsonSchema.json, NExJsonSchema.data) :: :ok | {:error, errors}
def validate(schema = %{}, data) do
validate(Schema.resolve(schema), data)
end
@spec validate(Root.t, Schema.resolved, NExJsonSchema.data, [String.t | integer]) :: errors_with_list_paths
def validate(root, schema, data, path \\ []) do
Enum.flat_map(schema, &validate_aspect(root, schema, &1, data))
|> Enum.map(fn {%{} = rules, p} -> {rules, path ++ p} end)
end
@spec valid?(Root.t, NExJsonSchema.data) :: boolean
def valid?(root = %Root{}, data), do: valid?(root, root.schema, data)
@spec valid?(NExJsonSchema.json, NExJsonSchema.data) :: boolean
def valid?(schema = %{}, data), do: valid?(Schema.resolve(schema), data)
@spec valid?(Root.t, Schema.resolved, NExJsonSchema.data) :: boolean
def valid?(root, schema, data), do: validate(root, schema, data) |> Enum.empty?
defp errors_with_string_paths(errors) do
Enum.map errors, fn {msg, path} -> {msg, Enum.join(path, ".")} end
end
defp validate_aspect(root, _, {"$ref", path}, data) do
schema = Schema.get_ref_schema(root, path)
validate(root, schema, data)
end
defp validate_aspect(root, _, {"allOf", all_of}, data) do
invalid_indexes = validation_result_indexes(root, all_of, data, &(!elem(&1, 0)))
case Enum.empty?(invalid_indexes) do
true -> []
false ->
[{%{
description: "expected all of the schemata to match, " <>
"but the schemata at the following indexes did not: " <>
"#{Enum.join(invalid_indexes, ", ")}",
rule: :schemata,
params: []
}, []}]
end
end
defp validate_aspect(root, _, {"anyOf", any_of}, data) do
case Enum.any?(any_of, &valid?(root, &1, data)) do
true -> []
false -> [{%{
description: "expected any of the schemata to match but none did",
rule: :schemata,
params: []
}, []}]
end
end
defp validate_aspect(root, _, {"oneOf", one_of}, data) do
valid_indexes = validation_result_indexes(root, one_of, data, &(elem(&1, 0)))
case Enum.empty?(valid_indexes) do
true -> [{%{
description: "expected exactly one of the schemata to match, but none of them did",
rule: :schemata,
params: []
}, []}]
false -> if Enum.count(valid_indexes) == 1 do
[]
else
[{%{
description: "expected exactly one of the schemata to match, " <>
"but the schemata at the following indexes did: " <>
"#{Enum.join(valid_indexes, ", ")}",
rule: :schemata,
params: []
}, []}]
end
end
end
defp validate_aspect(root, _, {"not", not_schema}, data) do
case valid?(root, not_schema, data) do
true -> [{%{
description: "expected schema not to match but it did",
rule: :schema,
params: []
}, []}]
false -> []
end
end
defp validate_aspect(_, _, {"type", type}, data) do
Type.validate(type, data)
end
defp validate_aspect(root, schema, {"properties", _}, data = %{}) do
Properties.validate(root, schema, data)
end
defp validate_aspect(_, _, {"minProperties", min_properties}, data) when is_map(data) do
case Map.size(data) >= min_properties do
true -> []
false -> [{%{
description: "expected a minimum of #{min_properties} properties but got #{Map.size(data)}",
rule: :length,
params: [min: min_properties]
}, []}]
end
end
defp validate_aspect(_, _, {"maxProperties", max_properties}, data) when is_map(data) do
case Map.size(data) <= max_properties do
true -> []
false -> [{%{
description: "expected a maximum of #{max_properties} properties but got #{Map.size(data)}",
rule: :length,
params: [max: max_properties]
}, []}]
end
end
defp validate_aspect(_, _, {"required", required}, data = %{}) do
Enum.flat_map List.wrap(required), fn property ->
case Map.has_key?(data, property) do
true -> []
false -> [{%{
description: "required property #{property} was not present",
rule: :required,
params: []
}, []}]
end
end
end
defp validate_aspect(root, _, {"dependencies", dependencies}, data) do
Dependencies.validate(root, dependencies, data)
end
defp validate_aspect(root, schema, {"items", _}, items) do
Items.validate(root, schema, items)
end
defp validate_aspect(_, _, {"minItems", min_items}, items) when is_list(items) do
case (count = Enum.count(items)) >= min_items do
true -> []
false -> [{%{
description: "expected a minimum of #{min_items} items but got #{count}",
rule: :length,
params: [min: min_items]
}, []}]
end
end
defp validate_aspect(_, _, {"maxItems", max_items}, items) when is_list(items) do
case (count = Enum.count(items)) <= max_items do
true -> []
false -> [{%{
description: "expected a maximum of #{max_items} items but got #{count}",
rule: :length,
params: [max: max_items]
}, []}]
end
end
defp validate_aspect(_, _, {"uniqueItems", true}, items) when is_list(items) do
case Enum.uniq(items) == items do
true -> []
false -> [{%{
description: "expected items to be unique but they were not",
rule: :unique,
params: []
}, []}]
end
end
defp validate_aspect(_, _, {"enum", enum}, data) do
case Enum.any?(enum, &(&1 === data)) do
true -> []
false -> [{%{
description: "value is not allowed in enum",
rule: :inclusion,
params: enum
}, []}]
end
end
defp validate_aspect(_, schema, {"minimum", minimum}, data) when is_number(data) do
exclusive? = schema["exclusiveMinimum"]
fun = if exclusive?, do: &Kernel.>/2, else: &Kernel.>=/2
case fun.(data, minimum) do
true -> []
false -> [{%{
description: "expected the value to be #{if exclusive?, do: ">", else: ">="} #{minimum}",
rule: :number,
params: [min: minimum, exclusive: exclusive?]
}, []}]
end
end
defp validate_aspect(_, schema, {"maximum", maximum}, data) when is_number(data) do
exclusive? = schema["exclusiveMaximum"]
fun = if exclusive?, do: &Kernel.</2, else: &Kernel.<=/2
case fun.(data, maximum) do
true -> []
false -> [{%{
description: "expected the value to be #{if exclusive?, do: "<", else: "<="} #{maximum}",
rule: :number,
params: [max: maximum, exclusive: exclusive?]
}, []}]
end
end
defp validate_aspect(_, _, {"multipleOf", multiple_of}, data) when is_number(data) do
factor = data / multiple_of
case Float.floor(factor) == factor do
true -> []
false -> [{%{
description: "expected value to be a multiple of #{multiple_of} but got #{data}",
rule: :number,
params: [multiple_of: multiple_of]
}, []}]
end
end
defp validate_aspect(_, _, {"minLength", min_length}, data) when is_binary(data) do
case (length = String.length(data)) >= min_length do
true -> []
false -> [{%{
description: "expected value to have a minimum length of #{min_length} but was #{length}",
rule: :length,
params: [min: min_length]
}, []}]
end
end
defp validate_aspect(_, _, {"maxLength", max_length}, data) when is_binary(data) do
case (length = String.length(data)) <= max_length do
true -> []
false -> [{%{
description: "expected value to have a maximum length of #{max_length} but was #{length}",
rule: :length,
params: [max: max_length]
}, []}]
end
end
defp validate_aspect(_, _, {"pattern", pattern}, data) when is_binary(data) do
case pattern |> Regex.compile! |> Regex.match?(data) do
true -> []
false -> [{%{
description: "string does not match pattern #{inspect(pattern)}",
rule: :format,
params: [pattern]
}, []}]
end
end
defp validate_aspect(_, _, {"format", format}, data) do
Format.validate(format, data)
end
defp validate_aspect(_, _, _, _), do: []
defp validation_result_indexes(root, schemata, data, filter) do
schemata
|> Enum.map(&valid?(root, &1, data))
|> Enum.with_index
|> Enum.filter(filter)
|> Dict.values
end
end