lib/types/list.ex

defmodule DryValidation.Types.List do
  @moduledoc """
  Represents a list of elements. Type or a function can also be specified.
  ```
  DryValidation.schema do
    required :words, Types.List.type(Types.String)
    required :numbers, Types.List.type(Types.Integer)
    required :numbers_greater_than_ten, Types.List.type(Types.Integer.greater_than(10))
  end
  ```
  """

  defstruct [:type]

  alias DryValidation.Types

  @doc """
  Specify the required type of the elements within the list.
  """
  def type(type) do
    %Types.List{type: type}
  end

  @doc false
  def call(%Types.List{type: nil}, value) when is_list(value) do
    {:ok, value}
  end

  def call(Types.List, value) when is_list(value) do
    {:ok, value}
  end

  def call(%Types.List{type: %Types.Func{} = func}, value) when is_list(value) do
    value = Enum.map(value, &Types.Func.cast(func, &1))

    invalid_types =
      if func.type,
        do: Enum.reject(value, &func.type.valid?(&1)),
        else: []

    if Enum.empty?(invalid_types) do
      invalid = Enum.reject(value, &Types.Func.call(func, &1))

      if Enum.empty?(invalid),
        do: {:ok, value},
        else: {:error, invalid, func.error_message}
    else
      {:error, invalid_types, "are not of type #{inspect(func.type)}"}
    end
  end

  def call(%Types.List{type: type}, value) when is_list(value) do
    value = Enum.map(value, &type.cast/1)
    invalid = Enum.reject(value, &type.valid?/1)

    if Enum.empty?(invalid),
      do: {:ok, value},
      else: {:error, invalid}
  end

  def call(Types.List, _value), do: {:error, :not_a_list}
  def call(%Types.List{}, _value), do: {:error, :not_a_list}
end