Skip to content

Commit

Permalink
Simplify and unify result tuples
Browse files Browse the repository at this point in the history
This moves the predicate input value to the meta portion of the result
tuple. Previously it caused issues as it was hard to differentiate path
from input value.

In general, result tuples shouldn't use values as left-side of a tuple.
These positions should be reserved for defining types of information that
a tuple carry. ie `{:error, {:map, meta}}` uses `:map` to indicate it's
a map result.
  • Loading branch information
solnic committed Jan 23, 2024
1 parent 36139aa commit 82c4940
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 95 deletions.
12 changes: 7 additions & 5 deletions lib/drops/predicates/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ defmodule Drops.Predicates.Helpers do
if apply(Predicates, name, apply_args) do
{:ok, value}
else
if is_list(value) do
{:error, [input: value, predicate: name, args: apply_args]}
else
{:error, {value, predicate: name, args: apply_args}}
end
{:error, [input: value, predicate: name, args: apply_args]}
end
end

def apply_predicate(_, {:error, _} = error) do
error
end

def is_ok(results) when is_list(results), do: Enum.all?(results, &is_ok/1)
def is_ok(:ok), do: true
def is_ok({:ok, _}), do: true
def is_ok(:error), do: false
def is_ok({:error, _}), do: false
end
37 changes: 4 additions & 33 deletions lib/drops/types/list.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,47 +35,18 @@ defmodule Drops.Types.List do

defimpl Validator, for: List do
def validate(%{constraints: constraints, member_type: member_type}, data) do
case apply_predicates(data, constraints) do
case Predicates.Helpers.apply_predicates(data, constraints) do
{:ok, members} ->
results = Enum.map(members, &Validator.validate(member_type, &1))
errors = Enum.reject(results, &is_ok/1)
errors = Enum.reject(results, &Predicates.Helpers.is_ok/1)

if Enum.empty?(errors),
do: {:ok, {:list, results}},
else: {:error, {:list, results}}

{:error, result} ->
{:error, {:list, result}}
result ->
result
end
end

defp apply_predicates(value, predicates) do
Enum.reduce(predicates, {:ok, value}, &apply_predicate(&1, &2))
end

defp apply_predicate({:predicate, {name, args}}, {:ok, value}) do
apply_args =
case args do
[arg] -> [arg, value]
[] -> [value]
arg -> [arg, value]
end

if apply(Predicates, name, apply_args) do
{:ok, value}
else
{:error, {value, predicate: name, args: apply_args}}
end
end

defp apply_predicate(_, {:error, _} = error) do
error
end

defp is_ok(results) when is_list(results), do: Enum.all?(results, &is_ok/1)
defp is_ok(:ok), do: true
defp is_ok({:ok, _}), do: true
defp is_ok(:error), do: false
defp is_ok({:error, _}), do: false
end
end
45 changes: 6 additions & 39 deletions lib/drops/types/map.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,25 @@ defmodule Drops.Types.Map do

defimpl Drops.Type.Validator, for: Map do
def validate(%{atomize: true, keys: keys} = type, data) do
case apply_predicates(Map.atomize(data, keys), type.constraints) do
case Predicates.Helpers.apply_predicates(Map.atomize(data, keys), type.constraints) do
{:ok, result} ->
results = Enum.map(type.keys, &Key.validate(&1, result)) |> List.flatten()
errors = Enum.reject(results, &is_ok/1)
errors = Enum.reject(results, &Predicates.Helpers.is_ok/1)

if Enum.empty?(errors),
do: {:ok, {:map, results}},
else: {:error, {:map, results}}

{:error, errors} ->
{:error, errors}
result ->
result
end
end

def validate(type, data) do
case apply_predicates(data, type.constraints) do
case Predicates.Helpers.apply_predicates(data, type.constraints) do
{:ok, result} ->
results = Enum.map(type.keys, &Key.validate(&1, result)) |> List.flatten()
errors = Enum.reject(results, &is_ok/1)
errors = Enum.reject(results, &Predicates.Helpers.is_ok/1)

if Enum.empty?(errors),
do: {:ok, {:map, results}},
Expand All @@ -82,39 +82,6 @@ defmodule Drops.Types.Map do
{:error, errors}
end
end

defp apply_predicates(value, {:and, predicates}) do
apply_predicates(value, predicates)
end

defp apply_predicates(value, predicates) do
Enum.reduce(predicates, {:ok, value}, &apply_predicate(&1, &2))
end

defp apply_predicate({:predicate, {name, args}}, {:ok, value}) do
apply_args =
case args do
[arg] -> [arg, value]
[] -> [value]
arg -> [arg, value]
end

if apply(Predicates, name, apply_args) do
{:ok, value}
else
{:error, {value, predicate: name, args: apply_args}}
end
end

defp apply_predicate(_, {:error, _} = error) do
error
end

defp is_ok(results) when is_list(results), do: Enum.all?(results, &is_ok/1)
defp is_ok(:ok), do: true
defp is_ok({:ok, _}), do: true
defp is_ok(:error), do: false
defp is_ok({:error, _}), do: false
end

def atomize(data, keys, initial \\ %{}) do
Expand Down
2 changes: 1 addition & 1 deletion lib/drops/types/map/key.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule Drops.Types.Map.Key do
nest_result(result, path)
else
case presence do
:required -> {:error, {path, {data, [predicate: :has_key?, args: []]}}}
:required -> {:error, {path, [input: data, predicate: :has_key?, args: [path]]}}
:optional -> :ok
end
end
Expand Down
34 changes: 19 additions & 15 deletions lib/drops/validator/messages/backend.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,11 @@ defmodule Drops.Validator.Messages.Backend do
defp error(%{path: path} = error), do: error
defp error(%Error.Sum{} = error), do: error

defp error({:error, {path, {input, [predicate: :has_key?, args: [value]] = meta}}}) do
defp error({:error, {path, [input: value, predicate: :has_key?, args: [value]]}}) do
dbg()

%Error.Key{
path: path ++ [value],
path: path ++ value,
text: text(:has_key?, value),
meta: %{
predicate: :has_key?,
Expand All @@ -83,17 +85,23 @@ defmodule Drops.Validator.Messages.Backend do

defp error(
{:error,
{path, {input, [predicate: predicate, args: [value, _] = args] = meta}}}
) do
%Error.Type{path: path, text: text(predicate, value, input), meta: meta}
end

defp error({:error, {path, [input: input, predicate: predicate, args: [value, _] = args] = meta}}) do
%Error.Type{path: path, text: text(predicate, value, input), meta: meta}
{path, [input: input, predicate: predicate, args: [value, _]] = meta}}
)
when is_list(path) do
%Error.Type{
path: path,
text: text(predicate, value, input),
meta: Keyword.drop(meta, [:input])
}
end

defp error({:error, {path, {input, [predicate: predicate, args: _] = meta}}}) do
%Error.Type{path: path, text: text(predicate, input), meta: meta}
defp error({:error, {path, [input: value, predicate: predicate, args: _] = meta}})
when is_list(path) do
%Error.Type{
path: path,
text: text(predicate, value),
meta: Keyword.drop(meta, [:input])
}
end

defp error({:error, {path, {:map, results}}}) do
Expand All @@ -109,10 +117,6 @@ defmodule Drops.Validator.Messages.Backend do
}
end

defp error({:error, {value, [predicate: predicate, args: _] = meta}}) do
%Error.Type{text: text(predicate, value), meta: meta}
end

defp error({:error, {path, {:list, results}}}) when is_list(results) do
errors = Enum.map(results, &error/1) |> Enum.reject(&is_nil/1)
if Enum.empty?(errors), do: nil, else: %Error.Set{errors: errors}
Expand Down
3 changes: 2 additions & 1 deletion lib/drops/validator/messages/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ defmodule Drops.Validator.Messages.Error do
defstruct [:left, :right]

defimpl String.Chars, for: Sum do
def to_string(%Error.Sum{left: left, right: right}) when is_list(left) and is_list(right) do
def to_string(%Error.Sum{left: left, right: right})
when is_list(left) and is_list(right) do
"#{Enum.map(left, &Kernel.to_string/1)} or #{Enum.map(right, &Kernel.to_string/1)}"
end

Expand Down
2 changes: 1 addition & 1 deletion test/contract/messages_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ defmodule Drops.Validator.MessagesTest do
contract.conform(%{name: "Jane Doe"})

assert path == [:age]
assert meta == [predicate: :has_key?, args: []]
assert meta == [predicate: :has_key?, args: [[:age]]]
assert to_string(error) == "age key must be present"
end
end
Expand Down

0 comments on commit 82c4940

Please sign in to comment.