From d07d65efe7b24e0a716ce67461c718a567b8fddd Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 14 Sep 2023 09:05:09 +0200 Subject: [PATCH 1/2] Move building up validation steps to Key --- lib/drops/contract.ex | 38 ++++++++++++----------- lib/drops/contract/dsl.ex | 20 ++++-------- lib/drops/contract/schema.ex | 42 ++++++-------------------- lib/drops/contract/schema/key.ex | 52 ++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 64 deletions(-) create mode 100644 lib/drops/contract/schema/key.ex diff --git a/lib/drops/contract.ex b/lib/drops/contract.ex index cfcfdc5..232f67d 100644 --- a/lib/drops/contract.ex +++ b/lib/drops/contract.ex @@ -45,6 +45,26 @@ defmodule Drops.Contract do end end + def step( + data, + {:validate, + %{type: {:coerce, {{input_type, input_predicates}, output_type}}} = key} + ) do + value = get_in(data, key.path) + + case apply_predicates(value, input_predicates) do + {:ok, _} -> + validate( + Coercions.coerce(input_type, output_type, value), + key.predicates, + path: key.path + ) + + {:error, {predicate, value}} -> + {:error, {predicate, key.path, value}} + end + end + def step(data, {:validate, key}) do validate(data, key) end @@ -75,24 +95,6 @@ defmodule Drops.Contract do end end - def validate( - value, - {:coerce, input_type, output_type, input_predicates, output_predicates}, - path: name - ) do - case apply_predicates(value, input_predicates) do - {:ok, _} -> - validate( - Coercions.coerce(input_type, output_type, value), - output_predicates, - path: name - ) - - {:error, {predicate, value}} -> - {:error, {predicate, name, value}} - end - end - def apply_predicates(value, predicates) do Enum.reduce( predicates, diff --git a/lib/drops/contract/dsl.ex b/lib/drops/contract/dsl.ex index bf8d8d2..b0c35ae 100644 --- a/lib/drops/contract/dsl.ex +++ b/lib/drops/contract/dsl.ex @@ -11,23 +11,15 @@ defmodule Drops.Contract.DSL do {:coerce, type} end - def type(type, predicates) when is_list(predicates) do - [predicate(:type?, type) | Enum.map(predicates, &predicate/1)] - end - - def type({:coerce, input_type}, output_type) when is_atom(output_type) do - {:coerce, input_type, output_type, type(input_type), type(output_type)} + def type(type) do + {:type, {type, []}} end - def type({:coerce, input_type}, output_type, predicates) do - {:coerce, input_type, output_type, type(input_type), type(output_type, predicates)} - end - - def type(type) when is_atom(type) do - [predicate(:type?, type)] + def type(type, predicates) when is_list(predicates) do + {:type, {type, predicates}} end - def predicate(name, args \\ []) do - {:predicate, {name, args}} + def type({:coerce, input_type}, output_type) do + {:coerce, {type(input_type), type(output_type)}} end end diff --git a/lib/drops/contract/schema.ex b/lib/drops/contract/schema.ex index 996630b..71e714c 100644 --- a/lib/drops/contract/schema.ex +++ b/lib/drops/contract/schema.ex @@ -1,28 +1,9 @@ defmodule Drops.Contract.Schema do alias __MODULE__ + alias Drops.Contract.Schema.Key defstruct [:keys, :plan, :atomize] - defmodule Key do - defstruct [:path, :presence, :predicates, children: []] - - def present?(map, _) when not is_map(map) do - true - end - - def present?(_map, []) do - true - end - - def present?(map, %Key{} = key) do - present?(map, key.path) - end - - def present?(map, [key | tail]) do - Map.has_key?(map, key) and present?(map[key], tail) - end - end - def new(map, opts) do atomize = opts[:atomize] || false keys = to_key_list(map) @@ -50,26 +31,23 @@ defmodule Drops.Contract.Schema do end defp to_key_list(map, root \\ []) do - Enum.map(map, fn {{presence, name}, value} -> - case value do + Enum.map(map, fn {{presence, name}, spec} -> + path = root ++ [name] + + case spec do %{} -> - build_key( - presence, - root ++ [name], - [{:predicate, {:type?, :map}}], - to_key_list(value, root ++ [name]) + Key.new({:type, {:map, []}}, + presence: presence, + path: path, + children: to_key_list(spec, path) ) _ -> - build_key(presence, root ++ [name], value) + Key.new(spec, presence: presence, path: path) end end) end - defp build_key(presence, path, predicates, children \\ []) do - %Key{path: path, presence: presence, predicates: predicates, children: children} - end - defp build_plan(keys) do Enum.map(keys, &key_step/1) end diff --git a/lib/drops/contract/schema/key.ex b/lib/drops/contract/schema/key.ex new file mode 100644 index 0000000..f2724e5 --- /dev/null +++ b/lib/drops/contract/schema/key.ex @@ -0,0 +1,52 @@ +defmodule Drops.Contract.Schema.Key do + alias __MODULE__ + + defstruct [:path, :presence, :type, :predicates, children: []] + + def new(spec, attrs) do + Map.merge( + %Key{}, + Enum.into(attrs, %{type: infer_type(spec), predicates: infer_predicates(spec)}) + ) + end + + def present?(map, _) when not is_map(map) do + true + end + + def present?(_map, []) do + true + end + + def present?(map, %Key{} = key) do + present?(map, key.path) + end + + def present?(map, [key | tail]) do + Map.has_key?(map, key) and present?(map[key], tail) + end + + defp infer_type({:type, {type, _}}) do + type + end + + defp infer_type({:coerce, {input_type, output_type}}) do + {:coerce, {{infer_type(input_type), infer_predicates(input_type)}, infer_type(output_type)}} + end + + defp infer_predicates({:coerce, {_input_type, output_type}}) do + infer_predicates(output_type) + end + + defp infer_predicates({:type, {type, predicates}}) do + [predicate(:type?, type) | Enum.map(predicates, &predicate/1)] + end + + defp predicate(name, args) do + {:predicate, {name, args}} + end + + defp predicate(name) do + {:predicate, {name, []}} + end +end From 55c7bf5fe6f08f23c35c2e29603c954668136d66 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Wed, 13 Sep 2023 17:06:19 +0200 Subject: [PATCH 2/2] Support for defining multiple types --- lib/drops/contract.ex | 34 ++++++++++++++---- lib/drops/contract/dsl.ex | 8 +++++ lib/drops/contract/schema/key.ex | 16 +++++++-- test/contract/type_test.exs | 59 ++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 test/contract/type_test.exs diff --git a/lib/drops/contract.ex b/lib/drops/contract.ex index 232f67d..38a1093 100644 --- a/lib/drops/contract.ex +++ b/lib/drops/contract.ex @@ -95,17 +95,37 @@ defmodule Drops.Contract do end end + def validate(value, {:and, predicates}, path: path) do + validate(value, predicates, path: path) + end + + def validate(value, {:or, [head | tail]}, path: path) do + case validate(value, head, path: path) do + {:ok, _} = success -> + success + + {:error, _} = error -> + if length(tail) > 0, do: validate(value, {:or, tail}, path: path), else: error + end + end + def apply_predicates(value, predicates) do Enum.reduce( predicates, {:ok, value}, - fn {:predicate, {name, args}}, {:ok, value} -> - case args do - [] -> - apply(Predicates, name, [value]) - - arg -> - apply(Predicates, name, [arg, value]) + fn {:predicate, {name, args}}, result -> + case result do + {:ok, _} -> + case args do + [] -> + apply(Predicates, name, [value]) + + arg -> + apply(Predicates, name, [arg, value]) + end + + {:error, _} = error -> + error end end ) diff --git a/lib/drops/contract/dsl.ex b/lib/drops/contract/dsl.ex index b0c35ae..bc93e06 100644 --- a/lib/drops/contract/dsl.ex +++ b/lib/drops/contract/dsl.ex @@ -11,6 +11,14 @@ defmodule Drops.Contract.DSL do {:coerce, type} end + def type({type, predicates}) when is_atom(type) do + type(type, predicates) + end + + def type(types) when is_list(types) do + Enum.map(types, &type/1) + end + def type(type) do {:type, {type, []}} end diff --git a/lib/drops/contract/schema/key.ex b/lib/drops/contract/schema/key.ex index f2724e5..14bd6a7 100644 --- a/lib/drops/contract/schema/key.ex +++ b/lib/drops/contract/schema/key.ex @@ -30,6 +30,10 @@ defmodule Drops.Contract.Schema.Key do type end + defp infer_type(spec) when is_list(spec) do + Enum.map(spec, &infer_type/1) + end + defp infer_type({:coerce, {input_type, output_type}}) do {:coerce, {{infer_type(input_type), infer_predicates(input_type)}, infer_type(output_type)}} end @@ -38,8 +42,16 @@ defmodule Drops.Contract.Schema.Key do infer_predicates(output_type) end - defp infer_predicates({:type, {type, predicates}}) do - [predicate(:type?, type) | Enum.map(predicates, &predicate/1)] + defp infer_predicates(spec) when is_list(spec) do + {:or, Enum.map(spec, &infer_predicates/1)} + end + + defp infer_predicates({:type, {type, predicates}}) when length(predicates) > 0 do + {:and, [predicate(:type?, type) | Enum.map(predicates, &predicate/1)]} + end + + defp infer_predicates({:type, {type, []}}) do + [predicate(:type?, type)] end defp predicate(name, args) do diff --git a/test/contract/type_test.exs b/test/contract/type_test.exs new file mode 100644 index 0000000..b30f6a7 --- /dev/null +++ b/test/contract/type_test.exs @@ -0,0 +1,59 @@ +defmodule Drops.Contract.TypeTest do + use Drops.ContractCase + + describe "type/1 with a type atom" do + contract do + schema do + %{required(:test) => type(:string)} + end + end + + test "returns success with valid data", %{contract: contract} do + assert {:ok, %{test: "Hello"}} = contract.conform(%{test: "Hello"}) + end + + test "returns error with invalid data", %{contract: contract} do + assert {:error, [{:error, {:string?, [:test], 312}}]} = + contract.conform(%{test: 312}) + end + end + + describe "type/1 with multiple types" do + contract do + schema do + %{required(:test) => type([:integer, :string])} + end + end + + test "returns success with valid data", %{contract: contract} do + assert {:ok, %{test: 312}} = contract.conform(%{test: 312}) + assert {:ok, %{test: "Hello"}} = contract.conform(%{test: "Hello"}) + end + + test "returns error with invalid data", %{contract: contract} do + assert {:error, [{:error, {:string?, [:test], :invalid}}]} = + contract.conform(%{test: :invalid}) + end + end + + describe "type/1 with multiple types and extra predicates" do + contract do + schema do + %{required(:test) => type([:integer, {:string, [:filled?]}])} + end + end + + test "returns success with valid data", %{contract: contract} do + assert {:ok, %{test: 312}} = contract.conform(%{test: 312}) + assert {:ok, %{test: "Hello"}} = contract.conform(%{test: "Hello"}) + end + + test "returns error with invalid data", %{contract: contract} do + assert {:error, [{:error, {:string?, [:test], :invalid}}]} = + contract.conform(%{test: :invalid}) + + assert {:error, [{:error, {:filled?, [:test], ""}}]} = + contract.conform(%{test: ""}) + end + end +end