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