lib/validators/protocols/iterable.ex

defprotocol Iterable do
  @moduledoc """
  Protocol used to iterate over elements of the iterable. This protocol is used in `Dsv.Any`, `Dsv.All` and `Dsv.None` validators.

  The first parameter is the data we want to go through.
  The second is the functions that receives one element of the iterable at a time and return one of the value:
    * :cont - the iteration should be continue
    * :done - the iteration should be halt immediately


  There are default implementations for: `List`, `BitString` and `Tuple`.


  `BitString` implementation goes through graphemes:
      iex> Iterable.iterate("Hello! Dzień dobry!", :cont, fn elem -> if elem == "D", do: :halt, else: :cont end)
      {:done, :not_empty}

      iex> Iterable.iterate("Hello! Dzień dobry!", :cont, fn elem -> if elem == "e", do: :halt, else: :cont end)
      {:done, :not_empty}

      iex> Iterable.iterate("Hello! Dzień dobry!", :cont, fn elem -> if elem == "h", do: :halt, else: :cont end)
      {:done, :empty}


  `List`  implementation goes through all elements of the list:
      iex> Iterable.iterate(["a", :b, ["c"], %{d: "d"}], :cont, fn elem -> if elem == ["c"], do: :halt, else: :cont end)
      {:done, :not_empty}

      iex> Iterable.iterate(["a", :b, ["c"], %{d: "d"}], :cont, fn elem -> if elem == %{d: "d"}, do: :halt, else: :cont end)
      {:done, :not_empty}

  `Tuple` implementation goes throught all elements of the tuple:
      iex> Iterable.iterate({"a", :b, ["c"], %{d: "d"}}, :cont, fn elem -> if elem == ["c"], do: :halt, else: :cont end)
      {:done, :not_empty}

      iex> Iterable.iterate({"a", :b, ["c"], %{d: "d"}}, :cont, fn elem -> if elem == %{d: "d"}, do: :halt, else: :cont end)
      {:done, :not_empty}

      iex> Iterable.iterate({:a, :b, :c}, :cont, fn elem -> if elem == :d, do: :halt, else: :cont end)
      {:done, :empty}

  `Map` implementation goes throught all elements of the map:
      iex> Iterable.iterate(%{a: :b, c: :d}, :cont, fn {_key, value} -> if value == :d, do: :halt, else: :cont end)
      {:done, :not_empty}

      iex> Iterable.iterate(%{a: :b, c: :d}, :cont, fn {_key, value} -> if value == :e, do: :halt, else: :cont end)
      {:done, :empty}

      iex> Iterable.iterate(%{}, :cont, fn {_key, value} -> if value == :e, do: :halt, else: :cont end)
      {:done, :empty}

  """

  @typedoc """
  The value for each step.
  """
  @type element :: any()

  @typedoc """
  It must be a value that is one of the following "tags":

    * `:cont`    - the iteration should continue
    * `:halt`    - the iteration should halt immediately
  """
  @type tag :: :cont | :halt

  @typedoc """
  The result value for iterate function.
  """
  @type result :: {:done, :empty} | {:done, :not_empty}

  @typedoc """
  The next function
  """
  @type next :: (current_element :: element() -> tag())

  @doc """
  Go through every element and provide that element to the `t:next/0` function.
  """
  @spec iterate(t, tag, next) :: result
  def iterate(data, tag, fun)
end

defimpl Iterable, for: List do
  def iterate([], :cont, _fun), do: {:done, :empty}
  def iterate(_data, :halt, _fun), do: {:done, :not_empty}
  def iterate([head | tail], :cont, fun), do: iterate(tail, fun.(head), fun)
end

defimpl Iterable, for: BitString do
  def iterate([], :cont, _fun), do: {:done, :empty}
  def iterate(_data, :halt, _fun), do: {:done, :not_empty}
  def iterate([head | tail], :cont, fun), do: iterate(tail, fun.(head), fun)
  def iterate(data, :cont, fun), do: String.graphemes(data) |> iterate(:cont, fun)
end

defimpl Iterable, for: Tuple do
  def iterate([], :cont, _fun), do: {:done, :empty}
  def iterate(_data, :halt, _fun), do: {:done, :not_empty}
  def iterate([head | tail], :cont, fun), do: iterate(tail, fun.(head), fun)

  def iterate(data, :cont, fun) do
    l = Tuple.to_list(data)
    iterate(l, :cont, fun)
  end
end

defimpl Iterable, for: Map do
  def iterate([], :cont, _fun), do: {:done, :empty}
  def iterate(%{} = data, :cont, _fun) when data == %{}, do: {:done, :empty}
  def iterate(_data, :halt, _fun), do: {:done, :not_empty}
  def iterate([head | tail], :cont, fun), do: iterate(tail, fun.(head), fun)

  def iterate(data, :cont, fun) do
    l = Map.to_list(data)
    iterate(l, :cont, fun)
  end
end