Skip to main content

lib/vex/validators/string.ex

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