lib/runbox/utils/enum.ex

defmodule Runbox.Utils.Enum do
  @moduledoc group: :utilities
  @moduledoc """
  A set of utility functions for Enums.
  """

  @doc """
  Returns `:ok` if `fun` applied to every element of `enumerable` returns `:ok`.
  Otherwise it returns the first error occurred during `fun` call.

  ## Example
      iex> each_while_ok([], fn _ -> :not_called_at_all end)
      :ok

      iex> each_while_ok([1, 2, 3], fn _ -> :ok end)
      :ok

      iex> each_while_ok([1, 2, 3], &(if &1 == 2, do: {:error, &1}, else: :ok))
      {:error, 2}
  """
  @spec each_while_ok(Enumerable.t(), (any() -> result)) :: result
        when result: :ok | {:error, reason :: any()}
  def each_while_ok(enumerable, fun) do
    with {:ok, :ok} <- reduce_while_ok(enumerable, :ok, &each_if_ok_reducer(&1, &2, fun)) do
      :ok
    end
  end

  @doc """
  Returns `{:ok, list_of_values}` if `fun` applied to every element of
  `enumerable` returns `{:ok, value}`. Otherwise it returns the first
  error occurred during `fun` call.

  ## Example
      iex> map_while_ok([], fn _ -> :not_called_at_all end)
      {:ok, []}

      iex> map_while_ok([1, 2, 3], &({:ok, &1 * 2}))
      {:ok, [2, 4, 6]}

      iex> map_while_ok([1, 2, 3], &(if &1 == 2, do: {:error, &1 * 2}, else: {:ok, &1 * 2}))
      {:error, 4}
  """
  @spec map_while_ok(Enumerable.t(), (any() -> {:ok, any()} | {:error, reason :: any()})) ::
          {:ok, list()} | {:error, reason :: any()}
  def map_while_ok(enumerable, fun) do
    with {:ok, items} <- reduce_while_ok(enumerable, [], &map_if_ok_reducer(&1, &2, fun)) do
      {:ok, Enum.reverse(items)}
    end
  end

  @doc """
  Invokes `fun` for each element in the `enumerable` with the accumulator.
  If every `fun` call returns `{:ok, new_acc}`, functions returns `{:ok, last_acc}`.
  Otherwise it returns the first error occurred during `fun` call.

  ## Example
      iex> reduce_while_ok([], :initial, fn _, _ -> :not_called_at_all end)
      {:ok, :initial}

      iex> reduce_while_ok([1, 2, 3], 0, &({:ok, &1 + &2}))
      {:ok, 6}

      iex> reduce_while_ok([1, 2, 3], 0, &(if &1 == 2, do: {:error, &1 + &2}, else: {:ok, &1 + &2}))
      {:error, 3}
  """
  @spec reduce_while_ok(
          Enumerable.t(),
          any(),
          (any(), any() -> {:ok, any()} | {:error, reason: any()})
        ) :: {:ok, any()} | {:error, reason :: any()}
  def reduce_while_ok(enumerable, acc, fun) do
    Enum.reduce_while(enumerable, {:ok, acc}, &halt_on_error(&1, &2, fun))
  end

  defp each_if_ok_reducer(item, :ok, fun) do
    with :ok <- fun.(item) do
      {:ok, :ok}
    end
  end

  defp halt_on_error(item, {:ok, acc}, fun) do
    case fun.(item, acc) do
      {:ok, _acc} = new -> {:cont, new}
      {:error, _reason} = error -> {:halt, error}
    end
  end

  defp map_if_ok_reducer(item, acc, fun) do
    with {:ok, value} <- fun.(item) do
      {:ok, [value | acc]}
    end
  end
end