Current section

Files

Jump to
request_validator lib validator rules.ex
Raw

lib/validator/rules.ex

defmodule Request.Validator.Rules do
alias EmailChecker.Check.Format
alias EmailChecker.Check.MX
alias Request.Validator.Utils
import Gettext.Macros, only: [gettext_with_backend: 3]
require Decimal
@type result :: :ok | {:error, String.t()}
@type validator :: (... -> result())
@type rule ::
validator()
| %{
required(:validator) => validator(),
optional(:implicit?) => boolean()
}
@backend Application.compile_env(
:request_validator,
:gettext_backend,
Request.Validator.Gettext
)
defmacro gettext(msgid, opts) do
quote do
gettext_with_backend(unquote(@backend), unquote(msgid), unquote(opts))
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [required: 0]
iex> %{validator: fun} = required()
iex> fun.("name", "")
{:error, "The name field is required."}
iex> fun.("gender", nil)
{:error, "The gender field is required."}
iex> fun.("coordinates", [])
{:error, "The coordinates field is required."}
iex> fun.("metadata", %{})
{:error, "The metadata field is required."}
iex> fun.("message", "hello world")
:ok
iex> fun.("products", [1, 2])
:ok
iex> fun.("metadata", %{"hello" => "world"})
:ok
"""
@spec required() :: rule()
def required do
# TODO: check for empty `Plug.Upload`.
validator_fn = fn attr, value ->
message = gettext("The %{attribute} field is required.", attribute: attr)
cond do
is_nil(value) -> {:error, message}
is_binary(value) and String.trim(value) == "" -> {:error, message}
Enumerable.impl_for(value) != nil and Enum.empty?(value) -> {:error, message}
true -> :ok
end
end
%{implicit?: true, validator: validator_fn}
end
@doc ~S"""
## Examples
iex> alias Request.Validator.Fields
iex> import Request.Validator.Rules, only: [required_if: 1]
iex> fields = Fields.new(%{})
iex> %{validator: fun} = required_if(true)
iex> fun.("description", "", fields)
{:error, "The description field is required."}
iex> fun.("age", nil, fields)
{:error, "The age field is required."}
iex> %{validator: fun} = required_if(fn -> false end)
iex> fun.("key", "", fields)
:ok
iex> fun.("state", nil, fields)
:ok
iex> %{validator: fun} = required_if(["passenger", "kid"])
iex> data = Fields.new(%{"passenger" => "kid"})
iex> fun.("parent", nil, data)
{:error, "The parent field is required when passenger is kid."}
"""
@spec required_if((... -> boolean()) | boolean() | [term()]) :: rule()
def required_if(condition) when is_function(condition) or is_boolean(condition) do
%{validator: required} = required()
validator_fn = fn
false, _, _, _, _ ->
:ok
true, attr, value, _fields, _fun ->
required.(attr, value)
cond_fn, attr, value, fields, fun when is_function(cond_fn, 0) ->
fun.(cond_fn.(), attr, value, fields, fun)
cond_fn, attr, value, fields, fun ->
fun.(cond_fn.(fields), attr, value, fields, fun)
end
%{
implicit?: true,
validator: &validator_fn.(condition, &1, &2, &3, validator_fn)
}
end
def required_if([other, value]) do
required_if(other, value, &Kernel.==/2)
end
@spec required_if(term(), term(), (term(), term() -> boolean())) :: term()
def required_if(other, value, op) do
%{validator: required} = required()
validator_fn = fn
false, _, _, _, _ ->
:ok
true, attr, value, other, cond ->
message =
gettext(
"The %{attribute} field is required when %{other} is %{value}.",
attribute: attr,
other: other,
value: cond
)
check(required.(attr, value) == :ok, message)
end
%{
implicit?: true,
validator: &(op.(&3[other], value) |> validator_fn.(&1, &2, other, value))
}
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [string: 0]
iex> fun = string()
iex> fun.("content", "")
:ok
iex> fun.("content", 1)
{:error, "The content field must be a string."}
iex> fun.("content", nil)
{:error, "The content field must be a string."}
iex> fun.("content", %{})
{:error, "The content field must be a string."}
iex> fun.("content", [])
{:error, "The content field must be a string."}
"""
@spec string() :: rule()
def string do
fn attr, value ->
message = gettext("The %{attribute} field must be a string.", attribute: attr)
check(is_binary(value), message)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [alpha: 0]
iex> fun = alpha()
iex> fun.("uid", "abcde")
:ok
iex> fun.("uid", 1)
{:error, "The uid field must only contain letters."}
iex> fun.("uid", nil)
{:error, "The uid field must only contain letters."}
iex> fun.("uid", %{})
{:error, "The uid field must only contain letters."}
iex> fun.("uid", [])
{:error, "The uid field must only contain letters."}
"""
@spec alpha() :: rule()
def alpha do
fn attr, value ->
message = gettext("The %{attribute} field must only contain letters.", attribute: attr)
value
|> is_binary()
|> Kernel.and(String.match?(value, ~r/^[a-zA-Z]+$/))
|> check(message)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [alpha_num: 0]
iex> fun = alpha_num()
iex> fun.("ref", "1ab2de3")
:ok
iex> fun.("ref", 1)
{:error, "The ref field must only contain letters and numbers."}
iex> fun.("ref", "abc_de2")
{:error, "The ref field must only contain letters and numbers."}
iex> fun.("ref", nil)
{:error, "The ref field must only contain letters and numbers."}
iex> fun.("ref", %{})
{:error, "The ref field must only contain letters and numbers."}
iex> fun.("ref", [])
{:error, "The ref field must only contain letters and numbers."}
"""
@spec alpha_num() :: rule()
def alpha_num do
fn attr, value ->
message =
gettext("The %{attribute} field must only contain letters and numbers.",
attribute: attr
)
value
|> is_binary()
|> Kernel.and(String.match?(value, ~r/^[a-zA-Z0-9]+$/))
|> check(message)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [alpha_dash: 0]
iex> fun = alpha_dash()
iex> fun.("username", "abcde2")
:ok
iex> fun.("username", "ab_d-2")
:ok
iex> fun.("username", 1)
{:error, "The username field must only contain letters, numbers, dashes, and underscores."}
iex> fun.("username", nil)
{:error, "The username field must only contain letters, numbers, dashes, and underscores."}
iex> fun.("username", %{})
{:error, "The username field must only contain letters, numbers, dashes, and underscores."}
iex> fun.("username", [])
{:error, "The username field must only contain letters, numbers, dashes, and underscores."}
"""
@spec alpha_dash() :: rule()
def alpha_dash do
fn attr, value ->
message =
gettext(
"The %{attribute} field must only contain letters, numbers, dashes, and underscores.",
attribute: attr
)
value
|> is_binary()
|> Kernel.and(String.match?(value, ~r/^[a-zA-Z0-9-_]+$/))
|> check(message)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [integer: 0]
iex> fun = integer()
iex> fun.("age", 1)
:ok
iex> fun.("age", 2.0)
{:error, "The age field must be an integer."}
iex> fun.("age", "abcde2")
{:error, "The age field must be an integer."}
iex> fun.("age", nil)
{:error, "The age field must be an integer."}
iex> fun.("age", %{})
{:error, "The age field must be an integer."}
iex> fun.("age", [])
{:error, "The age field must be an integer."}
"""
@spec integer() :: rule()
def integer do
fn attr, value ->
message = gettext("The %{attribute} field must be an integer.", attribute: attr)
check(is_integer(value), message)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [decimal: 0, decimal: 1, decimal: 2]
iex> fun = decimal()
iex> fun.("amount", 2.0)
:ok
iex> fun.("amount", Decimal.new("9.999"))
:ok
iex> fun.("amount", 1)
:ok
iex> fun.("amount", "abcde2")
{:error, "The amount field must be a decimal."}
iex> fun.("amount", nil)
{:error, "The amount field must be a decimal."}
iex> fun.("amount", %{})
{:error, "The amount field must be a decimal."}
iex> fun.("amount", [])
{:error, "The amount field must be a decimal."}
iex> fun = decimal(2)
iex> fun.("amount", 1.36)
:ok
iex> fun.("amount", 1.366484)
{:error, "The amount field must have 2 decimal places."}
iex> fun = decimal(2, 5)
iex> fun.("amount", 1.36)
:ok
iex> fun.("amount", 1.366484)
{:error, "The amount field must have between 2 and 5 decimal places."}
"""
@spec decimal(nil | pos_integer()) :: rule()
def decimal(min \\ nil) do
decimal(min, min)
end
@spec decimal(nil | pos_integer(), nil | pos_integer()) :: rule()
def decimal(nil, nil) do
do_decimal(nil, nil)
end
def decimal(min, nil) when is_integer(min) do
do_decimal(min, min)
end
def decimal(min, max) when is_integer(min) and is_integer(max) and min >= 0 and min <= max do
do_decimal(min, max)
end
defp do_decimal(min, max) do
fn attr, value ->
try do
decimal = to_decimal_fn!(value)
exp = abs(decimal.exp)
min = min || exp
max = max || min
within_boundary? = exp >= min and exp <= max
message =
case exp > max and min != max do
false ->
gettext("The %{attribute} field must have %{decimal} decimal places.",
attribute: attr,
decimal: min
)
true ->
gettext(
"The %{attribute} field must have between %{min} and %{max} decimal places.",
attribute: attr,
max: max,
min: min
)
end
check(within_boundary?, message)
rescue
_ in [Decimal.Error, FunctionClauseError] ->
message = gettext("The %{attribute} field must be a decimal.", attribute: attr)
{:error, message}
end
end
end
defp to_decimal_fn!(value) do
cond do
Decimal.is_decimal(value) -> value
is_float(value) -> Decimal.from_float(value)
true -> Decimal.new(value)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [numeric: 0]
iex> fun = numeric()
iex> fun.("width", 2.0)
:ok
iex> fun.("width", 1)
:ok
iex> fun.("width", "abcde2")
{:error, "The width field must be a number."}
iex> fun.("width", nil)
{:error, "The width field must be a number."}
iex> fun.("width", %{})
{:error, "The width field must be a number."}
iex> fun.("width", [])
{:error, "The width field must be a number."}
"""
@spec numeric() :: rule()
def numeric do
fn attr, value ->
message = gettext("The %{attribute} field must be a number.", attribute: attr)
check(is_number(value), message)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [email: 0, email: 1]
iex> fun = email()
iex> fun.("email", "test@gmail.com")
:ok
iex> fun = email([:format])
iex> fun.("email", "a@b.com")
:ok
iex> fun = email(["mx"])
iex> fun.("email", "a@b.com")
{:error, "The email field must be a valid email address."}
iex> fun.("email", 2.0)
{:error, "The email field must be a valid email address."}
iex> fun.("email", nil)
{:error, "The email field must be a valid email address."}
iex> fun.("email", %{})
{:error, "The email field must be a valid email address."}
iex> fun.("email", [])
{:error, "The email field must be a valid email address."}
"""
@email_checks %{format: Format, mx: MX}
@spec email([:format | :mx | String.t()] | String.t()) :: rule()
def email(validations \\ []) do
validations =
case validations do
[] ->
[Format, MX]
validations ->
validations |> List.wrap() |> Enum.map(&Utils.to_atom/1) |> Enum.map(&@email_checks[&1])
end
fn attr, value ->
message = gettext("The %{attribute} field must be a valid email address.", attribute: attr)
check(is_binary(value) and EmailChecker.valid?(value, validations), message)
end
end
@doc ~S"""
## Examples
iex> alias Request.Validator.Fields
iex> import Request.Validator.Rules, only: [confirmed: 0, confirmed: 1]
iex> fields = Fields.new(%{
...> "password" => 12345678,
...> "password_confirmation" => 12345678,
...> "list" => [%{"a" => 1, "a_confirmation" => 1}]
...> })
iex> fun = confirmed()
iex> fun.("password", 12345678, fields)
:ok
iex> fun.("list.0.a", 1, fields)
:ok
iex> fun.("password", "yikes!", fields)
{:error, "The password field confirmation does not match."}
iex> fun.("list.0.a", 10, fields)
{:error, "The list.0.a field confirmation does not match."}
"""
@spec confirmed(nil | String.t()) :: rule()
def confirmed(confirmation \\ nil) do
fn attr, value, fields ->
confirmation =
case confirmation do
nil -> attr <> "_confirmation"
confirmation -> confirmation
end
message = gettext("The %{attribute} field confirmation does not match.", attribute: attr)
check(fields[confirmation] == value, message)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [allowed: 1]
iex> fun = allowed(["male", "female"])
iex> fun.("gender", "male")
:ok
iex> fun.("gender", "female")
:ok
iex> fun.("gender", "child")
{:error, "The selected gender is invalid."}
iex> fun.("gender", "goat")
{:error, "The selected gender is invalid."}
"""
@spec allowed([term()]) :: rule()
def allowed(options) when is_list(options) do
fn attr, value ->
message = gettext("The selected %{attribute} is invalid.", attribute: attr)
options
|> Enum.member?(value)
|> check(message)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [min: 1]
iex> fun = min(30)
iex> fun.("age", 40)
:ok
iex> fun.("age", 10)
{:error, "The age field must be at least 30."}
iex> fun = min(6)
iex> fun.("password", "pass")
{:error, "The password field must be at least 6 characters."}
iex> fun.("password", "password")
:ok
iex> fun = min(1)
iex> fun.("tags", [])
{:error, "The tags field must be at least 1 items."}
iex> fun.("tags", [1, 3])
:ok
"""
@spec min(number()) :: rule()
def min(bound) when is_number(bound) do
# TODO: check for `Plug.Upload` size.
fn attr, value ->
messages = %{
numeric:
gettext("The %{attribute} field must be at least %{min}.",
attribute: attr,
min: bound
),
string:
gettext("The %{attribute} field must be at least %{min} characters.",
attribute: attr,
min: bound
),
list:
gettext("The %{attribute} field must be at least %{min} items.",
attribute: attr,
min: bound
)
}
check_size_with_op(value, bound, &Kernel.>=/2, messages)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [max: 1]
iex> fun = max(30)
iex> fun.("age", 20)
:ok
iex> fun.("age", 30)
:ok
iex> fun.("age", 40)
{:error, "The age field must not be greater than 30."}
iex> fun = max(6)
iex> fun.("otp", "1611675")
{:error, "The otp field must not be greater than 6 characters."}
iex> fun.("otp", "955764")
:ok
iex> fun = max(2)
iex> fun.("tags", [1, 2, 3])
{:error, "The tags field must not be greater than 2 items."}
iex> fun.("tags", [1, 3])
:ok
"""
@spec max(number()) :: rule()
def max(bound) when is_number(bound) do
# TODO: check for `Plug.Upload` size.
fn attr, value ->
messages = %{
numeric:
gettext("The %{attribute} field must not be greater than %{max}.",
attribute: attr,
max: bound
),
list:
gettext("The %{attribute} field must not be greater than %{max} items.",
attribute: attr,
max: bound
),
string:
gettext("The %{attribute} field must not be greater than %{max} characters.",
attribute: attr,
max: bound
)
}
check_size_with_op(value, bound, &Kernel.<=/2, messages)
end
end
@doc ~S"""
## Examples
iex> alias Request.Validator.Fields
iex> import Request.Validator.Rules, only: [gt: 1]
iex> fields = Fields.new(%{
...> "age" => 30,
...> "items" => [0, 1],
...> "passphrase" => "tango"
...> })
iex> fun = gt("age")
iex> fun.("mother_age", 25, fields)
{:error, "The mother_age field must be greater than 30."}
iex> fun.("mother_age", 45, fields)
:ok
iex> fun = gt(30)
iex> fun.("mother_age", 20, fields)
{:error, "The mother_age field must be greater than 30."}
iex> fun.("mother_age", "20", fields)
{:error, "The mother_age field must be greater than 30 characters."}
iex> fun.("mother_age", 50, fields)
:ok
iex> fun = gt("passphrase")
iex> fun.("passphrase_hash", "milk", fields)
{:error, "The passphrase_hash field must be greater than 5 characters."}
iex> fun.("passphrase_hash", "aHsychxUY", fields)
:ok
iex> fun = gt("items")
iex> fun.("sub_items", [2, 3, 4], fields)
:ok
iex> fun.("sub_items", [1], fields)
{:error, "The sub_items field must have more than 2 items."}
iex> fun.("sub_items", "milk", fields)
{:error, "The sub_items field must be greater than 2 characters."}
"""
@spec gt(String.t() | number()) :: rule()
def gt(bound) when is_binary(bound) or is_number(bound) do
fn attr, value, fields ->
compared_value = fields[bound]
v = get_size(compared_value) || bound
messages = %{
numeric:
gettext("The %{attribute} field must be greater than %{value}.",
attribute: attr,
value: v
),
list:
gettext("The %{attribute} field must have more than %{value} items.",
attribute: attr,
value: v
),
string:
gettext("The %{attribute} field must be greater than %{value} characters.",
attribute: attr,
value: v
)
}
cond do
is_nil(compared_value) and (is_number(value) and is_number(bound)) ->
check_size_with_op(value, bound, &Kernel.>/2, messages)
is_number(bound) ->
{:error, messages.string}
same_type?(value, compared_value) ->
check_size_with_op(value, compared_value, &Kernel.>/2, messages)
true ->
{:error, messages.string}
end
end
end
@doc ~S"""
## Examples
iex> alias Request.Validator.Fields
iex> import Request.Validator.Rules, only: [lt: 1]
iex> fields = Fields.new(%{
...> "mother_age" => 30,
...> "items" => [0, 1],
...> "essay" => "lorem ipsum"
...> })
iex> fun = lt("mother_age")
iex> fun.("child_age", 30, fields)
{:error, "The child_age field must be less than 30."}
iex> fun.("child_age", 18, fields)
:ok
iex> fun = lt(30)
iex> fun.("child_age", 50, fields)
{:error, "The child_age field must be less than 30."}
iex> fun.("child_age", "20", fields)
{:error, "The child_age field must be less than 30 characters."}
iex> fun.("child_age", 20, fields)
:ok
iex> fun = lt("essay")
iex> fun.("comment", "lorem ipsum dolor sit amet", fields)
{:error, "The comment field must be less than 11 characters."}
iex> fun.("comment", "lorem", fields)
:ok
iex> fun = lt("items")
iex> fun.("sub_items", [], fields)
:ok
iex> fun.("sub_items", [2, 3, 4], fields)
{:error, "The sub_items field must have less than 2 items."}
iex> fun.("sub_items", "milk", fields)
{:error, "The sub_items field must be less than 2 characters."}
"""
@spec lt(String.t() | number()) :: rule()
def lt(bound) when is_binary(bound) or is_number(bound) do
fn attr, value, fields ->
compared_value = fields[bound]
v = get_size(compared_value) || bound
messages = %{
numeric:
gettext("The %{attribute} field must be less than %{value}.",
attribute: attr,
value: v
),
list:
gettext("The %{attribute} field must have less than %{value} items.",
attribute: attr,
value: v
),
string:
gettext("The %{attribute} field must be less than %{value} characters.",
attribute: attr,
value: v
)
}
cond do
is_nil(compared_value) and (is_number(value) and is_number(bound)) ->
check_size_with_op(value, bound, &Kernel.</2, messages)
is_number(bound) ->
{:error, messages.string}
same_type?(value, compared_value) ->
check_size_with_op(value, compared_value, &Kernel.</2, messages)
true ->
{:error, messages.string}
end
end
end
@doc ~S"""
## Examples
iex> alias Request.Validator.Fields
iex> import Request.Validator.Rules, only: [gte: 1]
iex> fields = Fields.new(%{
...> "age" => 30,
...> "items" => [0, 1],
...> "passphrase" => "tango"
...> })
iex> fun = gte("age")
iex> fun.("mother_age", 25, fields)
{:error, "The mother_age field must be greater than or equal to 30."}
iex> fun.("mother_age", 45, fields)
:ok
iex> fun = gte(30)
iex> fun.("mother_age", 20, fields)
{:error, "The mother_age field must be greater than or equal to 30."}
iex> fun.("mother_age", "20", fields)
{:error, "The mother_age field must be greater than or equal to 30 characters."}
iex> fun.("mother_age", 50, fields)
:ok
iex> fun = gte("passphrase")
iex> fun.("passphrase_hash", "milk", fields)
{:error, "The passphrase_hash field must be greater than or equal to 5 characters."}
iex> fun.("passphrase_hash", "aHsychxUY", fields)
:ok
iex> fun = gte("items")
iex> fun.("sub_items", [2, 3], fields)
:ok
iex> fun.("sub_items", [1], fields)
{:error, "The sub_items field must have 2 items or more."}
iex> fun.("sub_items", "milk", fields)
{:error, "The sub_items field must be greater than or equal to 2 characters."}
"""
@spec gte(String.t() | number()) :: rule()
def gte(bound) when is_binary(bound) or is_number(bound) do
fn attr, value, fields ->
compared_value = fields[bound]
v = get_size(compared_value) || bound
messages = %{
numeric:
gettext("The %{attribute} field must be greater than or equal to %{value}.",
attribute: attr,
value: v
),
list:
gettext("The %{attribute} field must have %{value} items or more.",
attribute: attr,
value: v
),
string:
gettext("The %{attribute} field must be greater than or equal to %{value} characters.",
attribute: attr,
value: v
)
}
cond do
is_nil(compared_value) and (is_number(value) and is_number(bound)) ->
check_size_with_op(value, bound, &Kernel.>=/2, messages)
is_number(bound) ->
{:error, messages.string}
same_type?(value, compared_value) ->
check_size_with_op(value, compared_value, &Kernel.>=/2, messages)
true ->
{:error, messages.string}
end
end
end
@doc ~S"""
## Examples
iex> alias Request.Validator.Fields
iex> import Request.Validator.Rules, only: [lte: 1]
iex> fields = Fields.new(%{
...> "mother_age" => 30,
...> "items" => [0, 1],
...> "essay" => "lorem ipsum"
...> })
iex> fun = lte("mother_age")
iex> fun.("child_age", 31, fields)
{:error, "The child_age field must be less than or equal to 30."}
iex> fun.("child_age", 18, fields)
:ok
iex> fun = lte(30)
iex> fun.("child_age", 50, fields)
{:error, "The child_age field must be less than or equal to 30."}
iex> fun.("child_age", "20", fields)
{:error, "The child_age field must be less than or equal to 30 characters."}
iex> fun.("child_age", 20, fields)
:ok
iex> fun = lte("essay")
iex> fun.("comment", "lorem ipsum dolor sit amet", fields)
{:error, "The comment field must be less than or equal to 11 characters."}
iex> fun.("comment", "lorem", fields)
:ok
iex> fun = lte("items")
iex> fun.("sub_items", [6, 7], fields)
:ok
iex> fun.("sub_items", [2, 3, 4], fields)
{:error, "The sub_items field must not have more than 2 items."}
iex> fun.("sub_items", "milk", fields)
{:error, "The sub_items field must be less than or equal to 2 characters."}
"""
@spec lte(String.t() | number()) :: rule()
def lte(bound) when is_binary(bound) or is_number(bound) do
fn attr, value, fields ->
compared_value = fields[bound]
v = get_size(compared_value) || bound
messages = %{
numeric:
gettext("The %{attribute} field must be less than or equal to %{value}.",
attribute: attr,
value: v
),
list:
gettext("The %{attribute} field must not have more than %{value} items.",
attribute: attr,
value: v
),
string:
gettext("The %{attribute} field must be less than or equal to %{value} characters.",
attribute: attr,
value: v
)
}
cond do
is_nil(compared_value) and (is_number(value) and is_number(bound)) ->
check_size_with_op(value, bound, &Kernel.<=/2, messages)
is_number(bound) ->
{:error, messages.string}
same_type?(value, compared_value) ->
check_size_with_op(value, compared_value, &Kernel.<=/2, messages)
true ->
{:error, messages.string}
end
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [size: 1]
iex> fun = size(5)
iex> fun.("age", 10)
{:error, "The age field must be 5."}
iex> fun.("age", 5)
:ok
iex> fun.("username", "chic")
{:error, "The username field must be 5 characters."}
iex> fun.("username", "chick")
:ok
iex> fun.("tags", ["chic"])
{:error, "The tags field must contain 5 items."}
iex> fun.("tags", Enum.to_list(1..5))
:ok
"""
@spec size(non_neg_integer()) :: rule()
def size(bound) when is_integer(bound) and bound >= 0 do
# TODO: check for file size
fn attr, value ->
messages = %{
numeric: gettext("The %{attribute} field must be %{size}.", attribute: attr, size: bound),
list:
gettext("The %{attribute} field must contain %{size} items.",
attribute: attr,
size: bound
),
string:
gettext("The %{attribute} field must be %{size} characters.",
attribute: attr,
size: bound
)
}
check_size_with_op(value, bound, &Kernel.==/2, messages)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [url: 0]
iex> fun = url()
iex> fun.("url", "https://google.com")
:ok
iex> fun.("url", "invalid_url")
{:error, "The url field must be a valid URL."}
"""
@spec url() :: rule()
def url do
fn attr, value ->
message = gettext("The %{attribute} field must be a valid URL.", attribute: attr)
value
|> is_binary()
|> Kernel.and(
match?(
%URI{host: <<_::binary>>, scheme: <<_::binary>>, port: port} when is_integer(port),
URI.parse(value)
)
)
|> check(message)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [active_url: 0]
iex> fun = active_url()
iex> fun.("url", "https://google.com")
:ok
iex> fun.("url", "invalid_url")
{:error, "The url field must be a valid URL."}
iex> fun.("url", "https://dummy.test")
{:error, "The url field must be a valid URL."}
"""
@spec active_url() :: rule()
def active_url do
url_fn = url()
fn attr, value ->
message = gettext("The %{attribute} field must be a valid URL.", attribute: attr)
case url_fn.(attr, value) do
:ok ->
%URI{host: host} = URI.parse(value)
host
|> String.to_charlist()
|> :inet.gethostbyname()
|> then(&check(match?({:ok, _}, &1), message))
error ->
error
end
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [list: 0]
iex> fun = list()
iex> fun.("emails", 4)
{:error, "The emails field must be a list."}
iex> fun.("emails", ["a@b.com"])
:ok
"""
@spec list() :: rule()
def list do
fn attr, value ->
message = gettext("The %{attribute} field must be a list.", attribute: attr)
check(is_list(value), message)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [map: 0, map: 1]
iex> fun = map()
iex> metadata = %{"a" => "b", "c" => "d"}
iex> fun.("metadata", 4)
{:error, "The metadata field must be a map."}
iex> fun.("metadata", metadata)
:ok
iex> fun = map(["a", "c"])
iex> fun.("metadata", metadata)
:ok
iex> fun.("metadata", Map.put(metadata, "e", "f"))
{:error, "The metadata field must be a map."}
"""
@spec map([String.t()]) :: rule()
def map(keys \\ []) when is_list(keys) do
fn attr, value ->
message = gettext("The %{attribute} field must be a map.", attribute: attr)
cond =
case keys do
[] -> is_map(value)
keys -> is_map(value) and diff_keys_empty?(value, keys)
end
check(cond, message)
end
end
@doc ~S"""
## Examples
iex> import Request.Validator.Rules, only: [boolean: 0]
iex> fun = boolean()
iex> fun.("notify_me", true)
:ok
iex> fun.("notify_me", 2)
{:error, "The notify_me field must be true or false."}
"""
@spec boolean() :: rule()
def boolean do
acceptables = [true, false, "1", "0", 1, 0]
fn attr, value ->
message = gettext("The %{attribute} field must be true or false.", attribute: attr)
check(value in acceptables, message)
end
end
defp diff_keys_empty?(map, keys) when is_map(map) and is_list(keys) do
map
|> Map.keys()
|> MapSet.new()
|> MapSet.difference(MapSet.new(keys))
|> Enum.empty?()
end
defp check_size_with_op(first, second, op, messages) do
message =
case messages do
%{} -> messages[get_type(first)]
end
first
|> get_size()
|> op.(get_size(second))
|> check(message)
end
defp check(cond, message) do
case cond do
true -> :ok
false -> {:error, message}
end
end
defp same_type?(a, b), do: get_type(a) == get_type(b)
defp get_size(num) when is_number(num), do: num
defp get_size(value) when is_binary(value), do: String.length(value)
defp get_size(list) when is_list(list), do: Enum.count(list)
defp get_size(nil), do: nil
defp get_type(num) when is_number(num), do: :numeric
defp get_type(value) when is_binary(value), do: :string
defp get_type(list) when is_list(list), do: :list
defp get_type(nil), do: nil
end