From 4abf14ddae2ad5855da2945999177711cc82c605 Mon Sep 17 00:00:00 2001 From: Gerasim Stanchev Date: Wed, 8 Apr 2020 17:19:16 +0300 Subject: [PATCH 1/3] Include valid?/3 in the Conformable protocol --- lib/norm.ex | 5 +---- lib/norm/conformer.ex | 18 ++++++++++++++++-- lib/norm/core/all_of.ex | 7 ++++++- lib/norm/core/alt.ex | 6 ++++++ lib/norm/core/any_of.ex | 6 ++++++ lib/norm/core/collection.ex | 12 ++++++++++++ lib/norm/core/schema.ex | 21 +++++++++++++++++++++ lib/norm/core/selection.ex | 18 ++++++++++++++++++ lib/norm/core/spec.ex | 5 +++++ lib/norm/core/spec/and.ex | 4 ++++ lib/norm/core/spec/or.ex | 4 ++++ lib/norm/generator.ex | 4 ++++ 12 files changed, 103 insertions(+), 7 deletions(-) diff --git a/lib/norm.ex b/lib/norm.ex index 95b01f5..b6c192d 100644 --- a/lib/norm.ex +++ b/lib/norm.ex @@ -76,10 +76,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 25f7f1a..5cb425a 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 -> @@ -48,10 +52,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 @@ -70,12 +76,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 @@ -104,4 +114,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..d909850 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.all?(& &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..8f06178 100644 --- a/lib/norm/core/collection.ex +++ b/lib/norm/core/collection.ex @@ -47,6 +47,18 @@ 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) + 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 972fec6..7e8ebb8 100644 --- a/lib/norm/core/schema.ex +++ b/lib/norm/core/schema.ex @@ -68,6 +68,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 f4d2abc..762d81c 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 988829e..21e7790 100644 --- a/lib/norm/core/spec.ex +++ b/lib/norm/core/spec.ex @@ -124,6 +124,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 From cded853ee825ef35b0efce58555f36a899a966d6 Mon Sep 17 00:00:00 2001 From: Gerasim Stanchev Date: Thu, 9 Apr 2020 11:38:16 +0300 Subject: [PATCH 2/3] Verify for a single valid check --- lib/norm/core/any_of.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/norm/core/any_of.ex b/lib/norm/core/any_of.ex index d909850..19d00fc 100644 --- a/lib/norm/core/any_of.ex +++ b/lib/norm/core/any_of.ex @@ -28,7 +28,7 @@ defmodule Norm.Core.AnyOf do def valid?(%{specs: specs}, input, path) do specs |> Stream.map(fn spec -> Conformable.valid?(spec, input, path) end) - |> Enum.all?(& &1) + |> Enum.any?(& &1) end end From 33395025db1b2750d86a361ad7bb60c94b9c4196 Mon Sep 17 00:00:00 2001 From: Gerasim Stanchev Date: Thu, 9 Apr 2020 11:38:41 +0300 Subject: [PATCH 3/3] Flee at a failing collection check --- lib/norm/core/collection.ex | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/norm/core/collection.ex b/lib/norm/core/collection.ex index 8f06178..38a138f 100644 --- a/lib/norm/core/collection.ex +++ b/lib/norm/core/collection.ex @@ -50,12 +50,15 @@ defmodule Norm.Core.Collection do 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 + :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