Repository: keathley/norm (elixir)

Issue:
SpecError from selections direct attention away from the problem
```elixir
defmodule Extra do
  use Norm
  defstruct [:not_the_problem]
end

defmodule A do
  use Norm
  def assigns, do: schema(%{extra: schema(%Extra{})})
  def test, do: conform!(%{extras: %Extra{}}, selection(assigns(), :all))
end

A.test()
```
→
```
** (Norm.SpecError) key: :not_the_problem was not found in schema:
%{
  :not_the_problem => :
}

    (norm 0.12.0) lib/norm/core/selection.ex:56: Norm.Core.Selection.assert_spec!/2
    (norm 0.12.0) lib/norm/core/selection.ex:36: Norm.Core.Selection.select/3
    (norm 0.12.0) lib/norm/core/selection.ex:32: Norm.Core.Selection.select/3
    iex:70: A.test/0
```
→ wasting a whole bunch of time trying to figure out how `not_the_problem` could possibly be missing from a struct, when actually the problem is forgetting you meant the singular `extra` not the plural `extras`.

Pull request description:
Ensure key exists in selection
Fixes #84 

Patch:
diff --git a/lib/norm/core/schema.ex b/lib/norm/core/schema.ex
index 972fec6..7a89c2b 100644
--- a/lib/norm/core/schema.ex
+++ b/lib/norm/core/schema.ex
@@ -30,6 +30,11 @@ defmodule Norm.Core.Schema do
     |> Enum.at(0)
   end
 
+  def key_present?(schema, key) do
+    schema.specs
+    |> Enum.any?(fn {name, _} -> name == key end)
+  end
+
   defimpl Norm.Conformer.Conformable do
     alias Norm.Conformer
     alias Norm.Conformer.Conformable
diff --git a/lib/norm/core/selection.ex b/lib/norm/core/selection.ex
index f4d2abc..39217df 100644
--- a/lib/norm/core/selection.ex
+++ b/lib/norm/core/selection.ex
@@ -52,9 +52,9 @@ defmodule Norm.Core.Selection do
   defp validate_selectors!(other), do: raise ArgumentError, "select expects a list of keys but received: #{inspect other}"
 
   defp assert_spec!(%Schema{}=schema, key) do
-    case Schema.spec(schema, key) do
-      nil -> raise SpecError, {:selection, key, schema}
-      spec -> spec
+    case Schema.key_present?(schema, key) do
+      false -> raise SpecError, {:selection, key, schema}
+      true -> Schema.spec(schema, key)
     end
   end
   defp assert_spec!(%__MODULE__{}, _key) do


Test patch:
diff --git a/test/norm/core/selection_test.exs b/test/norm/core/selection_test.exs
index f6ee3f8..8d3eb7b 100644
--- a/test/norm/core/selection_test.exs
+++ b/test/norm/core/selection_test.exs
@@ -121,6 +121,15 @@ defmodule Norm.Core.SelectionTest do
 
     test "works with structs" do
       assert %Event{} = conform!(%Event{data: %{type: :foo}}, selection(Event.s()))
+
+      assert {:error, errors} = conform(%{typo: %Event{}}, selection(schema(%{event: schema(%Event{data: spec(is_atom)})})))
+      assert errors == [
+        %{input: %{typo: %Norm.Core.SelectionTest.Event{data: nil}}, path: [:event], spec: ":required"}
+      ]
+    end
+
+    test "allows default spec values" do
+      assert %Event{} = conform!(%Event{data: %{type: :foo}}, selection(schema(%Event{})))
     end
 
     test "returns deeply nested errors" do
@@ -222,8 +231,19 @@ defmodule Norm.Core.SelectionTest do
 
   describe "inspect" do
     test "single selection" do
-      assert inspect(selection(user_schema())) == "#Norm.Selection<%{required: [:name, :email, :age], schema: #Norm.Schema<%{age: #Norm.Spec<is_integer() and &(&1 > 0)>, email: #Norm.Spec<is_binary() and &(&1 =~ ~r\"@\")>, name: #Norm.Spec<is_binary()>}>}>"
-      assert inspect(selection(user_schema(), [:name])) == "#Norm.Selection<%{required: [:name], schema: #Norm.Schema<%{age: #Norm.Spec<is_integer() and &(&1 > 0)>, email: #Norm.Spec<is_binary() and &(&1 =~ ~r\"@\")>, name: #Norm.Spec<is_binary()>}>}>"
+      # The regex inspection changed in elixir 1.10. So for now we're going to support
+      # both versions in the test. We can remove this once elixir 1.12 is out.
+      if version().minor >= 10 do
+        assert inspect(selection(user_schema())) == "#Norm.Selection<%{required: [:name, :email, :age], schema: #Norm.Schema<%{age: #Norm.Spec<is_integer() and &(&1 > 0)>, email: #Norm.Spec<is_binary() and &(&1 =~ ~r/@/)>, name: #Norm.Spec<is_binary()>}>}>"
+        assert inspect(selection(user_schema(), [:name])) == "#Norm.Selection<%{required: [:name], schema: #Norm.Schema<%{age: #Norm.Spec<is_integer() and &(&1 > 0)>, email: #Norm.Spec<is_binary() and &(&1 =~ ~r/@/)>, name: #Norm.Spec<is_binary()>}>}>"
+      else
+        assert inspect(selection(user_schema())) == "#Norm.Selection<%{required: [:name, :email, :age], schema: #Norm.Schema<%{age: #Norm.Spec<is_integer() and &(&1 > 0)>, email: #Norm.Spec<is_binary() and &(&1 =~ ~r\"@\")>, name: #Norm.Spec<is_binary()>}>}>"
+        assert inspect(selection(user_schema(), [:name])) == "#Norm.Selection<%{required: [:name], schema: #Norm.Schema<%{age: #Norm.Spec<is_integer() and &(&1 > 0)>, email: #Norm.Spec<is_binary() and &(&1 =~ ~r\"@\")>, name: #Norm.Spec<is_binary()>}>}>"
+      end
     end
   end
+
+  defp version do
+    Version.parse!(System.version())
+  end
 end