diff --git a/lib/norm.ex b/lib/norm.ex index 65e0b17..5ba9e4c 100644 --- a/lib/norm.ex +++ b/lib/norm.ex @@ -77,10 +77,7 @@ defmodule Norm do false """ def valid?(input, spec) do - case Conformer.conform(spec, input) do - {:ok, _} -> true - {:error, _} -> false - end + Conformer.valid?(spec, input) end @doc ~S""" diff --git a/lib/norm/conformer.ex b/lib/norm/conformer.ex index f67d4b9..fac1065 100644 --- a/lib/norm/conformer.ex +++ b/lib/norm/conformer.ex @@ -7,6 +7,10 @@ defmodule Norm.Conformer do Norm.Conformer.Conformable.conform(spec, input, []) end + def valid?(spec, input) do + Norm.Conformer.Conformable.valid?(spec, input, []) + end + def group_results(results) do results |> Enum.reduce(%{ok: [], error: []}, fn {result, s}, acc -> @@ -49,10 +53,12 @@ defmodule Norm.Conformer do defprotocol Conformable do @moduledoc false # Defines a conformable type. Must take the type, current path, and input and - # return an success tuple with the conformed data or a list of errors. + # return a success tuple with the conformed data or a list of errors. # @fallback_to_any true def conform(spec, path, input) + + def valid?(spec, path, input) end end @@ -71,12 +77,16 @@ defimpl Norm.Conformer.Conformable, for: Atom do {:error, [Conformer.error(path, input, "is not an atom.")]} atom != input -> - {:error, [Conformer.error(path, input, "== :#{atom}")]} + {:error, [Conformer.error(path, input, "== #{inspect(atom)}")]} true -> {:ok, atom} end end + + def valid?(atom, input, path) do + conform(atom, input, path) |> elem(0) == :ok + end end defimpl Norm.Conformer.Conformable, for: Tuple do @@ -105,4 +115,8 @@ defimpl Norm.Conformer.Conformable, for: Tuple do {:ok, List.to_tuple(results.ok)} end end + + def valid?(spec, input, path) do + conform(spec, input, path) |> elem(0) == :ok + end end diff --git a/lib/norm/core/all_of.ex b/lib/norm/core/all_of.ex index cd5f75c..8e9b685 100644 --- a/lib/norm/core/all_of.ex +++ b/lib/norm/core/all_of.ex @@ -23,6 +23,11 @@ defmodule Norm.Core.AllOf do {:ok, Enum.at(result.ok, 0)} end end + + def valid?(%{specs: specs}, input, path) do + specs + |> Stream.map(fn spec -> Conformable.valid?(spec, input, path) end) + |> Enum.all?(& &1) + end end end - diff --git a/lib/norm/core/alt.ex b/lib/norm/core/alt.ex index fe4deb4..17203a3 100644 --- a/lib/norm/core/alt.ex +++ b/lib/norm/core/alt.ex @@ -27,6 +27,12 @@ defmodule Norm.Core.Alt do {:error, List.flatten(result.error)} end end + + def valid?(%{specs: specs}, input, path) do + specs + |> Stream.map(fn {name, spec} -> Conformable.valid?(spec, input, path ++ [name]) end) + |> Enum.any?(& &1) + end end if Code.ensure_loaded?(StreamData) do diff --git a/lib/norm/core/any_of.ex b/lib/norm/core/any_of.ex index 51fed56..19d00fc 100644 --- a/lib/norm/core/any_of.ex +++ b/lib/norm/core/any_of.ex @@ -24,6 +24,12 @@ defmodule Norm.Core.AnyOf do {:error, List.flatten(result.error)} end end + + def valid?(%{specs: specs}, input, path) do + specs + |> Stream.map(fn spec -> Conformable.valid?(spec, input, path) end) + |> Enum.any?(& &1) + end end if Code.ensure_loaded?(StreamData) do diff --git a/lib/norm/core/collection.ex b/lib/norm/core/collection.ex index 9291190..38a138f 100644 --- a/lib/norm/core/collection.ex +++ b/lib/norm/core/collection.ex @@ -47,6 +47,21 @@ defmodule Norm.Core.Collection do end end + def valid?(%{spec: spec, opts: opts}, input, path) do + with :ok <- check_enumerable(input, path, opts), + :ok <- check_kind_of(input, path, opts), + :ok <- check_distinct(input, path, opts), + :ok <- check_counts(input, path, opts) do + input + |> Stream.with_index() + |> Stream.map(fn {elem, i} -> Conformable.valid?(spec, elem, path ++ [i]) end) + |> Enum.all?(& &1) + + else + _ -> false + end + end + defp convert(results, type) do Enum.into(results, type) end diff --git a/lib/norm/core/schema.ex b/lib/norm/core/schema.ex index 7a89c2b..5a8d6d4 100644 --- a/lib/norm/core/schema.ex +++ b/lib/norm/core/schema.ex @@ -73,6 +73,27 @@ defmodule Norm.Core.Schema do end end + def valid?(%Schema{specs: specs}, %{__struct__: module} = input, path) when not is_nil(module) do + check_specs_validity(specs, Map.from_struct(input), path) + end + + def valid?(%Schema{specs: specs}, input, path) do + check_specs_validity(specs, input, path) + end + + defp check_specs_validity(specs, input, path) do + input + |> Stream.map(fn spec -> check_spec_validity(spec, specs, path) end) + |> Enum.all?(& &1) + end + + defp check_spec_validity({key, value}, specs, path) do + case Map.get(specs, key) do + nil -> true + spec -> Conformable.valid?(spec, value, path ++ [key]) + end + end + defp check_specs(specs, input, path) do results = input diff --git a/lib/norm/core/selection.ex b/lib/norm/core/selection.ex index 39217df..fc1ab9a 100644 --- a/lib/norm/core/selection.ex +++ b/lib/norm/core/selection.ex @@ -93,6 +93,24 @@ defmodule Norm.Core.Selection do end end + def valid?(%{required: required, schema: schema}, input, path) do + Conformable.valid?(schema, input, path) && valid_keys?(required, input) + end + + defp valid_keys?([] = _required, _input), do: true + + defp valid_keys?([{key, _inner} | rest], input) do + if valid_key?(key, input), do: valid_keys?(rest, input), else: false + end + + defp valid_keys?([key | rest], input) do + if valid_key?(key, input), do: valid_keys?(rest, input), else: false + end + + defp valid_key?(key, input) when is_map(input), do: Map.has_key?(input, key) + + defp valid_key?(_key, _input), do: true + defp ensure_keys([], _conformed, _path, errors), do: errors defp ensure_keys([{key, inner} | rest], conformed, path, errors) do case ensure_key(key, conformed, path) do diff --git a/lib/norm/core/spec.ex b/lib/norm/core/spec.ex index b988622..0646124 100644 --- a/lib/norm/core/spec.ex +++ b/lib/norm/core/spec.ex @@ -143,6 +143,11 @@ defmodule Norm.Core.Spec do raise ArgumentError, "Predicates must return a boolean value" end end + + def valid?(%{f: _f, predicate: _pred} = spec, input, path) do + {status, _} = conform(spec, input, path) + status == :ok + end end @doc false diff --git a/lib/norm/core/spec/and.ex b/lib/norm/core/spec/and.ex index da2bdaa..c9039ae 100644 --- a/lib/norm/core/spec/and.ex +++ b/lib/norm/core/spec/and.ex @@ -27,6 +27,10 @@ defmodule Norm.Core.Spec.And do Conformable.conform(r, input, path) end end + + def valid?(%{left: l, right: r}, input, path) do + Conformable.valid?(l, input, path) && Conformable.valid?(r, input, path) + end end if Code.ensure_loaded?(StreamData) do diff --git a/lib/norm/core/spec/or.ex b/lib/norm/core/spec/or.ex index 80f249a..b35b638 100644 --- a/lib/norm/core/spec/or.ex +++ b/lib/norm/core/spec/or.ex @@ -21,6 +21,10 @@ defmodule Norm.Core.Spec.Or do end end end + + def valid?(%{left: l, right: r}, input, path) do + Conform.valid?(l, input, path) or Conform.valid?(r, input, path) + end end if Code.ensure_loaded?(StreamData) do diff --git a/lib/norm/generator.ex b/lib/norm/generator.ex index 9585efb..62b2e8e 100644 --- a/lib/norm/generator.ex +++ b/lib/norm/generator.ex @@ -14,6 +14,10 @@ defmodule Norm.Generator do def conform(%{conformer: c}, input, path) do Norm.Conformer.Conformable.conform(c, input, path) end + + def valid?(%{conformer: c}, input, path) do + Norm.Conformer.Conformable.valid?(c, input, path) + end end defimpl Norm.Generatable do