NAME

Open::API::UI - a Swagger UI clone for a compiled spec

SYNOPSIS

# the usual way: one option on the Plack app
my $app = Open::API::Plack->new(
    spec     => 'openapi.json',
    handlers => { ... },
    ui       => 1,                    # serve /docs and /openapi.json
)->to_app;

# or standalone, for other frameworks and mounting by hand
use Open::API::UI;

my $ui = Open::API::UI->new(api => $api);
my $html   = $ui->index_html;         # the whole docs page
my $spec   = $ui->spec_json;          # the document, JSON-encoded
my $routes = $ui->routes;             # the adapter contract
my $app    = $ui->to_app;             # standalone PSGI app

DESCRIPTION

An interactive documentation UI for an Open::API spec, in the spirit of Swagger UI but written from scratch: one HTML page, one stylesheet, one script, all shipped inside this distribution. No CDN, no bundled third-party code, works offline.

CONSTRUCTOR

new

my $ui = Open::API::UI->new(
    api       => $api,              # required, an Open::API
    path      => '/docs',           # UI mount prefix
    spec_path => '/openapi.json',   # where the spec JSON is served
    title     => undef,             # page title; defaults to info.title
    try_it    => 1,                 # render try-it-out forms
    csrf      => { header => 'X-CSRF-Token', cookie => 'csrf' },
    headers   => { ... },           # adjust the UI response headers
);

api is the compiled Open::API whose document the page describes. Unknown options croak. The page is rendered here, so a template problem is a startup error, never a per-request one.

csrf names the header the try-it-out script sends its token in and the cookie it reads a rotated token from; give a false value to disable CSRF handling in the browser entirely. When the UI is mounted through Open::API::Plack's ui option these names are taken from the app's own csrf configuration automatically and this option is ignored.

headers adjusts the response header set stamped on every UI response, with the same semantics as "RESPONSE HEADERS" in Open::API::Plack: a listed name overrides the default (matched case-insensitively), an unlisted name is added, and mapping a name to undef removes it. The defaults are

Content-Security-Policy: default-src 'none'; script-src 'self';
    style-src 'self'; connect-src 'self'; img-src 'self' data:;
    frame-ancestors 'none'; base-uri 'none'; form-action 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Cache-Control: no-cache

The page carries no inline script or style, so 'self' covers all of it. connect-src 'self' means try-it-out reaches the same origin only; to exercise a cross-origin entry from the spec's servers list, widen connect-src here (and configure CORS on the target).

METHODS

Everything the page needs is precomputed and cached on the object; these accessors hand out bytes.

index_html

my $bytes = $ui->index_html;

The complete documentation page as UTF-8 bytes.

spec_json

my $bytes = $ui->spec_json;

The spec document (exactly what "spec" in Open::API returns) encoded as JSON, cached after the first call. This is what is served at spec_path and what app.js renders from.

asset

my ($content_type, $bytes) = $ui->asset('app.js');   # or 'app.css'

One static asset with its content type. Any other name returns the empty list.

headers

my $pairs = $ui->headers;   # [ name => value, ... ]

A fresh arrayref of the header pairs stamped on every UI response, after the headers option has been applied.

routes

for my $r (@{ $ui->routes }) {
    # { method => 'GET', path => '/docs', response => sub { ... } }
}

The framework contract. Every UI route is a static GET; path is absolute and already prefixed; response returns a finished PSGI-shaped triplet - [ $status, \@headers, [ $bytes ] ] - fresh on every call, so an adapter may mutate it freely. The set is the index page (with and without a trailing slash), the two assets, and the spec JSON. A Catalyst adapter unpacks the triplet into $c->res; a Mojolicious adapter into $c->render; a PSGI framework can use it as-is.

to_app

my $app = $ui->to_app;

A standalone PSGI app serving exactly "routes" (GET and HEAD; anything else is a 404). Useful under Plack::Builder's mount or for serving the docs separately from the API.

THE PAGE

The shell lists every operation grouped by its first tag, collapsed, with method, path and summary. Expanding an operation renders its parameter tables, request body schema, response schemas and - when try_it is on - a form that executes real requests with fetch() and shows the status, headers, body and elapsed time. Schemas resolve $ref into components.schemas (cycles guarded), and a schema browser for the components sits at the bottom of the page. Deep links work: #op-<operationId> expands and scrolls to an operation.

Descriptions are CommonMark, as the OpenAPI spec says: the info, tag and operation descriptions are rendered to HTML at boot with Markdown::Simple (GFM defaults - raw HTML and dangerous URLs are stripped, so a hostile spec cannot script the page). Parameter and schema descriptions, built in the browser, stay plain text.

A request body declared as multipart/form-data gets one field per schema property - a file picker where the schema says format: binary (multiple for arrays of them), a text input otherwise - and is sent as real FormData, the browser writing the boundary. An application/octet-stream (or any body whose schema is format: binary) gets a single file picker and ships the file raw.

The Authorize box takes a bearer token, an API key, or basic credentials per scheme declared in the spec. Values live in page memory; an explicit checkbox opts into sessionStorage. OAuth2 token flows are out of scope - paste an already-issued token.

For CSRF-protected specs the script sends the configured header on every unsafe request, preferring a manually pasted token and falling back to the configured cookie, so the rotate-on-use flow of "CSRF" in Open::API::Plack just works. If the app restricts csrf origins, list the docs origin there or try-it-out requests will be rejected.

Not currently supported: XML rendering, OAuth2 token flows (paste an already-issued token).

SEE ALSO

Open::API, Open::API::Plack, Template::Stencil, Markdown::Simple.

AUTHOR

LNATION <email@lnation.org>

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION <email@lnation.org>.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)