Skip to content

Commit

Permalink
Fix handling errors when sums fail
Browse files Browse the repository at this point in the history
  • Loading branch information
solnic committed Oct 13, 2023
1 parent 549a47a commit 8dfc323
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 60 deletions.
27 changes: 22 additions & 5 deletions lib/drops/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ defmodule Drops.Contract do
all_errors = schema_errors ++ rule_errors

if length(all_errors) > 0 do
{:error, all_errors}
{:error, collapse_errors(all_errors)}
else
{:ok, output}
end
Expand All @@ -99,7 +99,7 @@ defmodule Drops.Contract do
{:ok, {root, value}}

{:error, errors} ->
nest_errors(errors, root)
{:error, nest_errors(errors, root)}
end
end

Expand Down Expand Up @@ -175,10 +175,27 @@ defmodule Drops.Contract do
{:error, {path, name, args}} ->
{:error, {root ++ path, name, args}}

{:error, [] = error_list} ->
{:error, nest_errors(error_list, root)}
{:error, error_list} ->
nest_errors(error_list, root)
end)
|> List.flatten()
end

defp collapse_errors(errors) when is_list(errors) do
Enum.map(errors, fn
{:error, {path, name, args}} ->
{:error, {path, name, args}}

{:error, error_list} ->
collapse_errors(error_list)

result ->
result
end)
|> List.flatten()
end

defp collapse_errors(errors), do: errors
end
end

Expand Down Expand Up @@ -371,7 +388,7 @@ defmodule Drops.Contract do
iex> UserContract.conform(%{email: nil, login: "jane"})
{:ok, %{email: nil, login: "jane"}}
iex> UserContract.conform(%{email: nil, login: nil})
{:error, [error: "email or login must be present"]}
{:error, ["email or login must be present"]}
"""
defmacro rule(name, input, do: block) do
Expand Down
17 changes: 9 additions & 8 deletions lib/drops/validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,6 @@ defmodule Drops.Validator do
apply_predicates(value, predicates, path: path)
end

def validate(value, {:and, predicates}, path: path) do
validate(value, predicates, path: path)
end

def validate(value, %Types.Sum{} = type, path: path) do
case validate(value, type.left, path: path) do
{:ok, _} = success ->
Expand All @@ -65,17 +61,18 @@ defmodule Drops.Validator do
success

{:error, _} = right_error ->
{:error, {:or, {left_error, right_error}}}
{:error, [{:or, {left_error, right_error}}]}
end
end
end

def validate(value, %Types.List{member_type: member_type} = type, path: path) do
case validate(value, type.constraints, path: path) do
{:ok, {_, members}} ->
result = List.flatten(
Enum.with_index(members, &validate(&1, member_type, path: path ++ [&2]))
)
result =
List.flatten(
Enum.with_index(members, &validate(&1, member_type, path: path ++ [&2]))
)

errors = Enum.reject(result, &is_ok/1)

Expand All @@ -88,6 +85,10 @@ defmodule Drops.Validator do
end
end

def validate(value, {:and, predicates}, path: path) do
validate(value, predicates, path: path)
end

defp apply_predicates(value, {:and, predicates}, path: path) do
apply_predicates(value, predicates, path: path)
end
Expand Down
2 changes: 1 addition & 1 deletion test/contract/casters_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ defmodule Drops.CastersTest do
end

test "returns error when casting could not be applied", %{contract: contract} do
assert {:error, [error: {:cast, {:error, {[:test], :type?, [:integer, "12"]}}}]} =
assert {:error, [{:cast, {:error, {[:test], :type?, [:integer, "12"]}}}]} =
contract.conform(%{test: "12"})
end
end
Expand Down
32 changes: 13 additions & 19 deletions test/contract/maybe_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ defmodule Drops.MaybeTest do
test "returns error with a non-string value", %{contract: contract} do
assert {:error,
[
error:
{:or,
{{:error, {[:test], :type?, [nil, 312]}},
{:error, {[:test], :type?, [:string, 312]}}}}
or:
{{:error, {[:test], :type?, [nil, 312]}},
{:error, {[:test], :type?, [:string, 312]}}}
]} =
contract.conform(%{test: 312})
end
Expand All @@ -46,21 +45,19 @@ defmodule Drops.MaybeTest do
test "returns error with a non-string value", %{contract: contract} do
assert {:error,
[
{:error,
{:or,
or:
{{:error, {[:test], :type?, [nil, 312]}},
{:error, {[:test], :type?, [:string, 312]}}}}}
{:error, {[:test], :type?, [:string, 312]}}}
]} =
contract.conform(%{test: 312})
end

test "returns error when extra predicates fail", %{contract: contract} do
assert {:error,
[
{:error,
{:or,
or:
{{:error, {[:test], :type?, [nil, ""]}},
{:error, {[:test], :filled?, [""]}}}}}
{:error, {[:test], :filled?, [""]}}}
]} =
contract.conform(%{test: ""})
end
Expand All @@ -87,10 +84,9 @@ defmodule Drops.MaybeTest do
test "returns error with a non-map value", %{contract: contract} do
assert {:error,
[
{:error,
{:or,
or:
{{:error, {[:test], :type?, [nil, 312]}},
{:error, {[:test], :type?, [:map, 312]}}}}}
{:error, {[:test], :type?, [:map, 312]}}}
]} =
contract.conform(%{"test" => 312})
end
Expand All @@ -114,10 +110,9 @@ defmodule Drops.MaybeTest do
test "returns error with a non-map value", %{contract: contract} do
assert {:error,
[
{:error,
{:or,
or:
{{:error, {[:test], :type?, [nil, 312]}},
{:error, {[:test], :type?, [:map, 312]}}}}}
{:error, {[:test], :type?, [:map, 312]}}}
]} =
contract.conform(%{test: 312})
end
Expand Down Expand Up @@ -146,10 +141,9 @@ defmodule Drops.MaybeTest do
test "returns error with a non-map value", %{contract: contract} do
assert {:error,
[
{:error,
{:or,
or:
{{:error, {[:test], :type?, [nil, 312]}},
{:error, {[:test], :type?, [:map, 312]}}}}}
{:error, {[:test], :type?, [:map, 312]}}}
]} =
contract.conform(%{test: 312})
end
Expand Down
9 changes: 4 additions & 5 deletions test/contract/rule_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,15 @@ defmodule Drops.Contract.RuleTest do
test "returns predicate errors and skips rules", %{contract: contract} do
assert {:error,
[
error:
{:or,
{{:error, {[:login], :type?, [nil, ""]}},
{:error, {[:login], :filled?, [""]}}}}
or:
{{:error, {[:login], :type?, [nil, ""]}},
{:error, {[:login], :filled?, [""]}}}
]} =
contract.conform(%{login: "", email: nil})
end

test "returns rule errors", %{contract: contract} do
assert {:error, [{:error, "either login or email required"}]} =
assert {:error, ["either login or email required"]} =
contract.conform(%{login: nil, email: nil})
end
end
Expand Down
33 changes: 33 additions & 0 deletions test/contract/schema_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,39 @@ defmodule Drops.Contract.SchemaTest do
end
end

describe "using list shortcut for sum types" do
contract do
schema(:left) do
%{required(:name) => string()}
end

schema(:right) do
%{required(:login) => string()}
end

schema do
%{
required(:user) => [@schemas.left, @schemas.right]
}
end
end

test "returns success when either of the schemas passed", %{contract: contract} do
assert {:ok, %{user: %{name: "John Doe"}}} =
contract.conform(%{user: %{name: "John Doe"}})
end

test "returns error when both schemas didn't pass", %{contract: contract} do
assert {:error,
[
or:
{{:error, [error: {[:user], :has_key?, [:name]}]},
{:error, [error: {[:user], :has_key?, [:login]}]}}
]} =
contract.conform(%{user: %{}})
end
end

describe "sum of schemas" do
contract do
schema(:left) do
Expand Down
41 changes: 19 additions & 22 deletions test/contract/type_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ defmodule Drops.Contract.TypeTest do
test "returns error with invalid data", %{contract: contract} do
assert {:error,
[
error: {
:or,
{{:error, {[:test], :type?, [nil, :invalid]}},
{:error,
{:or,
{{:error, {[:test], :type?, [:integer, :invalid]}},
{:error, {[:test], :type?, [:string, :invalid]}}}}}}
or: {
{:error, {[:test], :type?, [nil, :invalid]}},
{:error,
[
or:
{{:error, {[:test], :type?, [:integer, :invalid]}},
{:error, {[:test], :type?, [:string, :invalid]}}}
]}
}
]} =
contract.conform(%{test: :invalid})
Expand All @@ -62,19 +63,17 @@ defmodule Drops.Contract.TypeTest do
test "returns error with invalid data", %{contract: contract} do
assert {:error,
[
error:
{:or,
{{:error, {[:test], :type?, [:integer, :invalid]}},
{:error, {[:test], :type?, [:string, :invalid]}}}}
or:
{{:error, {[:test], :type?, [:integer, :invalid]}},
{:error, {[:test], :type?, [:string, :invalid]}}}
]} =
contract.conform(%{test: :invalid})

assert {:error,
[
{:error,
{:or,
or:
{{:error, {[:test], :type?, [:integer, ""]}},
{:error, {[:test], :filled?, [""]}}}}}
{:error, {[:test], :filled?, [""]}}}
]} =
contract.conform(%{test: ""})
end
Expand All @@ -95,19 +94,17 @@ defmodule Drops.Contract.TypeTest do
test "returns error with invalid data", %{contract: contract} do
assert {:error,
[
error:
{:or,
{{:error, {[:test], :filled?, [[]]}},
{:error, {[:test], :type?, [:map, []]}}}}
or:
{{:error, {[:test], :filled?, [[]]}},
{:error, {[:test], :type?, [:map, []]}}}
]} =
contract.conform(%{test: []})

assert {:error,
[
error:
{:or,
{{:error, {[:test], :type?, [:list, %{}]}},
{:error, {[:test], :filled?, [%{}]}}}}
or:
{{:error, {[:test], :type?, [:list, %{}]}},
{:error, {[:test], :filled?, [%{}]}}}
]} =
contract.conform(%{test: %{}})
end
Expand Down

0 comments on commit 8dfc323

Please sign in to comment.