diff --git a/lib/drops/type/dsl.ex b/lib/drops/type/dsl.ex index 6d9cb91..99373fc 100644 --- a/lib/drops/type/dsl.ex +++ b/lib/drops/type/dsl.ex @@ -123,8 +123,8 @@ defmodule Drops.Type.DSL do {:type, {type, predicates}} end - def type({:type, {type, []}}, predicates) when is_atom(type) and is_list(predicates) do - {:type, {type, predicates}} + def type({:type, {type, predicates}}, more_predicates) when is_atom(type) do + {:type, {type, predicates ++ more_predicates}} end def type({:cast, {input_type, cast_opts}}, output_type) diff --git a/lib/drops/types/sum.ex b/lib/drops/types/union.ex similarity index 76% rename from lib/drops/types/sum.ex rename to lib/drops/types/union.ex index 19f8cdf..8baed90 100644 --- a/lib/drops/types/sum.ex +++ b/lib/drops/types/union.ex @@ -20,6 +20,26 @@ defmodule Drops.Types.Union do """ defmodule Validator do + def validate(%{left: %{primitive: _} = left, right: %{primitive: _} = right}, input) do + case Drops.Type.Validator.validate(left, input) do + {:ok, value} -> + {:ok, value} + + {:error, meta} = left_error -> + if is_list(meta) and meta[:predicate] != :type? do + left_error + else + case Drops.Type.Validator.validate(right, input) do + {:ok, value} -> + {:ok, value} + + {:error, _} = right_error -> + {:error, {:or, {left_error, right_error}}} + end + end + end + end + def validate(%{left: left, right: right}, input) do case Drops.Type.Validator.validate(left, input) do {:ok, value} -> diff --git a/test/contract/type_test.exs b/test/contract/type_test.exs index 5e6da15..8d777d9 100644 --- a/test/contract/type_test.exs +++ b/test/contract/type_test.exs @@ -76,10 +76,7 @@ defmodule Drops.Contract.TypeTest do end test "returns error from left predicates", %{contract: contract} do - assert_errors( - ["test must be filled or test must be a map"], - contract.conform(%{test: []}) - ) + assert_errors(["test must be filled"], contract.conform(%{test: []})) end test "returns errors from right predicates", %{contract: contract} do diff --git a/test/contract/types/custom_test.exs b/test/contract/types/custom_test.exs index 80580b2..9b72d32 100644 --- a/test/contract/types/custom_test.exs +++ b/test/contract/types/custom_test.exs @@ -88,10 +88,7 @@ defmodule Drops.Contract.Types.CustomTest do contract.conform(%{test: "Hello World"}) ) - assert_errors( - ["test must be greater than 0 or test must be a float"], - contract.conform(%{test: -1}) - ) + assert_errors(["test must be greater than 0"], contract.conform(%{test: -1})) end end diff --git a/test/contract/types/union_test.exs b/test/contract/types/union_test.exs new file mode 100644 index 0000000..7bf3431 --- /dev/null +++ b/test/contract/types/union_test.exs @@ -0,0 +1,117 @@ +defmodule Drops.Contract.Types.UnionTest do + use Drops.ContractCase + + describe "a union of two primitive types" do + contract do + schema do + %{required(:test) => union([string(), integer()])} + end + end + + test "returns success when left side is a success", %{contract: contract} do + assert {:ok, _} = contract.conform(%{test: "Hello"}) + end + + test "returns success when right side is a success", %{contract: contract} do + assert {:ok, _} = contract.conform(%{test: 312}) + end + + test "returns error when left side and right are failures", %{contract: contract} do + assert_errors( + ["test must be a string or test must be an integer"], + contract.conform(%{test: []}) + ) + end + end + + describe "a union of two primitive types when left side is constrained" do + contract do + schema do + %{required(:test) => union([string(size?: 5), integer()])} + end + end + + test "returns success when left side is a success", %{contract: contract} do + assert {:ok, _} = contract.conform(%{test: "Hello"}) + end + + test "returns success when right side is a success", %{contract: contract} do + assert {:ok, _} = contract.conform(%{test: 312}) + end + + test "returns error when left side is a failure", %{contract: contract} do + assert_errors(["test size must be 5"], contract.conform(%{test: "Hello World"})) + end + + test "returns error when left side and right are failures", %{contract: contract} do + assert_errors( + ["test must be a string or test must be an integer"], + contract.conform(%{test: []}) + ) + end + end + + describe "a union of two primitive types when right side is constrained" do + contract do + schema do + %{required(:test) => union([string(), integer(gt?: 0)])} + end + end + + test "returns success when left side is a success", %{contract: contract} do + assert {:ok, _} = contract.conform(%{test: "Hello"}) + end + + test "returns success when right side is a success", %{contract: contract} do + assert {:ok, _} = contract.conform(%{test: 312}) + end + + test "returns success when right side is a failure", %{contract: contract} do + assert_errors( + ["test must be a string or test must be greater than 0"], + contract.conform(%{test: -3}) + ) + end + + test "returns error when left side and right are failures", %{contract: contract} do + assert_errors( + ["test must be a string or test must be an integer"], + contract.conform(%{test: []}) + ) + end + end + + describe "a union of two primitive types when both sides are constrained" do + contract do + schema do + %{required(:test) => union([string(size?: 5), integer(gt?: 0)])} + end + end + + test "returns success when left side is a success", %{contract: contract} do + assert {:ok, _} = contract.conform(%{test: "Hello"}) + end + + test "returns success when right side is a success", %{contract: contract} do + assert {:ok, _} = contract.conform(%{test: 312}) + end + + test "returns error when left side is a failure", %{contract: contract} do + assert_errors(["test size must be 5"], contract.conform(%{test: "Hello World"})) + end + + test "returns success when right side is a failure", %{contract: contract} do + assert_errors( + ["test must be a string or test must be greater than 0"], + contract.conform(%{test: -3}) + ) + end + + test "returns error when left side and right are failures", %{contract: contract} do + assert_errors( + ["test must be a string or test must be an integer"], + contract.conform(%{test: []}) + ) + end + end +end