lib/client/handler.ex

defmodule Wampex.Client.Handler do
  @moduledoc """
  Macros and behaviours for client implementations
  """
  @callback do_init(any()) :: %{client_name: atom()}

  defmacro __using__(_opts) do
    quote do
      use GenServer
      alias Wampex.Client
      alias Wampex.Roles.Callee.Yield
      alias Wampex.Roles.Peer.Error
      import unquote(__MODULE__)
      @before_compile unquote(__MODULE__)
      @procedures []
      @topics []

      def start_link(opts) do
        {name, opts} = Keyword.pop(opts, :name)
        GenServer.start_link(__MODULE__, opts, name: name)
      end

      @impl true
      def init(opts) do
        {cn, opts} = Keyword.pop(opts, :client_name)
        state = do_init(opts)
        Client.add(cn, self())
        {:ok, add_client(state, cn)}
      end

      defp add_client(state, cn) when is_struct(state) do
        struct(state, %{client_name: cn})
      end

      defp add_client(state, cn), do: %{state | client_name: cn}

      @impl true
      def handle_continue({:registered, _}, state), do: {:noreply, state}

      def do_init(_opts), do: %{client_name: nil}

      def handle_invocation_block({:ok, al, kw, state}, id) do
        Client.yield(state.client_name, %Yield{request_id: id, arg_list: al, arg_kw: kw})
        state
      end

      def handle_invocation_block({:error, error, al, kw, state}, id) do
        Client.error(state.client_name, %Error{
          request_id: id,
          error: error,
          arg_list: al,
          arg_kw: kw
        })

        state
      end

      defoverridable do_init: 1, handle_continue: 2
    end
  end

  defmacro __before_compile__(env) do
    procedures = Module.get_attribute(env.module, :procedures) |> Enum.uniq()
    topics = Module.get_attribute(env.module, :topics) |> Enum.uniq()

    quote do
      alias Wampex.Client
      alias Wampex.Roles.Callee.Register
      alias Wampex.Roles.Subscriber.Subscribe
      @impl true
      def handle_info({:connected, _}, %{client_name: cn} = state) do
        regs =
          Enum.map(unquote(procedures), fn p ->
            Client.register(cn, %Register{procedure: p})
          end)

        subs =
          Enum.map(unquote(topics), fn
            {t, match} ->
              Client.subscribe(cn, %Subscribe{topic: t, options: %{"match" => match}})

            t ->
              Client.subscribe(cn, %Subscribe{topic: t})
          end)

        {:noreply, state, {:continue, {:registered, %{registrations: regs, subscriptions: subs}}}}
      end
    end
  end

  defmacro invocation(procedure, list, kws, state, do: block) do
    quote location: :keep do
      alias Wampex.Roles.Dealer.Invocation

      procs = Module.get_attribute(__MODULE__, :procedures)
      Module.put_attribute(__MODULE__, :procedures, [unquote(procedure) | procs])

      @impl true
      def handle_info(
            %Invocation{
              request_id: id,
              details: %{"procedure" => unquote(procedure)},
              arg_list: unquote(list),
              arg_kw: unquote(kws)
            },
            unquote(state) = state
          ) do
        state = handle_invocation_block(unquote(block), id)
        {:noreply, state}
      end
    end
  end

  defmacro event(topic, list, kws, state, do: block) do
    quote location: :keep do
      alias Wampex.Client
      alias Wampex.Roles.Broker.Event

      topics = Module.get_attribute(__MODULE__, :topics)
      Module.put_attribute(__MODULE__, :topics, [unquote(topic) | topics])

      @impl true
      def handle_info(
            %Event{
              details: %{"topic" => unquote(topic)},
              arg_list: unquote(list),
              arg_kw: unquote(kws)
            },
            unquote(state) = state
          ) do
        state = unquote(block)
        {:noreply, state}
      end
    end
  end
end