lib/gcode/result/enum.ex

defmodule Gcode.Result.Enum do
  use Gcode.Result

  @moduledoc """
  Common enumerableish functions on results.
  """

  @type result :: Result.t()
  @type result(result) :: Result.t(result)
  @type result(result, error) :: Result.t(result, error)

  @doc "As long as the result of the reducer is ok, continue reducing, otherwise short circuit"
  @spec reduce_while_ok(
          enumerable :: any,
          accumulator :: any,
          reducer :: (any, any -> result(any))
        ) :: result(any)
  def reduce_while_ok(elements, acc, reducer) when is_function(reducer, 2) do
    Enum.reduce_while(elements, ok(acc), fn element, ok(acc) ->
      case reducer.(element, acc) do
        ok(acc) -> {:cont, ok(acc)}
        error(reason) -> {:halt, error(reason)}
      end
    end)
  end

  @doc """
  Maps a collection of results using a mapping function.

  Both the input to the map must be an ok result and the result of each mapping
  function.
  """
  @spec map(result([any]), mapper :: (any -> result(any))) :: result([any])
  def map(ok(enumerable), mapper) when is_function(mapper, 1) do
    reduce_while_ok(enumerable, [], fn element, acc ->
      case mapper.(element) do
        ok(mapped) -> ok([mapped | acc])
        error(reason) -> error(reason)
      end
    end)
    |> reverse()
  end

  def map(error(reason), _mapper), do: error(reason)

  @doc """
  Reverse the enumerable contents of an ok result.
  """
  @spec reverse(result([any])) :: result([any])
  def reverse(ok(enumerable)), do: ok(Enum.reverse(enumerable))
  def reverse(error(reason)), do: error(reason)

  @doc """
  Join the string contents of an ok result.
  """
  @spec join(result([String.t()]), String.t()) :: result(String.t())
  def join(ok(strings), joiner) when is_binary(joiner), do: ok(Enum.join(strings, joiner))
  def join(error(reason), _joiner), do: error(reason)
end