lib/client/handler.ex

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

  require Logger

  defmacro __using__(_opts) do
    quote do
      use GenServer
      alias Wampex.Client
      alias Wampex.Roles.Callee.{Unregister, Yield}
      alias Wampex.Roles.Peer.Error
      alias Wampex.Roles.Subscriber.Unsubscribe
      require Logger
      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
        Process.flag(:trap_exit, true)
        {cn, opts} = Keyword.pop(opts, :client_name)
        state = do_init(opts)
        Client.add(cn, self())
        {:ok, add_client(state, cn)}
      end

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

      @impl true
      def terminate(_reason, %{
            client_name: name,
            registrations: %{subscriptions: subs, registrations: regs}
          }) do
        Enum.each(subs, fn id ->
          Client.unsubscribe(name, %Unsubscribe{subscription_id: id})
        end)

        Enum.each(regs, fn id ->
          Client.unregister(name, %Unregister{registration_id: id})
        end)
      end

      @impl true
      def terminate(_reason, _state), do: :ok

      def do_init(_opts), do: %{client_name: nil, registrations: 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

      defp add_regs(state, regs) when is_struct(state) do
        struct(state, %{registrations: regs})
      end

      defp add_regs(state, regs), do: Map.put(state, :registrations, regs)

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

      defp add_client(state, cn), do: Map.put(state, :client_name, cn)

      defoverridable start_link: 1, do_init: 1, handle_continue: 2, terminate: 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()

    Logger.info(
      "#{env.module} | Registering topics: #{inspect(topics)}, procedures: #{inspect(procedures)}"
    )

    quote do
      require Logger
      alias Wampex.Client
      alias Wampex.Roles.Callee.Register
      alias Wampex.Roles.Subscriber.Subscribe
      @impl true
      def handle_info({:connected, _}, %{client_name: cn} = state) do
        Logger.info("Connected")

        regs =
          Enum.map(unquote(procedures), fn p ->
            {:ok, reg} = Client.register(cn, %Register{procedure: p})
            reg
          end)

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

              proc

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

        Logger.info("Registered!")
        {: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
        unquote(block)
      end
    end
  end
end