lib/gnat/server.ex

defmodule Gnat.Server do
  require Logger

  @moduledoc """
  A behavior for acting as a server for nats messages.

  You can use this behavior in your own module and then use the `Gnat.ConsumerSupervisor` to listen for and respond to nats messages.

  ## Example

      defmodule MyApp.RpcServer do
        use Gnat.Server

        def request(%{body: _body}) do
          {:reply, "hi"}
        end

        # defining an error handler is optional, the default one will just call Logger.error for you
        def error(%{gnat: gnat, reply_to: reply_to}, _error) do
          Gnat.pub(gnat, reply_to, "Something went wrong and I can't handle your request")
        end
      end
  """

  @doc """
  Called when a message is received from the broker
  """
  @callback request(message::Gnat.message()) :: :ok | {:reply, iodata()} | {:error, term()}

  @doc """
  Called when an error occured during the `request/1`

  If your `request/1` function returned `{:error, term}`, then the `term` you returned will be passed as the second argument.
  If an exception was raised during your `request/1` function, then the exception will be passed as the second argument.
  If your `request/1` function returned something other than the supported return types, then its return value will be passed as the second argument.
  """
  @callback error(message::Gnat.message(), error::term()) :: :ok | {:reply, iodata()}

  defmacro __using__(_opts) do
    quote do
      @behaviour Gnat.Server

      def error(_message, error) do
        require Logger
        Logger.error(
          "Gnat.Server encountered an error while handling a request: #{inspect(error)}",
          type: :gnat_server_error,
          error: error
        )
      end

      defoverridable error: 2
    end
  end

  # The functions below are not documented because they are used internally to run
  # the callback modules

  @doc false
  def execute(module, message) do
    try do
      case apply(module, :request, [message]) do
        :ok -> :done
        {:reply, data} -> send_reply(message, data)
        {:error, error} -> execute_error(module, message, error)
        other -> execute_error(module, message, other)
      end

    rescue e ->
      execute_error(module, message, e)
    end
  end

  @doc false
  defp execute_error(module, message, error) do
    try do
      case apply(module, :error, [message, error]) do
        :ok -> :done
        {:reply, data} -> send_reply(message, data)
        other ->
          Logger.error(
            "error handler for #{module} returned something unexpected: #{inspect(other)}",
            type: :gnat_server_error
          )
      end

    rescue e ->
      Logger.error(
        "error handler for #{module} encountered an error: #{inspect(e)}",
        type: :gnat_server_error
      )
    end
  end

  @doc false
  def send_reply(%{gnat: gnat, reply_to: return_address}, iodata) when is_binary(return_address) do
    Gnat.pub(gnat, return_address, iodata)
  end
  def send_reply(_other, _iodata) do
    Logger.error(
      "Could not send reply because no reply_to was provided with the original message",
      type: :gnat_server_error
    )
  end
end