Skip to content

Commit

Permalink
Add Type.Validator protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
solnic committed Jan 19, 2024
1 parent 0576c92 commit f34684e
Show file tree
Hide file tree
Showing 17 changed files with 501 additions and 225 deletions.
158 changes: 64 additions & 94 deletions lib/drops/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,70 +55,44 @@ defmodule Drops.Contract do
@before_compile Drops.Contract

def conform(data) do
conform(data, schema(), root: true)
end

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, root: root) do
case validate(data, schema) do
{:ok, {_, validated_data}} ->
conform(validated_data, schema.keys, root: root)

error ->
{:error, @message_backend.errors(error)}
end
end

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, root: root) do
{:ok, value} ->
{:ok, value}

{:error, error} = right_errors ->
{:error, @message_backend.errors({:or, {left_errors, right_errors}})}
end
end
conform(data, schema(), path: [])
end

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
case Drops.Type.Validator.validate(schema, data) do
{outcome, {:map, items}} = result ->
output = to_output(result)
errors = if outcome == :ok, do: [], else: Enum.reject(items, &is_ok/1)

def conform(data, keys, root: root) when is_list(keys) do
results = validate(data, keys)
output = to_output(results)
errors = Enum.reject(results, &is_ok/1)
all_errors = if Enum.empty?(path), do: errors ++ apply_rules(output), else: errors

all_errors = if root, do: errors ++ apply_rules(output), else: errors
if length(all_errors) > 0 do
{:error, @message_backend.errors(all_errors)}
else
{:ok, output}
end

if length(all_errors) > 0 do
{:error, @message_backend.errors(collapse_errors(all_errors))}
else
{:ok, output}
{:error, meta} ->
{:error, @message_backend.errors({:error, {path, meta}})}
end
end

def validate(value, %Types.Map{} = schema, path: path) do
case validate(value, schema.constraints, path: path) do
{:ok, {_, validated_value}} ->
conform(validated_value, schema, path: path)

error ->
error
def conform(data, %Types.Sum{} = type, path: path) do
case conform(data, type.left, path: path) do
{:ok, output} = success ->
success

{:error, left_error} ->
case conform(data, type.right, path: path) do
{:ok, output} = success ->
success

{:error, right_error} ->
{:error,
@message_backend.errors(
{:error, {path, {:or, {left_error, right_error}}}}
)}
end
end
end

Expand Down Expand Up @@ -339,60 +313,56 @@ defmodule Drops.Contract do
end
end

def collapse_errors(errors) when is_list(errors) do
Enum.map(errors, fn
{:error, {path, name, args}} ->
{:error, {path, name, args}}
def to_output({_, {:map, [head | tail]}}) do
to_output(tail, to_output(head, %{}))
end

{:error, error_list} ->
collapse_errors(error_list)
def to_output({_, {:list, results}}) do
to_output(results)
end

def to_output({:list, results}) do
to_output(results)
end

{:or, {left_errors, right_errors}} ->
{:or, {collapse_errors(left_errors), collapse_errors(right_errors)}}
def to_output({:map, results}) do
to_output(results, %{})
end

result ->
result
end)
|> List.flatten()
def to_output([head | tail]) do
[to_output(head) | to_output(tail)]
end

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

def collapse_errors(errors), do: errors
def to_output(value) do
value
end

def nest_errors(errors, root) do
List.flatten(Enum.map(errors, &Messages.Error.Conversions.nest(&1, root)))
def to_output(:ok, output) do
output
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([], output) do
output
end

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

def to_output(results) do
Enum.reduce(results, %{}, fn result, acc ->
case result do
{:ok, {path, value}} ->
if is_list(value),
do: put_in(acc, path, map_list_results(value)),
else: put_in(acc, path, value)
def to_output({:error, _}, output) do
output
end

:ok ->
acc
def to_output({:list, results}, output) do
to_output(results, output)
end

{:error, _} ->
acc
end
end)
def to_output([head | tail], output) do
to_output(tail, to_output(head, output))
end

defp set_schema(_caller, name, opts, block) do
Expand Down
34 changes: 34 additions & 0 deletions lib/drops/predicates/helpers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule Drops.Predicates.Helpers do
alias Drops.Predicates

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

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

def 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
if is_list(value) do
{:error, [input: value, predicate: name, args: apply_args]}
else
{:error, {value, predicate: name, args: apply_args}}
end
end
end

def apply_predicate(_, {:error, _} = error) do
error
end
end
12 changes: 12 additions & 0 deletions lib/drops/type.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ defmodule Drops.Type do
end

defoverridable new: 1

defimpl Drops.Type.Validator, for: __MODULE__ do
def validate(type, value) do
Drops.Predicates.Helpers.apply_predicates(value, type.constraints)
end
end
end
end

Expand All @@ -57,6 +63,8 @@ defmodule Drops.Type do

defmacro deftype(attributes) when is_list(attributes) do
quote do
alias __MODULE__

@type t :: %__MODULE__{}

defstruct(unquote(attributes))
Expand All @@ -80,6 +88,10 @@ defmodule Drops.Type do
def infer_constraints([]), do: []
def infer_constraints(type) when is_atom(type), do: [predicate(:type?, [type])]

def infer_constraints(predicates) when is_list(predicates) do
Enum.map(predicates, &predicate/1)
end

def infer_constraints({:type, {type, predicates}}) when length(predicates) > 0 do
{:and, [predicate(:type?, type) | Enum.map(predicates, &predicate/1)]}
end
Expand Down
4 changes: 4 additions & 0 deletions lib/drops/type/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ defmodule Drops.Type.Compiler do
List.new(visit(member_type, opts))
end

def visit({:type, {:list, predicates}}, opts) do
List.new(visit({:type, {:any, []}}, opts), predicates)
end

def visit({:cast, {input_type, output_type, cast_opts}}, opts) do
Cast.new(visit(input_type, opts), visit(output_type, opts), cast_opts)
end
Expand Down
8 changes: 8 additions & 0 deletions lib/drops/type/validator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defprotocol Drops.Type.Validator do
@moduledoc ~S"""
Protocol for validating input using types
"""

@spec validate(struct(), any()) :: {:ok, any()} | {:error, {any(), keyword()}}
def validate(type, value)
end
30 changes: 29 additions & 1 deletion lib/drops/types/cast.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,39 @@ defmodule Drops.Types.Cast do
}
"""

alias Drops.Type.Validator
alias Drops.Casters

use Drops.Type do
deftype [:input_type, :output_type, opts: []]
deftype([:input_type, :output_type, opts: []])

def new(input_type, output_type, opts) do
struct(__MODULE__, input_type: input_type, output_type: output_type, opts: opts)
end
end

defimpl Validator do
def validate(
%{input_type: input_type, output_type: output_type, opts: cast_opts},
value
) do
caster = cast_opts[:caster] || Casters

case Validator.validate(input_type, value) do
{:ok, result} ->
casted_value =
apply(
caster,
:cast,
[input_type.primitive, output_type.primitive, result] ++ cast_opts
)

Validator.validate(output_type, casted_value)

{:error, error} ->
{:error, {:cast, error}}
end
end
end
end
59 changes: 56 additions & 3 deletions lib/drops/types/list.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,64 @@ 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, constraints \\ []) when is_struct(member_type) do
struct(__MODULE__,
member_type: member_type,
constraints: Drops.Type.infer_constraints(:list) ++ infer_constraints(constraints)
)
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)

def new(member_type) when is_struct(member_type) do
struct(__MODULE__, member_type: member_type)
if Enum.empty?(errors),
do: {:ok, {:list, results}},
else: {:error, {:list, results}}

{:error, result} ->
{:error, {:list, 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
Loading

0 comments on commit f34684e

Please sign in to comment.