Skip to content

Commit

Permalink
Merge pull request #18 from solnic/17-support-multiple-types
Browse files Browse the repository at this point in the history
17 support multiple types
  • Loading branch information
solnic authored Sep 14, 2023
2 parents d629c4c + 55c7bf5 commit 9bda413
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 63 deletions.
64 changes: 43 additions & 21 deletions lib/drops/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ defmodule Drops.Contract do
end
end

def step(
data,
{:validate,
%{type: {:coerce, {{input_type, input_predicates}, output_type}}} = key}
) do
value = get_in(data, key.path)

case apply_predicates(value, input_predicates) do
{:ok, _} ->
validate(
Coercions.coerce(input_type, output_type, value),
key.predicates,
path: key.path
)

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

def step(data, {:validate, key}) do
validate(data, key)
end
Expand Down Expand Up @@ -75,35 +95,37 @@ defmodule Drops.Contract do
end
end

def validate(
value,
{:coerce, input_type, output_type, input_predicates, output_predicates},
path: name
) do
case apply_predicates(value, input_predicates) do
{:ok, _} ->
validate(
Coercions.coerce(input_type, output_type, value),
output_predicates,
path: name
)
def validate(value, {:and, predicates}, path: path) do
validate(value, predicates, path: path)
end

{:error, {predicate, value}} ->
{:error, {predicate, name, value}}
def validate(value, {:or, [head | tail]}, path: path) do
case validate(value, head, path: path) do
{:ok, _} = success ->
success

{:error, _} = error ->
if length(tail) > 0, do: validate(value, {:or, tail}, path: path), else: error
end
end

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

arg ->
apply(Predicates, name, [arg, 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
end
)
Expand Down
20 changes: 10 additions & 10 deletions lib/drops/contract/dsl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ defmodule Drops.Contract.DSL do
{:coerce, type}
end

def type(type, predicates) when is_list(predicates) do
[predicate(:type?, type) | Enum.map(predicates, &predicate/1)]
def type({type, predicates}) when is_atom(type) do
type(type, predicates)
end

def type({:coerce, input_type}, output_type) when is_atom(output_type) do
{:coerce, input_type, output_type, type(input_type), type(output_type)}
def type(types) when is_list(types) do
Enum.map(types, &type/1)
end

def type({:coerce, input_type}, output_type, predicates) do
{:coerce, input_type, output_type, type(input_type), type(output_type, predicates)}
def type(type) do
{:type, {type, []}}
end

def type(type) when is_atom(type) do
[predicate(:type?, type)]
def type(type, predicates) when is_list(predicates) do
{:type, {type, predicates}}
end

def predicate(name, args \\ []) do
{:predicate, {name, args}}
def type({:coerce, input_type}, output_type) do
{:coerce, {type(input_type), type(output_type)}}
end
end
42 changes: 10 additions & 32 deletions lib/drops/contract/schema.ex
Original file line number Diff line number Diff line change
@@ -1,28 +1,9 @@
defmodule Drops.Contract.Schema do
alias __MODULE__
alias Drops.Contract.Schema.Key

defstruct [:keys, :plan, :atomize]

defmodule Key do
defstruct [:path, :presence, :predicates, children: []]

def present?(map, _) when not is_map(map) do
true
end

def present?(_map, []) do
true
end

def present?(map, %Key{} = key) do
present?(map, key.path)
end

def present?(map, [key | tail]) do
Map.has_key?(map, key) and present?(map[key], tail)
end
end

def new(map, opts) do
atomize = opts[:atomize] || false
keys = to_key_list(map)
Expand Down Expand Up @@ -50,26 +31,23 @@ defmodule Drops.Contract.Schema do
end

defp to_key_list(map, root \\ []) do
Enum.map(map, fn {{presence, name}, value} ->
case value do
Enum.map(map, fn {{presence, name}, spec} ->
path = root ++ [name]

case spec do
%{} ->
build_key(
presence,
root ++ [name],
[{:predicate, {:type?, :map}}],
to_key_list(value, root ++ [name])
Key.new({:type, {:map, []}},
presence: presence,
path: path,
children: to_key_list(spec, path)
)

_ ->
build_key(presence, root ++ [name], value)
Key.new(spec, presence: presence, path: path)
end
end)
end

defp build_key(presence, path, predicates, children \\ []) do
%Key{path: path, presence: presence, predicates: predicates, children: children}
end

defp build_plan(keys) do
Enum.map(keys, &key_step/1)
end
Expand Down
64 changes: 64 additions & 0 deletions lib/drops/contract/schema/key.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
defmodule Drops.Contract.Schema.Key do
alias __MODULE__

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

def new(spec, attrs) do
Map.merge(
%Key{},
Enum.into(attrs, %{type: infer_type(spec), predicates: infer_predicates(spec)})
)
end

def present?(map, _) when not is_map(map) do
true
end

def present?(_map, []) do
true
end

def present?(map, %Key{} = key) do
present?(map, key.path)
end

def present?(map, [key | tail]) do
Map.has_key?(map, key) and present?(map[key], tail)
end

defp infer_type({:type, {type, _}}) do
type
end

defp infer_type(spec) when is_list(spec) do
Enum.map(spec, &infer_type/1)
end

defp infer_type({:coerce, {input_type, output_type}}) do
{: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_list(spec) do
{:or, Enum.map(spec, &infer_predicates/1)}
end

defp infer_predicates({:type, {type, predicates}}) when length(predicates) > 0 do
{:and, [predicate(:type?, type) | Enum.map(predicates, &predicate/1)]}
end

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

defp predicate(name, args) do
{:predicate, {name, args}}
end

defp predicate(name) do
{:predicate, {name, []}}
end
end
59 changes: 59 additions & 0 deletions test/contract/type_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
defmodule Drops.Contract.TypeTest do
use Drops.ContractCase

describe "type/1 with a type atom" do
contract do
schema do
%{required(:test) => type(:string)}
end
end

test "returns success with valid data", %{contract: contract} do
assert {:ok, %{test: "Hello"}} = contract.conform(%{test: "Hello"})
end

test "returns error with invalid data", %{contract: contract} do
assert {:error, [{:error, {:string?, [:test], 312}}]} =
contract.conform(%{test: 312})
end
end

describe "type/1 with multiple types" do
contract do
schema do
%{required(:test) => type([:integer, :string])}
end
end

test "returns success with valid data", %{contract: contract} do
assert {:ok, %{test: 312}} = contract.conform(%{test: 312})
assert {:ok, %{test: "Hello"}} = contract.conform(%{test: "Hello"})
end

test "returns error with invalid data", %{contract: contract} do
assert {:error, [{:error, {:string?, [:test], :invalid}}]} =
contract.conform(%{test: :invalid})
end
end

describe "type/1 with multiple types and extra predicates" do
contract do
schema do
%{required(:test) => type([:integer, {:string, [:filled?]}])}
end
end

test "returns success with valid data", %{contract: contract} do
assert {:ok, %{test: 312}} = contract.conform(%{test: 312})
assert {:ok, %{test: "Hello"}} = contract.conform(%{test: "Hello"})
end

test "returns error with invalid data", %{contract: contract} do
assert {:error, [{:error, {:string?, [:test], :invalid}}]} =
contract.conform(%{test: :invalid})

assert {:error, [{:error, {:filled?, [:test], ""}}]} =
contract.conform(%{test: ""})
end
end
end

0 comments on commit 9bda413

Please sign in to comment.