From 9090ba59f5b86c45d8488042404f1f0ba93ab4d8 Mon Sep 17 00:00:00 2001 From: Zakaria El Orche Date: Wed, 23 Sep 2026 15:47:35 +0000 Subject: [PATCH] feat(ext-openapi): a route can say it is not part of the API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every class-based route is documented, which is what keeps a document from lying by omission. Some routes are not API at all — a health check, an internal callback, something on its way out — and @Undocumented says so, once, where the handler is. Inherited, so a base class leaves out every handler written against it. Co-Authored-By: Claude Opus 5 --- flash-extensions/flash-ext-openapi/README.md | 12 +++++++++++ .../flash/ext/openapi/OpenApiBuilder.java | 7 ++++++- .../flash/ext/openapi/Undocumented.java | 21 +++++++++++++++++++ .../flash/ext/openapi/OpenApiBuilderTest.java | 18 ++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/Undocumented.java diff --git a/flash-extensions/flash-ext-openapi/README.md b/flash-extensions/flash-ext-openapi/README.md index 6b2c166..e32d760 100644 --- a/flash-extensions/flash-ext-openapi/README.md +++ b/flash-extensions/flash-ext-openapi/README.md @@ -33,6 +33,18 @@ Every class-based route is documented, annotated or not. Read off the code: Annotations add what the code cannot say: prose, extra statuses, examples. They never repeat it. +## Leaving a route out + +```java +@GET("/healthz") +@Undocumented +public final class Health extends RequestHandler { ... } +``` + +Every route is documented, so a document never lies by omission. `@Undocumented` says a route is +not part of the API — a health check, an internal callback, something on its way out. On a base +class it leaves out every handler written against it. + ## Request bodies A handler that extends `BodyHandler` — `JsonHandler` and `XmlHandler`, and anything else that diff --git a/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/OpenApiBuilder.java b/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/OpenApiBuilder.java index 92b89d1..5cacaf4 100644 --- a/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/OpenApiBuilder.java +++ b/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/OpenApiBuilder.java @@ -65,8 +65,13 @@ public final class OpenApiBuilder { public OpenApiBuilder description(String description) { this.description = description; return this; } void setContributorRegistry(OpenApiContributorRegistry registry) { this.contributorRegistry = registry; } - /** Documents one route. {@code op} is optional: a route without it is still an operation. */ + /** + * Documents one route. {@code op} is optional: a route without it is still an operation. + * A handler marked {@link Undocumented} is left out entirely. + */ public void addOperation(Route route, ApiOperation op, Class handlerClass) { + if (handlerClass.isAnnotationPresent(Undocumented.class)) return; + String path = normalizePath(route.path()); String method = route.method().name().toLowerCase(Locale.ROOT); diff --git a/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/Undocumented.java b/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/Undocumented.java new file mode 100644 index 0000000..e7fe50d --- /dev/null +++ b/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/Undocumented.java @@ -0,0 +1,21 @@ +package dev.relism.flash.ext.openapi; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Keeps a route out of the published document. + * + *

Every class-based route is documented, which is what stops a document from lying by + * omission. Some routes are not part of the API anyway — a health check, an internal callback, + * something on its way out — and this says so, once, where the handler is. + * + *

Inherited: on a base class it leaves out every handler written against it. + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Undocumented {} diff --git a/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/flash/ext/openapi/OpenApiBuilderTest.java b/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/flash/ext/openapi/OpenApiBuilderTest.java index 8e41ee3..debca82 100644 --- a/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/flash/ext/openapi/OpenApiBuilderTest.java +++ b/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/flash/ext/openapi/OpenApiBuilderTest.java @@ -220,6 +220,24 @@ class OpenApiBuilderTest { assertEquals("array", schema.get("type")); } + @GET("/internal") + @Undocumented + static class InternalHandler extends RequestHandler { + @Override public Object handle(Request request, Response response) { return null; } + } + + @Test + void a_route_that_says_it_is_not_part_of_the_api_is_left_out() { + OpenApiBuilder b = new OpenApiBuilder(); + b.addOperation(OpenApiBuilder.routeOf(InternalHandler.class), null, InternalHandler.class); + b.addOperation(OpenApiBuilder.routeOf(BareHandler.class), null, BareHandler.class); + + Map paths = cast(b.build().get("paths")); + + assertFalse(paths.containsKey("/internal")); + assertTrue(paths.containsKey("/bare"), "the others are still documented"); + } + @Test void a_route_with_no_annotations_is_still_documented() { OpenApiBuilder b = new OpenApiBuilder(); -- 2.54.0