Skip to main content

lib/openapi/phoenix.ex

defmodule Openapi.Phoenix do
  @moduledoc """
  Basic Phoenix integration helpers for OpenAPI support.

  This module provides macros that help integrate OpenAPI definition tracking into Phoenix routers
  and applications.

  It enables routers to declare OpenAPI files and participate in automatic discovery and aggregation
  of API definitions across the system.
  """

  require Phoenix.Router

  @doc """
  Injects OpenAPI router functionality into a module.

  When used, the module:
  - Registers itself in `:persistent_term` as an OpenAPI router on load
  - Enables accumulation of `@openapi_files` module attributes
  - Exposes `__openapi_files__/0` for retrieving declared OpenAPI files
  - Imports `Openapi.Phoenix` helpers

  This allows the module to participate automatically in OpenAPI definition discovery and generation.
  """
  defmacro __using__(_) do
    quote do
      import Openapi.Phoenix
      @on_load :__register_openapi_router__

      Module.register_attribute(__MODULE__, :openapi_files,
        accumulate: true,
        persist: true
      )

      def __register_openapi_router__ do
        routers = :persistent_term.get({:openapi, :routers}, [])
        :persistent_term.put({:openapi, :routers}, [__MODULE__ | routers])
      end

      def __openapi_files__ do
        __MODULE__.__info__(:attributes)
        |> Keyword.get_values(:openapi_files)
        |> List.flatten()
      end
    end
  end

  @doc """
  Register an OpenAPI spec and its routes with the application.

  Options:
    - `handler`: Default handler of the routes can be overwritten with `x-handler`
    - `strict` (Default `true`): Validates routes at compile time.
    - `server`: The server/namespace for this spec. Auto-detected from router module if not provided.
    - `prefix`: Explicit route prefix applied to all generated OpenAPI paths.
  """
  defmacro openapi(path, options \\ []) do
    server =
      Keyword.get_lazy(options, :server, fn ->
        __CALLER__.module
        |> Module.split()
        |> hd()
        |> String.to_atom()
      end)

    quote bind_quoted: [server: server, path: path, options: options] do
      handler = Keyword.get(options, :handler)
      server = Keyword.get(options, :server, server)
      strict? = Keyword.get(options, :strict, true)
      prefix = Keyword.get(options, :prefix)

      definition = Openapi.read_file!(path)
      routes = Openapi.Definition.phoenix_routes(definition)
      if strict?, do: Openapi.RouteValidator.validate(routes, server, handler)

      for route <- routes do
        Phoenix.Router.match(
          route.method,
          route.path,
          Openapi.DispatchPlug,
          [],
          alias: false,
          private: %{
            openapi: %{
              server: server,
              handler: route.handler || handler,
              operation_id: route.operation_id,
              schemas: route.schemas
            }
          }
        )
      end

      @openapi_files %{
        server: server,
        file: path,
        prefix: prefix
      }
    end
  end

  @doc """
  Register swagger-ui to a given path.

  Options:
    - `server`: The server/namespace whose spec will be served on swagger-ui.
    - `prefix`: Explicit route prefix applied to the path.
  """
  defmacro swagger_docs(path, options \\ []) do
    server =
      Keyword.get_lazy(options, :server, fn ->
        __CALLER__.module
        |> Module.split()
        |> hd()
        |> String.to_atom()
      end)

    quote bind_quoted: [server: server, path: path, options: options] do
      scope path do
        server = Keyword.get(options, :server, server)
        prefix = Keyword.get(options, :prefix)
        path = "#{prefix}#{path}"
        Phoenix.Router.match(:get, "/", Openapi.DocsPlug, {:index, server, path}, alias: false)

        Phoenix.Router.match(:get, "/openapi.json", Openapi.DocsPlug, {:spec, server, path},
          alias: false
        )

        Phoenix.Router.match(:get, "/*path", Openapi.DocsPlug, {:asset, server, path},
          alias: false
        )
      end
    end
  end
end