diff --git a/README.md b/README.md index d249dc9..1f164b0 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ user_schema = schema(%{ input = %{user: %{name: "chris", age: 30, email: "c@keathley.io"}} conform!(input, user_schema) -=> %{user: %{name: "chris", age: 30}} +=> %{user: %{name: "chris", age: 30, email: "c@keathley.io"}} user_schema |> gen() @@ -35,7 +35,7 @@ user_schema => [ %{user: %{age: 0, name: ""}}, %{user: %{age: 2, name: "x"}}, - %{user: %{age: -2, name: ""}} + %{user: %{age: 1, name: ""}} ] ``` diff --git a/lib/norm.ex b/lib/norm.ex index b6c192d..5ba9e4c 100644 --- a/lib/norm.ex +++ b/lib/norm.ex @@ -16,6 +16,7 @@ defmodule Norm do Schema, Selection, Spec, + Delegate } @doc false @@ -147,6 +148,20 @@ defmodule Norm do Spec.build(predicate) end + @doc ~S""" + Allows encapsulation of a spec in another function. This enables late-binding of + specs which enables definition of recursive specs. + + ## Examples: + iex> conform!(%{"value" => 1, "left" => %{"value" => 2, "right" => %{"value" => 4}}}, Norm.Core.DelegateTest.TreeTest.spec()) + %{"value" => 1, "left" => %{"value" => 2, "right" => %{"value" => 4}}} + iex> conform(%{"value" => 1, "left" => %{"value" => 2, "right" => %{"value" => 4, "right" => %{"value" => "12"}}}}, Norm.Core.DelegateTest.TreeTest.spec()) + {:error, [%{input: "12", path: ["left", "right", "right", "value"], spec: "is_integer()"}]} + """ + def delegate(predicate) do + Delegate.build(predicate) + end + @doc ~S""" Creates a re-usable schema. Schema's are open which means that all keys are optional and any non-specified keys are passed through without being conformed. diff --git a/lib/norm/core/delegate.ex b/lib/norm/core/delegate.ex new file mode 100644 index 0000000..0db42bf --- /dev/null +++ b/lib/norm/core/delegate.ex @@ -0,0 +1,15 @@ +defmodule Norm.Core.Delegate do + @moduledoc false + + defstruct [:fun] + + def build(fun) when is_function(fun, 0) do + %__MODULE__{fun: fun} + end + + defimpl Norm.Conformer.Conformable do + def conform(%{fun: fun}, input, path) do + Norm.Conformer.Conformable.conform(fun.(), input, path) + end + end +end diff --git a/mix.lock b/mix.lock index 6176c71..38eefc6 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,13 @@ %{ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"}, + "credo": {:hex, :credo, "1.5.1", "4fe303cc828412b9d21eed4eab60914c401e71f117f40243266aafb66f30d036", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "0b219ca4dcc89e4e7bc6ae7e6539c313e738e192e10b85275fa1e82b5203ecd7"}, "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, - "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"}, - "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, - "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, + "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, + "file_system": {:hex, :file_system, "0.2.9", "545b9c9d502e8bfa71a5315fac2a923bd060fd9acb797fe6595f54b0f975fd32", [:mix], [], "hexpm", "3cf87a377fe1d93043adeec4889feacf594957226b4f19d5897096d6f61345d8"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, } diff --git a/test/norm/core/delegate_test.exs b/test/norm/core/delegate_test.exs new file mode 100644 index 0000000..ae2c5a8 --- /dev/null +++ b/test/norm/core/delegate_test.exs @@ -0,0 +1,35 @@ +defmodule Norm.Core.DelegateTest do + use Norm.Case, async: true + + defmodule TreeTest do + def spec() do + schema(%{ + "value" => spec(is_integer()), + "left" => delegate(&TreeTest.spec/0), + "right" => delegate(&TreeTest.spec/0) + }) + end + end + + describe "delegate/1" do + test "can write recursive specs with 'delegate'" do + assert {:ok, _} = conform(%{}, TreeTest.spec()) + + assert {:ok, _} = + conform( + %{"value" => 4, "left" => %{"value" => 2}, "right" => %{"value" => 12}}, + TreeTest.spec() + ) + + assert {:error, [%{input: "12", path: ["left", "left", "value"], spec: "is_integer()"}]} = + conform( + %{ + "value" => 4, + "left" => %{"value" => 2, "left" => %{"value" => "12"}}, + "right" => %{"value" => 12} + }, + TreeTest.spec() + ) + end + end +end