Current section
Files
Jump to
Current section
Files
lib/ash_oaskit/router.ex
defmodule AshOaskit.Router do
@moduledoc """
Router macro for serving OpenAPI specs with minimal configuration.
Works with both Phoenix Router and Plug.Router. Automatically detects the
router type and generates appropriate routes.
## Usage with Phoenix Router
defmodule MyAppWeb.Router do
use MyAppWeb, :router
use AshOaskit.Router,
domains: [MyApp.Blog, MyApp.Accounts],
open_api: "/docs/openapi",
title: "My API",
version: "1.0.0"
end
## Usage with Plug.Router
defmodule MyApp.Router do
use Plug.Router
plug :match
plug :dispatch
use AshOaskit.Router,
domains: [MyApp.Blog],
open_api: "/openapi",
title: "My API"
end
## Options
* `:domains` - List of Ash domains to include (required)
* `:open_api` - Base path for OpenAPI endpoints (required)
* `:title` - API title (default: "API")
* `:version` - API version string (default: "1.0.0")
* `:description` - API description (optional)
* `:openapi_versions` - List of OpenAPI versions to serve (default: ["3.0", "3.1"])
* `:default_version` - Default OpenAPI version (default: "3.1")
* `:formats` - Output formats (default: [:json])
* `:servers` - List of server URLs or server objects (optional)
* `:router` - Phoenix router module for controller introspection (optional)
* `:modify_open_api` - Post-processing function for spec customization (optional)
* `:spec_builder` - Custom SpecBuilder module (default: `AshOaskit.SpecBuilder.Default`)
## Generated Routes
With `open_api: "/openapi"` and default options, generates:
GET /openapi.json -> Default version (3.1) spec
GET /openapi/3.0.json -> OpenAPI 3.0 spec
GET /openapi/3.1.json -> OpenAPI 3.1 spec
"""
@doc false
defmacro __using__(opts) do
# Both Phoenix Router and Plug.Router routes must be generated inline
# (not via @before_compile) because:
# - Plug.Router: routes after catch-all `match _` are unreachable
# - Phoenix Router: routes registered after Phoenix's own @before_compile
# are excluded from the routing table
#
# Detection happens at macro expansion time via __CALLER__.
is_phoenix =
Enum.any?(__CALLER__.macros, fn {mod, _} -> mod == Phoenix.Router end)
if is_phoenix do
build_phoenix_quoted(opts)
else
build_plug_quoted(opts)
end
end
defp build_phoenix_quoted(opts) do
quote bind_quoted: [opts: opts] do
require Phoenix.Router
base_path = Keyword.fetch!(opts, :open_api)
openapi_versions = Keyword.get(opts, :openapi_versions, ["3.0", "3.1"])
default_version = Keyword.get(opts, :default_version, "3.1")
formats = Keyword.get(opts, :formats, [:json])
config = %{
domains: Keyword.fetch!(opts, :domains),
title: Keyword.get(opts, :title, "API"),
version: Keyword.get(opts, :version, "1.0.0"),
description: Keyword.get(opts, :description),
servers: Keyword.get(opts, :servers, []),
router: Keyword.get(opts, :router),
modify_open_api: Keyword.get(opts, :modify_open_api),
spec_builder: Keyword.get(opts, :spec_builder, AshOaskit.SpecBuilder.Default)
}
for format <- formats do
ext = Atom.to_string(format)
Phoenix.Router.get(
"#{base_path}.#{ext}",
AshOaskit.Router.Plug,
Map.merge(config, %{openapi_version: default_version, format: format})
)
for openapi_version <- openapi_versions do
Phoenix.Router.get(
"#{base_path}/#{openapi_version}.#{ext}",
AshOaskit.Router.Plug,
Map.merge(config, %{openapi_version: openapi_version, format: format})
)
end
end
end
end
defp build_plug_quoted(opts) do
quote bind_quoted: [opts: opts] do
base_path = Keyword.fetch!(opts, :open_api)
openapi_versions = Keyword.get(opts, :openapi_versions, ["3.0", "3.1"])
default_version = Keyword.get(opts, :default_version, "3.1")
formats = Keyword.get(opts, :formats, [:json])
# credo:disable-for-next-line Credo.Check.Design.DuplicatedCode
@__ash_oaskit_config %{
domains: Keyword.fetch!(opts, :domains),
title: Keyword.get(opts, :title, "API"),
version: Keyword.get(opts, :version, "1.0.0"),
description: Keyword.get(opts, :description),
servers: Keyword.get(opts, :servers, []),
router: Keyword.get(opts, :router),
modify_open_api: Keyword.get(opts, :modify_open_api),
spec_builder: Keyword.get(opts, :spec_builder, AshOaskit.SpecBuilder.Default)
}
for format <- formats do
ext = Atom.to_string(format)
Plug.Router.get "#{base_path}.#{ext}" do
config =
Map.merge(@__ash_oaskit_config, %{
openapi_version: unquote(default_version),
format: unquote(format)
})
conn = Plug.Conn.put_private(var!(conn), :ash_oaskit, config)
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
AshOaskit.Router.Plug.call(conn, [])
end
for openapi_version <- openapi_versions do
Plug.Router.get "#{base_path}/#{openapi_version}.#{ext}" do
config =
Map.merge(@__ash_oaskit_config, %{
openapi_version: unquote(openapi_version),
format: unquote(format)
})
conn = Plug.Conn.put_private(var!(conn), :ash_oaskit, config)
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
AshOaskit.Router.Plug.call(conn, [])
end
end
end
end
end
end