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