diff --git a/lib/drops/type.ex b/lib/drops/type.ex index 5d31d81..e3229ea 100644 --- a/lib/drops/type.ex +++ b/lib/drops/type.ex @@ -4,6 +4,8 @@ defmodule Drops.Type do """ alias __MODULE__ + alias Drops.Type.Compiler + alias Drops.Types.Map.Key defmacro __using__(do: block) do quote do @@ -14,6 +16,20 @@ defmodule Drops.Type do end end + defmacro __using__({:%{}, _, _} = spec) do + quote do + import Drops.Type + import Drops.Type.DSL + + keys = + Enum.map(unquote(spec), fn {{presence, name}, type_spec} -> + %Key{path: [name], presence: presence, type: Compiler.visit(type_spec, [])} + end) + + use Drops.Types.Map, keys: keys + end + end + defmacro __using__(spec) do quote do import Drops.Type @@ -82,10 +98,12 @@ defmodule Drops.Type do end def infer_primitive([]), do: :any + def infer_primitive(map) when is_map(map), do: :map def infer_primitive(name) when is_atom(name), do: name def infer_primitive({:type, {name, _}}), do: name def infer_constraints([]), do: [] + def infer_constraints(map) when is_map(map), do: [] def infer_constraints(type) when is_atom(type), do: [predicate(:type?, [type])] def infer_constraints(predicates) when is_list(predicates) do diff --git a/lib/drops/types/map.ex b/lib/drops/types/map.ex index 4245c82..fa632cb 100644 --- a/lib/drops/types/map.ex +++ b/lib/drops/types/map.ex @@ -37,19 +37,11 @@ defmodule Drops.Types.Map do """ + alias __MODULE__ alias Drops.Predicates alias Drops.Types.Map.Key - use Drops.Type do - deftype(:map, keys: [], atomize: false) - - def new(keys, opts) when is_list(keys) do - atomize = opts[:atomize] || false - struct(__MODULE__, keys: keys, atomize: atomize) - end - end - - defimpl Drops.Type.Validator, for: Map do + defmodule Validator do def validate(%{atomize: true, keys: keys} = type, data) do case Predicates.Helpers.apply_predicates(Map.atomize(data, keys), type.constraints) do {:ok, result} -> @@ -84,6 +76,37 @@ defmodule Drops.Types.Map do end end + defmacro __using__(opts) do + quote do + use Drops.Type do + deftype(:map, keys: unquote(opts[:keys]), atomize: false) + + import Drops.Types.Map + + def new(opts) do + struct(__MODULE__, opts) + end + + defimpl Drops.Type.Validator, for: __MODULE__ do + def validate(type, data), do: Validator.validate(type, data) + end + end + end + end + + use Drops.Type do + deftype(:map, keys: [], atomize: false) + end + + defimpl Drops.Type.Validator, for: Map do + def validate(type, data), do: Validator.validate(type, data) + end + + def new(keys, opts) when is_list(keys) do + atomize = opts[:atomize] || false + struct(__MODULE__, keys: keys, atomize: atomize) + end + def atomize(data, keys, initial \\ %{}) do Enum.reduce(keys, initial, fn %{path: path} = key, acc -> stringified_key = Key.stringify(key) diff --git a/test/contract/types/custom_test.exs b/test/contract/types/custom_test.exs index 3dc28ab..3ae96ec 100644 --- a/test/contract/types/custom_test.exs +++ b/test/contract/types/custom_test.exs @@ -41,4 +41,31 @@ defmodule Drops.Contract.Types.CustomTest do assert_errors(["test must be filled"], contract.conform(%{test: ""})) end end + + describe "using a custom map type" do + defmodule User do + use Drops.Type, %{ + required(:name) => string() + } + end + + contract do + schema do + %{required(:user) => User} + end + end + + test "returns success with a valid input", %{contract: contract} do + assert {:ok, %{user: %{name: "John"}}} = contract.conform(%{user: %{name: "John"}}) + end + + test "returns errors with invalid input", %{contract: contract} do + assert_errors(["user must be a map"], contract.conform(%{user: 312})) + + assert_errors( + ["user.name must be a string"], + contract.conform(%{user: %{name: 312}}) + ) + end + end end