defmodule Vex.Validators.String do
@moduledoc """
A string validator that allows nil values and requires non-blank strings
Intended to be used with `Vex` to validate string fields.
"""
use Vex.Validator
def validate(data, options \\ [])
def validate(data, true), do: validate(data)
def validate(data, options) do
options = Enum.into(options, %{})
{:cont, data}
|> validate_type(options)
|> validate_presence(options)
|> validate_length(options)
|> validate_format(options)
|> build_return_value(options)
end
defp validate_type({:cont, nil}, _options), do: {:cont, nil}
defp validate_type({:cont, data}, _options) when is_binary(data) do
if String.valid?(data) do
{:cont, data}
else
{:stop, {:error, :not_a_string}}
end
end
defp validate_type({:cont, _data}, _options), do: {:stop, {:error, :not_a_string}}
defp validate_presence({:stop, result}, _options), do: {:stop, result}
defp validate_presence({:cont, nil}, options) do
allow_nil = Map.get(options, :allow_nil, false)
if allow_nil do
{:stop, :ok}
else
{:stop, {:error, :not_a_string}}
end
end
defp validate_presence({:cont, data}, options) do
allow_blank = Map.get(options, :allow_blank, false)
cond do
data !== to_string(data) ->
{:stop, {:error, :not_a_string}}
allow_blank ->
{:cont, data}
data |> String.trim() |> String.length() > 0 ->
{:cont, data}
true ->
{:stop, {:error, :blank_string}}
end
end
defp validate_length({:stop, result}, _options), do: {:stop, result}
defp validate_length({:cont, data}, options) do
max_length = Map.get(options, :max_length, nil)
if is_nil(max_length) do
{:cont, data}
else
if String.length(data) <= max_length do
{:cont, data}
else
{:stop, {:error, :too_long}}
end
end
end
defp validate_format({:stop, result}, _options), do: {:stop, result}
defp validate_format({:cont, data}, options) do
format = Map.get(options, :format, nil)
if is_nil(format) do
{:cont, data}
else
if Regex.match?(format, data) do
{:cont, data}
else
{:stop, {:error, :invalid_format}}
end
end
end
defp build_return_value({:stop, :ok}, _options), do: :ok
defp build_return_value({:cont, _data}, _options), do: :ok
defp build_return_value({:stop, {:error, type}}, %{} = options) do
if message = Map.get(options, :message, nil) do
{:error, message}
else
build_return_value_for_error_type({:stop, {:error, type}}, options)
end
end
defp build_return_value_for_error_type({:stop, {:error, :not_a_string}}, options) do
allow_nil = Map.get(options, :allow_nil, false)
allow_blank = Map.get(options, :allow_blank, false)
message =
cond do
allow_nil && allow_blank ->
"must be a string if provided"
allow_nil ->
"must be a non-blank string if provided"
allow_blank ->
"must be a string"
true ->
"must be a non-blank string"
end
{:error, message}
end
defp build_return_value_for_error_type({:stop, {:error, :blank_string}}, options) do
allow_nil = Map.get(options, :allow_nil, false)
message =
if allow_nil do
"must be a non-blank string if provided"
else
"must be a non-blank string"
end
{:error, message}
end
defp build_return_value_for_error_type({:stop, {:error, :too_long}}, options) do
max_length = Map.fetch!(options, :max_length)
message = "must be at most #{max_length} characters"
{:error, message}
end
defp build_return_value_for_error_type({:stop, {:error, :invalid_format}}, _options) do
message = "must have the correct format"
{:error, message}
end
end