Current section
Files
Jump to
Current section
Files
lib/exonerate/type/array.ex
defmodule Exonerate.Type.Array do
@moduledoc false
@behaviour Exonerate.Type
alias Exonerate.Combining
alias Exonerate.Tools
alias Exonerate.Type.Array.Iterator
@combining_modules Combining.modules()
@combining_filters Combining.filters()
@seen_filters ~w(allOf anyOf if oneOf $ref)
defmacro filter(resource, pointer, opts) do
__CALLER__
|> Tools.subschema(resource, pointer)
|> build_filter(resource, pointer, opts)
|> Tools.maybe_dump(__CALLER__, opts)
end
#########################################################
# trivial exceptions
defguardp trivial_contains(context)
when is_map_key(context, "contains") and
:erlang.map_get("contains", context) === false and
(not is_map_key(context, "minContains") or
:erlang.map_get("minContains", context) > 0)
defguardp trivial_max_items(context)
when is_map_key(context, "maxItems") and :erlang.map_get("maxItems", context) === 0
defguardp trivial(context)
when trivial_contains(context) or trivial_max_items(context)
defp build_filter(context, resource, pointer, opts) when trivial_contains(context) do
call = Tools.call(resource, pointer, opts)
pointer = JsonPtr.join(pointer, "contains")
quote do
defp unquote(call)(array, path) when is_list(array) do
require Exonerate.Tools
Exonerate.Tools.mismatch(array, unquote(resource), unquote(pointer), path)
end
end
end
defp build_filter(context, resource, pointer, opts) when trivial_max_items(context) do
empty_only(context, "maxItems", resource, pointer, opts)
end
defp build_filter(context, resource, pointer, opts) do
call = Tools.call(resource, pointer, opts)
combining_prong = List.wrap(combining(context, resource, pointer, opts))
iterator_prong = List.wrap(iterator(context, resource, pointer, opts))
quote do
defp unquote(call)(array, path) when is_list(array) do
require Exonerate.Combining
Exonerate.Combining.initializer(
first_unseen_index,
unquote(resource),
unquote(pointer),
unquote(opts)
)
with unquote_splicing(combining_prong ++ iterator_prong) do
unquote(result_expr(context, opts))
end
end
end
end
defp empty_only(context, reason, resource, pointer, opts) do
call = Tools.call(resource, pointer, opts)
case context do
# these three cases have no solutions because arrays will always fail
# because one condition requires empty and the other condition requires
# the existence of at least one.
%{"contains" => _, "minContains" => min} when min > 0 ->
fatal("minContains", resource, pointer, opts)
%{"contains" => _} when not is_map_key(context, "minContains") ->
fatal("contains", resource, pointer, opts)
%{"minItems" => min} when min > 0 ->
fatal("minItems", resource, pointer, opts)
_ ->
quote do
defp unquote(call)([], _path), do: :ok
defp unquote(call)(array, path) when is_list(array) do
require Exonerate.Tools
Exonerate.Tools.mismatch(
array,
unquote(resource),
unquote(JsonPtr.join(pointer, reason)),
path
)
end
end
end
end
defp fatal(reason, resource, pointer, opts) do
call = Tools.call(resource, pointer, opts)
quote do
defp unquote(call)([], path) do
require Exonerate.Tools
Exonerate.Tools.mismatch(
[],
unquote(resource),
unquote(JsonPtr.join(pointer, reason)),
path
)
end
defp unquote(call)(array, path) when is_list(array) do
require Exonerate.Tools
Exonerate.Tools.mismatch(
array,
unquote(resource),
unquote(JsonPtr.join(pointer, "maxItems")),
path
)
end
end
end
defp combining(context, resource, pointer, opts) do
needs_ok_tuple = needs_seen_tracking?(context, opts)
Enum.flat_map(
context,
fn {filter, _} ->
List.wrap(
if filter in @combining_filters,
do: filter_clauses(resource, pointer, opts, filter, needs_ok_tuple)
)
end
)
end
defp iterator(context, resource, pointer, opts) do
if iterator_args = Iterator.args(context) do
iterator_call = Iterator.call(resource, pointer, opts)
uniqueness_initializer =
List.wrap(
if :unique_items in iterator_args do
uniqueness_opts = Keyword.take(opts, [:use_xor_filter])
[
quote do
require Exonerate.Filter.UniqueItems
end,
quote do
unique_items = Exonerate.Filter.UniqueItems.initialize(unquote(uniqueness_opts))
end
]
end
)
uniqueness_initializer ++
[
quote do
:ok <- unquote(iterator_call)(unquote_splicing(to_arg_vars(iterator_args)))
end
]
end
end
defp to_arg_vars(list) do
Enum.map(list, fn
atom when is_atom(atom) -> {atom, [], __MODULE__}
number -> number
end)
end
defp filter_clauses(resource, pointer, opts, filter, true) when filter in @seen_filters do
filter_call =
Tools.call(
resource,
JsonPtr.join(pointer, Combining.adjust(filter)),
Keyword.put(opts, :tracked, :array)
)
quote do
[
{:ok, new_index} <- unquote(filter_call)(array, path),
first_unseen_index = max(first_unseen_index, new_index)
]
end
end
defp filter_clauses(resource, pointer, opts, filter, _) do
filter_call = Tools.call(resource, JsonPtr.join(pointer, Combining.adjust(filter)), opts)
[
quote do
:ok <- unquote(filter_call)(array, path)
end
]
end
defp iterator_clause(resource, pointer, opts, needs_combining_seen) do
call_opts =
if needs_combining_seen do
Keyword.put(opts, :tracked, :array)
else
opts
end
iterator_call = Tools.call(resource, pointer, :array_iterator, call_opts)
quote do
unquote(iterator_call)(array, array, 0, path)
end
end
defp result_expr(context, opts) do
# we only need to return result_expr if we are being tracked
local_length = local_length(context)
cond do
opts[:tracked] !== :array ->
:ok
local_length === :length ->
{:ok, :length}
Enum.any?(context, &match?({key, _} when key in @seen_filters, &1)) ->
quote do
{:ok, max(first_unseen_index, unquote(local_length))}
end
true ->
{:ok, local_length}
end
end
defp local_length(%{"unevaluatedItems" => _}), do: :length
defp local_length(%{"additionalItems" => _}), do: :length
defp local_length(%{"items" => list}) when is_list(list), do: length(list)
defp local_length(%{"items" => _}), do: :length
defp local_length(%{"prefixItems" => list}), do: length(list)
defp local_length(_), do: 0
defmacro accessories(resource, pointer, opts) do
__CALLER__
|> Tools.subschema(resource, pointer)
|> build_accessories(resource, pointer, opts)
|> Tools.maybe_dump(__CALLER__, opts)
end
defp build_accessories(context, _, _, _) when trivial(context), do: []
defp build_accessories(context, resource, pointer, opts) do
combining_opts = combining_opts(context, opts)
build_tracked_filters(context, resource, pointer, combining_opts) ++
build_iterator(context, resource, pointer, opts)
end
def needs_combining_seen?(context) do
is_map_key(context, "unevaluatedItems") and Enum.any?(@seen_filters, &is_map_key(context, &1))
end
def needs_seen_tracking?(context, opts) do
needs_combining_seen?(context) or opts[:tracked] === :array
end
# appends special options to the passed otions of combining functions.
defp combining_opts(context, opts) do
if needs_seen_tracking?(context, opts) do
Keyword.merge(opts, only: ["array"], tracked: :array)
else
opts
end
end
defp build_tracked_filters(context, resource, pointer, opts) do
# if we're tracked, then we need to rebuild all the filters, with the
# tracked appendage.
List.wrap(
if needs_seen_tracking?(context, opts) do
for filter <- @seen_filters, is_map_key(context, filter) do
module = @combining_modules[filter]
pointer = JsonPtr.join(pointer, filter)
quote do
require unquote(module)
unquote(module).filter(unquote(resource), unquote(pointer), unquote(opts))
end
end
end
)
end
defp build_iterator(context, resource, pointer, opts) do
List.wrap(
if Iterator.mode(context) do
quote do
require Exonerate.Type.Array.Iterator
Exonerate.Type.Array.Iterator.filter(
unquote(resource),
unquote(pointer),
unquote(opts)
)
Exonerate.Type.Array.Iterator.accessories(
unquote(resource),
unquote(pointer),
unquote(opts)
)
end
end
)
end
end