Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

13 support for lists #19

Merged
merged 2 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 78 additions & 28 deletions lib/drops/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ defmodule Drops.Contract do
conform(data, schema.plan)
end

def conform(data, %Schema{} = schema, path: root) do
case conform(data, schema.plan) do
{:ok, _result} = success ->
success

{:error, errors} ->
{:error,
Enum.map(errors, fn {:error, {predicate, path, value}} ->
{predicate, root ++ path, value}
end)}
end
end

def conform(data, plan) do
results = Enum.map(plan, &step(data, &1)) |> List.flatten()
schema_errors = Enum.reject(results, &is_ok/1)
Expand Down Expand Up @@ -61,7 +74,7 @@ defmodule Drops.Contract do
) do
value = get_in(data, key.path)

case apply_predicates(value, input_predicates) do
case apply_predicates(value, input_predicates, path: key.path) do
{:ok, _} ->
validate(
Coercions.coerce(input_type, output_type, value),
Expand Down Expand Up @@ -95,13 +108,7 @@ defmodule Drops.Contract do
end

def validate(value, predicates, path: path) when is_list(predicates) do
case apply_predicates(value, predicates) do
{:error, {predicate, value}} ->
{:error, {predicate, path, value}}

{:ok, value} ->
{:ok, {path, value}}
end
apply_predicates(value, predicates, path: path)
end

def validate(value, {:and, predicates}, path: path) do
Expand All @@ -118,26 +125,57 @@ defmodule Drops.Contract do
end
end

def apply_predicates(value, predicates) do
Enum.reduce(
predicates,
{:ok, value},
fn {:predicate, {name, args}}, result ->
case result do
{:ok, _} ->
case args do
[] ->
apply(Predicates, name, [value])

arg ->
apply(Predicates, name, [arg, value])
end

{:error, _} = error ->
error
end
def apply_predicates(value, {:and, [left, %Schema{} = schema]}, path: path) do
case apply_predicate(left, {:ok, {path, value}}) do
{:ok, _} ->
conform(value, schema, path: path)

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

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

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

def apply_predicate({:each, predicates}, {:ok, {path, members}}) do
result =
Enum.with_index(
members,
&apply_predicates(&1, predicates, path: path ++ [&2])
)

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

if length(errors) == 0,
do: {:ok, {path, result}},
else: errors
end

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

case apply(Predicates, name, apply_args) do
{:ok, result} ->
{:ok, {path, result}}

{:error, {predicate, value}} ->
{:error, {predicate, path, value}}
end
end

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

def apply_rules(output) do
Expand All @@ -157,13 +195,25 @@ defmodule Drops.Contract do
Enum.reduce(results, %{}, fn result, acc ->
case result do
{:ok, {path, value}} ->
put_in(acc, path, value)
if is_list(value),
do: put_in(acc, path, map_list_results(value)),
else: put_in(acc, path, value)

:ok ->
acc
end
end)
end

defp map_list_results(members) do
Enum.map(members, fn member ->
case member do
{:ok, {_, value}} -> value
{:ok, value} -> value
value -> value
end
end)
end
end
end

Expand Down
8 changes: 8 additions & 0 deletions lib/drops/contract/dsl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ defmodule Drops.Contract.DSL do
{:coerce, type}
end

def type([list: members]) when is_map(members) do
{:type, {:list, members}}
end

def type([list: [type | predicates]]) do
{:type, {:list, type(type, predicates)}}
end

def type({type, predicates}) when is_atom(type) do
type(type, predicates)
end
Expand Down
16 changes: 15 additions & 1 deletion lib/drops/contract/schema/key.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule Drops.Contract.Schema.Key do
alias __MODULE__
alias Drops.Contract.Schema

defstruct [:path, :presence, :type, :predicates, children: []]

Expand Down Expand Up @@ -35,17 +36,30 @@ defmodule Drops.Contract.Schema.Key do
end

defp infer_type({:coerce, {input_type, output_type}}) do
{:coerce, {{infer_type(input_type), infer_predicates(input_type)}, infer_type(output_type)}}
{:coerce,
{{infer_type(input_type), infer_predicates(input_type)}, infer_type(output_type)}}
end

defp infer_predicates({:coerce, {_input_type, output_type}}) do
infer_predicates(output_type)
end

defp infer_predicates(spec) when is_map(spec) do
{:and, [predicate(:type?, :map), Schema.new(spec, [])]}
end

defp infer_predicates(spec) when is_list(spec) do
{:or, Enum.map(spec, &infer_predicates/1)}
end

defp infer_predicates({:type, {:list, []}}) do
[predicate(:type?, :list)]
end

defp infer_predicates({:type, {:list, member_type}}) do
{:and, [predicate(:type?, :list), {:each, infer_predicates(member_type)}]}
end

defp infer_predicates({:type, {type, predicates}}) when length(predicates) > 0 do
{:and, [predicate(:type?, type) | Enum.map(predicates, &predicate/1)]}
end
Expand Down
65 changes: 65 additions & 0 deletions test/contract/list_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
defmodule Drops.Contract.ListTest do
use Drops.ContractCase

describe "defining a typed list" do
contract do
schema do
%{
required(:tags) => type(list: [:string])
}
end
end

test "returns success with valid data", %{contract: contract} do
assert {:ok, %{tags: ["red", "green", "blue"]}} =
contract.conform(%{tags: ["red", "green", "blue"]})
end

test "defining required keys with types", %{contract: contract} do
assert {:error, [{:error, {:string?, [:tags, 1], 312}}]} =
contract.conform(%{tags: ["red", 312, "blue"]})
end
end

describe "defining a typed list with extra predicates" do
contract do
schema do
%{
required(:tags) => type(list: [:string, :filled?])
}
end
end

test "returns success with valid data", %{contract: contract} do
assert {:ok, %{tags: ["red", "green", "blue"]}} =
contract.conform(%{tags: ["red", "green", "blue"]})
end

test "defining required keys with types", %{contract: contract} do
assert {:error, [{:error, {:filled?, [:tags, 1], ""}}]} =
contract.conform(%{tags: ["red", "", "blue"]})
end
end

describe "defining a typed list with a member schema" do
contract do
schema do
%{
required(:tags) => type(list: %{
required(:name) => type(:string)
})
}
end
end

test "returns success with valid data", %{contract: contract} do
assert {:ok, %{tags: [%{name: "red"}, %{name: "green"}, %{name: "blue"}]}} =
contract.conform(%{tags: [%{name: "red"}, %{name: "green"}, %{name: "blue"}]})
end

test "defining required keys with types", %{contract: contract} do
assert {:error, [{:error, [{:string?, [:tags, 1, :name], 312}]}]} =
contract.conform(%{tags: [%{name: "red"}, %{name: 312}, %{name: "blue"}]})
end
end
end
Loading