Repository: keathley/norm (elixir)

Issue:
"protocol Norm.Conformer.Conformable not implemented" errors on incomplete schemas
I've only come across this error when working with lists, but perhaps it's a more general issue when one forgets to write a full schema.

For example:

```elixir
defmodule Movie do
  use Norm
  
  defstruct directors: [], actors: []

  defmodule Person do
    use Norm
    
    defstruct name: ""

    def s(),
      do:
        schema(%__MODULE__{
          name: spec(is_binary())
        })
  end

  def s(),
    do:
      schema(%__MODULE__{
        directors: coll_of(Person.s())
      })

  def new(), do: %__MODULE__{}
end
```

When you try:

```elixir
Movie.new() |> conform!(Movie.s())
```

You get the error message:

```
** (Protocol.UndefinedError) protocol Norm.Conformer.Conformable not implemented for [] of type List
```

The "workaround" is to include a schema entry for `actors` in the `Movie` module, alongside the one for `directors`. Beyond being an unhelpful error message, I presume it's a bug in Norm, which has the concept of incremental "speccing".

For reference, a fuller version of the error message is:

```elixir
** (Protocol.UndefinedError) protocol Norm.Conformer.Conformable not implemented for [] of type List.

This protocol is implemented for the following type(s): Norm.Spec.Or, Atom, Norm.Spec.Union, Norm.Schema, Norm.Spec.And, Tuple, Norm.Spec.Collection, Norm.Generator, Norm.Spec, Norm.Spec.Selection, Norm.Spec.Alt
```


Pull request:
Allow structs to conform with default keys
Closes #31 

Patch:
diff --git a/lib/norm/conformer.ex b/lib/norm/conformer.ex
index c5bd8f4..25f7f1a 100644
--- a/lib/norm/conformer.ex
+++ b/lib/norm/conformer.ex
@@ -50,10 +50,17 @@ defmodule Norm.Conformer do
     # 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.
 
+    # @fallback_to_any true
     def conform(spec, path, input)
   end
 end
 
+# defimpl Norm.Conformer.Conformable, for: Any do
+#   def conform(_thing, input, _path) do
+#     {:ok, input}
+#   end
+# end
+
 defimpl Norm.Conformer.Conformable, for: Atom do
   alias Norm.Conformer
 
diff --git a/lib/norm/schema.ex b/lib/norm/schema.ex
index b51accd..781cb30 100644
--- a/lib/norm/schema.ex
+++ b/lib/norm/schema.ex
@@ -6,13 +6,15 @@ defmodule Norm.Schema do
 
   defstruct specs: %{}, struct: nil
 
-  # If we're building a schema from a struct then we need to add a default spec
-  # for each key that only checks for presence. This allows users to specify
-  # struct types without needing to specify specs for each key
   def build(%{__struct__: name} = struct) do
+    # If we're building a schema from a struct then we need to reject any keys with
+    # values that don't implement the conformable protocol. This allows users to specify
+    # struct types without needing to specify specs for each key
     specs =
       struct
       |> Map.from_struct()
+      |> Enum.reject(fn {_, value} -> Norm.Conformer.Conformable.impl_for(value) == nil end)
+      |> Enum.into(%{})
 
     %Schema{specs: specs, struct: name}
   end
diff --git a/lib/norm/spec.ex b/lib/norm/spec.ex
index 7bdcdf7..76b3606 100644
--- a/lib/norm/spec.ex
+++ b/lib/norm/spec.ex
@@ -89,37 +89,24 @@ defmodule Norm.Spec do
 
   if Code.ensure_loaded?(StreamData) do
     defimpl Norm.Generatable do
-      @supported_primitives [
-        :is_atom,
-        :is_binary,
-        :is_bitstring,
-        :is_boolean,
-        :is_float,
-        :is_integer
-      ]
-
       def gen(%{generator: gen, predicate: pred}) do
-        predicate =
-          Enum.find(@supported_primitives, {:error, pred}, fn predicate ->
-            gen == predicate
-          end)
-
-        generator_from_predicate(predicate)
+        case build_generator(gen) do
+          nil       -> {:error, pred}
+          generator -> {:ok, generator}
+        end
       end
 
-      defp generator_from_predicate({:error, predicate}), do: {:error, predicate}
-
-      defp generator_from_predicate(:is_atom),
-        do: {:ok, apply(StreamData, :atom, [:alphanumeric])}
-
-      defp generator_from_predicate(predicate) do
-        data_type =
-          predicate
-          |> Atom.to_string()
-          |> String.slice(3..-1)
-          |> String.to_atom()
-
-        {:ok, apply(StreamData, data_type, [])}
+      defp build_generator(gen) do
+        case gen do
+          :is_atom      -> StreamData.atom(:alphanumeric)
+          :is_binary    -> StreamData.binary()
+          :is_bitstring -> StreamData.bitstring()
+          :is_boolean   -> StreamData.boolean()
+          :is_float     -> StreamData.float()
+          :is_integer   -> StreamData.integer()
+          :is_list      -> StreamData.list_of(StreamData.term())
+          _             -> nil
+        end
       end
     end
   end


Test patch:
diff --git a/test/norm/schema_test.exs b/test/norm/schema_test.exs
index cf37423..c6eb4ab 100644
--- a/test/norm/schema_test.exs
+++ b/test/norm/schema_test.exs
@@ -171,6 +171,15 @@ defmodule Norm.SchemaTest do
       assert errors == [%{spec: "is_binary()", input: 23, path: [:name]}]
     end
 
+    defmodule Movie do
+      defstruct directors: [:foo, :bar, :baz], producers: []
+    end
+
+    test "allows defaults" do
+      spec = schema(%Movie{})
+      assert movie = conform(%Movie{}, spec)
+    end
+
     property "can generate proper structs" do
       check all(user <- gen(User.s())) do
         assert match?(%User{}, user)
@@ -187,6 +196,12 @@ defmodule Norm.SchemaTest do
         assert is_nil(user.name)
         assert is_nil(user.email)
       end
+
+      check all(movie <- gen(schema(%Movie{producers: spec(is_list())}))) do
+        assert match?(%Movie{}, movie)
+        assert movie.directors == [:foo, :bar, :baz]
+        assert is_list(movie.producers)
+      end
     end
   end