headerObject) {
+ headers.put(name, Map.copyOf(headerObject));
+ return this;
+ }
+
+ public OpenApiResponseContribution build() {
+ return new OpenApiResponseContribution(description, headers);
+ }
+ }
+}
diff --git a/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiSecurityContributor.java b/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiSecurityContributor.java
deleted file mode 100644
index c33aae5..0000000
--- a/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiSecurityContributor.java
+++ /dev/null
@@ -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.
- *
- * 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.
- *
- *
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.}.
- *
- * Example for OIDC:
- *
{@code
- * Map.of("type", "openIdConnect",
- * "openIdConnectUrl", "https://idp.example.com/.well-known/openid-configuration")
- * }
- */
- Map 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.
- *
- *
- * - {@code null} — handler is not secured by this contributor (skip)
- * - empty list — handler requires authentication, no specific scopes
- * - non-empty list — handler requires these scopes/roles
- *
- */
- List requiredFor(Class> handlerClass);
-
- /**
- * Optional auto-injected operation responses for handlers secured by this contributor.
- * Key = HTTP status code, value = description.
- */
- default Map autoResponsesFor(Class> handlerClass) {
- return Map.of();
- }
-}
diff --git a/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiSecurityRegistry.java b/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiSecurityRegistry.java
deleted file mode 100644
index 0ed8f0e..0000000
--- a/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiSecurityRegistry.java
+++ /dev/null
@@ -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.
- *
- * 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.
- *
- *
Thread-safe: {@link CopyOnWriteArrayList} allows concurrent reads during spec
- * generation without blocking registration.
- */
-public final class OpenApiSecurityRegistry {
-
- private final List 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 contributors() {
- return Collections.unmodifiableList(contributors);
- }
-}
diff --git a/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/ext/openapi/OpenApiBuilderTest.java b/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/ext/openapi/OpenApiBuilderTest.java
index 626bac3..b10fae0 100644
--- a/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/ext/openapi/OpenApiBuilderTest.java
+++ b/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/ext/openapi/OpenApiBuilderTest.java
@@ -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 schemeDefinition() { return Map.of("type", "oauth2"); }
- @Override public List requiredFor(Class> handlerClass) { return List.of(); }
- @Override public Map autoResponsesFor(Class> handlerClass) {
- return Map.of(401, "Authentication required", 403, "Auto forbidden");
+ OpenApiContributorRegistry registry = new OpenApiContributorRegistry();
+ registry.add(new OpenApiContributor() {
+ @Override
+ public Map 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 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 spec = b.build();
+ Map get = getOperation(spec, "/merge", "get");
+ Map responses = cast(get.get("responses"));
+
+ Map resp200 = cast(responses.get("200"));
+ assertEquals("Manual 200", resp200.get("description"));
+ Map headers200 = cast(resp200.get("headers"));
+ assertTrue(headers200.containsKey("X-Trace"));
+
+ Map resp429 = cast(responses.get("429"));
+ assertEquals("Auto 429", resp429.get("description"));
+ Map 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 spec = b.build();
+ Map get = getOperation(spec, "/merge", "get");
+ Map responses = cast(get.get("responses"));
+ Map resp200 = cast(responses.get("200"));
+ Map headers = cast(resp200.get("headers"));
+ Map header = cast(headers.get("X-RateLimit-Limit"));
+ assertEquals("new", header.get("description"));
+ }
+
private static Map getOperation(Map spec, String path, String method) {
Map paths = cast(spec.get("paths"));
Map pathItem = cast(paths.get(path));
diff --git a/flash-extensions/flash-ext-routeviewer/src/main/resources/routeviewer/index.html b/flash-extensions/flash-ext-routeviewer/src/main/resources/routeviewer/index.html
index 720f499..bab0ef8 100644
--- a/flash-extensions/flash-ext-routeviewer/src/main/resources/routeviewer/index.html
+++ b/flash-extensions/flash-ext-routeviewer/src/main/resources/routeviewer/index.html
@@ -1,13 +1,13 @@
-
-
-
-
-
- Flash Route Viewer
+
+
+
+
+
+ Flash Route Viewer
-
-
-
-
-
+
+
+
+
+
diff --git a/flash-extensions/flash-ext-view/README.md b/flash-extensions/flash-ext-view/README.md
index 12b70c3..36d0c1e 100644
--- a/flash-extensions/flash-ext-view/README.md
+++ b/flash-extensions/flash-ext-view/README.md
@@ -52,4 +52,3 @@ public final class HomePage extends ViewHandler {
- `docs/partials.md`
- `docs/adapters.md`
- `docs/performance.md`
-- `docs/migration-from-legacy-view.md`
diff --git a/flash-extensions/flash-ext-view/docs/migration-from-legacy-view.md b/flash-extensions/flash-ext-view/docs/migration-from-legacy-view.md
deleted file mode 100644
index ee1418d..0000000
--- a/flash-extensions/flash-ext-view/docs/migration-from-legacy-view.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Migration from Legacy View API
-
-Legacy API (`@View`, `ViewEngine`, `Renderer`, `Template`) has been removed.
-
-## Replace annotations
-
-- `@View("page")` -> `@Page("page")`
-- `@View(value = "x", fragment = true)` -> `@Partial(template = "x")`
-
-`@Page` and `@Partial` must be declared on classes extending `ViewHandler`.
-
-## Replace handler return contract
-
-Before (legacy):
-
-```java
-public Object handle(Request req, Response res) {
- return Map.of("name", "Flash");
-}
-```
-
-Now:
-
-```java
-public ViewModel render(Request req) {
- return ViewModel.of("name", "Flash");
-}
-```
-
-## Replace engine integration
-
-- Implement `ViewEngineAdapter` directly.
-- Or use `ViewEngineType.THYMELEAF`.
diff --git a/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/WebBundlerExtensionIntegrationTest.java b/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/WebBundlerExtensionIntegrationTest.java
index d9465ce..2dbedf6 100644
--- a/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/WebBundlerExtensionIntegrationTest.java
+++ b/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/WebBundlerExtensionIntegrationTest.java
@@ -49,7 +49,7 @@ class WebBundlerExtensionIntegrationTest {
.basePath("/app")
.build();
- app = FlashApp.create(FlashConfiguration.builder().port(port).host("127.0.0.1").build());
+ app = FlashApp.create(port);
app.install(new WebBundlerExtension(config));
app.get("/api/ping", (req, res) -> "pong");
app.start();