implement OpenAPI contributor integration for rate limiting and response headers
This commit is contained in:
+67
@@ -1,13 +1,18 @@
|
||||
package dev.relism.ext.limiter;
|
||||
|
||||
import dev.relism.ext.openapi.OpenApiContributorRegistry;
|
||||
import dev.relism.ext.openapi.OpenApiOperationContribution;
|
||||
import dev.relism.ext.openapi.OpenApiResponseContribution;
|
||||
import dev.relism.extension.ExtensionPhase;
|
||||
import dev.relism.extension.FlashContext;
|
||||
import dev.relism.extension.FlashExtension;
|
||||
import dev.relism.extension.FlashRegistrar;
|
||||
import dev.relism.http.HttpStatus;
|
||||
import dev.relism.routing.Middleware;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Rate-limiting extension for Flash.
|
||||
@@ -87,6 +92,15 @@ public final class LimiterExtension implements FlashExtension {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void routes(FlashRegistrar<?> app, FlashContext ctx) {
|
||||
try {
|
||||
OpenApiIntegration.register(ctx);
|
||||
} catch (NoClassDefFoundError ignored) {
|
||||
// flash-ext-openapi not available — OpenAPI integration disabled
|
||||
}
|
||||
}
|
||||
|
||||
// ── Package-private helper — shared with Guard ────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -126,4 +140,57 @@ public final class LimiterExtension implements FlashExtension {
|
||||
return next.handle(req, res);
|
||||
};
|
||||
}
|
||||
|
||||
private static final class OpenApiIntegration {
|
||||
private static final Map<String, Object> INTEGER_SCHEMA = Map.of("type", "integer");
|
||||
private static final Map<String, Object> LIMIT_HEADER = Map.of(
|
||||
"description", "Maximum requests allowed in current window",
|
||||
"schema", INTEGER_SCHEMA
|
||||
);
|
||||
private static final Map<String, Object> REMAINING_HEADER = Map.of(
|
||||
"description", "Requests remaining in current window",
|
||||
"schema", INTEGER_SCHEMA
|
||||
);
|
||||
private static final Map<String, Object> RESET_HEADER = Map.of(
|
||||
"description", "Unix epoch seconds when quota resets or next token arrives",
|
||||
"schema", INTEGER_SCHEMA
|
||||
);
|
||||
private static final Map<String, Object> RETRY_AFTER_HEADER = Map.of(
|
||||
"description", "Seconds to wait before retrying",
|
||||
"schema", INTEGER_SCHEMA
|
||||
);
|
||||
|
||||
static void register(FlashContext ctx) {
|
||||
ctx.find(OpenApiContributorRegistry.class)
|
||||
.ifPresent(registry -> registry.add(new dev.relism.ext.openapi.OpenApiContributor() {
|
||||
@Override
|
||||
public dev.relism.ext.openapi.OpenApiOperationContribution operationFor(Class<?> handlerClass) {
|
||||
if (handlerClass.getAnnotation(Limit.class) == null) {
|
||||
return OpenApiOperationContribution.builder().build();
|
||||
}
|
||||
|
||||
OpenApiResponseContribution common =
|
||||
OpenApiResponseContribution.builder()
|
||||
.header("X-RateLimit-Limit", LIMIT_HEADER)
|
||||
.header("X-RateLimit-Remaining", REMAINING_HEADER)
|
||||
.header("X-RateLimit-Reset", RESET_HEADER)
|
||||
.build();
|
||||
|
||||
OpenApiResponseContribution tooManyRequests =
|
||||
OpenApiResponseContribution.builder()
|
||||
.description("Too Many Requests")
|
||||
.header("X-RateLimit-Limit", LIMIT_HEADER)
|
||||
.header("X-RateLimit-Remaining", REMAINING_HEADER)
|
||||
.header("X-RateLimit-Reset", RESET_HEADER)
|
||||
.header("Retry-After", RETRY_AFTER_HEADER)
|
||||
.build();
|
||||
|
||||
return OpenApiOperationContribution.builder()
|
||||
.allResponses(common)
|
||||
.response(429, tooManyRequests)
|
||||
.build();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package dev.relism.ext.limiter;
|
||||
|
||||
import dev.relism.ext.openapi.OpenApiContributor;
|
||||
import dev.relism.ext.openapi.OpenApiContributorRegistry;
|
||||
import dev.relism.ext.openapi.OpenApiOperationContribution;
|
||||
import dev.relism.ext.openapi.OpenApiResponseContribution;
|
||||
import dev.relism.extension.FlashContext;
|
||||
import dev.relism.models.Request;
|
||||
import dev.relism.models.RequestHandler;
|
||||
import dev.relism.models.Response;
|
||||
import dev.relism.routing.GET;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class LimiterOpenApiInteropTest {
|
||||
|
||||
@GET("/limited")
|
||||
@Limit(requests = 10, window = 1)
|
||||
static class LimitedHandler extends RequestHandler {
|
||||
@Override
|
||||
public Object handle(Request request, Response response) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@GET("/plain")
|
||||
static class PlainHandler extends RequestHandler {
|
||||
@Override
|
||||
public Object handle(Request request, Response response) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void registersContributor_whenOpenApiRegistryExists() {
|
||||
FlashContext ctx = new FlashContext();
|
||||
OpenApiContributorRegistry registry = new OpenApiContributorRegistry();
|
||||
ctx.provide(OpenApiContributorRegistry.class, registry);
|
||||
|
||||
new LimiterExtension().routes(null, ctx);
|
||||
|
||||
assertEquals(1, registry.contributors().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void limitedHandler_contributesHeadersAnd429() {
|
||||
FlashContext ctx = new FlashContext();
|
||||
OpenApiContributorRegistry registry = new OpenApiContributorRegistry();
|
||||
ctx.provide(OpenApiContributorRegistry.class, registry);
|
||||
new LimiterExtension().routes(null, ctx);
|
||||
|
||||
OpenApiContributor contributor = registry.contributors().getFirst();
|
||||
OpenApiOperationContribution operation = contributor.operationFor(LimitedHandler.class);
|
||||
|
||||
OpenApiResponseContribution all = operation.allResponses();
|
||||
assertNotNull(all);
|
||||
assertTrue(all.headers().containsKey("X-RateLimit-Limit"));
|
||||
assertTrue(all.headers().containsKey("X-RateLimit-Remaining"));
|
||||
assertTrue(all.headers().containsKey("X-RateLimit-Reset"));
|
||||
|
||||
OpenApiResponseContribution tooMany = operation.responses().get(429);
|
||||
assertNotNull(tooMany);
|
||||
assertEquals("Too Many Requests", tooMany.description());
|
||||
assertTrue(tooMany.headers().containsKey("Retry-After"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void plainHandler_hasNoContribution() {
|
||||
FlashContext ctx = new FlashContext();
|
||||
OpenApiContributorRegistry registry = new OpenApiContributorRegistry();
|
||||
ctx.provide(OpenApiContributorRegistry.class, registry);
|
||||
new LimiterExtension().routes(null, ctx);
|
||||
|
||||
OpenApiContributor contributor = registry.contributors().getFirst();
|
||||
OpenApiOperationContribution operation = contributor.operationFor(PlainHandler.class);
|
||||
|
||||
assertTrue(operation.isEmpty());
|
||||
assertFalse(operation.responses().containsKey(429));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user