defmodule Orcha do @type message() :: any() @type service_state() :: any() @type callback() :: {module(), func :: atom(), args :: list()} @opaque realm() :: %__MODULE__{ registry: %{atom() => service_state()} } @callback handle_request(message(), realm(), service_state()) :: {:result, result :: any()} | {:result, result :: any(), service_state()} | {:result, result :: any(), service_state(), realm()} defstruct registry: %{} @default_init :initialize @spec initialize() :: realm() def initialize(), do: %__MODULE__{} @spec register(realm(), callback() | module()) :: realm() def register(%__MODULE__{} = realm, {mod, func, args}) do state = apply(mod, func, args) put_state(mod, state, realm) end def register(%__MODULE__{} = realm, mod) when is_atom(mod) do register(realm, {mod, @default_init, []}) end @spec request({module(), message()}, realm()) :: result :: any() | {result :: any(), realm()} def request({mod, msg}, %__MODULE__{} = realm) do with {:ok, state} <- Map.fetch(realm.registry, mod) do case apply(mod, :handle_request, [msg, realm, state]) do {:result, result} -> result {:result, result, new_state} -> new_realm = put_state(mod, new_state, realm) {result, new_realm} {:result, result, new_state, %__MODULE__{} = new_realm} -> new_realm = put_state(mod, new_state, new_realm) {result, new_realm} end else :error -> raise ArgumentError, "Requesting unregistered service: #{inspect(mod)}." end end defp put_state(mod, state, %__MODULE__{} = realm) do update_in(realm.registry, &Map.put(&1, mod, state)) end end