lib/vial.ex

defmodule Vial do
  @moduledoc """
  `Vial` is a library for creating Vials, which are self-contained units of functionality that operate on data and create events.
  """

  alias Vial.Cauldron

  @callback init(any()) :: any()
  @callback call(Vial.Glass.t(), any()) :: Vial.Glass.t()

  def run(cauldron, vials, reagents, opts \\ [])

  def run(%Cauldron{halted: true} = cauldron, _vials, reagents, _opts) do
    do_events(cauldron, reagents)
  end

  def run(%Cauldron{} = cauldron, vials, reagents, _opts) do
    do_run(cauldron, vials)
    |> do_events(reagents)
  end

  defp do_run(%Cauldron{} = cauldron, [{vial_mod, vial_opts} | vials]) do
    case vial_mod.call(cauldron, vial_mod.init(vial_opts)) do
      %Cauldron{halted: true} = cauldron ->
        cauldron

      %Cauldron{} = cauldron ->
        do_run(cauldron, vials)

      other ->
        raise "expected #{inspect(vial_mod)} to return Vial.Cauldron, got: #{inspect(other)}"
    end
  end

  defp do_run(%Cauldron{} = cauldron, [fun | vials]) when is_function(fun, 1) do
    case fun.(cauldron) do
      %Cauldron{halted: true} = cauldron ->
        cauldron

      %Cauldron{} = cauldron ->
        do_run(cauldron, vials)

      other ->
        raise "expected #{inspect(fun)} to return Vial.Cauldron, got: #{inspect(other)}"
    end
  end

  defp do_run(%Cauldron{} = cauldron, []) do
    cauldron
  end

  defp do_events(%Cauldron{} = cauldron, [reagent_fun | reagents]) when is_function(reagent_fun, 1) do
    events = Enum.reverse(cauldron.events)

    Enum.each(events, reagent_fun)

    do_events(cauldron, reagents)
  end

  defp do_events(%Cauldron{} = cauldron, [{reagent_mod, reagent_opts} | reagents]) do
    events = Enum.reverse(cauldron.events)

    processed_reagent_opts = reagent_mod.init(reagent_opts)

    Enum.each(events, &reagent_mod.handle(&1, processed_reagent_opts))

    do_events(cauldron, reagents)
  end

  defp do_events(%Cauldron{} = cauldron, []) do
    cauldron
  end
end