lib/grpc/endpoint.ex

defmodule GRPC.Endpoint do
  @moduledoc """
  GRPC endpoint for multiple servers and interceptors.

  ## Usage

      defmodule Your.Endpoint do
        use GRPC.Endpoint

        intercept GRPC.Logger.Server, level: :info
        intercept Other.Interceptor
        run HelloServer, interceptors: [HelloHaltInterceptor]
        run FeatureServer
      end

  Interceptors will be run around your rpc calls from top to bottom. And you can even set
  interceptors for some of servers. In the above example, `[GRPC.Logger.Server, Other.Interceptor,
  HelloHaltInterceptor]` will be run for `HelloServer`, and `[GRPC.Logger.Server, Other.Interceptor]`
  will be run for `FeatureServer`.
  """

  @doc false
  defmacro __using__(_opts) do
    quote do
      import GRPC.Endpoint, only: [intercept: 1, intercept: 2, run: 1, run: 2]

      Module.register_attribute(__MODULE__, :interceptors, accumulate: true)
      Module.register_attribute(__MODULE__, :servers, accumulate: true)
      @before_compile GRPC.Endpoint
    end
  end

  @doc false
  defmacro __before_compile__(env) do
    interceptors =
      Module.get_attribute(env.module, :interceptors)
      |> Macro.escape()
      |> Enum.reverse()
      |> init_interceptors()

    servers = Module.get_attribute(env.module, :servers)
    servers = Enum.map(servers, fn {ss, opts} -> {ss, parse_run_opts(opts, %{})} end)
    server_interceptors = server_interceptors(servers, %{})
    servers = parse_servers(servers)

    quote do
      def __meta__(:interceptors), do: unquote(interceptors)
      def __meta__(:servers), do: unquote(servers)
      def __meta__(:server_interceptors), do: unquote(Macro.escape(server_interceptors))
    end
  end

  defmacro intercept(name) do
    quote do
      @interceptors unquote(name)
    end
  end

  @doc """
  ## Options

  `opts` keyword will be passed to Interceptor's init/1
  """
  defmacro intercept(name, opts) do
    quote do
      @interceptors {unquote(name), unquote(opts)}
    end
  end

  @doc """
  ## Options

    * `:interceptors` - custom interceptors for these servers
  """
  defmacro run(servers, opts \\ []) do
    quote do
      @servers {unquote(servers), unquote(opts)}
    end
  end

  defp server_interceptors([], acc), do: acc

  defp server_interceptors([{servers, %{interceptors: interceptors}} | tail], acc0)
       when is_list(interceptors) do
    acc =
      Enum.reduce(List.wrap(servers), acc0, fn server, acc ->
        Map.put(acc, server, init_interceptors(interceptors))
      end)

    server_interceptors(tail, acc)
  end

  defp server_interceptors([_ | tail], acc) do
    server_interceptors(tail, acc)
  end

  defp parse_servers(servers) do
    servers
    |> Enum.map(fn {server, _} -> server end)
    |> List.flatten()
  end

  defp parse_run_opts([], acc), do: acc

  defp parse_run_opts([{:interceptors, interceptors} | tail], acc) do
    parse_run_opts(tail, Map.put(acc, :interceptors, interceptors))
  end

  defp parse_run_opts([{k, _} | _], _) do
    raise ArgumentError, message: "Unknown option for GRPC.Endpoint.run/2: #{k}"
  end

  defp init_interceptors(interceptors) do
    Enum.map(interceptors, fn
      {interceptor, opts} ->
        {interceptor, interceptor.init(opts)}

      interceptor ->
        {interceptor, interceptor.init([])}
    end)
  end
end