From 9378505d0a594cedf1e5c8d3525fba23500ed5a8 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 27 Oct 2023 11:35:51 +0200 Subject: [PATCH] Fix applying rules to nested maps --- lib/drops/contract.ex | 43 ++++++++++++++-------------- test/contract/rule_test.exs | 56 ++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/lib/drops/contract.ex b/lib/drops/contract.ex index b4c5bb5..2043143 100644 --- a/lib/drops/contract.ex +++ b/lib/drops/contract.ex @@ -58,30 +58,30 @@ defmodule Drops.Contract do @before_compile Drops.Contract.Runtime def conform(data) do - conform(data, schema()) + conform(data, schema(), root: true) end - def conform(data, %Types.Map{atomize: true} = schema) do - conform(Types.Map.atomize(data, schema.keys), schema.keys) + def conform(data, %Types.Map{atomize: true} = schema, root: root) do + conform(Types.Map.atomize(data, schema.keys), schema.keys, root: root) end - def conform(data, %Types.Map{} = schema) do + def conform(data, %Types.Map{} = schema, root: root) do case validate(data, schema) do {:ok, {_, validated_data}} -> - conform(validated_data, schema.keys) + conform(validated_data, schema.keys, root: root) error -> {:error, @message_backend.errors(error)} end end - def conform(data, %Types.Sum{} = type) do - case conform(data, type.left) do + def conform(data, %Types.Sum{} = type, root: root) do + case conform(data, type.left, root: root) do {:ok, value} -> {:ok, value} {:error, _} = left_errors -> - case conform(data, type.right) do + case conform(data, type.right, root: root) do {:ok, value} -> {:ok, value} @@ -91,13 +91,22 @@ defmodule Drops.Contract do end end - def conform(data, keys) when is_list(keys) do + def conform(data, %Types.Map{} = schema, path: path) do + case conform(data, schema, root: false) do + {:ok, value} -> + {:ok, {path, value}} + + {:error, errors} -> + {:error, nest_errors(errors, path)} + end + end + + def conform(data, keys, root: root) when is_list(keys) do results = validate(data, keys) output = to_output(results) - schema_errors = Enum.reject(results, &is_ok/1) - rule_errors = apply_rules(output) + errors = Enum.reject(results, &is_ok/1) - all_errors = schema_errors ++ rule_errors + all_errors = if root, do: errors ++ apply_rules(output), else: errors if length(all_errors) > 0 do {:error, @message_backend.errors(collapse_errors(all_errors))} @@ -106,16 +115,6 @@ defmodule Drops.Contract do end end - def conform(data, %Types.Map{} = schema, path: root) do - case conform(data, schema) do - {:ok, value} -> - {:ok, {root, value}} - - {:error, errors} -> - {:error, nest_errors(errors, root)} - end - end - def validate(data, keys) when is_list(keys) do Enum.map(keys, &validate(data, &1)) |> List.flatten() end diff --git a/test/contract/rule_test.exs b/test/contract/rule_test.exs index 4ca652b..fdcb545 100644 --- a/test/contract/rule_test.exs +++ b/test/contract/rule_test.exs @@ -104,7 +104,7 @@ defmodule Drops.Contract.RuleTest do end end - describe "rule/1 with guard clauses" do + describe "rule/1 for the whole input" do contract do schema do %{ @@ -136,4 +136,58 @@ defmodule Drops.Contract.RuleTest do ) end end + + describe "rule/1 for the whole input when nested" do + contract do + schema do + %{ + required(:name) => string(), + optional(:contact) => %{ + required(:email) => string() + }, + optional(:address) => %{ + required(:street) => string(), + required(:city) => string(), + required(:country) => string(), + required(:zipcode) => string() + } + } + end + + rule(:info_required, data) do + if is_nil(data[:contact]) and is_nil(data[:address]) do + {:error, "either contact or address info is required"} + else + :ok + end + end + end + + test "returns success when schema and rules passed", %{contract: contract} do + assert {:ok, %{name: "jane", contact: %{email: "jane@doe.org"}}} = + contract.conform(%{name: "jane", contact: %{email: "jane@doe.org"}}) + + assert {:ok, + %{ + name: "jane", + address: %{street: "Main St", city: "NY", country: "US", zipcode: "12345"} + }} = + contract.conform(%{ + name: "jane", + address: %{ + street: "Main St", + city: "NY", + country: "US", + zipcode: "12345" + } + }) + end + + test "returns rule errors", %{contract: contract} do + assert_errors( + ["either contact or address info is required"], + contract.conform(%{name: "jane"}) + ) + end + end end