lib/runbox/scenario/helper.ex

defmodule Runbox.Scenario.Helper do
  @moduledoc """
  Support for helper processes for scenarios.

  Provides a supervisor and process registry under which scenarios can start
  their side-kick processes intended for housekeeping tasks.
  """

  use Supervisor

  @worker_sup Runbox.Scenario.Helper.WorkerSup
  @registry Runbox.Scenario.Helper.Registry

  @doc """
  Starts a supervisor with dynamic supervisor and process registry.
  """
  @spec start_link(any()) :: Supervisor.on_start()
  def start_link(_) do
    Supervisor.start_link(__MODULE__, [], name: __MODULE__)
  end

  @doc """
  Executes a given `callback` over running helper.

  Checks whether the helper with `name` registered under helper registry is
  already running. If not, then it starts the process according to `child_spec`
  under the dynamic supervisor. In either case, it passes the helper process
  pid to the `callback`.

  It is the responsibility of the helper process to register its name with the
  helper registry via the `via_registry/1`.

  ## Examples

      iex> child_spec = %{
      ...>   id: MyHelper,
      ...>   start: {
      ...>     Agent,
      ...>     :start_link,
      ...>     [fn -> 1 end, [name: Helper.via_registry(MyHelper)]]
      ...>   }
      ...> }
      iex> :ok = Helper.with_helper(MyHelper, child_spec, fn pid -> Agent.update(pid, & &1 + 1) end)
      iex> Helper.with_helper(MyHelper, child_spec, fn pid -> Agent.get(pid, & &1) end)
      2
  """
  @spec with_helper(any(), child_spec, (pid() -> any())) :: any()
        when child_spec: :supervisor.child_spec() | {module(), term()} | module()
  def with_helper(name, child_spec, callback) do
    with {:ok, pid} <- ensure_started(name, child_spec) do
      callback.(pid)
    end
  end

  @doc """
  Returns a `:via` name to be used in the helper process registry.
  """
  @spec via_registry(term()) :: {:via, Registry, term()}
  def via_registry(name), do: {:via, Registry, {@registry, name}}

  @impl true
  def init(_) do
    children = [
      {DynamicSupervisor, strategy: :one_for_one, name: @worker_sup},
      {Registry, keys: :unique, name: @registry}
    ]

    Supervisor.init(children, strategy: :one_for_all)
  end

  defp ensure_started(name, child_spec) do
    case Registry.lookup(@registry, name) do
      [{pid, _}] -> {:ok, pid}
      [] -> start_worker(child_spec)
    end
  end

  defp start_worker(child_spec) do
    case DynamicSupervisor.start_child(@worker_sup, child_spec) do
      {:ok, pid} -> {:ok, pid}
      {:error, {:already_started, pid}} -> {:ok, pid}
      {:error, _} = error -> error
    end
  end
end