lib/orion_web/router.ex

defmodule OrionWeb.Router do
  @moduledoc """
  Provides LiveView routing for Orion.
  """

  @doc """
  Define an Orion route.

  It expects the `path` Orion will be mounted at and a set of options

  All the Orion Pages will be under a live_session.

    ## Options
    * `:live_socket_path` - Configures the socket path. it must match the
      `socket "/live", Phoenix.LiveView.Socket` in your endpoint.

    * `:csp_nonce_assign_key` - an assign key to find the CSP nonce value used
      for assets. Supports either `atom()` or a map of type `%{optional(:img) =>
      atom(), optional(:script) => atom(), optional(:style) => atom()}`

    * `:on_mount` - Pass a list of `on_mount` hooks to live_session, like if you
      want to validate authentication with `:on_mount`. See [the
      phoenix_liveview
      docs](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Router.html#live_session/3)

    * `:live` - Configure options to inject into the Orion route

    * `:self_profile` - Configure if the tracing is applied to the node running Orion itself, default to true

    * `:fake_data` - Configure if the tracing form allow you to run with fake data, default to false

  ## Examples
      defmodule MyAppWeb.Router do
        use Phoenix.Router
        import OrionWeb.Router
        scope "/", MyAppWeb do
          pipe_through [:browser]
          live_orion "/orion",
            on_mount: {MyAppWeb.SomeHook, :admin}
        end
      end
  """

  defmacro live_orion(path, opts \\ []) do
    opts =
      if Macro.quoted_literal?(opts) do
        Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
      else
        opts
      end

    scope =
      quote bind_quoted: binding() do
        scope path, alias: false, as: false do
          {session_name, session_opts, route_opts} = OrionWeb.Router.__options__(opts)

          import Phoenix.LiveView.Router, only: [live: 4, live_session: 3]

          live_session session_name, session_opts do
            # All helpers are public contracts and cannot be changed
            live "/", OrionWeb.PageLive, :home, route_opts
          end
        end
      end

    # TODO: Remove check once we require Phoenix v1.7
    if Code.ensure_loaded?(Phoenix.VerifiedRoutes) do
      quote do
        unquote(scope)

        unless Module.get_attribute(__MODULE__, :live_orion_prefix) do
          @live_orion_prefix Phoenix.Router.scoped_path(__MODULE__, path)
          def __live_orion_prefix__, do: @live_orion_prefix
        end
      end
    else
      scope
    end
  end

  defp expand_alias({:__aliases__, _, _} = alias, env),
    do: Macro.expand(alias, %{env | function: {:live_orion, 2}})

  defp expand_alias(other, _env), do: other

  @doc false
  def __options__(options) do
    live_socket_path = Keyword.get(options, :live_socket_path, "/live")
    on_mount = Keyword.get(options, :on_mount, [])

    csp_nonce_assign_key =
      case options[:csp_nonce_assign_key] do
        nil -> nil
        key when is_atom(key) -> %{img: key, style: key, script: key}
        %{} = keys -> Map.take(keys, [:img, :style, :script])
      end

    self_profile = Keyword.get(options, :self_profile, true)
    fake_data = Keyword.get(options, :fake_data, false)

    session_args = [
      csp_nonce_assign_key,
      self_profile,
      fake_data
    ]

    {
      options[:live_session_name] || :orion,
      [
        session: {__MODULE__, :__session__, session_args},
        root_layout: {OrionWeb.LayoutView, :home},
        on_mount: on_mount
      ],
      [
        private: %{live_socket_path: live_socket_path, csp_nonce_assign_key: csp_nonce_assign_key},
        as: :orion
      ]
    }
  end

  @doc false
  def __session__(conn, csp_nonce_assign_key, self_profile, fake_data) do
    %{
      "self_profile" => self_profile,
      "fake_data" => fake_data,
      "csp_nonces" => %{
        img: conn.assigns[csp_nonce_assign_key[:img]],
        style: conn.assigns[csp_nonce_assign_key[:style]],
        script: conn.assigns[csp_nonce_assign_key[:script]]
      }
    }
  end
end