implement OpenAPI contributor integration for rate limiting and response headers
This commit is contained in:
+99
-29
@@ -51,7 +51,7 @@ public final class OpenApiBuilder {
|
||||
private final Map<String, Map<String, Object>> paths = new LinkedHashMap<>();
|
||||
private final Map<String, Map<String, Class<?>>> operationHandlers = new LinkedHashMap<>();
|
||||
private final SchemaRegistry schemas = new SchemaRegistry();
|
||||
private OpenApiSecurityRegistry securityRegistry;
|
||||
private OpenApiContributorRegistry contributorRegistry;
|
||||
private int revision;
|
||||
private int builtRevision = -1;
|
||||
private Map<String, Object> cachedSpec;
|
||||
@@ -59,7 +59,7 @@ public final class OpenApiBuilder {
|
||||
public OpenApiBuilder title(String title) { this.title = title; return this; }
|
||||
public OpenApiBuilder version(String version) { this.version = version; return this; }
|
||||
public OpenApiBuilder description(String description) { this.description = description; return this; }
|
||||
void setSecurityRegistry(OpenApiSecurityRegistry registry) { this.securityRegistry = registry; }
|
||||
void setContributorRegistry(OpenApiContributorRegistry registry) { this.contributorRegistry = registry; }
|
||||
|
||||
public void addOperation(Route route, ApiOperation op, Class<?> handlerClass) {
|
||||
String path = normalizePath(route.path());
|
||||
@@ -90,8 +90,8 @@ public final class OpenApiBuilder {
|
||||
info.put("version", version);
|
||||
if (!description.isEmpty()) info.put("description", description);
|
||||
|
||||
List<OpenApiSecurityContributor> contributors = securityRegistry != null
|
||||
? securityRegistry.contributors() : List.of();
|
||||
List<OpenApiContributor> contributors = contributorRegistry != null
|
||||
? contributorRegistry.contributors() : List.of();
|
||||
|
||||
Map<String, Object> renderedPaths = new LinkedHashMap<>();
|
||||
for (var pathEntry : paths.entrySet()) {
|
||||
@@ -103,8 +103,7 @@ public final class OpenApiBuilder {
|
||||
Map<String, Object> op = new LinkedHashMap<>(original);
|
||||
Class<?> handler = handlers.get(methodEntry.getKey());
|
||||
if (handler != null && !contributors.isEmpty()) {
|
||||
List<Map<String, List<String>>> security = buildOperationSecurity(contributors, handler);
|
||||
if (!security.isEmpty()) op.put("security", security);
|
||||
applyContributorOperation(op, handler, contributors);
|
||||
}
|
||||
renderedPathItem.put(methodEntry.getKey(), op);
|
||||
}
|
||||
@@ -119,11 +118,7 @@ public final class OpenApiBuilder {
|
||||
Map<String, Object> components = new LinkedHashMap<>();
|
||||
Map<String, Object> renderedSchemas = schemas.render();
|
||||
if (!renderedSchemas.isEmpty()) components.put("schemas", renderedSchemas);
|
||||
if (!contributors.isEmpty()) {
|
||||
Map<String, Object> securitySchemes = new LinkedHashMap<>();
|
||||
for (OpenApiSecurityContributor c : contributors) securitySchemes.put(c.schemeName(), c.schemeDefinition());
|
||||
components.put("securitySchemes", securitySchemes);
|
||||
}
|
||||
if (!contributors.isEmpty()) applyContributorComponents(components, contributors);
|
||||
if (!components.isEmpty()) spec.put("components", components);
|
||||
|
||||
cachedSpec = spec;
|
||||
@@ -181,16 +176,12 @@ public final class OpenApiBuilder {
|
||||
responseByCode.put(code, buildAnnotatedResponse(code, ann, cls));
|
||||
}
|
||||
|
||||
for (OpenApiSecurityContributor c : securityContributors()) {
|
||||
for (var auto : c.autoResponsesFor(cls).entrySet()) {
|
||||
responseByCode.putIfAbsent(auto.getKey(), Map.of("description", auto.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
if (responseByCode.isEmpty()) {
|
||||
responseByCode.put(200, Map.of("description", "OK"));
|
||||
}
|
||||
|
||||
applyContributorResponses(responseByCode, cls);
|
||||
|
||||
Map<String, Object> responses = new LinkedHashMap<>();
|
||||
responseByCode.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey(Comparator.naturalOrder()))
|
||||
@@ -198,8 +189,97 @@ public final class OpenApiBuilder {
|
||||
op.put("responses", responses);
|
||||
}
|
||||
|
||||
private List<OpenApiSecurityContributor> securityContributors() {
|
||||
return securityRegistry != null ? securityRegistry.contributors() : List.of();
|
||||
private List<OpenApiContributor> contributors() {
|
||||
return contributorRegistry != null ? contributorRegistry.contributors() : List.of();
|
||||
}
|
||||
|
||||
private void applyContributorResponses(Map<Integer, Map<String, Object>> responseByCode, Class<?> handlerClass) {
|
||||
APIResponse[] manual = handlerClass.getAnnotationsByType(APIResponse.class);
|
||||
Set<Integer> manualStatusCodes = new HashSet<>();
|
||||
for (APIResponse ann : manual) {
|
||||
manualStatusCodes.add(parseStatus(ann.responseCode()));
|
||||
}
|
||||
|
||||
for (OpenApiContributor contributor : contributors()) {
|
||||
OpenApiOperationContribution contribution = contributor.operationFor(handlerClass);
|
||||
if (contribution == null) continue;
|
||||
|
||||
for (Map.Entry<Integer, OpenApiResponseContribution> entry : contribution.responses().entrySet()) {
|
||||
int status = entry.getKey();
|
||||
OpenApiResponseContribution responseContribution = entry.getValue();
|
||||
if (responseContribution == null) continue;
|
||||
|
||||
Map<String, Object> response = responseByCode.computeIfAbsent(status, __ -> new LinkedHashMap<>());
|
||||
mergeContributorResponse(response, responseContribution, status, manualStatusCodes);
|
||||
}
|
||||
|
||||
OpenApiResponseContribution allResponses = contribution.allResponses();
|
||||
if (allResponses != null) {
|
||||
for (Map.Entry<Integer, Map<String, Object>> entry : responseByCode.entrySet()) {
|
||||
mergeContributorResponse(entry.getValue(), allResponses, entry.getKey(), manualStatusCodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void mergeContributorResponse(Map<String, Object> response,
|
||||
OpenApiResponseContribution contribution,
|
||||
int status,
|
||||
Set<Integer> manualStatusCodes) {
|
||||
String desc = contribution.description();
|
||||
if (!manualStatusCodes.contains(status) && desc != null && !desc.isBlank()) {
|
||||
response.put("description", desc);
|
||||
}
|
||||
|
||||
Map<String, Map<String, Object>> headerContributions = contribution.headers();
|
||||
if (!headerContributions.isEmpty()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> headers = (Map<String, Object>) response.computeIfAbsent("headers", __ -> new LinkedHashMap<>());
|
||||
for (Map.Entry<String, Map<String, Object>> h : headerContributions.entrySet()) {
|
||||
headers.put(h.getKey(), new LinkedHashMap<>(h.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.containsKey("description")) {
|
||||
response.put("description", defaultDescription(status));
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyContributorComponents(Map<String, Object> components, List<OpenApiContributor> contributors) {
|
||||
for (OpenApiContributor contributor : contributors) {
|
||||
Map<String, Object> c = contributor.componentContributions();
|
||||
if (c == null || c.isEmpty()) continue;
|
||||
deepMergeLastWins(components, c);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void deepMergeLastWins(Map<String, Object> target, Map<String, Object> incoming) {
|
||||
for (Map.Entry<String, Object> e : incoming.entrySet()) {
|
||||
Object existing = target.get(e.getKey());
|
||||
Object value = e.getValue();
|
||||
if (existing instanceof Map<?, ?> em && value instanceof Map<?, ?> vm) {
|
||||
Map<String, Object> merged = new LinkedHashMap<>((Map<String, Object>) em);
|
||||
deepMergeLastWins(merged, (Map<String, Object>) vm);
|
||||
target.put(e.getKey(), merged);
|
||||
} else {
|
||||
target.put(e.getKey(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyContributorOperation(Map<String, Object> op, Class<?> handlerClass, List<OpenApiContributor> contributors) {
|
||||
for (OpenApiContributor contributor : contributors) {
|
||||
OpenApiOperationContribution contribution = contributor.operationFor(handlerClass);
|
||||
if (contribution == null || contribution.isEmpty()) continue;
|
||||
|
||||
if (!contribution.security().isEmpty()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, List<String>>> security = (List<Map<String, List<String>>>) op
|
||||
.computeIfAbsent("security", __ -> new ArrayList<>());
|
||||
security.addAll(contribution.security());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> buildAnnotatedResponse(int code, APIResponse ann, Class<?> handlerClass) {
|
||||
@@ -272,16 +352,6 @@ public final class OpenApiBuilder {
|
||||
return reason == null ? "" : reason;
|
||||
}
|
||||
|
||||
private static List<Map<String, List<String>>> buildOperationSecurity(List<OpenApiSecurityContributor> contributors,
|
||||
Class<?> handlerClass) {
|
||||
List<Map<String, List<String>>> security = new ArrayList<>();
|
||||
for (OpenApiSecurityContributor c : contributors) {
|
||||
List<String> scopes = c.requiredFor(handlerClass);
|
||||
if (scopes != null) security.add(Map.of(c.schemeName(), scopes));
|
||||
}
|
||||
return security;
|
||||
}
|
||||
|
||||
private static Class<?> rawType(Type type) {
|
||||
if (type instanceof Class<?> c) return c;
|
||||
if (type instanceof ParameterizedType p && p.getRawType() instanceof Class<?> c) return c;
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface OpenApiContributor {
|
||||
|
||||
default Map<String, Object> componentContributions() {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
default OpenApiOperationContribution operationFor(Class<?> handlerClass) {
|
||||
return OpenApiOperationContribution.empty();
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public final class OpenApiContributorRegistry {
|
||||
private final List<OpenApiContributor> contributors = new CopyOnWriteArrayList<>();
|
||||
|
||||
public void add(OpenApiContributor contributor) {
|
||||
contributors.add(contributor);
|
||||
}
|
||||
|
||||
public List<OpenApiContributor> contributors() {
|
||||
return Collections.unmodifiableList(contributors);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -65,12 +65,12 @@ public class OpenApiExtension implements FlashExtension {
|
||||
|
||||
@Override
|
||||
public void provide(FlashContext ctx) {
|
||||
OpenApiBuilder builder = new OpenApiBuilder().title(title).version(version).description(description);
|
||||
OpenApiSecurityRegistry secRegistry = new OpenApiSecurityRegistry();
|
||||
OpenApiBuilder builder = new OpenApiBuilder().title(title).version(version).description(description);
|
||||
OpenApiContributorRegistry registry = new OpenApiContributorRegistry();
|
||||
|
||||
ctx.provide(OpenApiSecurityRegistry.class, secRegistry);
|
||||
ctx.provide(OpenApiBuilder.class, builder);
|
||||
builder.setSecurityRegistry(secRegistry);
|
||||
ctx.provide(OpenApiContributorRegistry.class, registry);
|
||||
ctx.provide(OpenApiBuilder.class, builder);
|
||||
builder.setContributorRegistry(registry);
|
||||
|
||||
// Collect operation metadata from final compiled routes.
|
||||
// This guarantees full runtime paths (namespaces/prefixes/rewrites) in the spec.
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class OpenApiOperationContribution {
|
||||
private static final OpenApiOperationContribution EMPTY = new OpenApiOperationContribution(List.of(), Map.of(), null);
|
||||
|
||||
private final List<Map<String, List<String>>> security;
|
||||
private final Map<Integer, OpenApiResponseContribution> responses;
|
||||
private final OpenApiResponseContribution allResponses;
|
||||
|
||||
private OpenApiOperationContribution(List<Map<String, List<String>>> security,
|
||||
Map<Integer, OpenApiResponseContribution> responses,
|
||||
OpenApiResponseContribution allResponses) {
|
||||
this.security = List.copyOf(security);
|
||||
this.responses = Map.copyOf(responses);
|
||||
this.allResponses = allResponses;
|
||||
}
|
||||
|
||||
public List<Map<String, List<String>>> security() {
|
||||
return security;
|
||||
}
|
||||
|
||||
public Map<Integer, OpenApiResponseContribution> responses() {
|
||||
return responses;
|
||||
}
|
||||
|
||||
public OpenApiResponseContribution allResponses() {
|
||||
return allResponses;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return security.isEmpty() && responses.isEmpty() && allResponses == null;
|
||||
}
|
||||
|
||||
public static OpenApiOperationContribution empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final List<Map<String, List<String>>> security = new ArrayList<>();
|
||||
private final Map<Integer, OpenApiResponseContribution> responses = new LinkedHashMap<>();
|
||||
private OpenApiResponseContribution allResponses;
|
||||
|
||||
public Builder security(String schemeName, List<String> scopes) {
|
||||
security.add(Map.of(schemeName, List.copyOf(scopes)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder response(int statusCode, OpenApiResponseContribution response) {
|
||||
responses.put(statusCode, response);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder allResponses(OpenApiResponseContribution response) {
|
||||
this.allResponses = response;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OpenApiOperationContribution build() {
|
||||
return new OpenApiOperationContribution(security, responses, allResponses);
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class OpenApiResponseContribution {
|
||||
private final String description;
|
||||
private final Map<String, Map<String, Object>> headers;
|
||||
|
||||
private OpenApiResponseContribution(String description, Map<String, Map<String, Object>> headers) {
|
||||
this.description = description;
|
||||
this.headers = Map.copyOf(headers);
|
||||
}
|
||||
|
||||
public String description() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public Map<String, Map<String, Object>> headers() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static OpenApiResponseContribution of(String description) {
|
||||
return builder().description(description).build();
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private String description;
|
||||
private final Map<String, Map<String, Object>> headers = new LinkedHashMap<>();
|
||||
|
||||
public Builder description(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder header(String name, Map<String, Object> headerObject) {
|
||||
headers.put(name, Map.copyOf(headerObject));
|
||||
return this;
|
||||
}
|
||||
|
||||
public OpenApiResponseContribution build() {
|
||||
return new OpenApiResponseContribution(description, headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Pluggable security scheme contributor for the OpenAPI spec.
|
||||
*
|
||||
* <p>Extensions that enforce authentication (e.g. {@code flash-ext-oidc}) implement
|
||||
* this interface and register an instance into {@link OpenApiSecurityRegistry} via the
|
||||
* {@link dev.relism.extension.FlashContext}. {@link OpenApiExtension} picks it up
|
||||
* at spec-generation time — no coupling between the two extensions at install time.
|
||||
*
|
||||
* <p>Multi-tenant: multiple contributors may coexist. For handlers secured by
|
||||
* {@code @Authenticated}/{@code @RolesAllowed}, each matching contributor adds its
|
||||
* own entry to the operation's {@code security} array (OpenAPI OR semantics).
|
||||
*/
|
||||
public interface OpenApiSecurityContributor {
|
||||
|
||||
/**
|
||||
* Unique scheme name used as a key in {@code components.securitySchemes}
|
||||
* and referenced from each operation's {@code security} array.
|
||||
*/
|
||||
String schemeName();
|
||||
|
||||
/**
|
||||
* The OpenAPI security scheme definition object placed under
|
||||
* {@code components.securitySchemes.<schemeName>}.
|
||||
*
|
||||
* <p>Example for OIDC:
|
||||
* <pre>{@code
|
||||
* Map.of("type", "openIdConnect",
|
||||
* "openIdConnectUrl", "https://idp.example.com/.well-known/openid-configuration")
|
||||
* }</pre>
|
||||
*/
|
||||
Map<String, Object> schemeDefinition();
|
||||
|
||||
/**
|
||||
* Returns the scopes/roles required for the given handler class under this scheme,
|
||||
* or {@code null} if this contributor does not secure the handler.
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code null} — handler is not secured by this contributor (skip)</li>
|
||||
* <li>empty list — handler requires authentication, no specific scopes</li>
|
||||
* <li>non-empty list — handler requires these scopes/roles</li>
|
||||
* </ul>
|
||||
*/
|
||||
List<String> requiredFor(Class<?> handlerClass);
|
||||
|
||||
/**
|
||||
* Optional auto-injected operation responses for handlers secured by this contributor.
|
||||
* Key = HTTP status code, value = description.
|
||||
*/
|
||||
default Map<Integer, String> autoResponsesFor(Class<?> handlerClass) {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Mutable registry of {@link OpenApiSecurityContributor}s.
|
||||
*
|
||||
* <p>Created and provided to the {@link dev.relism.extension.FlashContext} by
|
||||
* {@link OpenApiExtension} at install time. Other extensions (e.g. {@code flash-ext-oidc})
|
||||
* retrieve it via {@code ctx.find(OpenApiSecurityRegistry.class)} and register their
|
||||
* contributor — the OpenAPI extension then picks it up lazily at spec-generation time.
|
||||
*
|
||||
* <p>Thread-safe: {@link CopyOnWriteArrayList} allows concurrent reads during spec
|
||||
* generation without blocking registration.
|
||||
*/
|
||||
public final class OpenApiSecurityRegistry {
|
||||
|
||||
private final List<OpenApiSecurityContributor> contributors = new CopyOnWriteArrayList<>();
|
||||
|
||||
/** Registers a contributor. Safe to call concurrently. */
|
||||
public void add(OpenApiSecurityContributor contributor) {
|
||||
contributors.add(contributor);
|
||||
}
|
||||
|
||||
/** Returns an unmodifiable snapshot of all registered contributors. */
|
||||
public List<OpenApiSecurityContributor> contributors() {
|
||||
return Collections.unmodifiableList(contributors);
|
||||
}
|
||||
}
|
||||
+96
-8
@@ -88,6 +88,16 @@ class OpenApiBuilderTest {
|
||||
}
|
||||
}
|
||||
|
||||
@GET("/merge")
|
||||
@ApiOperation(summary = "Merge")
|
||||
@APIResponse(responseCode = "200", description = "Manual 200")
|
||||
static class MergeHandler extends dev.relism.models.RequestHandler {
|
||||
@Override
|
||||
public Object handle(dev.relism.models.Request request, dev.relism.models.Response response) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "UserDTO", title = "User model", description = "DTO", deprecated = true)
|
||||
@JsonIgnoreProperties({"ignoredByType"})
|
||||
static class UserDto {
|
||||
@@ -177,16 +187,23 @@ class OpenApiBuilderTest {
|
||||
@Test
|
||||
void auto_security_responses_are_added_and_manual_wins_sorted() {
|
||||
OpenApiBuilder b = new OpenApiBuilder();
|
||||
OpenApiSecurityRegistry registry = new OpenApiSecurityRegistry();
|
||||
registry.add(new OpenApiSecurityContributor() {
|
||||
@Override public String schemeName() { return "oidc"; }
|
||||
@Override public Map<String, Object> schemeDefinition() { return Map.of("type", "oauth2"); }
|
||||
@Override public List<String> requiredFor(Class<?> handlerClass) { return List.of(); }
|
||||
@Override public Map<Integer, String> autoResponsesFor(Class<?> handlerClass) {
|
||||
return Map.of(401, "Authentication required", 403, "Auto forbidden");
|
||||
OpenApiContributorRegistry registry = new OpenApiContributorRegistry();
|
||||
registry.add(new OpenApiContributor() {
|
||||
@Override
|
||||
public Map<String, Object> componentContributions() {
|
||||
return Map.of("securitySchemes", Map.of("oidc", Map.of("type", "oauth2")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenApiOperationContribution operationFor(Class<?> handlerClass) {
|
||||
return OpenApiOperationContribution.builder()
|
||||
.security("oidc", List.of())
|
||||
.response(401, OpenApiResponseContribution.of("Authentication required"))
|
||||
.response(403, OpenApiResponseContribution.of("Auto forbidden"))
|
||||
.build();
|
||||
}
|
||||
});
|
||||
b.setSecurityRegistry(registry);
|
||||
b.setContributorRegistry(registry);
|
||||
b.addOperation(OpenApiBuilder.routeOf(SecureHandler.class), SecureHandler.class.getAnnotation(ApiOperation.class), SecureHandler.class);
|
||||
|
||||
Map<String, Object> spec = b.build();
|
||||
@@ -250,6 +267,77 @@ class OpenApiBuilderTest {
|
||||
assertEquals("#/components/schemas/UserDTO", additionalProperties.get("$ref"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void contributor_response_merge_appliesAllResponses_andManualDescriptionWins() {
|
||||
OpenApiBuilder b = new OpenApiBuilder();
|
||||
OpenApiContributorRegistry registry = new OpenApiContributorRegistry();
|
||||
registry.add(new OpenApiContributor() {
|
||||
@Override
|
||||
public OpenApiOperationContribution operationFor(Class<?> handlerClass) {
|
||||
return OpenApiOperationContribution.builder()
|
||||
.allResponses(OpenApiResponseContribution.builder()
|
||||
.header("X-Trace", Map.of("schema", Map.of("type", "string")))
|
||||
.build())
|
||||
.response(200, OpenApiResponseContribution.of("Auto 200"))
|
||||
.response(429, OpenApiResponseContribution.of("Auto 429"))
|
||||
.build();
|
||||
}
|
||||
});
|
||||
b.setContributorRegistry(registry);
|
||||
b.addOperation(OpenApiBuilder.routeOf(MergeHandler.class), MergeHandler.class.getAnnotation(ApiOperation.class), MergeHandler.class);
|
||||
|
||||
Map<String, Object> spec = b.build();
|
||||
Map<String, Object> get = getOperation(spec, "/merge", "get");
|
||||
Map<String, Object> responses = cast(get.get("responses"));
|
||||
|
||||
Map<String, Object> resp200 = cast(responses.get("200"));
|
||||
assertEquals("Manual 200", resp200.get("description"));
|
||||
Map<String, Object> headers200 = cast(resp200.get("headers"));
|
||||
assertTrue(headers200.containsKey("X-Trace"));
|
||||
|
||||
Map<String, Object> resp429 = cast(responses.get("429"));
|
||||
assertEquals("Auto 429", resp429.get("description"));
|
||||
Map<String, Object> headers429 = cast(resp429.get("headers"));
|
||||
assertTrue(headers429.containsKey("X-Trace"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void contributor_lastWins_forResponseHeaderCollisions() {
|
||||
OpenApiBuilder b = new OpenApiBuilder();
|
||||
OpenApiContributorRegistry registry = new OpenApiContributorRegistry();
|
||||
registry.add(new OpenApiContributor() {
|
||||
@Override
|
||||
public OpenApiOperationContribution operationFor(Class<?> handlerClass) {
|
||||
return OpenApiOperationContribution.builder()
|
||||
.response(200, OpenApiResponseContribution.builder()
|
||||
.header("X-RateLimit-Limit", Map.of("description", "old", "schema", Map.of("type", "integer")))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
});
|
||||
registry.add(new OpenApiContributor() {
|
||||
@Override
|
||||
public OpenApiOperationContribution operationFor(Class<?> handlerClass) {
|
||||
return OpenApiOperationContribution.builder()
|
||||
.response(200, OpenApiResponseContribution.builder()
|
||||
.header("X-RateLimit-Limit", Map.of("description", "new", "schema", Map.of("type", "integer")))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
});
|
||||
|
||||
b.setContributorRegistry(registry);
|
||||
b.addOperation(OpenApiBuilder.routeOf(MergeHandler.class), MergeHandler.class.getAnnotation(ApiOperation.class), MergeHandler.class);
|
||||
|
||||
Map<String, Object> spec = b.build();
|
||||
Map<String, Object> get = getOperation(spec, "/merge", "get");
|
||||
Map<String, Object> responses = cast(get.get("responses"));
|
||||
Map<String, Object> resp200 = cast(responses.get("200"));
|
||||
Map<String, Object> headers = cast(resp200.get("headers"));
|
||||
Map<String, Object> header = cast(headers.get("X-RateLimit-Limit"));
|
||||
assertEquals("new", header.get("description"));
|
||||
}
|
||||
|
||||
private static Map<String, Object> getOperation(Map<String, Object> spec, String path, String method) {
|
||||
Map<String, Object> paths = cast(spec.get("paths"));
|
||||
Map<String, Object> pathItem = cast(paths.get(path));
|
||||
|
||||
Reference in New Issue
Block a user