defmodule GenServer.Proxy do @moduledoc """ Invokes the following functions with a GenServer registered via a server ID: - `GenServer.call/3` - `GenServer.cast/2` - `GenServer.stop/3` Will wait a bit if the GenServer is not yet registered on restarts. Note this is an assumption as the GenServer may have never started. """ @typedoc "Server ID" @type server_id :: term @doc """ Called to convert the `server_id` into a server name. ## Examples @impl GenServer.Proxy def server_name(game_name), do: {:via, Registry, {:registry, game_name}} @impl GenServer.Proxy def server_name(game_name), do: {:global, {GameServer, game_name}} """ @callback server_name(server_id) :: GenServer.name() @doc ~S''' Called when the server remains unregistered despite waiting a bit. Should serve to print a relevant message about the failed request. ## Examples @impl GenServer.Proxy def server_unregistered(game_name), do: :ok = IO.puts("Game #{game_name} not started.") ''' @callback server_unregistered(server_id) :: term @doc """ Either aliases `GenServer.Proxy` (this module) and requires the alias or imports `GenServer.Proxy`. In the latter case, you could instead simply `import GenServer.Proxy`. ## Examples use GenServer.Proxy, alias: Proxy use GenServer.Proxy import GenServer.Proxy """ defmacro __using__(options) do alias = options[:alias] if alias do quote do alias unquote(__MODULE__), as: unquote(alias) require unquote(alias) end else quote do import unquote(__MODULE__) end end end @doc ~S''' Makes a synchronous call to the GenServer registered via `server_id`. Will wait a bit if the GenServer is not yet registered on restarts. The given `module` (or by default `.GenServerProxy`) must implement the 2 callbacks of `GenServer.Proxy` (this module). ## Examples # Assuming the following callback module: defmodule Game.Engine.GenServerProxy do @behaviour GenServer.Proxy @impl GenServer.Proxy def server_name(game_name), do: {:via, Registry, {:registry, game_name}} @impl GenServer.Proxy def server_unregistered(game_name), do: :ok = IO.puts("Game #{game_name} not started.") end # We could use the call macro like so: defmodule Game.Engine do use GenServer.Proxy def summary(game_name), do: call(game_name, :summary) ... end ''' defmacro call(server_id, request, timeout \\ 5000, module \\ nil) do if module do quote bind_quoted: [ server_id: server_id, request: request, timeout: timeout, module: module ] do GenServer.Proxy.Caller.call(server_id, request, timeout, module) end else quote bind_quoted: [ server_id: server_id, request: request, timeout: timeout ] do GenServer.Proxy.Caller.call( server_id, request, timeout, __MODULE__.GenServerProxy ) end end end @doc """ Sends an async request to the GenServer registered via `server_id`. Will wait a bit if the GenServer is not yet registered on restarts. The given `module` (or by default `.GenServerProxy`) must implement the 2 callbacks of `GenServer.Proxy` (this module). """ defmacro cast(server_id, request, module \\ nil) do if module do quote bind_quoted: [ server_id: server_id, request: request, module: module ] do GenServer.Proxy.Caster.cast(server_id, request, module) end else quote bind_quoted: [server_id: server_id, request: request] do GenServer.Proxy.Caster.cast( server_id, request, __MODULE__.GenServerProxy ) end end end @doc """ Synchronously stops the GenServer registered via `server_id`. Will wait a bit if the GenServer is not yet registered on restarts. The given `module` (or by default `.GenServerProxy`) must implement the 2 callbacks of `GenServer.Proxy` (this module). """ defmacro stop( server_id, reason \\ :normal, timeout \\ :infinity, module \\ nil ) do if module do quote bind_quoted: [ server_id: server_id, reason: reason, timeout: timeout, module: module ] do GenServer.Proxy.Stopper.stop(server_id, reason, timeout, module) end else quote bind_quoted: [ server_id: server_id, reason: reason, timeout: timeout ] do GenServer.Proxy.Stopper.stop( server_id, reason, timeout, __MODULE__.GenServerProxy ) end end end end