feat(ext-openapi): document what the code already says
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
adcd6376b6
commit
2fbe65fcc5
@@ -1,6 +1,7 @@
|
||||
# flash-ext-openapi
|
||||
|
||||
OpenAPI 3.0.3 generation + Swagger UI for Flash.
|
||||
OpenAPI 3.0.3 generation and Swagger UI, built from what the handlers already say about
|
||||
themselves.
|
||||
|
||||
## What it provides
|
||||
|
||||
@@ -10,8 +11,6 @@ OpenAPI 3.0.3 generation + Swagger UI for Flash.
|
||||
| `GET /openapi.yaml` | OpenAPI spec YAML |
|
||||
| `GET /openapi/swagger` | Swagger UI |
|
||||
|
||||
## Install
|
||||
|
||||
```java
|
||||
FlashApp.create(8080)
|
||||
.install(new JacksonExtension())
|
||||
@@ -20,131 +19,122 @@ FlashApp.create(8080)
|
||||
.startAndBlock();
|
||||
```
|
||||
|
||||
## Operation annotation
|
||||
## 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"})
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
description = "User found",
|
||||
content = @Content(contentType = ContentType.JSON, schema = UserDto.class)
|
||||
)
|
||||
public final class GetUser extends RequestHandler { ... }
|
||||
```
|
||||
|
||||
## Response patterns
|
||||
`@ApiOperation` is optional: without it the route is still in the document, with no summary.
|
||||
|
||||
### Single object
|
||||
## Responses
|
||||
|
||||
The success response is inferred. Declare one only to say more:
|
||||
|
||||
```java
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
description = "User found",
|
||||
content = @Content(contentType = ContentType.JSON, schema = UserDto.class)
|
||||
)
|
||||
@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))
|
||||
```
|
||||
|
||||
### Array
|
||||
- `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
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
description = "Users listed",
|
||||
content = @Content(contentType = ContentType.JSON, schema = UserDto.class, array = true)
|
||||
)
|
||||
@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) {}
|
||||
```
|
||||
|
||||
### No content
|
||||
|
||||
```java
|
||||
@APIResponse(
|
||||
responseCode = "204",
|
||||
description = "Deleted",
|
||||
content = @Content(contentType = ContentType.NONE)
|
||||
)
|
||||
```
|
||||
|
||||
### Inferred from handler return type
|
||||
|
||||
```java
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = @Content
|
||||
)
|
||||
```
|
||||
|
||||
If `content.schema` is omitted, schema is inferred from the handler `handle(...)` return type.
|
||||
Explicit `content.schema` always wins over inference.
|
||||
|
||||
Inference defaults:
|
||||
|
||||
- `UserDto` -> object schema for `UserDto`
|
||||
- `List<UserDto>` / `Set<UserDto>` / `UserDto[]` -> `array` with `items: UserDto`
|
||||
- `Map<String, UserDto>` -> `object` with `additionalProperties: UserDto`
|
||||
|
||||
## DTO schema metadata
|
||||
|
||||
```java
|
||||
@Schema(name = "User", title = "User DTO", description = "Public user", deprecated = false)
|
||||
public class UserDto {
|
||||
|
||||
@SchemaProperty(title = "ID", required = true, example = "USR-100", enumeration = {"USR-100", "USR-101"})
|
||||
public String id;
|
||||
|
||||
@SchemaProperty(hidden = true)
|
||||
public String internalDebug;
|
||||
}
|
||||
```
|
||||
|
||||
Supported field-level exclusion:
|
||||
|
||||
- `@Schema(hidden = true)` / `@SchemaProperty(hidden = true)`
|
||||
- `@JsonIgnore`
|
||||
- `@JsonIgnoreProperties(...)`
|
||||
- `transient` / `static`
|
||||
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 with `OpenApiContributor` via
|
||||
`OpenApiContributorRegistry`.
|
||||
OpenAPI is extension-agnostic. Other extensions contribute through `OpenApiContributor`, held in
|
||||
`OpenApiContributorRegistry`:
|
||||
|
||||
Supported contribution surfaces:
|
||||
|
||||
- `components` fragments (merged with last-wins)
|
||||
- `components` fragments (merged last-wins)
|
||||
- operation `security` requirements (additive)
|
||||
- operation `responses` and response `headers` (additive)
|
||||
|
||||
Merge policy:
|
||||
Manual `@APIResponse` description always wins over a contributor's for the same status.
|
||||
|
||||
- contributor collisions use **last-wins**
|
||||
- manual `@APIResponse` description always wins over contributors for the same status
|
||||
|
||||
## Security interop
|
||||
### 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.
|
||||
|
||||
Manual `@APIResponse` for the same status code always wins.
|
||||
### Limiter interop
|
||||
|
||||
## Limiter interop
|
||||
|
||||
When `flash-ext-limiter` is installed, handlers with `@Limit` automatically get response
|
||||
headers documented in OpenAPI:
|
||||
|
||||
- `X-RateLimit-Limit`
|
||||
- `X-RateLimit-Remaining`
|
||||
- `X-RateLimit-Reset`
|
||||
- `Retry-After` on `429`
|
||||
|
||||
If `429` is missing, it is auto-added as `Too Many Requests`.
|
||||
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 are collected from final boot-time routes for class-based handlers with `@ApiOperation`.
|
||||
- Documented paths always match runtime paths (including scope namespaces/prefixes/rewrites).
|
||||
- Route path params are auto-discovered from `/{id}`.
|
||||
- Parameter annotations are mainly for query/header/cookie enrichment.
|
||||
- Output responses are sorted by numeric status code.
|
||||
- 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.
|
||||
|
||||
Reference in New Issue
Block a user