lib/types/string_type.ex

defmodule Talos.Types.StringType do
  @moduledoc """
  Type for check value is string

  For example:
  ```elixir
    iex> import Talos, only: [string: 1]
    iex> short_domain = string(length: 3)
    iex> domains_list = ["cats", "foo", "baz", "pron"]
    iex> Enum.filter(domains_list, fn str -> Talos.valid?(short_domain, str) end)
    ["foo", "baz"]

  ```

  Additional parameters:

  `min_length`, same as `String.length(str) <= max_length`

  `max_length`, same as `String.length(str) >= max_length`

  `length`, same as `String.length(str) >= length`

  `regexp`, same as `String.match?(str, regexp)`

  """
  defstruct [
    :min_length,
    :length,
    :max_length,
    :regexp,
    allow_nil: false,
    allow_blank: false,
    example_value: nil
  ]

  @type t :: %{
          __struct__: atom,
          min_length: nil | integer,
          length: nil | integer,
          max_length: nil | integer,
          allow_nil: nil | boolean,
          allow_blank: nil | boolean,
          regexp: nil | Regex.t(),
          example_value: any
        }

  @behaviour Talos.Types

  @spec valid?(Talos.Types.StringType.t(), any) :: boolean
  def valid?(type, value) do
    errors(type, value) == []
  end

  @spec errors(Talos.Types.StringType.t(), binary) :: list(String.t())
  def errors(%__MODULE__{allow_blank: true}, "") do
    []
  end

  def errors(%__MODULE__{allow_blank: false}, "") do
    ["can not be blank"]
  end

  def errors(%__MODULE__{allow_nil: true}, nil) do
    []
  end

  def errors(
        %__MODULE__{regexp: regexp, min_length: min_len, length: len, max_length: max_len},
        value
      ) do
    errors =
      case is_binary(value) && String.valid?(value) do
        true ->
          str_len = String.length(value)

          [
            {is_nil(min_len) || min_len <= str_len, "minimum length: #{min_len}"},
            {is_nil(len) || len == str_len, "length should be equal #{len}"},
            {is_nil(max_len) || str_len <= max_len, "maximum length #{max_len}"},
            {is_nil(regexp) || Regex.match?(regexp, value),
             "should match given regexp #{inspect(regexp)}"}
          ]
          |> Enum.filter(fn {bool, _} -> bool == false end)
          |> Enum.map(fn {_, error_text} -> error_text end)

        false ->
          ["should be StringType"]
      end

    case errors == [] do
      true -> []
      false -> [inspect(value)] ++ errors
    end
  end
end