Skip to content

Commit

Permalink
Merge pull request #32 from solnic/fix-rules-with-nested-schemas
Browse files Browse the repository at this point in the history
Fix applying rules to nested maps
  • Loading branch information
solnic authored Oct 27, 2023
2 parents 70b495c + 9378505 commit 01fa4fc
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 23 deletions.
43 changes: 21 additions & 22 deletions lib/drops/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -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))}
Expand All @@ -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
Expand Down
56 changes: 55 additions & 1 deletion test/contract/rule_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
%{
Expand Down Expand Up @@ -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: "[email protected]"}}} =
contract.conform(%{name: "jane", contact: %{email: "[email protected]"}})

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

0 comments on commit 01fa4fc

Please sign in to comment.