Every class-based route is documented now, annotated or not: a route without @ApiOperation used to be dropped with a warning, which made the document lie by omission. Read off the handler: the request body from the type it declares (or from the new @RequestBody, for one that reads the body itself), the success schema from the most specific handle it implements, and the media type from @Consumes. Failures are described too. Any 4xx or 5xx without an explicit schema documents the error object Flash actually answers with, written once under components.schemas.Error — before this, a declared 4xx inherited the success schema, which was simply wrong. And an answer two or more operations give identically is hoisted into components.responses and referenced, so the 401 of every guarded route appears once rather than on every path. @Content gained an example, and the schema registry moved into its own class. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
141 lines
5.2 KiB
Markdown
141 lines
5.2 KiB
Markdown
# flash-ext-openapi
|
|
|
|
OpenAPI 3.0.3 generation and Swagger UI, built from what the handlers already say about
|
|
themselves.
|
|
|
|
## What it provides
|
|
|
|
| Route | Description |
|
|
|---|---|
|
|
| `GET /openapi.json` | OpenAPI spec JSON |
|
|
| `GET /openapi.yaml` | OpenAPI spec YAML |
|
|
| `GET /openapi/swagger` | Swagger UI |
|
|
|
|
```java
|
|
FlashApp.create(8080)
|
|
.install(new JacksonExtension())
|
|
.install(new OpenApiExtension("/openapi", "My API", "1.0.0"))
|
|
.scan("com.acme.handlers")
|
|
.startAndBlock();
|
|
```
|
|
|
|
## What you get without writing anything
|
|
|
|
Every class-based route is documented, annotated or not. Read off the code:
|
|
|
|
- **path parameters**, from `/{id}` in the route
|
|
- **the request body**, from the handler's own body type (see below)
|
|
- **the response schema**, from what `handle` returns — an object, a `List<T>`, a `Map<String, T>`
|
|
- **error responses**, in the one shape Flash answers failures with: `{"error": "...", "status": 404}`
|
|
- **security and rate limiting**, from the extensions that enforce them
|
|
|
|
Annotations add what the code cannot say: prose, extra statuses, examples. They never repeat it.
|
|
|
|
## Request bodies
|
|
|
|
A handler that extends `BodyHandler` — `JsonHandler` and `XmlHandler`, and anything else that
|
|
reads a format — declares its body type in its signature, and that is the whole documentation:
|
|
|
|
```java
|
|
@POST("/users")
|
|
public final class CreateUser extends JsonHandler<NewUser> {
|
|
@Override protected Object handle(Request req, Response res, NewUser body) {
|
|
return users.create(body);
|
|
}
|
|
}
|
|
```
|
|
|
|
```yaml
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/NewUser' }
|
|
```
|
|
|
|
The media type comes from `@Consumes` on the base class, so an XML handler documents itself as
|
|
XML without a word from the route.
|
|
|
|
For a handler that reads the body by hand, or to describe it as something else, declare it:
|
|
|
|
```java
|
|
@PUT("/users")
|
|
@RequestBody(value = User.class, array = true, description = "Users to store")
|
|
public final class ReplaceUsers extends RequestHandler { ... }
|
|
```
|
|
|
|
## Operations
|
|
|
|
```java
|
|
@GET("/users/{id}")
|
|
@ApiOperation(summary = "Get user", description = "Returns one user", tags = {"users"})
|
|
@Parameter(name = "expand", in = ParameterIn.QUERY, type = SchemaType.STRING, examples = {"roles", "permissions"})
|
|
public final class GetUser extends RequestHandler { ... }
|
|
```
|
|
|
|
`@ApiOperation` is optional: without it the route is still in the document, with no summary.
|
|
|
|
## Responses
|
|
|
|
The success response is inferred. Declare one only to say more:
|
|
|
|
```java
|
|
@APIResponse(responseCode = "200", description = "User found",
|
|
content = @Content(schema = UserDto.class, example = "{\"id\":\"usr-1\"}"))
|
|
@APIResponse(responseCode = "409", description = "That email is taken")
|
|
@APIResponse(responseCode = "204", content = @Content(contentType = ContentType.NONE))
|
|
```
|
|
|
|
- `content.schema` omitted on a 2xx: the handler's return type.
|
|
- Any 4xx or 5xx without an explicit schema: Flash's error object, referenced from `components`.
|
|
- `content.array = true` wraps whichever schema was chosen.
|
|
- `contentType = NONE` documents a response with no body.
|
|
|
|
**A response several operations share is written once.** Identical answers — the 401 of every
|
|
guarded route, the 429 of every limited one — become `components.responses` entries referenced by
|
|
`$ref`, instead of being repeated on every path.
|
|
|
|
## DTO schemas
|
|
|
|
```java
|
|
@Schema(name = "User", title = "User DTO", description = "Public user")
|
|
public record UserDto(
|
|
@SchemaProperty(title = "ID", example = "USR-100") String id,
|
|
@SchemaProperty(hidden = true) String internalDebug) {}
|
|
```
|
|
|
|
Each type is described once under `components.schemas` and referenced everywhere it appears.
|
|
Field-level exclusion: `@Schema(hidden = true)`, `@SchemaProperty(hidden = true)`, `@JsonIgnore`,
|
|
`@JsonIgnoreProperties`, `transient`, `static`. `jakarta.validation` constraints (`@NotNull`,
|
|
`@NotBlank`, `@NotEmpty`, `@Size`, `@Min`, `@Max`, `@Email`, `@Pattern`) become the schema's own
|
|
bounds and required fields, so a rule is written once and documented for free.
|
|
|
|
## Contributor API
|
|
|
|
OpenAPI is extension-agnostic. Other extensions contribute through `OpenApiContributor`, held in
|
|
`OpenApiContributorRegistry`:
|
|
|
|
- `components` fragments (merged last-wins)
|
|
- operation `security` requirements (additive)
|
|
- operation `responses` and response `headers` (additive)
|
|
|
|
Manual `@APIResponse` description always wins over a contributor's for the same status.
|
|
|
|
### Security interop
|
|
|
|
With `flash-ext-security-core` installed, every registered mechanism's scheme lands under
|
|
`components.securitySchemes`, and every operation carrying a security annotation lists them as
|
|
`security` alternatives with automatic `401` and — for roles or scopes — `403` responses.
|
|
|
|
### Limiter interop
|
|
|
|
When `flash-ext-limiter` is installed, handlers with `@Limit` document `X-RateLimit-Limit`,
|
|
`X-RateLimit-Remaining`, `X-RateLimit-Reset`, and a `429` with `Retry-After`.
|
|
|
|
## Notes
|
|
|
|
- Operations come from the final boot-time routes, so documented paths match runtime paths,
|
|
namespaces, prefixes and rewrites included.
|
|
- Lambda routes are not documented: there is no class to read.
|
|
- Responses are sorted by status code; the document is rebuilt only when a route is added.
|