lib/machete/matchers/list_matcher.ex

defmodule Machete.ListMatcher do
  @moduledoc """
  Defines a matcher that matches lists
  """

  import Machete.Mismatch
  import Machete.Operators

  defstruct elements: nil, length: nil, min: nil, max: nil

  @typedoc """
  Describes an instance of this matcher
  """
  @opaque t :: %__MODULE__{}

  @typedoc """
  Describes the arguments that can be passed to this matcher
  """
  @type opts :: [
          {:elements, Machete.Matchable.t()},
          {:length, non_neg_integer()},
          {:min, non_neg_integer()},
          {:max, non_neg_integer()}
        ]

  @doc """
  Matches lists. Useful for cases where you wish to match against the general shape / size of
  a list, but cannot match against a literal list

  Takes the following arguments:

  * `elements`: A matcher to use against all elements in the list
  * `length`: Requires the matched list to be exactly the specified length
  * `min`: Requires the matched list to be greater than or equal to the specified length
  * `max`: Requires the matched list to be less than or equal to the specified length

  Examples:
      iex> assert [1] ~> list(elements: integer())
      true

      iex> assert [1] ~> list(length: 1)
      true

      iex> assert [1] ~> list(min: 1)
      true

      iex> assert [1] ~> list(max: 2)
      true
  """
  @spec list(opts()) :: t()

  def list(opts \\ []), do: struct!(__MODULE__, opts)

  defimpl Machete.Matchable do
    def mismatches(%@for{}, b) when not is_list(b), do: mismatch("#{inspect(b)} is not a list")

    def mismatches(%@for{} = a, b) do
      with nil <- matches_length(b, a.length),
           nil <- matches_min(b, a.min),
           nil <- matches_max(b, a.max),
           nil <- matches_elements(b, a.elements) do
      end
    end

    defp matches_length(b, length) when is_number(length) and length(b) != length,
      do: mismatch("#{inspect(b)} is not exactly #{length} elements in length")

    defp matches_length(_, _), do: nil

    defp matches_min(b, length) when is_number(length) and length(b) < length,
      do: mismatch("#{inspect(b)} is less than #{length} elements in length")

    defp matches_min(_, _), do: nil

    defp matches_max(b, length) when is_number(length) and length(b) > length,
      do: mismatch("#{inspect(b)} is more than #{length} elements in length")

    defp matches_max(_, _), do: nil

    defp matches_elements(_, nil), do: nil
    defp matches_elements(b, matcher), do: b ~>> List.duplicate(matcher, length(b))
  end
end