defmodule Fussy.Validators.String do
@behaviour Fussy.Validator
alias Fussy.ValidationError
defstruct [:min_length, :max_length, :regex]
@opaque t :: %__MODULE__{}
@spec new(
min_length: integer() | nil,
max_length: integer() | nil,
regex: Regex.t() | nil
) :: __MODULE__.t()
def new(args \\ []), do: struct!(%__MODULE__{}, args)
def validate(%__MODULE__{} = v, term), do: validate(v, [], term)
@impl true
def validate(%__MODULE__{} = v, path, term) when is_binary(term) do
if String.valid?(term) do
validate_str(v, path, term)
else
{:error,
[
%ValidationError{
path: path,
mod: __MODULE__,
msg: "must be a valid utf-8 string",
term: term
}
]}
end
end
@impl true
def validate(%__MODULE__{}, path, term),
do:
{:error,
[
%ValidationError{
path: path,
mod: __MODULE__,
msg: "must be a valid utf-8 string",
term: term
}
]}
defp validate_str(
%__MODULE__{min_length: min_length, max_length: max_length, regex: regex},
path,
term
) do
[min_length: min_length, max_length: max_length, regex: regex]
|> Enum.filter(fn
{_, nil} -> false
_ -> true
end)
|> Enum.reduce([], fn validator, errors ->
case validate_with(validator, term) do
:ok -> errors
reason -> [reason | errors]
end
end)
|> then(fn
[] ->
{:ok, term}
errors ->
{:error,
errors
|> Enum.map(fn msg ->
%ValidationError{path: path, mod: __MODULE__, msg: msg, term: term}
end)}
end)
end
defp validate_with({:min_length, min_length}, str) do
if String.length(str) >= min_length do
:ok
else
"must have at least #{min_length} character(s)"
end
end
defp validate_with({:max_length, max_length}, str) do
if String.length(str) <= max_length do
:ok
else
"must have at most #{max_length} character(s)"
end
end
defp validate_with({:regex, regex}, str) do
if Regex.match?(regex, str) do
:ok
else
"must match regex /#{Regex.source(regex)}/"
end
end
end