Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ defmodule UserContract do
age: integer(),
active: boolean(),
tags: list(:string),
settings: map(:string),
settings: map(values: :string),
address: maybe(:string)
}
end
Expand Down
36 changes: 36 additions & 0 deletions lib/drops/predicates.ex
Original file line number Diff line number Diff line change
Expand Up @@ -408,4 +408,40 @@ defmodule Drops.Predicates do
"""
@spec is_struct?(struct :: atom(), input :: map()) :: boolean()
def is_struct?(struct, input) when is_atom(struct) and is_map(input), do: is_struct(input, struct)

@doc ~S"""
Checks if that the keys of a given map match a given type

## Examples

iex> Drops.Predicates.keys(:string, %{"a" => "b", "c" => "d"})
true
iex> Drops.Predicates.keys(:string, %{"a" => "b", 1 => "d"})
false
"""
@spec keys(key_type :: any(), input :: map()) :: boolean()
def keys(key_type, input) when is_map(input) do
key_type = Drops.Type.Compiler.visit(key_type, [])
Enum.all?(input, fn {key, _} ->
Kernel.match?({:ok, _}, Drops.Type.Validator.validate(key_type, key))
end)
end

@doc ~S"""
Checks if that the values of a given map match a given type

## Examples

iex> Drops.Predicates.values(:string, %{"a" => "b", "c" => "d"})
true
iex> Drops.Predicates.values(:string, %{"a" => "b", "c" => 4})
false
"""
@spec values(value_type :: any(), input :: map()) :: boolean()
def values(value_type, input) when is_map(input) do
value_type = Drops.Type.Compiler.visit(value_type, [])
Enum.all?(input, fn {_, value} ->
Kernel.match?({:ok, _}, Drops.Type.Validator.validate(value_type, value))
end)
end
end
12 changes: 12 additions & 0 deletions lib/drops/validator/messages/default_backend.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ defmodule Drops.Validator.Messages.DefaultBackend do
in?: "must be one of: %input%",
not_in?: "must not be one of: %input%",
is_struct?: "must be a struct of type %input%",
keys: "must have keys of the right type",
values: "must have values of the right type",

# built-in types
number: "must be a number",
Expand Down Expand Up @@ -79,6 +81,16 @@ defmodule Drops.Validator.Messages.DefaultBackend do
String.replace(@text_mapping[:not_in?], "%input%", Enum.join(values, ", "))
end

@impl true
def text(:keys, _values, _input) do
@text_mapping[:keys]
end

@impl true
def text(:values, _values, _input) do
@text_mapping[:values]
end

@impl true
def text(:cast, error_message, _input) do
error_message
Expand Down
65 changes: 65 additions & 0 deletions test/drops/contract/types/map_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,69 @@ defmodule Drops.Contract.Types.MapTest do
assert_errors(["test must be a map"], contract.conform(%{test: 312}))
end
end

describe "map/1 with type specification" do
contract do
schema do
%{
optional(:string_to_string) => map(keys: string(), values: string()),
optional(:even_integer_to_filled_string) => map(keys: integer(:even?), values: string(:filled?)),
optional(:nested_map) => map(keys: string(), values: map(keys: string(), values: list(string())))
}
end
end

test "returns success with maps with correct types", %{contract: contract} do
assert {:ok, _} =
contract.conform(
%{
string_to_string: %{"Hello" => "World", "foo" => "bar"},
even_integer_to_filled_string: %{2 => "baz"},
nested_map: %{"parent" => %{"child" => ["grandchild1", "grandchild2"]}}
}
)
end

test "returns error with non-string => string", %{contract: contract} do
assert_errors(
["string_to_string must have keys of the right type"],
contract.conform(%{string_to_string: %{1 => "foo"}})
)
end

test "returns error with string => non-string", %{contract: contract} do
assert_errors(
["string_to_string must have values of the right type"],
contract.conform(%{string_to_string: %{"foo" => true}})
)
end

test "returns error with odd integer => string", %{contract: contract} do
assert_errors(
["even_integer_to_filled_string must have keys of the right type"],
contract.conform(%{even_integer_to_filled_string: %{1 => "foo"}})
)
end

test "returns error with even integer => empty string", %{contract: contract} do
assert_errors(
["even_integer_to_filled_string must have values of the right type"],
contract.conform(%{even_integer_to_filled_string: %{2 => ""}})
)
end

test "returns error with string => non-map", %{contract: contract} do
assert_errors(
["nested_map must have values of the right type"],
contract.conform(%{nested_map: %{"Hello" => "World!"}})
)
end

test "returns error with string => map with wrong types", %{contract: contract} do
assert_errors(
["nested_map must have values of the right type"],
contract.conform(%{nested_map: %{"parent" => %{"child" => "grandchild"}}})
)
end
end
end
Loading