Repository: keathley/norm
Pull request #21 (2019-10-21 17:58:32)

Return a list of errors from conform/2
After this PR `conform/2` will no longer return a list of strings and will return a list of errors. This should help clients and other libraries craft error messages that make the most sense for their use case.


Patch:
diff --git a/lib/norm.ex b/lib/norm.ex
index 419a21b..827e67e 100644
--- a/lib/norm.ex
+++ b/lib/norm.ex
@@ -12,7 +12,7 @@ defmodule Norm do
   => 123
 
   conform!(-50, spec(is_integer() and &(&1 > 0)))
-  ** (Norm.MismatchError) val: -50 fails: &(&1 > 0)
+  ** (Norm.MismatchError) val: -50 spec: &(&1 > 0)
       (norm) lib/norm.ex:44: Norm.conform!/2
 
   user_schema = schema(%{
@@ -65,7 +65,7 @@ defmodule Norm do
   conform!(10, spec(greater?(5)))
   => 10
   conform!(3, spec(greater?(5)))
-  ** (Norm.MismatchError) val: 3 fails: greater?(5)
+  ** (Norm.MismatchError) val: 3 spec: greater?(5)
       (norm) lib/norm.ex:44: Norm.conform!/2
   ```
 
@@ -362,9 +362,9 @@ defmodule Norm do
       iex> conform(42, spec(&(&1 >= 0)))
       {:ok, 42}
       iex> conform(42, spec(&(&1 >= 100)))
-      {:error, ["val: 42 fails: &(&1 >= 100)"]}
+      {:error, [%{spec: "&(&1 >= 100)", input: 42, path: []}]}
       iex> conform("foo", spec(is_integer()))
-      {:error, ["val: \"foo\" fails: is_integer()"]}
+      {:error, [%{spec: "is_integer()", input: "foo", path: []}]}
   """
   def conform(input, spec) do
     Conformer.conform(spec, input)
@@ -461,13 +461,13 @@ defmodule Norm do
       iex> conform!(21, spec(is_integer() and &(&1 >= 21)))
       21
       iex> conform("21", spec(is_integer() and &(&1 >= 21)))
-      {:error, ["val: \"21\" fails: is_integer()"]}
+      {:error, [%{spec: "is_integer()", input: "21", path: []}]}
       iex> conform!(:foo, spec(is_atom() or is_binary()))
       :foo
       iex> conform!("foo", spec(is_atom() or is_binary()))
       "foo"
       iex> conform(21, spec(is_atom() or is_binary()))
-      {:error, ["val: 21 fails: is_atom()", "val: 21 fails: is_binary()"]}
+      {:error, [%{spec: "is_atom()", input: 21, path: []}, %{spec: "is_binary()", input: 21, path: []}]}
   """
   defmacro spec(predicate) do
     spec = Spec.build(predicate)
@@ -506,7 +506,7 @@ defmodule Norm do
       iex> conform!("foo", alt(num: spec(is_integer()), str: spec(is_binary())))
       {:str, "foo"}
       iex> conform(true, alt(num: spec(is_integer()), str: spec(is_binary())))
-      {:error, ["val: true in: :num fails: is_integer()", "val: true in: :str fails: is_binary()"]}
+      {:error, [%{spec: "is_integer()", input: true, path: [:num]}, %{spec: "is_binary()", input: true, path: [:str]}]}
   """
   def alt(specs) when is_list(specs) do
     %Alt{specs: specs}
@@ -585,11 +585,11 @@ defmodule Norm do
   # iex> conform!([31, "Chris"], cat(age: integer?(), name: string?()))
   # [age: 31, name: "Chris"]
   # iex> conform([true, "Chris"], cat(age: integer?(), name: string?()))
-  # {:error, ["in: [0] at: :age val: true fails: integer?()"]}
+  # {:error, ["in: [0] at: :age val: true spec: integer?()"]}
   # iex> conform([31, :chris], cat(age: integer?(), name: string?()))
-  # {:error, ["in: [1] at: :name val: :chris fails: string?()"]}
+  # {:error, ["in: [1] at: :name val: :chris spec: string?()"]}
   # iex> conform([31], cat(age: integer?(), name: string?()))
-  # {:error, ["in: [1] at: :name val: nil fails: Insufficient input"]}
+  # {:error, ["in: [1] at: :name val: nil spec: Insufficient input"]}
   # """
   # def cat(opts) do
   #   fn path, input ->
diff --git a/lib/norm/conformer.ex b/lib/norm/conformer.ex
index d512738..3d92bcb 100644
--- a/lib/norm/conformer.ex
+++ b/lib/norm/conformer.ex
@@ -4,11 +4,7 @@ defmodule Norm.Conformer do
   # conformable types
 
   def conform(spec, input) do
-    # If we get errors then we should convert them to messages. Otherwise
-    # we just let good results fall through.
-    with {:error, errors} <- Norm.Conformer.Conformable.conform(spec, input, []) do
-      {:error, Enum.map(errors, &error_to_msg/1)}
-    end
+    Norm.Conformer.Conformable.conform(spec, input, [])
   end
 
   def group_results(results) do
@@ -21,10 +17,10 @@ defmodule Norm.Conformer do
   end
 
   def error(path, input, msg) do
-    %{path: path, input: input, msg: msg, at: nil}
+    %{path: path, input: input, spec: msg}
   end
 
-  def error_to_msg(%{path: path, input: input, msg: msg}) do
+  def error_to_msg(%{path: path, input: input, spec: msg}) do
     path = if path == [], do: nil, else: "in: " <> build_path(path)
     val = "val: #{format_val(input)}"
     fails = "fails: #{msg}"
diff --git a/lib/norm/errors.ex b/lib/norm/errors.ex
index 0a7d195..e6670a8 100644
--- a/lib/norm/errors.ex
+++ b/lib/norm/errors.ex
@@ -2,7 +2,10 @@ defmodule Norm.MismatchError do
   defexception [:message]
 
   def exception(errors) do
-    msg = Enum.join(errors, "\n")
+    msg =
+      errors
+      |> Enum.map(&Norm.Conformer.error_to_msg/1)
+      |> Enum.join("\n")
 
     %__MODULE__{message: "Could not conform input:\n" <> msg}
   end
diff --git a/lib/norm/schema.ex b/lib/norm/schema.ex
index b7b2e44..48f0afd 100644
--- a/lib/norm/schema.ex
+++ b/lib/norm/schema.ex
@@ -34,10 +34,11 @@ defmodule Norm.Schema do
   end
 
   defimpl Norm.Conformer.Conformable do
+    alias Norm.Conformer
     alias Norm.Conformer.Conformable
 
     def conform(_, input, path) when not is_map(input) do
-      {:error, [error(path, input, "not a map")]}
+      {:error, [Conformer.error(path, input, "not a map")]}
     end
 
     def conform(%{specs: specs, struct: target}, input, path) when not is_nil(target) do
@@ -52,7 +53,7 @@ defmodule Norm.Schema do
           |> Atom.to_string()
           |> String.replace("Elixir.", "")
 
-        {:error, [error(path, input, "#{short_name}")]}
+        {:error, [Conformer.error(path, input, "#{short_name}")]}
       end
     end
 
@@ -82,7 +83,7 @@ defmodule Norm.Schema do
     defp check_spec({key, nil}, input, path) do
       case Map.has_key?(input, key) do
         false ->
-          {key, {:error, [error(path ++ [key], input, ":required")]}}
+          {key, {:error, [Conformer.error(path ++ [key], input, ":required")]}}
 
         true ->
           {key, {:ok, Map.get(input, key)}}
@@ -92,17 +93,13 @@ defmodule Norm.Schema do
     defp check_spec({key, spec}, input, path) do
       case Map.has_key?(input, key) do
         false ->
-          {key, {:error, [error(path ++ [key], input, ":required")]}}
+          {key, {:error, [Conformer.error(path ++ [key], input, ":required")]}}
 
         true ->
           val = Map.get(input, key)
           {key, Conformable.conform(spec, val, path ++ [key])}
       end
     end
-
-    defp error(path, input, msg) do
-      %{path: path, input: input, msg: msg, at: nil}
-    end
   end
 
   if Code.ensure_loaded?(StreamData) do
diff --git a/lib/norm/spec.ex b/lib/norm/spec.ex
index b411feb..a11cedc 100644
--- a/lib/norm/spec.ex
+++ b/lib/norm/spec.ex
@@ -118,16 +118,12 @@ defmodule Norm.Spec do
           {:ok, input}
 
         false ->
-          {:error, [error(path, input, pred)]}
+          {:error, [Norm.Conformer.error(path, input, pred)]}
 
         _ ->
           raise ArgumentError, "Predicates must return a boolean value"
       end
     end
-
-    def error(path, input, msg) do
-      %{path: path, input: input, msg: msg, at: nil}
-    end
   end
 
   @doc false
diff --git a/lib/norm/spec/selection.ex b/lib/norm/spec/selection.ex
index c3789a6..42dbdc1 100644
--- a/lib/norm/spec/selection.ex
+++ b/lib/norm/spec/selection.ex
@@ -66,6 +66,7 @@ defmodule Norm.Spec.Selection do
   end
 
   defimpl Norm.Conformer.Conformable do
+    alias Norm.Conformer
     alias Norm.Conformer.Conformable
 
     def conform(%{subset: subset}, input, path) do
@@ -77,7 +78,7 @@ defmodule Norm.Spec.Selection do
           if val do
             {key, Conformable.conform(spec, val, path ++ [key])}
           else
-            {key, {:error, [error(path ++ [key], input, ":required")]}}
+            {key, {:error, [Conformer.error(path ++ [key], input, ":required")]}}
           end
         end)
         |> Enum.reduce(%{ok: [], error: []}, fn {key, {result, r}}, acc ->
@@ -94,9 +95,5 @@ defmodule Norm.Spec.Selection do
         {:ok, Enum.into(results.ok, %{})}
       end
     end
-
-    defp error(path, input, msg) do
-      %{path: path, input: input, msg: msg, at: nil}
-    end
   end
 end


Test patch:
diff --git a/test/norm/generator_test.exs b/test/norm/generator_test.exs
index 557467c..19f5dcc 100644
--- a/test/norm/generator_test.exs
+++ b/test/norm/generator_test.exs
@@ -9,7 +9,7 @@ defmodule Norm.GeneratorTest do
       spec = Generator.new(spec(is_integer()), :null)
 
       assert 123 == conform!(123, spec)
-      assert {:error, ["val: \"foo\" fails: is_integer()"]} = conform("foo", spec)
+      assert {:error, [%{spec: "is_integer()", input: "foo", path: []}]} = conform("foo", spec)
     end
 
     test "raises when generating" do
diff --git a/test/norm/schema_test.exs b/test/norm/schema_test.exs
index ee4163a..81a3b61 100644
--- a/test/norm/schema_test.exs
+++ b/test/norm/schema_test.exs
@@ -44,11 +44,11 @@ defmodule Norm.SchemaTest do
 
     assert %{name: "chris", age: 31} == conform!(%{name: "chris", age: 31}, s)
     assert {:error, errors} = conform(%{name: "chris"}, s)
-    assert errors == ["val: %{name: \"chris\"} in: :age fails: :required"]
+    assert errors == [%{spec: ":required", input: %{name: "chris"}, path: [:age]}]
 
     user = schema(%{user: s})
     assert {:error, errors} = conform(%{user: %{age: 31}}, user)
-    assert errors == ["val: %{age: 31} in: :user/:name fails: :required"]
+    assert errors == [%{spec: ":required", input: %{age: 31}, path: [:user, :name]}]
   end
 
   test "works with boolean values" do
@@ -63,7 +63,7 @@ defmodule Norm.SchemaTest do
 
     assert %{foo: nil} == conform!(%{foo: nil}, s)
     assert {:error, errors} = conform(%{foo: 123}, s)
-    assert errors == ["val: 123 in: :foo fails: is_nil()"]
+    assert errors == [%{spec: "is_nil()", input: 123, path: [:foo]}]
   end
 
   describe "generation" do
@@ -109,9 +109,9 @@ defmodule Norm.SchemaTest do
     assert {:error, errors} = conform(%{}, user_or_other)
 
     assert errors == [
-             "val: %{} in: :user fails: Norm.SchemaTest.User",
-             "val: %{} in: :other fails: Norm.SchemaTest.OtherUser"
-           ]
+      %{spec: "Norm.SchemaTest.User", input: %{}, path: [:user]},
+      %{spec: "Norm.SchemaTest.OtherUser", input: %{}, path: [:other]}
+    ]
   end
 
   test "can have nested alts" do
@@ -123,9 +123,9 @@ defmodule Norm.SchemaTest do
     assert {:error, errors} = conform(%{a: "test"}, s)
 
     assert errors == [
-             "val: \"test\" in: :a/:bool fails: is_boolean()",
-             "val: \"test\" in: :a/:int fails: is_integer()"
-           ]
+      %{spec: "is_boolean()", input: "test", path: [:a, :bool]},
+      %{spec: "is_integer()", input: "test", path: [:a, :int]}
+    ]
   end
 
   test "only returns specced keys" do
@@ -153,9 +153,9 @@ defmodule Norm.SchemaTest do
     assert {:error, errors} = conform(%{"name" => 31, age: "chris"}, user)
 
     assert errors == [
-             "val: \"chris\" in: :age fails: is_integer()",
-             "val: 31 in: \"name\" fails: is_binary()"
-           ]
+      %{spec: "is_integer()", input: "chris", path: [:age]},
+      %{spec: "is_binary()", input: 31, path: ["name"]}
+    ]
   end
 
   describe "schema/1 with struct" do
@@ -165,7 +165,7 @@ defmodule Norm.SchemaTest do
       assert {:error, errors} = conform(input, schema(%User{}))
 
       assert errors == [
-               "val: %{age: 31, email: \"c@keathley.io\", name: \"chris\"} fails: Norm.SchemaTest.User"
+        %{spec: "Norm.SchemaTest.User", input: %{age: 31, email: "c@keathley.io", name: "chris"}, path: []}
              ]
     end
 
@@ -175,7 +175,7 @@ defmodule Norm.SchemaTest do
       assert {:error, errors} = conform(input, schema(%OtherUser{}))
 
       assert errors == [
-               "val: %Norm.SchemaTest.User{age: 31, email: \"c@keathley.io\", name: \"chris\"} fails: Norm.SchemaTest.OtherUser"
+        %{spec: "Norm.SchemaTest.OtherUser", input: %Norm.SchemaTest.User{age: 31, email: "c@keathley.io", name: "chris"}, path: []}
              ]
     end
 
@@ -192,10 +192,10 @@ defmodule Norm.SchemaTest do
       assert {:error, errors} = conform(%User{name: :foo, age: "31", email: 42}, User.s())
 
       assert errors == [
-               "val: \"31\" in: :age fails: is_integer()",
-               "val: 42 in: :email fails: is_binary()",
-               "val: :foo in: :name fails: is_binary()"
-             ]
+        %{spec: "is_integer()", input: "31", path: [:age]},
+        %{spec: "is_binary()", input: 42, path: [:email]},
+        %{spec: "is_binary()", input: :foo, path: [:name]}
+      ]
     end
 
     test "only checks the keys that have specs" do
@@ -204,7 +204,7 @@ defmodule Norm.SchemaTest do
 
       assert input == conform!(input, spec)
       assert {:error, errors} = conform(%User{name: 23}, spec)
-      assert errors == ["val: 23 in: :name fails: is_binary()"]
+      assert errors == [%{spec: "is_binary()", input: 23, path: [:name]}]
     end
 
     property "can generate proper structs" do
diff --git a/test/norm/selection_test.exs b/test/norm/selection_test.exs
index 36a69cc..3da8bfa 100644
--- a/test/norm/selection_test.exs
+++ b/test/norm/selection_test.exs
@@ -24,7 +24,7 @@ defmodule Norm.SelectionTest do
                conform!(@input, selection(user_schema(), [:age, :name]))
 
       assert {:error, errors} = conform(%{age: -100}, selection(user_schema(), [:age]))
-      assert errors == ["val: -100 in: :age fails: &(&1 > 0)"]
+      assert errors == [%{spec: "&(&1 > 0)", input: -100, path: [:age]}]
     end
 
     test "works with nested schemas" do
@@ -33,11 +33,11 @@ defmodule Norm.SelectionTest do
 
       assert %{user: %{age: 31}} == conform!(%{user: %{age: 31}}, selection)
       assert {:error, errors} = conform(%{user: %{age: -100}}, selection)
-      assert errors == ["val: -100 in: :user/:age fails: &(&1 > 0)"]
+      assert errors == [%{spec: "&(&1 > 0)", input: -100, path: [:user, :age]}]
       assert {:error, errors} = conform(%{user: %{name: "chris"}}, selection)
-      assert errors == ["val: %{name: \"chris\"} in: :user/:age fails: :required"]
+      assert errors == [%{spec: ":required", input: %{name: "chris"}, path: [:user, :age]}]
       assert {:error, errors} = conform(%{fauxuser: %{age: 31}}, selection)
-      assert errors == ["val: %{fauxuser: %{age: 31}} in: :user fails: :required"]
+      assert errors == [%{spec: ":required", input: %{fauxuser: %{age: 31}}, path: [:user]}]
     end
 
     test "errors if there are keys that aren't specified in a schema" do
diff --git a/test/norm/spec_test.exs b/test/norm/spec_test.exs
index 3f5890c..4dc084b 100644
--- a/test/norm/spec_test.exs
+++ b/test/norm/spec_test.exs
@@ -15,9 +15,9 @@ defmodule Norm.SpecTest do
 
       assert "#000000" == conform!("#000000", hex)
       assert {:error, errors} = conform(nil, hex)
-      assert errors == ["val: nil fails: is_binary()"]
+      assert errors == [%{spec: "is_binary()", input: nil, path: []}]
       assert {:error, errors} = conform("bad", hex)
-      assert errors == ["val: \"bad\" fails: &(String.starts_with?(&1, \"#\"))"]
+      assert errors == [%{spec: "&(String.starts_with?(&1, \"#\"))", input: "bad", path: []}]
     end
 
     test "'and' and 'or' can be chained" do
@@ -33,15 +33,15 @@ defmodule Norm.SpecTest do
 
       evens = spec(is_integer() and Integer.is_even())
       assert 2 == conform!(2, evens)
-      assert {:error, ["val: 3 fails: Integer.is_even()"]} == conform(3, evens)
+      assert {:error, [%{spec: "Integer.is_even()", input: 3, path: []}]} == conform(3, evens)
 
       hello = spec(Foo.hello?())
       assert "hello" == conform!("hello", hello)
-      assert {:error, ["val: \"foo\" fails: Foo.hello?()"]} == conform("foo", hello)
+      assert {:error, [%{spec: "Foo.hello?()", input: "foo", path: []}]} == conform("foo", hello)
 
       foo = spec(Foo.match?("foo"))
       assert "foo" == conform!("foo", foo)
-      assert {:error, ["val: \"bar\" fails: Foo.match?(\"foo\")"]} == conform("bar", foo)
+      assert {:error, [%{spec: "Foo.match?(\"foo\")", input: "bar", path: []}]} == conform("bar", foo)
     end
   end
 
diff --git a/test/norm/union_test.exs b/test/norm/union_test.exs
index 3853efa..658449b 100644
--- a/test/norm/union_test.exs
+++ b/test/norm/union_test.exs
@@ -12,9 +12,9 @@ defmodule Norm.UnionTest do
       assert {:error, errors} = conform(123, union)
 
       assert errors == [
-               "val: 123 fails: is not an atom.",
-               "val: 123 fails: is_binary()"
-             ]
+        %{spec: "is not an atom.", input: 123, path: []},
+        %{spec: "is_binary()", input: 123, path: []}
+      ]
     end
 
     test "accepts nil if part of the union" do
@@ -25,9 +25,9 @@ defmodule Norm.UnionTest do
       assert {:error, errors} = conform(42, union)
 
       assert errors == [
-               "val: 42 fails: is_nil()",
-               "val: 42 fails: is_binary()"
-             ]
+        %{spec: "is_nil()", input: 42, path: []},
+        %{spec: "is_binary()", input: 42, path: []}
+      ]
     end
   end
 
diff --git a/test/norm_test.exs b/test/norm_test.exs
index c3ac621..e7067c4 100644
--- a/test/norm_test.exs
+++ b/test/norm_test.exs
@@ -12,9 +12,9 @@ defmodule NormTest do
     test "can match atoms" do
       assert :ok == conform!(:ok, :ok)
       assert {:error, errors} = conform("foo", :ok)
-      assert errors == ["val: \"foo\" fails: is not an atom."]
+      assert errors == [%{spec: "is not an atom.", input: "foo", path: []}]
       assert {:error, errors} = conform(:mismatch, :ok)
-      assert errors == ["val: :mismatch fails: == :ok"]
+      assert errors == [%{spec: "== :ok", input: :mismatch, path: []}]
     end
 
     test "can match patterns of tuples" do
@@ -30,18 +30,18 @@ defmodule NormTest do
       assert {:error, errors} = conform({1, :bar, "foo"}, three)
 
       assert errors == [
-               "val: :bar in: 1 fails: is_integer()",
-               "val: \"foo\" in: 2 fails: is_integer()"
-             ]
+        %{spec: "is_integer()", input: :bar, path: [1]},
+        %{spec: "is_integer()", input: "foo", path: [2]}
+      ]
 
       assert {:error, errors} = conform({:ok, "foo"}, ok)
-      assert errors == ["val: \"foo\" in: 1 fails: is_integer()"]
+      assert errors == [%{spec: "is_integer()", input: "foo", path: [1]}]
 
       assert {:error, errors} = conform({:ok, "foo", 123}, ok)
-      assert errors == ["val: {:ok, \"foo\", 123} fails: incorrect tuple size"]
+      assert errors == [%{spec: "incorrect tuple size", input: {:ok, "foo", 123}, path: []}]
 
       assert {:error, errors} = conform({:ok, 123, "foo"}, ok)
-      assert errors == ["val: {:ok, 123, \"foo\"} fails: incorrect tuple size"]
+      assert errors == [%{spec: "incorrect tuple size", input: {:ok, 123, "foo"}, path: []}]
     end
 
     test "tuples can be composed with schema's and selections" do
@@ -50,7 +50,7 @@ defmodule NormTest do
 
       assert {:ok, %{name: "chris"}} == conform!({:ok, %{name: "chris", age: 31}}, ok)
       assert {:error, errors} = conform({:ok, %{age: 31}}, ok)
-      assert errors == ["val: %{age: 31} in: 1/:name fails: :required"]
+      assert errors == [%{spec: ":required", input: %{age: 31}, path: [1, :name]}]
     end
 
     @tag :skip
@@ -134,9 +134,9 @@ defmodule NormTest do
       assert {:error, errors} = conform(%{name: :alice}, spec)
 
       assert errors == [
-               "val: :alice in: :a/:name fails: is_binary()",
-               "val: %{name: :alice} in: :b fails: is_binary()"
-             ]
+        %{spec: "is_binary()", input: :alice, path: [:a, :name]},
+        %{spec: "is_binary()", input: %{name: :alice}, path: [:b]}
+      ]
     end
 
     test "can generate data" do
@@ -176,8 +176,8 @@ defmodule NormTest do
       assert [:foo, :bar, :baz] == conform!([:foo, :bar, :baz], spec)
       assert {:error, errors} = conform([:foo, 1, "test"], spec)
       assert errors == [
-        "val: 1 in: 1 fails: is_atom()",
-        "val: \"test\" in: 2 fails: is_atom()"
+        %{spec: "is_atom()", input: 1, path: [1]},
+        %{spec: "is_atom()", input: "test", path: [2]}
       ]
     end
 
@@ -196,8 +196,8 @@ defmodule NormTest do
       ]
       assert {:error, errors} = conform(input, spec)
       assert errors == [
-        "val: %{age: 31, email: \"c@keathley.io\"} in: 0/:name fails: :required",
-        "val: :andra in: 1/:name fails: is_binary()"
+        %{spec: ":required", input: %{age: 31, email: "c@keathley.io"}, path: [0, :name]},
+        %{spec: "is_binary()", input: :andra, path: [1, :name]}
       ]
     end
 
@@ -205,15 +205,15 @@ defmodule NormTest do
       spec = coll_of(spec(is_integer()), distinct: true)
 
       assert {:error, errors} = conform([1,1,1], spec)
-      assert errors == ["val: [1, 1, 1] fails: distinct?"]
+      assert errors == [%{spec: "distinct?", input: [1, 1, 1], path: []}]
     end
 
     test "can enforce min and max counts" do
       spec = coll_of(spec(is_integer()), min_count: 2, max_count: 3)
       assert [1, 1] == conform!([1, 1], spec)
       assert [1, 1, 1] == conform!([1, 1, 1], spec)
-      assert {:error, ["val: [1] fails: min_count: 2"]} == conform([1], spec)
-      assert {:error, ["val: [1, 1, 1, 1] fails: max_count: 3"]} == conform([1, 1, 1, 1], spec)
+      assert {:error, [%{spec: "min_count: 2", input: [1], path: []}]} == conform([1], spec)
+      assert {:error, [%{spec: "max_count: 3", input: [1, 1, 1, 1], path: []}]} == conform([1, 1, 1, 1], spec)
 
       spec = coll_of(spec(is_integer()), min_count: 3, max_count: 3)
       assert [1, 1, 1] == conform!([1, 1, 1], spec)