lib/squid_sonar/router.ex

defmodule SquidSonar.Router do
  @moduledoc """
  Router helpers for mounting SquidSonar inside a Phoenix application.

  The host owns its endpoint, authentication, layout, and deployment topology.
  SquidSonar contributes only the LiveView routes under the requested path.
  """

  @default_opts [
    socket_path: "/live",
    transport: "websocket"
  ]

  @transport_values ~w(longpoll websocket)

  defmacro __using__(_opts) do
    quote do
      import SquidSonar.Router, only: [squid_sonar: 1, squid_sonar: 2]
    end
  end

  @doc """
  Mounts SquidSonar under the given path.

      scope "/" do
        pipe_through [:browser]

        squid_sonar "/sonar", otp_app: :my_app
      end

  Supported options:

    * `:as` - route helper name for the mounted LiveView session.
    * `:socket_path` - LiveView socket path used by the host application.
      Defaults to `"/live"`.
    * `:transport` - LiveView client transport. Use `"websocket"` or
      `"longpoll"`. Defaults to `"websocket"`.
  """
  defmacro squid_sonar(path, opts \\ []) do
    quote bind_quoted: [path: path, opts: opts] do
      prefix = Phoenix.Router.scoped_path(__MODULE__, path)

      scope path, alias: false, as: false do
        import Phoenix.LiveView.Router, only: [live: 4, live_session: 3]

        {session_name, session_opts, route_opts} = SquidSonar.Router.__options__(prefix, opts)

        live_session session_name, session_opts do
          get "/css-:digest", SquidSonarWeb.Assets, :css, as: :squid_sonar_asset
          get "/js-:digest", SquidSonarWeb.Assets, :js, as: :squid_sonar_js
          get "/vendor/phoenix-:digest", SquidSonarWeb.Assets, :phoenix, as: :squid_sonar_phoenix

          get "/vendor/live-view-:digest", SquidSonarWeb.Assets, :live_view,
            as: :squid_sonar_live_view

          live "/", SquidSonarWeb.PageLive, :index, route_opts
          live "/runs/:id", SquidSonarWeb.RunLive, :show, route_opts
        end
      end
    end
  end

  @doc false
  def __options__(prefix, opts) do
    opts = Keyword.merge(@default_opts, opts)

    Enum.each(opts, &validate_opt!/1)

    session_args = [
      prefix,
      opts[:socket_path],
      opts[:transport]
    ]

    session_opts = [
      on_mount: [SquidSonarWeb.Hooks],
      session: {__MODULE__, :__session__, session_args},
      root_layout: {SquidSonarWeb.Layouts, :root}
    ]

    session_name = Keyword.get(opts, :as, :squid_sonar)

    {session_name, session_opts, as: session_name}
  end

  @doc false
  def __session__(_conn, prefix, live_path, live_transport) do
    %{
      "prefix" => prefix,
      "live_path" => live_path,
      "live_transport" => live_transport
    }
  end

  defp validate_opt!({:transport, transport}) do
    unless transport in @transport_values do
      raise ArgumentError, """
      invalid :transport, expected one of #{inspect(@transport_values)},
      got #{inspect(transport)}
      """
    end
  end

  defp validate_opt!({:socket_path, path}) do
    unless is_binary(path) and byte_size(path) > 0 do
      raise ArgumentError, """
      invalid :socket_path, expected a non-empty binary URL,
      got #{inspect(path)}
      """
    end
  end

  defp validate_opt!({:as, name}) do
    unless is_atom(name) do
      raise ArgumentError, """
      invalid :as, expected an atom route name,
      got #{inspect(name)}
      """
    end
  end

  defp validate_opt!(_option), do: :ok
end