Skip to main content

lib/safe_rpc/adapter/plug.ex

defmodule SafeRPC.Adapter.Plug do
  @moduledoc "Adapter from SafeRPC HTTP envelopes to Plug endpoints."

  alias Plug.Conn
  alias SafeRPC.Adapter.HTTP.{Request, Response}

  defmacro __using__(opts) do
    plug = Keyword.fetch!(opts, :plug)

    quote do
      use SafeRPC.Server

      def start_link(opts) do
        opts = Keyword.put_new(opts, :plug, unquote(plug))
        SafeRPC.Server.start_link(__MODULE__, opts)
      end

      @impl true
      def init(opts), do: SafeRPC.Adapter.Plug.Service.init(opts)

      @impl true
      def handle_call(op, payload, state) do
        {:reply, SafeRPC.Adapter.Plug.Service.call(op, payload, %{}, state), state}
      end

      @impl true
      def handle_request(%{kind: :call, op: op, payload: payload, meta: meta}, state) do
        {:reply, SafeRPC.Adapter.Plug.Service.call(op, payload, meta, state), state}
      end
    end
  end

  @spec call(Request.t(), module(), keyword()) :: Response.t()
  def call(%Request{} = request, plug, opts \\ []) do
    plug_opts = plug.init(Keyword.get(opts, :plug_opts, []))

    request
    |> conn_from_request()
    |> plug.call(plug_opts)
    |> response_from_conn()
  end

  defp conn_from_request(%Request{} = request) do
    body = request_body(request.body)
    path = path_with_query(request)

    request.method
    |> Plug.Test.conn(path, body)
    |> put_headers(request.headers)
    |> Map.put(:scheme, scheme(request.scheme))
    |> Map.put(:host, request.host)
    |> Map.put(:port, request.port)
    |> put_remote_ip(request.remote_ip)
  end

  defp response_from_conn(%Conn{} = conn) do
    %Response{
      status: conn.status || 200,
      headers: conn.resp_headers,
      body: {:full, conn.resp_body || <<>>}
    }
  end

  defp request_body(:empty), do: <<>>
  defp request_body({:full, body}), do: body

  defp path_with_query(%Request{path: path, query: query}) when query in [nil, "", <<>>], do: path
  defp path_with_query(%Request{path: path, query: query}), do: path <> "?" <> query

  defp put_headers(conn, headers) do
    Enum.reduce(headers, conn, fn {name, value}, conn ->
      put_header(conn, String.downcase(to_string(name)), to_string(value))
    end)
  end

  defp put_header(conn, "host", _value), do: conn
  defp put_header(conn, name, value), do: Conn.put_req_header(conn, name, value)

  defp scheme("http"), do: :http
  defp scheme("https"), do: :https

  defp put_remote_ip(conn, nil), do: conn
  defp put_remote_ip(conn, remote_ip), do: Map.put(conn, :remote_ip, remote_ip)
end