defmodule Talos.Types.MapType do
@moduledoc """
MapType for validation maps
`custom options`:
* `required_any_one`: true/false, if true - validation check if map has any valid field
* `required_groups`: list("key1", "key2", ...), list of keys from required group,
required group can be simple field or field with dependendencies,
you need to specify all field from the group to check dependencies
Fields are tuples `{key, type, options \\ []}`:
`key` - string or atom, key of map
`type` - Talos defined Type
`options`:
* `optional`: true/false, if false - there will be error on key missing
* `depends_on`: list("key1", "key2", ...), list of keys on which field depends,
field pass validation only if this field and all dependent fields pass validation
For example:
```elixir
iex> import Talos
iex> Talos.valid?(map(), %{foo: :bar})
true
iex> user_params = map(fields: [
...> field(key: "email", type: string(min_length: 5, max_length: 255, regexp: ~r/.*@.*/)),
...> field(key: "age", type: integer(gteq: 18, allow_nil: true)),
...> field(key: "interests", type: list(type: string()), optional: true)
...> ])
iex> Talos.valid?(user_params, %{})
false
iex> Talos.valid?(user_params, %{"email" => "bob@gmail.com", "age" => 30})
true
iex> Talos.valid?(user_params, %{"email" => "bob@gmail.com", "age" => 30, interests: ["elixir"]})
true
iex> deps = map(fields: [
...> field(
...> key: "lastname",
...> type: string(),
...> depends_on: ["birthdate", "firstname"]
...> ),
...> field(
...> key: "firstname",
...> type: string(),
...> depends_on: ["birthdate", "lastname"]
...> ),
...> field(
...> key: "birthdate",
...> type: string(),
...> depends_on: ["lastname", "firstname"]
...> )
...> ])
iex> Talos.valid?(deps, %{})
false
iex> Talos.valid?(deps, %{"firstname" => "John", "lastname" => "Galt"})
false
iex> Talos.valid?(deps, %{"firstname" => "John", "lastname" => "Galt", "birthdate" => "1957-01-01"})
true
"""
defstruct [
:fields,
allow_nil: false,
allow_blank: false,
required_any_one: false,
required_groups: nil
]
@behaviour Talos.Types
alias Talos.Types.MapType.Field
@type t :: %{
__struct__: atom,
allow_nil: boolean,
allow_blank: boolean,
required_any_one: boolean(),
required_groups: list(),
fields: list(Field.t()) | nil
}
@spec valid?(Talos.Types.MapType.t(), any) :: boolean
def valid?(%__MODULE__{} = module, value) do
errors(module, value) == %{}
end
def errors(%__MODULE__{allow_nil: true}, nil) do
%{}
end
def errors(%__MODULE__{fields: _fields}, map) when not is_map(map) do
[inspect(map), "should be MapType"]
end
def errors(
%__MODULE__{
fields: fields,
allow_blank: allow_blank,
required_any_one: any_one,
required_groups: groups
},
map
) do
keys = Map.keys(map)
cond do
Enum.empty?(keys) && allow_blank ->
%{}
any_one == true && Enum.empty?(keys) ->
["one of keys should exist"]
any_one == true && has_values(map) ->
keys
|> fields_by_key(fields)
|> find_field_errors(map)
is_list(groups) && Enum.empty?(groups) ->
["list should have value"]
is_list(groups) && !Enum.empty?(groups) && !Enum.empty?(keys) ->
groups
|> Enum.filter(fn key -> Enum.member?(keys, key) end)
|> fields_by_key(fields)
|> find_field_errors(map)
true ->
(fields || [])
|> find_field_errors(map)
end
end
defp fields_by_key(keys, fields) do
keys
|> Enum.map(fn key -> Enum.find(fields, &(&1.key == key)) end)
|> Enum.filter(&(!is_nil(&1)))
end
defp find_field_errors(fields, map) do
fields
|> Enum.map(fn field -> field_errors(field, map) end)
|> Enum.reject(fn {_key, errors} -> errors == [] || errors == %{} end)
|> Map.new()
end
defp has_values(map) do
if map == %{} do
false
else
Enum.all?(map, fn {_k, value} -> !is_nil(value) end)
end
end
@spec permit(Talos.Types.MapType.t(), any) :: any
def permit(%__MODULE__{fields: fields, allow_nil: allow_nil, allow_blank: allow_blank}, value) do
cond do
allow_blank && is_map(value) && Map.keys(value) == [] ->
value
allow_nil && is_nil(value) ->
value
is_nil(fields) ->
value
Enum.empty?(fields) ->
value
is_map(value) ->
map = value
Enum.reduce(fields, %{}, fn %Field{key: key, type: type}, acc ->
if Map.has_key?(value, key) do
Map.put(acc, key, Talos.permit(type, map[key]))
else
acc
end
end)
true ->
value
end
end
defp field_errors(%Field{} = field, map) do
Talos.errors(field, map)
end
end