lib/supervised_server.ex

defmodule SupervisedServer do
  @moduledoc """
  A behaviour module for starting supervised GenServer processes.

  With the use of `SupervisedServer`, you can avoid the need to add `c:start_link/1` function
  to your module every time you write a new one.
  And if you need your own `c:start_link/1` function, you can override it.

  ## Usage

  * Add `use SupervisedServer` to your module.
  * View [`Examples`](#module-examples).

  > #### `use SupervisedServer` {: .info}
  >
  > When you `use SupervisedServer`, the SupervisedServer module will:
  > * set `@behaviour SupervisedServer`,
  > * add `use GenServer`,
  > * pass options passed to `use SupervisedServer` to `use GenServer`,
  > * define a `c:start_link/1` function, so your module does not need to define it
  > if you don't want a custom one.

  ## Callbacks

  The following callbacks may be implemented by modules that use the `SupervisedServer` behavior:

  - `c:start_link/1`: Starts a GenServer process linked to the current process.
    This function is often used to start the server as part of a supervision tree.

  `c:start_link/1` callback should return one of the following values:

  - `{:ok, pid}`: The server was successfully created and initialized.

  - `:ignore`: The server should be ignored, and no further action is taken.

  - `{:error, {:already_started, pid}}`: A process with the specified name already exists.

  - `{:error, reason}`: The initialization of the server failed for the given reason.

  - `{:stop, reason}`: The process is terminated due to the provided reason.

  ## Options

  `c:start_link/1` callback accept the following option:

  - `:name` (optional): Used for name registration as described in the "Name Registration" section
    in the documentation for `GenServer`.

  ## Examples

  ### Basic usage
  ```elixir
  iex> defmodule MyServer do
  ...>  use SupervisedServer
  ...>
  ...>  @impl GenServer
  ...>  def init(arg) do
  ...>    {:ok, arg}
  ...>  end
  ...>end
  ...># Starting a supervised process
  ...>{:ok, _} = Supervisor.start_link([MyServer], strategy: :one_for_one)
  ```

  ### Additional options
  ```elixir
  iex> defmodule TransientServer do
  ...>  use SupervisedServer, restart: :transient, shutdown: 10_000
  ...>
  ...>  @impl GenServer
  ...>  def init(arg) do
  ...>    {:ok, arg}
  ...>  end
  ...>end
  ...># Starting a supervised process
  ...>{:ok, sup} = Supervisor.start_link([TransientServer], strategy: :one_for_one)
  ...>{:ok, chldspec} = :supervisor.get_childspec(sup, TransientServer)
  ...>assert chldspec.restart == :transient
  ...>assert chldspec.shutdown == 10_000
  ```

  ### Naming the process
  ```elixir
  iex>defmodule NamedServer do
  ...>  use SupervisedServer
  ...>
  ...>  @impl GenServer
  ...>  def init(arg) do
  ...>    {:ok, arg}
  ...>  end
  ...>
  ...>  @impl GenServer
  ...>  def handle_call(:get_my_arg, _from, state) do
  ...>    {:reply, {:ok, state[:my_arg]}, state}
  ...>  end
  ...>
  ...>  def get_my_arg(pid) do
  ...>    GenServer.call(pid, :get_my_arg)
  ...>  end
  ...>end
  ...>
  ...>children = [
  ...>  {NamedServer, [my_arg: "Hello named server!", name: :my_named_server]}
  ...>]
  ...># Starting a named supervised process
  ...>{:ok, _} = Supervisor.start_link(children, strategy: :one_for_one)
  ...>
  ...># Access the named process using its registered name
  ...>{:ok, "Hello named server!"} = NamedServer.get_my_arg(:my_named_server)
  ```

  ### Custom start_link/1
  ```elixir
  iex>defmodule CustomServer do
  ...>  use SupervisedServer
  ...>
  ...>  # Override start_link/1 to provide additional options
  ...>  @impl SupervisedServer
  ...>  def start_link(args) when is_list(args) do
  ...>    GenServer.start_link(__MODULE__, args, name: :custom_server)
  ...>  end
  ...>
  ...>  @impl GenServer
  ...>  def init(arg) do
  ...>    {:ok, arg}
  ...>  end
  ...>end
  ...>
  ...># Starting a custom supervised GenServer process
  ...>{:ok, _} = Supervisor.start_link([CustomServer], strategy: :one_for_one)
  ...>pid = Process.whereis(:custom_server)
  ...>true = is_pid(pid)
  ```

  ## References

  * `GenServer`
  * `Supervisor`
  """

  @doc """
  Starts a `SupervisedServer` process linked to the current process.

  This is often used to start the `SupervisedServer` as part of a supervision tree.

  Once the server is started, the `init/1` function of the given is called
  with `init_arg` as its argument to initialize the server. To ensure a
  synchronized start-up procedure, this function does not return until `init/1`
  has returned.

  Note that a `SupervisedServer` started with `c:start_link/1` is linked to the
  parent process and will exit in case of crashes from the parent. The `SupervisedServer`
  will also exit due to the `:normal` reasons in case it is configured to trap
  exits in the `init/1` callback.

  ## Options

  - `:name` (optional): Used for name registration as described in the "Name Registration" section
    in the documentation for `GenServer`.

  ## Return values

  If the server is successfully created and initialized, this function returns
  `{:ok, pid}`, where `pid` is the PID of the server. If a process with the
  specified server name already exists, this function returns
  `{:error, {:already_started, pid}}` with the PID of that process.

  If the `init/1` callback fails with `reason`, this function returns
  `{:error, reason}`. Otherwise, if it returns `{:stop, reason}`
  or `:ignore`, the process is terminated and this function returns
  `{:error, reason}` or `:ignore`, respectively.
  """
  @callback start_link(args) :: GenServer.on_start()
            when args: list

  @doc false
  @spec __using__(opts :: keyword) :: Macro.t()
  defmacro __using__(opts) do
    quote location: :keep, bind_quoted: [opts: opts] do
      @behaviour SupervisedServer

      use GenServer, Macro.escape(opts)

      @doc false
      @impl SupervisedServer
      def start_link(args) when is_list(args) do
        {name, init_args} = Keyword.pop(args, :name)

        case name do
          nil -> GenServer.start_link(__MODULE__, init_args)
          _ -> GenServer.start_link(__MODULE__, init_args, name: name)
        end
      end

      defoverridable start_link: 1
    end
  end
end