Repository: keathley/norm
Pull request #50 (2020-02-23 21:00:50)

Moving internal structs into core directory
This is a change to the organization of the internal modules and structs. It unifies all of the structs that make up Norm's AST into a `core` directory. I'm unifying things in this way to avoid the overloading of terms that was happening before. This should make it easier for other folks to work on Norm and understand where pieces are located.


Patch:
diff --git a/lib/norm.ex b/lib/norm.ex
index 614e181..3fe2acf 100644
--- a/lib/norm.ex
+++ b/lib/norm.ex
@@ -336,19 +336,17 @@ defmodule Norm do
   alias Norm.Conformer
   alias Norm.Generatable
   alias Norm.Generator
-  alias Norm.Spec
-
-  alias Norm.Spec.{
+  alias Norm.MismatchError
+  alias Norm.GeneratorError
+  alias Norm.Core.{
     Alt,
+    AnyOf,
+    Collection,
+    Schema,
     Selection,
-    Union,
-    Collection
+    Spec,
   }
 
-  alias Norm.Schema
-  alias Norm.MismatchError
-  alias Norm.GeneratorError
-
   @doc false
   defmacro __using__(_) do
     quote do
@@ -478,11 +476,7 @@ defmodule Norm do
       {:error, [%{spec: "is_atom()", input: 21, path: []}, %{spec: "is_binary()", input: 21, path: []}]}
   """
   defmacro spec(predicate) do
-    spec = Spec.build(predicate)
-
-    quote do
-      unquote(spec)
-    end
+    Spec.build(predicate)
   end
 
   @doc ~S"""
@@ -577,7 +571,7 @@ defmodule Norm do
       :alice
   """
   def one_of(specs) when is_list(specs) do
-    Union.new(specs)
+    AnyOf.new(specs)
   end
 
   @doc ~S"""
diff --git a/lib/norm/core/all_of.ex b/lib/norm/core/all_of.ex
new file mode 100644
index 0000000..cd5f75c
--- /dev/null
+++ b/lib/norm/core/all_of.ex
@@ -0,0 +1,28 @@
+defmodule Norm.Core.AllOf do
+  @moduledoc false
+
+  defstruct specs: []
+
+  def new(specs) do
+    %__MODULE__{specs: specs}
+  end
+
+  defimpl Norm.Conformer.Conformable do
+    alias Norm.Conformer
+    alias Norm.Conformer.Conformable
+
+    def conform(%{specs: specs}, input, path) do
+      result =
+        specs
+        |> Enum.map(fn spec -> Conformable.conform(spec, input, path) end)
+        |> Conformer.group_results()
+
+      if result.error != [] do
+        {:error, List.flatten(result.error)}
+      else
+        {:ok, Enum.at(result.ok, 0)}
+      end
+    end
+  end
+end
+
diff --git a/lib/norm/spec/alt.ex b/lib/norm/core/alt.ex
similarity index 98%
rename from lib/norm/spec/alt.ex
rename to lib/norm/core/alt.ex
index 710de0c..fe4deb4 100644
--- a/lib/norm/spec/alt.ex
+++ b/lib/norm/core/alt.ex
@@ -1,4 +1,4 @@
-defmodule Norm.Spec.Alt do
+defmodule Norm.Core.Alt do
   @moduledoc false
 
   defstruct specs: []
diff --git a/lib/norm/spec/union.ex b/lib/norm/core/any_of.ex
similarity index 97%
rename from lib/norm/spec/union.ex
rename to lib/norm/core/any_of.ex
index 2d8407f..51fed56 100644
--- a/lib/norm/spec/union.ex
+++ b/lib/norm/core/any_of.ex
@@ -1,4 +1,4 @@
-defmodule Norm.Spec.Union do
+defmodule Norm.Core.AnyOf do
   @moduledoc false
   # Provides the struct for unions of specifications
 
diff --git a/lib/norm/spec/collection.ex b/lib/norm/core/collection.ex
similarity index 99%
rename from lib/norm/spec/collection.ex
rename to lib/norm/core/collection.ex
index 56d856b..9291190 100644
--- a/lib/norm/spec/collection.ex
+++ b/lib/norm/core/collection.ex
@@ -1,4 +1,4 @@
-defmodule Norm.Spec.Collection do
+defmodule Norm.Core.Collection do
   @moduledoc false
 
   defstruct spec: nil, opts: []
diff --git a/lib/norm/schema.ex b/lib/norm/core/schema.ex
similarity index 97%
rename from lib/norm/schema.ex
rename to lib/norm/core/schema.ex
index 781cb30..972fec6 100644
--- a/lib/norm/schema.ex
+++ b/lib/norm/core/schema.ex
@@ -1,4 +1,4 @@
-defmodule Norm.Schema do
+defmodule Norm.Core.Schema do
   @moduledoc false
   # Provides the definition for schemas
 
@@ -58,7 +58,7 @@ defmodule Norm.Schema do
     end
 
     # conforming a map.
-    def conform(%Norm.Schema{specs: specs}, input, path) do
+    def conform(%Schema{specs: specs}, input, path) do
       if Map.get(input, :__struct__) != nil do
         with {:ok, conformed} <- check_specs(specs, Map.from_struct(input), path) do
           {:ok, struct(input.__struct__, conformed)}
diff --git a/lib/norm/spec/selection.ex b/lib/norm/core/selection.ex
similarity index 97%
rename from lib/norm/spec/selection.ex
rename to lib/norm/core/selection.ex
index d0c6964..ff842b1 100644
--- a/lib/norm/spec/selection.ex
+++ b/lib/norm/core/selection.ex
@@ -1,10 +1,10 @@
-defmodule Norm.Spec.Selection do
+defmodule Norm.Core.Selection do
   @moduledoc false
   # Provides the definition for selections
 
   defstruct required: [], schema: nil
 
-  alias Norm.Schema
+  alias Norm.Core.Schema
   alias Norm.SpecError
 
   def new(schema, selectors) do
@@ -156,7 +156,7 @@ defmodule Norm.Spec.Selection do
       defp to_gen(key, schema, generator) do
         # Its safe to just get the spec because at this point we *know* that the
         # keys that have been selected are in the schema.
-        with {:ok, g} <- Generatable.gen(Norm.Schema.spec(schema, key)) do
+        with {:ok, g} <- Generatable.gen(Norm.Core.Schema.spec(schema, key)) do
           Map.put(generator, key, g)
         end
       end
diff --git a/lib/norm/spec.ex b/lib/norm/core/spec.ex
similarity index 98%
rename from lib/norm/spec.ex
rename to lib/norm/core/spec.ex
index 76b3606..988829e 100644
--- a/lib/norm/spec.ex
+++ b/lib/norm/core/spec.ex
@@ -1,10 +1,10 @@
-defmodule Norm.Spec do
+defmodule Norm.Core.Spec do
   @moduledoc false
   # Provides a struct to encapsulate specs
 
   alias __MODULE__
 
-  alias Norm.Spec.{
+  alias Norm.Core.Spec.{
     And,
     Or
   }
diff --git a/lib/norm/spec/and.ex b/lib/norm/core/spec/and.ex
similarity index 95%
rename from lib/norm/spec/and.ex
rename to lib/norm/core/spec/and.ex
index b8c4b4f..da2bdaa 100644
--- a/lib/norm/spec/and.ex
+++ b/lib/norm/core/spec/and.ex
@@ -1,7 +1,7 @@
-defmodule Norm.Spec.And do
+defmodule Norm.Core.Spec.And do
   @moduledoc false
 
-  alias Norm.Spec
+  alias Norm.Core.Spec
   alias __MODULE__
 
   defstruct [:left, :right]
diff --git a/lib/norm/spec/or.ex b/lib/norm/core/spec/or.ex
similarity index 97%
rename from lib/norm/spec/or.ex
rename to lib/norm/core/spec/or.ex
index 6759fb4..80f249a 100644
--- a/lib/norm/spec/or.ex
+++ b/lib/norm/core/spec/or.ex
@@ -1,4 +1,4 @@
-defmodule Norm.Spec.Or do
+defmodule Norm.Core.Spec.Or do
   @moduledoc false
 
   defstruct [:left, :right]
diff --git a/lib/norm/errors.ex b/lib/norm/errors.ex
index 0ffce4c..bbd9401 100644
--- a/lib/norm/errors.ex
+++ b/lib/norm/errors.ex
@@ -32,11 +32,11 @@ end
 
 defmodule Norm.SpecError do
   defexception [:message]
-  alias Norm.Spec
-  alias Norm.Schema
-  alias Norm.Spec.Collection
-  alias Norm.Spec.Alt
-  alias Norm.Spec.Union
+  alias Norm.Core.Spec
+  alias Norm.Core.Schema
+  alias Norm.Core.Collection
+  alias Norm.Core.Alt
+  alias Norm.Core.AnyOf
 
   def exception(details) do
     %__MODULE__{message: msg(details)}
@@ -86,7 +86,7 @@ defmodule Norm.SpecError do
       "alt([])"
     end
   end
-  defp format(%Union{specs: specs}, i) do
+  defp format(%AnyOf{specs: specs}, i) do
     formatted =
       specs
       |> Enum.map(&format(&1, i))
diff --git a/mix.exs b/mix.exs
index 299c4be..fa9f941 100644
--- a/mix.exs
+++ b/mix.exs
@@ -8,6 +8,7 @@ defmodule Norm.MixProject do
       app: :norm,
       version: @version,
       elixir: "~> 1.7",
+      elixirc_paths: elixirc_paths(Mix.env()),
       start_permanent: Mix.env() == :prod,
       deps: deps(),
       description: description(),
@@ -18,6 +19,9 @@ defmodule Norm.MixProject do
     ]
   end
 
+  defp elixirc_paths(:test), do: ["lib", "test/support"]
+  defp elixirc_paths(_), do: ["lib"]
+
   def application do
     [
       env: [enable_contracts: true]


Test patch:
diff --git a/test/norm/alt_test.exs b/test/norm/core/alt_test.exs
similarity index 89%
rename from test/norm/alt_test.exs
rename to test/norm/core/alt_test.exs
index e5e058b..60a30bc 100644
--- a/test/norm/alt_test.exs
+++ b/test/norm/core/alt_test.exs
@@ -1,6 +1,5 @@
-defmodule Norm.Spec.AltTest do
-  use ExUnit.Case, async: true
-  import Norm
+defmodule Norm.Core.AltTest do
+  use Norm.Case, async: true
 
   describe "generation" do
     test "returns one of the options" do
diff --git a/test/norm/union_test.exs b/test/norm/core/any_of_test.exs
similarity index 91%
rename from test/norm/union_test.exs
rename to test/norm/core/any_of_test.exs
index c34da90..ea1c110 100644
--- a/test/norm/union_test.exs
+++ b/test/norm/core/any_of_test.exs
@@ -1,7 +1,5 @@
-defmodule Norm.UnionTest do
-  use ExUnit.Case, async: true
-  import ExUnitProperties, except: [gen: 1]
-  import Norm
+defmodule Norm.Core.AnyOfTest do
+  use Norm.Case, async: true
 
   describe "conforming" do
     test "returns the first match" do
diff --git a/test/norm/spec/collection_test.exs b/test/norm/core/collection_test.exs
similarity index 61%
rename from test/norm/spec/collection_test.exs
rename to test/norm/core/collection_test.exs
index 929ed81..2d2ae88 100644
--- a/test/norm/spec/collection_test.exs
+++ b/test/norm/core/collection_test.exs
@@ -1,6 +1,5 @@
-defmodule Norm.Spec.CollectionTest do
-  use ExUnit.Case, async: true
-  import Norm
+defmodule Norm.Core.CollectionTest do
+  use Norm.Case, async: true
 
   test "inspect" do
     spec = coll_of(spec(is_atom()))
diff --git a/test/norm/schema_test.exs b/test/norm/core/schema_test.exs
similarity index 90%
rename from test/norm/schema_test.exs
rename to test/norm/core/schema_test.exs
index c6eb4ab..729b60e 100644
--- a/test/norm/schema_test.exs
+++ b/test/norm/core/schema_test.exs
@@ -1,7 +1,5 @@
-defmodule Norm.SchemaTest do
-  use ExUnit.Case, async: true
-  import Norm
-  import ExUnitProperties, except: [gen: 1]
+defmodule Norm.Core.SchemaTest do
+  use Norm.Case, async: true
 
   defmodule User do
     import Norm
@@ -77,8 +75,8 @@ defmodule Norm.SchemaTest do
     assert {:error, errors} = conform(%{}, user_or_other)
 
     assert errors == [
-      %{spec: "Norm.SchemaTest.User", input: %{}, path: [:user]},
-      %{spec: "Norm.SchemaTest.OtherUser", input: %{}, path: [:other]}
+      %{spec: "Norm.Core.SchemaTest.User", input: %{}, path: [:user]},
+      %{spec: "Norm.Core.SchemaTest.OtherUser", input: %{}, path: [:other]}
     ]
   end
 
@@ -131,7 +129,7 @@ defmodule Norm.SchemaTest do
       assert {:error, errors} = conform(input, schema(%User{}))
 
       assert errors == [
-        %{spec: "Norm.SchemaTest.User", input: %{age: 31, email: "c@keathley.io", name: "chris"}, path: []}
+        %{spec: "Norm.Core.SchemaTest.User", input: %{age: 31, email: "c@keathley.io", name: "chris"}, path: []}
       ]
     end
 
@@ -141,7 +139,7 @@ defmodule Norm.SchemaTest do
       assert {:error, errors} = conform(input, schema(%OtherUser{}))
 
       assert errors == [
-        %{spec: "Norm.SchemaTest.OtherUser", input: %Norm.SchemaTest.User{age: 31, email: "c@keathley.io", name: "chris"}, path: []}
+        %{spec: "Norm.Core.SchemaTest.OtherUser", input: %User{age: 31, email: "c@keathley.io", name: "chris"}, path: []}
       ]
     end
 
@@ -245,7 +243,7 @@ defmodule Norm.SchemaTest do
     end
 
     test "struct schema" do
-      assert inspect(User.s()) == "#Norm.Schema<%Norm.SchemaTest.User{age: #Norm.Spec<is_integer() and &(&1 >= 0)>, email: #Norm.Spec<is_binary()>, name: #Norm.Spec<is_binary()>}>"
+      assert inspect(User.s()) == "#Norm.Schema<%Norm.Core.SchemaTest.User{age: #Norm.Spec<is_integer() and &(&1 >= 0)>, email: #Norm.Spec<is_binary()>, name: #Norm.Spec<is_binary()>}>"
     end
   end
 end
diff --git a/test/norm/selection_test.exs b/test/norm/core/selection_test.exs
similarity index 98%
rename from test/norm/selection_test.exs
rename to test/norm/core/selection_test.exs
index 5d97911..183ffd0 100644
--- a/test/norm/selection_test.exs
+++ b/test/norm/core/selection_test.exs
@@ -1,6 +1,5 @@
-defmodule Norm.SelectionTest do
-  use ExUnit.Case, async: true
-  import Norm
+defmodule Norm.Core.SelectionTest do
+  use Norm.Case, async: true
 
   defmodule Event do
     import Norm
diff --git a/test/norm/spec_test.exs b/test/norm/core/spec_test.exs
similarity index 97%
rename from test/norm/spec_test.exs
rename to test/norm/core/spec_test.exs
index e2b608d..c70b553 100644
--- a/test/norm/spec_test.exs
+++ b/test/norm/core/spec_test.exs
@@ -1,7 +1,5 @@
-defmodule Norm.SpecTest do
-  use ExUnit.Case, async: true
-  import Norm
-  import ExUnitProperties, except: [gen: 1]
+defmodule Norm.Core.SpecTest do
+  use Norm.Case, async: true
 
   defmodule Foo do
     def hello?(str), do: str == "hello"
diff --git a/test/norm_test.exs b/test/norm_test.exs
index 76cc593..eac68e6 100644
--- a/test/norm_test.exs
+++ b/test/norm_test.exs
@@ -56,9 +56,9 @@ defmodule NormTest do
       assert errors == [%{spec: ":required", input: %{age: 31}, path: [1, :name]}]
     end
 
-    @tag :skip
     test "can spec keyword lists" do
-      flunk("Not Implemented")
+      list = coll_of(one_of([name: spec(is_atom())]))
+      assert conform!([name: :foo], list) == [name: :foo]
     end
   end
 
diff --git a/test/support/norm_case.ex b/test/support/norm_case.ex
new file mode 100644
index 0000000..81acd36
--- /dev/null
+++ b/test/support/norm_case.ex
@@ -0,0 +1,11 @@
+defmodule Norm.Case do
+  @moduledoc false
+  use ExUnit.CaseTemplate
+
+  using do
+    quote do
+      import Norm
+      import ExUnitProperties, except: [gen: 1]
+    end
+  end
+end