implement OpenAPI contributor integration for rate limiting and response headers
This commit is contained in:
Generated
+5
@@ -13,6 +13,11 @@
|
||||
<option value="$PROJECT_DIR$/flash-bench/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="ignoredFiles">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$/flash-bench/pom.xml" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
|
||||
Generated
+76
-109
@@ -5,61 +5,26 @@
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="fc757130-fe3e-4ea9-8d69-c26ffb8545a4" name="Changes" comment="preparing for another refactoring...">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/README.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/pom.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/main/java/dev/relism/ext/jackson/JacksonExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/main/java/dev/relism/ext/jackson/JacksonExtension.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/docs/annotation.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/docs/annotation.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/docs/http-headers.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/docs/http-headers.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/pom.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/pom.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/LimiterExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/LimiterExtension.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcExtension.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/test/java/dev/relism/ext/oidc/OidcOpenApiInteropTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/test/java/dev/relism/ext/oidc/OidcOpenApiInteropTest.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/README.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/pom.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/ApiParam.java" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/ApiParams.java" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/ApiResponse.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/ApiResponse.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/ApiResponses.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/ApiResponses.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/ApiResponse.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/APIResponse.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/ApiResponses.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/APIResponses.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiBuilder.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiBuilder.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiExtension.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiSecurityContributor.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiSecurityContributor.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/Schema.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/Schema.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/SchemaProperty.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/SchemaProperty.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/pom.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/resources/routeviewer/app.css" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/resources/routeviewer/app.css" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/resources/routeviewer/app.js" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/resources/routeviewer/app.js" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiSecurityContributor.java" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiSecurityRegistry.java" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/ext/openapi/OpenApiBuilderTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/ext/openapi/OpenApiBuilderTest.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/resources/routeviewer/index.html" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/resources/routeviewer/index.html" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/pom.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/EngineCapabilities.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/EngineCapabilities.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/LegacyViewEngineAdapter.java" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/Page.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/Page.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/Partial.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/Partial.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/RenderOutput.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/RenderOutput.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/Renderer.java" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/Template.java" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ThymeleafEngine.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ThymeleafEngine.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/View.java" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewEngine.java" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewEngineAdapter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewEngineAdapter.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewEngineType.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewEngineType.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewExtension.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewGlobals.java" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewHandler.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewKind.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewKind.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewModel.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewModel.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewRuntime.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewRuntime.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewTarget.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewTarget.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewTargetResolver.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/main/java/dev/relism/ext/view/ViewTargetResolver.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewModelTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewModelTest.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewRuntimeGlobalsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewRuntimeGlobalsTest.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewTargetResolverTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/src/test/java/dev/relism/ext/view/ViewTargetResolverTest.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/pom.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/pom.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash/pom.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/ContentType.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/ContentType.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/HttpStatus.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/HttpStatus.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestHandler.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/ContentTypeTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/ContentTypeTest.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/HttpStatusTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/HttpStatusTest.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/pom.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view/README.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view/docs/migration-from-legacy-view.md" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/WebBundlerExtensionIntegrationTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/WebBundlerExtensionIntegrationTest.java" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -74,7 +39,7 @@
|
||||
</persistenceIdMap>
|
||||
</component>
|
||||
<component name="EmbeddingIndexingInfo">
|
||||
<option name="cachedIndexableFilesCount" value="287" />
|
||||
<option name="cachedIndexableFilesCount" value="352" />
|
||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||
</component>
|
||||
<component name="FileTemplateManagerImpl">
|
||||
@@ -95,12 +60,12 @@
|
||||
"assignee": "Relism"
|
||||
}
|
||||
}</component>
|
||||
<component name="GithubPullRequestsUISettings"><![CDATA[{
|
||||
"selectedUrlAndAccountId": {
|
||||
"url": "https://github.com/Relism/Flash5",
|
||||
"accountId": "4bebe1ce-b73c-44c8-9942-79b632bc7632"
|
||||
<component name="GithubPullRequestsUISettings">{
|
||||
"selectedUrlAndAccountId": {
|
||||
"url": "https://github.com/Relism/Flash5.git",
|
||||
"accountId": "86d8a39c-af27-4b79-8d5d-dc74375c3348"
|
||||
}
|
||||
}]]></component>
|
||||
}</component>
|
||||
<component name="McpProjectServerCommands">
|
||||
<commands />
|
||||
<urls />
|
||||
@@ -116,62 +81,62 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"Application.(dev) flash-bench.executor": "Run",
|
||||
"Application.ExternalBenchmark (1).executor": "Run",
|
||||
"Application.ExternalBenchmark.executor": "Run",
|
||||
"Application.Main.executor": "Run",
|
||||
"Application.MainAlt.executor": "Run",
|
||||
"Application.dev.relism.bench.Main.executor": "Run",
|
||||
"JUnit.RequestParserTest.executor": "Run",
|
||||
"JUnit.RequestParserTest.headers_caseInsensitive.executor": "Debug",
|
||||
"Maven.FlashPractice [test].executor": "Run",
|
||||
"Maven.flash [compile].executor": "Run",
|
||||
"Maven.flash [install].executor": "Run",
|
||||
"Maven.flash [test].executor": "Run",
|
||||
"Maven.flash [verify].executor": "Run",
|
||||
"Maven.flash-bench [clean].executor": "Run",
|
||||
"Maven.flash-bench [install].executor": "Run",
|
||||
"Maven.flash-bench [package].executor": "Run",
|
||||
"Maven.flash-bench [validate].executor": "Run",
|
||||
"Maven.flash-ext-limiter [install].executor": "Run",
|
||||
"Maven.flash-ext-limiter [package].executor": "Run",
|
||||
"Maven.flash-ext-view [verify].executor": "Run",
|
||||
"Maven.flash-parent [clean].executor": "Run",
|
||||
"Maven.flash-parent [compile].executor": "Run",
|
||||
"Maven.flash-parent [deploy].executor": "Run",
|
||||
"Maven.flash-parent [install].executor": "Run",
|
||||
"Maven.flash-parent [package].executor": "Run",
|
||||
"Maven.flash-parent [verify].executor": "Run",
|
||||
"Maven.flash-web-bundler [test].executor": "Run",
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||
"git-widget-placeholder": "master",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "C:/Users/elorc/Documents/Coding/Java/practice/Flash",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"npm.build.executor": "Run",
|
||||
"onboarding.tips.debug.path": "C:/Users/elorc/Documents/Coding/Java/practice/FlashPractice/flash-bench/src/main/java/dev/relism/Main.java",
|
||||
"project.structure.last.edited": "Modules",
|
||||
"project.structure.proportion": "0.15",
|
||||
"project.structure.side.proportion": "0.1150748",
|
||||
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings",
|
||||
"ts.external.directory.path": "C:\\Users\\elorc\\Documents\\Coding\\Java\\practice\\Flash\\nuxt-shadcn-dashboard\\node_modules\\typescript\\lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"Application.(dev) flash-bench.executor": "Run",
|
||||
"Application.ExternalBenchmark (1).executor": "Run",
|
||||
"Application.ExternalBenchmark.executor": "Run",
|
||||
"Application.Main.executor": "Run",
|
||||
"Application.MainAlt.executor": "Run",
|
||||
"Application.dev.relism.bench.Main.executor": "Run",
|
||||
"JUnit.RequestParserTest.executor": "Run",
|
||||
"JUnit.RequestParserTest.headers_caseInsensitive.executor": "Debug",
|
||||
"Maven.FlashPractice [test].executor": "Run",
|
||||
"Maven.flash [compile].executor": "Run",
|
||||
"Maven.flash [install].executor": "Run",
|
||||
"Maven.flash [test].executor": "Run",
|
||||
"Maven.flash [verify].executor": "Run",
|
||||
"Maven.flash-bench [clean].executor": "Run",
|
||||
"Maven.flash-bench [install].executor": "Run",
|
||||
"Maven.flash-bench [package].executor": "Run",
|
||||
"Maven.flash-bench [validate].executor": "Run",
|
||||
"Maven.flash-ext-limiter [install].executor": "Run",
|
||||
"Maven.flash-ext-limiter [package].executor": "Run",
|
||||
"Maven.flash-ext-view [verify].executor": "Run",
|
||||
"Maven.flash-parent [clean].executor": "Run",
|
||||
"Maven.flash-parent [compile].executor": "Run",
|
||||
"Maven.flash-parent [deploy].executor": "Run",
|
||||
"Maven.flash-parent [install].executor": "Run",
|
||||
"Maven.flash-parent [package].executor": "Run",
|
||||
"Maven.flash-parent [verify].executor": "Run",
|
||||
"Maven.flash-web-bundler [test].executor": "Run",
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||
"git-widget-placeholder": "master",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "C:/Users/elorc/Documents/Coding/Java/practice/Flash",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"npm.build.executor": "Run",
|
||||
"onboarding.tips.debug.path": "C:/Users/elorc/Documents/Coding/Java/practice/FlashPractice/flash-bench/src/main/java/dev/relism/Main.java",
|
||||
"project.structure.last.edited": "Modules",
|
||||
"project.structure.proportion": "0.15",
|
||||
"project.structure.side.proportion": "0.1150748",
|
||||
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings",
|
||||
"ts.external.directory.path": "C:\\Users\\elorc\\Documents\\Coding\\Java\\practice\\Flash\\nuxt-shadcn-dashboard\\node_modules\\typescript\\lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
}</component>
|
||||
<component name="RecentsManager">
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="C:\Users\elorc\Documents\Coding\Java\practice\Flash" />
|
||||
@@ -299,6 +264,8 @@
|
||||
<workItem from="1776198526994" duration="4187000" />
|
||||
<workItem from="1776240695245" duration="14368000" />
|
||||
<workItem from="1776410282788" duration="22340000" />
|
||||
<workItem from="1776515579524" duration="1172000" />
|
||||
<workItem from="1776626614705" duration="3750000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Initial">
|
||||
<option name="closed" value="true" />
|
||||
|
||||
@@ -92,6 +92,9 @@ public class PriceHandler extends RequestHandler { ... }
|
||||
Each annotation is processed by its own processor; Flash collects all middleware and
|
||||
composes them in processor registration order.
|
||||
|
||||
When `flash-ext-openapi` is installed, `@Limit` also contributes OpenAPI response
|
||||
headers (`X-RateLimit-*`) and `Retry-After` on `429` automatically.
|
||||
|
||||
```java
|
||||
@Route(method = HttpMethod.DELETE, path = "/admin/users/{id}")
|
||||
@Limit(key = "auth_user", requests = 5, window = 1, windowUnit = TimeUnit.MINUTES)
|
||||
|
||||
@@ -117,6 +117,12 @@ and **instead of** calling it on rejected requests. This means:
|
||||
|
||||
## Integration with Swagger UI (flash-ext-openapi)
|
||||
|
||||
Rate-limit headers are not currently injected into the OpenAPI spec. If you want to
|
||||
document them, add them manually via `@ApiOperation` on the handler class using the
|
||||
response headers section of the OpenAPI spec.
|
||||
When `flash-ext-openapi` is installed, handlers annotated with `@Limit` automatically
|
||||
contribute rate-limit response headers to generated OpenAPI responses:
|
||||
|
||||
- `X-RateLimit-Limit`
|
||||
- `X-RateLimit-Remaining`
|
||||
- `X-RateLimit-Reset`
|
||||
- `Retry-After` on `429`
|
||||
|
||||
If `429` is not manually declared, OpenAPI auto-adds `429 Too Many Requests`.
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
<groupId>dev.relism</groupId>
|
||||
<artifactId>flash</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.relism</groupId>
|
||||
<artifactId>flash-ext-openapi</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
|
||||
+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));
|
||||
}
|
||||
}
|
||||
+34
-32
@@ -263,62 +263,64 @@ public class OidcExtension implements FlashExtension {
|
||||
|
||||
/**
|
||||
* Loaded lazily so that {@code flash-ext-openapi} classes are only resolved at
|
||||
* runtime when {@link dev.relism.ext.openapi.OpenApiSecurityRegistry} is actually on the classpath.
|
||||
* runtime when {@link dev.relism.ext.openapi.OpenApiContributorRegistry} is actually on the classpath.
|
||||
*/
|
||||
private static final class OpenApiIntegration {
|
||||
static void register(dev.relism.extension.FlashContext ctx,
|
||||
OidcConfig config, OidcProviderMetadata meta) {
|
||||
ctx.find(dev.relism.ext.openapi.OpenApiSecurityRegistry.class)
|
||||
.ifPresent(registry -> registry.add(new dev.relism.ext.openapi.OpenApiSecurityContributor() {
|
||||
ctx.find(dev.relism.ext.openapi.OpenApiContributorRegistry.class)
|
||||
.ifPresent(registry -> registry.add(new dev.relism.ext.openapi.OpenApiContributor() {
|
||||
|
||||
@Override
|
||||
public String schemeName() { return config.schemeName(); }
|
||||
|
||||
@Override
|
||||
public java.util.Map<String, Object> schemeDefinition() {
|
||||
java.util.Map<String, String> scopesMap = new java.util.LinkedHashMap<>();
|
||||
for (String s : config.scopes().split("\\s+")) {
|
||||
if (!s.isBlank()) scopesMap.put(s, s);
|
||||
@Override
|
||||
public java.util.Map<String, Object> componentContributions() {
|
||||
java.util.Map<String, String> scopesMap = new java.util.LinkedHashMap<>();
|
||||
for (String s : config.scopes().split("\\s+")) {
|
||||
if (!s.isBlank()) scopesMap.put(s, s);
|
||||
}
|
||||
java.util.Map<String, Object> flow = new java.util.LinkedHashMap<>();
|
||||
flow.put("authorizationUrl", meta.authorizationEndpoint());
|
||||
flow.put("tokenUrl", meta.tokenEndpoint());
|
||||
flow.put("scopes", scopesMap);
|
||||
flow.put("scopes", scopesMap);
|
||||
|
||||
java.util.Map<String, Object> scheme = new java.util.LinkedHashMap<>();
|
||||
scheme.put("type", "oauth2");
|
||||
scheme.put("flows", java.util.Map.of("authorizationCode", flow));
|
||||
return scheme;
|
||||
}
|
||||
java.util.Map<String, Object> scheme = new java.util.LinkedHashMap<>();
|
||||
scheme.put("type", "oauth2");
|
||||
scheme.put("flows", java.util.Map.of("authorizationCode", flow));
|
||||
|
||||
@Override
|
||||
public java.util.List<String> requiredFor(Class<?> handlerClass) {
|
||||
return OidcAuthPolicy.openApiScopesFor(handlerClass);
|
||||
java.util.Map<String, Object> securitySchemes = new java.util.LinkedHashMap<>();
|
||||
securitySchemes.put(config.schemeName(), scheme);
|
||||
return java.util.Map.of("securitySchemes", securitySchemes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Map<Integer, String> autoResponsesFor(Class<?> handlerClass) {
|
||||
OidcAuthPolicy policy = OidcAuthPolicy.compileFromAnnotations(handlerClass);
|
||||
if (policy == null || policy.optionalAuth()) return java.util.Map.of();
|
||||
@Override
|
||||
public dev.relism.ext.openapi.OpenApiOperationContribution operationFor(Class<?> handlerClass) {
|
||||
dev.relism.ext.openapi.OpenApiOperationContribution.Builder out =
|
||||
dev.relism.ext.openapi.OpenApiOperationContribution.builder();
|
||||
|
||||
java.util.LinkedHashMap<Integer, String> out = new java.util.LinkedHashMap<>();
|
||||
out.put(401, "Authentication required");
|
||||
java.util.List<String> operationScopes = OidcAuthPolicy.openApiScopesFor(handlerClass);
|
||||
if (operationScopes != null) {
|
||||
out.security(config.schemeName(), operationScopes);
|
||||
}
|
||||
|
||||
OidcAuthPolicy policy = OidcAuthPolicy.compileFromAnnotations(handlerClass);
|
||||
if (policy == null || policy.optionalAuth()) return out.build();
|
||||
|
||||
out.response(401, dev.relism.ext.openapi.OpenApiResponseContribution.of("Authentication required"));
|
||||
|
||||
String[] roles = policy.requiredRoles();
|
||||
String[] scopes = policy.requiredScopes();
|
||||
if (roles.length == 0 && scopes.length == 0) return out;
|
||||
if (roles.length == 0 && scopes.length == 0) return out.build();
|
||||
|
||||
String roleMessage = roles.length == 0 ? null : roleRequiredMessage(roles);
|
||||
String scopeMessage = scopes.length == 0 ? null : scopeRequiredMessage(scopes);
|
||||
if (roleMessage != null && scopeMessage != null) {
|
||||
out.put(403, roleMessage + "; " + scopeMessage);
|
||||
out.response(403, dev.relism.ext.openapi.OpenApiResponseContribution.of(roleMessage + "; " + scopeMessage));
|
||||
} else if (roleMessage != null) {
|
||||
out.put(403, roleMessage);
|
||||
out.response(403, dev.relism.ext.openapi.OpenApiResponseContribution.of(roleMessage));
|
||||
} else {
|
||||
out.put(403, scopeMessage);
|
||||
out.response(403, dev.relism.ext.openapi.OpenApiResponseContribution.of(scopeMessage));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return out.build();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
+29
-11
@@ -1,10 +1,11 @@
|
||||
package dev.relism.ext.oidc;
|
||||
|
||||
import dev.relism.ext.openapi.OpenApiSecurityContributor;
|
||||
import dev.relism.ext.openapi.OpenApiContributor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -37,49 +38,57 @@ class OidcOpenApiInteropTest {
|
||||
|
||||
@Test
|
||||
void autoResponses_authOnly() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(AuthOnly.class);
|
||||
Map<Integer, String> responses = responses(AuthOnly.class);
|
||||
assertEquals("Authentication required", responses.get(401));
|
||||
assertFalse(responses.containsKey(403));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoResponses_optionalAuth_addsNothing() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(AuthOptional.class);
|
||||
Map<Integer, String> responses = responses(AuthOptional.class);
|
||||
assertTrue(responses.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoResponses_oneRole_formatsSingular() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(OneRole.class);
|
||||
Map<Integer, String> responses = responses(OneRole.class);
|
||||
assertEquals("Authentication required", responses.get(401));
|
||||
assertEquals("\"admin\" role required", responses.get(403));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoResponses_multiRoles_formatsPlural() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(MultiRole.class);
|
||||
Map<Integer, String> responses = responses(MultiRole.class);
|
||||
assertEquals("Roles \"admin, operator\" are required", responses.get(403));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoResponses_oneScope_formatsSingular() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(OneScope.class);
|
||||
Map<Integer, String> responses = responses(OneScope.class);
|
||||
assertEquals("\"orders:write\" scope required", responses.get(403));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoResponses_multiScopes_formatsPlural() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(MultiScope.class);
|
||||
Map<Integer, String> responses = responses(MultiScope.class);
|
||||
assertEquals("Scopes \"orders:write, payments:write\" are required", responses.get(403));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoResponses_roleAndScope_combinesMessages() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(RoleAndScope.class);
|
||||
Map<Integer, String> responses = responses(RoleAndScope.class);
|
||||
assertEquals("\"admin\" role required; \"orders:write\" scope required", responses.get(403));
|
||||
}
|
||||
|
||||
private static OpenApiSecurityContributor contributor() throws Exception {
|
||||
@Test
|
||||
void securityContribution_presentForAuthenticatedHandler() throws Exception {
|
||||
dev.relism.ext.openapi.OpenApiOperationContribution operation = contributor().operationFor(AuthOnly.class);
|
||||
List<Map<String, List<String>>> security = operation.security();
|
||||
assertEquals(1, security.size());
|
||||
assertTrue(security.getFirst().containsKey("issuer"));
|
||||
}
|
||||
|
||||
private static OpenApiContributor contributor() throws Exception {
|
||||
Class<?> clazz = Class.forName("dev.relism.ext.oidc.OidcExtension$OpenApiIntegration");
|
||||
Constructor<?> ctor = clazz.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
@@ -89,8 +98,8 @@ class OidcOpenApiInteropTest {
|
||||
m.setAccessible(true);
|
||||
|
||||
dev.relism.extension.FlashContext ctx = new dev.relism.extension.FlashContext();
|
||||
dev.relism.ext.openapi.OpenApiSecurityRegistry registry = new dev.relism.ext.openapi.OpenApiSecurityRegistry();
|
||||
ctx.provide(dev.relism.ext.openapi.OpenApiSecurityRegistry.class, registry);
|
||||
dev.relism.ext.openapi.OpenApiContributorRegistry registry = new dev.relism.ext.openapi.OpenApiContributorRegistry();
|
||||
ctx.provide(dev.relism.ext.openapi.OpenApiContributorRegistry.class, registry);
|
||||
|
||||
OidcConfig config = OidcConfig.builder("https://issuer", "c", "s", "/cb").build();
|
||||
OidcProviderMetadata meta = new OidcProviderMetadata("a", "t", "u", "j", "e");
|
||||
@@ -98,4 +107,13 @@ class OidcOpenApiInteropTest {
|
||||
|
||||
return registry.contributors().getFirst();
|
||||
}
|
||||
|
||||
private static Map<Integer, String> responses(Class<?> cls) throws Exception {
|
||||
Map<Integer, dev.relism.ext.openapi.OpenApiResponseContribution> byCode = contributor().operationFor(cls).responses();
|
||||
java.util.LinkedHashMap<Integer, String> out = new java.util.LinkedHashMap<>();
|
||||
for (Map.Entry<Integer, dev.relism.ext.openapi.OpenApiResponseContribution> e : byCode.entrySet()) {
|
||||
out.put(e.getKey(), e.getValue().description());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,22 @@ Supported field-level exclusion:
|
||||
- `@JsonIgnoreProperties(...)`
|
||||
- `transient` / `static`
|
||||
|
||||
## Contributor API
|
||||
|
||||
OpenAPI is extension-agnostic. Other extensions contribute with `OpenApiContributor` via
|
||||
`OpenApiContributorRegistry`.
|
||||
|
||||
Supported contribution surfaces:
|
||||
|
||||
- `components` fragments (merged with last-wins)
|
||||
- operation `security` requirements (additive)
|
||||
- operation `responses` and response `headers` (additive)
|
||||
|
||||
Merge policy:
|
||||
|
||||
- contributor collisions use **last-wins**
|
||||
- manual `@APIResponse` description always wins over contributors for the same status
|
||||
|
||||
## OIDC interop
|
||||
|
||||
When `flash-ext-oidc` is installed, OpenAPI integrates automatically:
|
||||
@@ -117,6 +133,18 @@ When `flash-ext-oidc` is installed, OpenAPI integrates automatically:
|
||||
|
||||
Manual `@APIResponse` for the same status code always wins.
|
||||
|
||||
## 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`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Operations are collected from final boot-time routes for class-based handlers with `@ApiOperation`.
|
||||
|
||||
+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));
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Flash Route Viewer</title>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Flash Route Viewer</title>
|
||||
<script type="module" crossorigin src="/routeviewer/app.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/routeviewer/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`.
|
||||
+1
-1
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user