OpenAPI Export

spider-silk-openapi turns app.routes() into an OpenAPI 3.1 document. It is a reader over that list and nothing else. Core hands the routes out as plain data and does not change to allow this: PathPattern’s `{deckId} already is OpenAPI’s path template, so there is nothing to translate and nothing to reflect over.

That is also why it is a module rather than a method on App. A spec format is not the web tier, and a version this framework does not own should not be pinned inside the one artifact every application depends on. That is the same reasoning that keeps the WebTest harness in spider-silk-test.

Installation

dependencies {
    implementation 'net.benelog.spidersilk:spider-silk-openapi:1.1.0'
}

It brings nothing along: the module is one class over core’s route list and core’s Json.

Serving the document

app.get("/openapi.json", req -> {
    Json.JsonValue resBody = OpenApi.document("Flashcard API", "1.0.0", app.routes());
    return WebResponse.json(resBody);
});

The title and version are arguments because OpenAPI requires both under info, and a default for either would be this module naming someone’s API. document returns a Json.JsonValue, so it composes with everything else in the JSON API. Writing it to a file at build time is the same call.

The handler reads app.routes() per request, so it lists whatever was registered, including routes added after this line.

Which routes go in

The whole list, unless you narrow it. An application that serves HTML alongside its API passes the API’s routes and not everything:

List<Route> documented = app.routes().stream()
        .filter(route -> route.path().startsWith("/api"))
        .toList();

That selection stays the application’s, which is the reason routes() hands back a list in the first place. This module has no way to tell an HTML page from an endpoint, and a guess dressed up as a default would be wrong in some application every time.

A route whose pattern contains a bare * throws IllegalArgumentException. That wildcard has no OpenAPI path template and no name to put in one, and dropping it quietly would publish a document claiming the application answers less than it does. It is refused instead, and leaving it out is a filter you write, visibly, next to the one above.

A named tail is written out rather than refused. /files/\{path*} becomes the path /files/{path}, and its parameter carries the description The rest of the path, slashes included. OpenAPI has no wildcard in a path template, so the star is core’s syntax and the variable is what crosses over. That is the only translation this module does.

What the document contains

Every route becomes an operation under its path, keyed by the lowercased method, in registration order. Every {name} in a path becomes a required path parameter of type string, which OpenAPI insists on declaring. Each operation carries a 200 response described as OK.

A route registered with a description carries it as the operation’s summary, which is the field a spec UI shows beside the route:

app.get("/api/decks", "List every deck", api::listDecks);
{"paths": {"/api/decks": {"get": {"summary": "List every deck", "responses": {"200": {"description": "OK"}}}}}}

A route registered without one gets no summary at all, rather than an empty string that would render as a blank line.

That is the whole of it, and it is a valid document. What is missing is missing on purpose: a request or response schema, a server list, and a security scheme. None of those is derivable from a route, so each would have to be declared somewhere, and where to declare it is a question this module answers by not asking it. The automatic HEAD and OPTIONS answers are not listed either, for the reason routes() does not list them.

Testing it

document is a pure function of a list, so a test needs no server and no app:

Json.JsonObject paths = OpenApi.document("Flashcard API", "1.0.0",
        List.of(new Route("GET", "/api/decks/{deckId}/cards"))).asObject().getObject("paths");

assertThat(paths.getObject("/api/decks/{deckId}/cards").getObject("get").getArray("parameters"))
        .hasSize(1);

Route is a record, so a fixture is a constructor call. The example app’s document is reachable at /openapi.json. See the example for how it is scoped for CORS, since a spec document is the one thing there meant to be read by something that is not the application.