Skip to content

Commit

Permalink
wip - more list rework
Browse files Browse the repository at this point in the history
  • Loading branch information
solnic committed Dec 4, 2023
1 parent 037bf48 commit 5a2c6a8
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 82 deletions.
51 changes: 37 additions & 14 deletions lib/drops/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ defmodule Drops.Contract do

def conform(data, %Types.Map{} = schema, path: path) do
results = Drops.Type.Validator.validate(schema, data)
output = to_output(results)
output = to_output(results, %{})
errors = Enum.reject(results, &is_ok/1)

all_errors = if Enum.empty?(path), do: errors ++ apply_rules(output), else: errors
Expand Down Expand Up @@ -289,29 +289,52 @@ defmodule Drops.Contract do
end
end

def map_list_results(members) do
Enum.map(members, fn member ->
case member do
{:ok, {_, value}} ->
if is_list(value), do: map_list_results(value), else: value
def to_output({:list, results}) do
Enum.map(results, &to_output/1)
end

value ->
value
end
end)
def to_output({:ok, {:list, results}}) do
Enum.map(results, &to_output/1)
end

def to_output(results), do: to_output(results, %{})
def to_output([head | tail]) do
if Enum.empty?(tail) do
to_output(head)
else
[to_output(head) | to_output(tail)]
end
end

def to_output({:ok, {path, value}}) do
put_in(%{}, Enum.map(path, &Access.key(&1, %{})), to_output(value))
end

def to_output({:ok, value}) do
to_output(value)
end

def to_output(value) do
value
end

def to_output([head | tail], output) do
case head do
{:ok, {path, results}} when is_list(results) ->
to_output(
tail,
put_in(output, Enum.map(path, &Access.key(&1, %{})), to_output(results))
)

{:ok, {path, value}} ->
to_output(tail, put_in(output, Enum.map(path, &Access.key(&1, %{})), value))
to_output(
tail,
put_in(output, Enum.map(path, &Access.key(&1, %{})), to_output(value))
)

{:error, _} ->
:ok ->
to_output(tail, output)

:ok ->
{:error, _} ->
to_output(tail, output)
end
end
Expand Down
52 changes: 51 additions & 1 deletion lib/drops/types/list.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,61 @@ defmodule Drops.Types.List do
}
}
"""

alias Drops.Predicates
alias Drops.Type.Validator

use Drops.Type do
deftype :list, [member_type: nil]
deftype(:list, member_type: nil)

def new(member_type) when is_struct(member_type) do
struct(__MODULE__, member_type: member_type)
end
end

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

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

error ->
error
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
63 changes: 19 additions & 44 deletions lib/drops/types/map/key.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,25 @@ defmodule Drops.Types.Map.Key do
@moduledoc false

alias __MODULE__
alias Drops.Type.Validator

defstruct [:path, :presence, :type]

defimpl Drops.Type.Validator, for: Key do
def validate(type, data), do: Key.validate(type, data)
end

def validate(%Key{presence: :required, path: path} = key, data) do
if Key.present?(data, key) do
case Drops.Type.Validator.validate(key.type, get_in(data, path)) do
{:ok, value} ->
{:ok, {path, value}}

{:error, {value, meta}} ->
{:error, {path, {value, meta}}}

{:error, error} ->
{:error, {path, error}}

results ->
Enum.map(results, &nest_result(&1, path))
end
def validate(%Key{presence: presence, path: path} = key, data) do
if present?(data, key) do
nest_result(Validator.validate(key.type, get_in(data, path)), path)
else
{:error, {path, {data, [predicate: :has_key?, args: []]}}}
end
end

def validate(%Key{presence: :optional, path: path} = key, data) do
if Key.present?(data, key) do
case Drops.Type.Validator.validate(key.type, get_in(data, path)) do
{:ok, value} ->
{:ok, {path, value}}

{:error, {value, meta}} ->
{:error, {path, {value, meta}}}

{:error, error} ->
{:error, {path, error}}

results ->
Enum.map(results, &nest_result(&1, path))
case presence do
:required -> {:error, {path, {data, [predicate: :has_key?, args: []]}}}
:optional -> :ok
end
else
:ok
end
end

def nest_result(results, root) when is_list(results) do
Enum.map(results, &nest_result(&1, root))
end

def nest_result({:ok, {path, value}}, root), do: {:ok, {root ++ path, value}}

def nest_result({:error, {path, result}}, root),
do: {:error, {root ++ path, result}}

def stringify(key) do
%Key{path: Enum.map(key.path, &to_string/1), presence: key.presence, type: key.type}
end
Expand All @@ -77,4 +40,16 @@ defmodule Drops.Types.Map.Key do
def present?(map, [key | tail]) do
Map.has_key?(map, key) and present?(map[key], tail)
end

defp nest_result({:error, {:list, results}}, root),
do:
{:error, {root, {:list, Enum.with_index(results, &nest_result(&1, root ++ [&2]))}}}

defp nest_result(results, root) when is_list(results),
do: Enum.map(results, &nest_result(&1, root))

defp nest_result({outcome, {path, result}}, root) when is_list(path),
do: {outcome, {root ++ path, result}}

defp nest_result({outcome, value}, root), do: {outcome, {root, value}}
end
21 changes: 3 additions & 18 deletions lib/drops/validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,30 +41,14 @@ defmodule Drops.Validator do
Enum.map(keys, &Drops.Type.Validator.validate(&1, data)) |> List.flatten()
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]))
)

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

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

error ->
error
end
end

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

def validate(value, %{primitive: primitive, constraints: constraints} = type,
path: path
) when primitive != :map do
)
when primitive != :map do
apply_predicates(value, constraints, path: path)
end

Expand Down Expand Up @@ -95,6 +79,7 @@ defmodule Drops.Validator 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
Expand Down
22 changes: 17 additions & 5 deletions lib/drops/validator/messages/backend.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,19 @@ defmodule Drops.Validator.Messages.Backend do

alias Drops.Validator.Messages.Error

def errors(results) when is_list(results) do
Enum.map(results, &error/1)
end

def errors(results) when is_tuple(results) do
[error(results)]
end

def errors(results) when is_list(results) do
Enum.map(results, &error/1)
end

defp error(text) when is_binary(text) do
%Error.Rule{text: text}
end

defp error({path, text}) when is_list(path) do
defp error({path, text}) when is_list(path) and is_binary(text) do
%Error.Rule{text: text, path: path}
end

Expand Down Expand Up @@ -92,6 +92,16 @@ defmodule Drops.Validator.Messages.Backend do
%Error.Type{path: path, text: text(predicate, input), 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}
end

defp error(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}
end

defp error({:error, results}) when is_list(results) do
%Error.Set{errors: Enum.map(results, &error/1)}
end
Expand All @@ -106,6 +116,8 @@ defmodule Drops.Validator.Messages.Backend do
defp error({:cast, error}) do
%Error.Caster{error: error(error)}
end

defp error({:ok, _}), do: nil
end
end
end

0 comments on commit 5a2c6a8

Please sign in to comment.