From b8912a183af16137d1cefbb76e672585b3ce6ea2 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Wed, 8 Jan 2025 22:40:00 +0100 Subject: [PATCH] move rootPath into plugin config --- .../smiley4/ktoropenapi/OpenApiPlugin.kt | 10 ++++--- .../ktoropenapi/builder/OpenApiSpecBuilder.kt | 1 + .../builder/openapi/PathsBuilder.kt | 15 +++++++--- .../builder/route/RouteCollector.kt | 2 +- .../ktoropenapi/data/OpenApiPluginData.kt | 2 +- .../dsl/config/OpenApiPluginConfig.kt | 17 ++++++++--- .../builder/OpenApiBuilderTest.kt | 7 +++-- .../builder/OperationBuilderTest.kt | 6 ++-- .../ktorswaggerui/builder/PathsBuilderTest.kt | 30 +++++++++++++++++-- 9 files changed, 67 insertions(+), 23 deletions(-) diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/OpenApiPlugin.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/OpenApiPlugin.kt index 6dc87d5..02d67fb 100644 --- a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/OpenApiPlugin.kt +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/OpenApiPlugin.kt @@ -22,7 +22,10 @@ import io.ktor.server.routing.get private val logger = KotlinLogging.logger {} val OpenApi = createApplicationPlugin(name = "OpenApi", createConfiguration = ::OpenApiPluginConfig) { - OpenApiPlugin.config = pluginConfig.build(OpenApiPluginData.DEFAULT) + OpenApiPlugin.config = pluginConfig.build( + OpenApiPluginData.DEFAULT, + application.environment.config.propertyOrNull("ktor.deployment.rootPath")?.getString() + ) on(MonitoringEvent(ApplicationStarted)) { application -> try { OpenApiPlugin.generateOpenApiSpecs(application) @@ -37,7 +40,8 @@ object OpenApiPlugin { internal var config = OpenApiPluginData.DEFAULT - internal val openApiSpecs = mutableMapOf>() + private val openApiSpecs = mutableMapOf>() + /** * Generates new openapi @@ -49,8 +53,6 @@ object OpenApiPlugin { openApiSpecs.putAll(specs) } - fun getOpenApiSpecNames(): Set = openApiSpecs.keys.toSet() - fun getOpenApiSpec(name: String): String = openApiSpecs[name]?.first ?: throw IllegalArgumentException("No OpenAPI documentation exists with name '$name'") diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/OpenApiSpecBuilder.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/OpenApiSpecBuilder.kt index aae5f71..af7cbae 100644 --- a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/OpenApiSpecBuilder.kt +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/OpenApiSpecBuilder.kt @@ -102,6 +102,7 @@ internal class OpenApiSpecBuilder { tagExternalDocumentationBuilder = TagExternalDocumentationBuilder() ), pathsBuilder = PathsBuilder( + config = config, pathBuilder = PathBuilder( operationBuilder = OperationBuilder( operationTagsBuilder = OperationTagsBuilder(config), diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/PathsBuilder.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/PathsBuilder.kt index dbecb64..407dbd0 100644 --- a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/PathsBuilder.kt +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/PathsBuilder.kt @@ -1,6 +1,7 @@ package io.github.smiley4.ktoropenapi.builder.openapi import io.github.smiley4.ktoropenapi.builder.route.RouteMeta +import io.github.smiley4.ktoropenapi.data.OpenApiPluginData import io.swagger.v3.oas.models.PathItem import io.swagger.v3.oas.models.Paths @@ -9,23 +10,29 @@ import io.swagger.v3.oas.models.Paths * See [OpenAPI Specification - Paths Object](https://swagger.io/specification/#paths-object). */ internal class PathsBuilder( + private val config: OpenApiPluginData, private val pathBuilder: PathBuilder ) { fun build(routes: Collection): Paths = Paths().also { routes.forEach { route -> - val existingPath = it[route.path] + val url = createUrl(route) + val existingPath = it[url] if (existingPath != null) { addToExistingPath(existingPath, route) } else { - addAsNewPath(it, route) + addAsNewPath(url, it, route) } } } - private fun addAsNewPath(paths: Paths, route: RouteMeta) { - paths.addPathItem(route.path, pathBuilder.build(route)) + private fun createUrl(route: RouteMeta): String { + return "${config.rootPath ?: ""}${route.path}" + } + + private fun addAsNewPath(url: String, paths: Paths, route: RouteMeta) { + paths.addPathItem(url, pathBuilder.build(route)) } private fun addToExistingPath(existing: PathItem, route: RouteMeta) { diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/route/RouteCollector.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/route/RouteCollector.kt index c192a2a..2d364f2 100644 --- a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/route/RouteCollector.kt +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/route/RouteCollector.kt @@ -37,7 +37,7 @@ internal class RouteCollector { val documentation = getDocumentation(route, RouteConfig()) RouteMeta( method = getMethod(route), - path = getPath(route, config).let { "${config.rootPath ?: ""}${it}" }, + path = getPath(route, config), documentation = documentation.build(), protected = documentation.protected ?: isProtected(route) ) diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/OpenApiPluginData.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/OpenApiPluginData.kt index e612528..d46a844 100644 --- a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/OpenApiPluginData.kt +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/OpenApiPluginData.kt @@ -40,7 +40,7 @@ internal data class OpenApiPluginData( securityConfig = SecurityData.DEFAULT, tagsConfig = TagsData.DEFAULT, outputFormat = OutputFormat.JSON, - rootPath = "todo" // TODO + rootPath = null ) } diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/dsl/config/OpenApiPluginConfig.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/dsl/config/OpenApiPluginConfig.kt index 28cd3b9..3ecbc5e 100644 --- a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/dsl/config/OpenApiPluginConfig.kt +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/dsl/config/OpenApiPluginConfig.kt @@ -1,9 +1,9 @@ package io.github.smiley4.ktoropenapi.dsl.config import io.github.smiley4.ktoropenapi.data.DataUtils.merge +import io.github.smiley4.ktoropenapi.data.OpenApiPluginData import io.github.smiley4.ktoropenapi.data.OutputFormat import io.github.smiley4.ktoropenapi.data.PathFilter -import io.github.smiley4.ktoropenapi.data.OpenApiPluginData import io.github.smiley4.ktoropenapi.data.PostBuild import io.github.smiley4.ktoropenapi.data.ServerData import io.github.smiley4.ktoropenapi.data.SpecAssigner @@ -123,11 +123,13 @@ class OpenApiPluginConfig { */ var ignoredRouteSelectors: Set> = OpenApiPluginData.DEFAULT.ignoredRouteSelectors + /** * List of all [RouteSelector] class names that should be ignored in the resulting url of any route. */ var ignoredRouteSelectorClassNames: Set = emptySet() + /** * The format of the generated api-spec */ @@ -140,11 +142,17 @@ class OpenApiPluginConfig { var postBuild: PostBuild? = null + /** + * Root path of the ktor-application to prepend to the paths. "null" to automatically fetch from ktor configuration. + */ + var rootPath: String? = OpenApiPluginData.DEFAULT.rootPath + + /** * Build the data object for this config. * @param base the base config to "inherit" from. Values from the base should be copied, replaced or merged together. */ - internal fun build(base: OpenApiPluginData): OpenApiPluginData { + internal fun build(base: OpenApiPluginData, ktorRootPath: String?): OpenApiPluginData { val securityConfig = security.build(base.securityConfig) return OpenApiPluginData( info = info.build(base.info), @@ -170,11 +178,12 @@ class OpenApiPluginConfig { specConfigs = mutableMapOf(), postBuild = merge(base.postBuild, postBuild), outputFormat = outputFormat, - rootPath = "todo" // todo + rootPath = merge(rootPath ?: ktorRootPath, base.rootPath) ).also { specConfigs.forEach { (specName, config) -> - it.specConfigs[specName] = config.build(it) + it.specConfigs[specName] = config.build(it, ktorRootPath) } } } + } diff --git a/ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt b/ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt index bed5e66..4f11e81 100644 --- a/ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt +++ b/ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt @@ -107,7 +107,7 @@ class OpenApiBuilderTest : StringSpec({ private val defaultPluginConfig = OpenApiPluginConfig() private fun schemaContext(routes: List, pluginConfig: OpenApiPluginConfig): SchemaContext { - val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT) + val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT, null) return SchemaContextImpl(pluginConfigData.schemaConfig).also { it.addGlobal(pluginConfigData.schemaConfig) it.add(routes) @@ -115,7 +115,7 @@ class OpenApiBuilderTest : StringSpec({ } private fun exampleContext(routes: List, pluginConfig: OpenApiPluginConfig): ExampleContext { - val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT) + val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT, null) return ExampleContextImpl(pluginConfigData.exampleConfig.exampleEncoder).also { it.addShared(pluginConfigData.exampleConfig) it.add(routes) @@ -125,7 +125,7 @@ class OpenApiBuilderTest : StringSpec({ private fun buildOpenApiObject(routes: List, pluginConfig: OpenApiPluginConfig = defaultPluginConfig): OpenAPI { val schemaContext = schemaContext(routes, pluginConfig) val exampleContext = exampleContext(routes, pluginConfig) - val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT) + val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT, null) return OpenApiBuilder( config = pluginConfigData, schemaContext = schemaContext, @@ -140,6 +140,7 @@ class OpenApiBuilderTest : StringSpec({ tagExternalDocumentationBuilder = TagExternalDocumentationBuilder() ), pathsBuilder = PathsBuilder( + config = pluginConfigData, pathBuilder = PathBuilder( operationBuilder = OperationBuilder( operationTagsBuilder = OperationTagsBuilder(pluginConfigData), diff --git a/ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt b/ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt index 3b2e985..93ad916 100644 --- a/ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt +++ b/ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt @@ -1002,7 +1002,7 @@ class OperationBuilderTest : StringSpec({ private fun schemaContext(routes: List, pluginConfig: OpenApiPluginConfig = defaultPluginConfig): SchemaContext { - val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT) + val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT, null) return SchemaContextImpl(pluginConfigData.schemaConfig).also { it.addGlobal(pluginConfigData.schemaConfig) it.add(routes) @@ -1010,7 +1010,7 @@ class OperationBuilderTest : StringSpec({ } private fun exampleContext(routes: List, pluginConfig: OpenApiPluginConfig = defaultPluginConfig): ExampleContext { - val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT) + val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT, null) return ExampleContextImpl(pluginConfigData.exampleConfig.exampleEncoder).also { it.addShared(pluginConfigData.exampleConfig) it.add(routes) @@ -1023,7 +1023,7 @@ class OperationBuilderTest : StringSpec({ exampleContext: ExampleContext, pluginConfig: OpenApiPluginConfig = defaultPluginConfig ): Operation { - val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT) + val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT, null) return OperationBuilder( operationTagsBuilder = OperationTagsBuilder(pluginConfigData), parameterBuilder = ParameterBuilder( diff --git a/ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt b/ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt index 9b260b4..7042188 100644 --- a/ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt +++ b/ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt @@ -72,6 +72,29 @@ class PathsBuilderTest : StringSpec({ } } + "custom root path" { + val config = defaultPluginConfig.apply { + rootPath = "custom/root/path" + } + val routes = listOf( + route(HttpMethod.Get, "/different/path"), + route(HttpMethod.Get, "/test/path"), + route(HttpMethod.Post, "/test/path"), + ) + val schemaContext = schemaContext(routes, config) + val exampleContext = exampleContext(routes, config) + buildPathsObject(routes, schemaContext, exampleContext, config).also { paths -> + paths shouldHaveSize 2 + paths.keys shouldContainExactlyInAnyOrder listOf( + "custom/root/path/different/path", + "custom/root/path/test/path" + ) + paths["custom/root/path/different/path"]!!.get.shouldNotBeNull() + paths["custom/root/path/test/path"]!!.get.shouldNotBeNull() + paths["custom/root/path/test/path"]!!.post.shouldNotBeNull() + } + } + }) { companion object { @@ -86,7 +109,7 @@ class PathsBuilderTest : StringSpec({ private val defaultPluginConfig = OpenApiPluginConfig() private fun schemaContext(routes: List, pluginConfig: OpenApiPluginConfig): SchemaContext { - val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT) + val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT, null) return SchemaContextImpl(pluginConfigData.schemaConfig).also { it.addGlobal(pluginConfigData.schemaConfig) it.add(routes) @@ -94,7 +117,7 @@ class PathsBuilderTest : StringSpec({ } private fun exampleContext(routes: List, pluginConfig: OpenApiPluginConfig): ExampleContext { - val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT) + val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT, null) return ExampleContextImpl(pluginConfigData.exampleConfig.exampleEncoder).also { it.addShared(pluginConfigData.exampleConfig) it.add(routes) @@ -107,8 +130,9 @@ class PathsBuilderTest : StringSpec({ exampleContext: ExampleContext, pluginConfig: OpenApiPluginConfig = defaultPluginConfig ): Paths { - val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT) + val pluginConfigData = pluginConfig.build(OpenApiPluginData.DEFAULT, null) return PathsBuilder( + config = pluginConfigData, pathBuilder = PathBuilder( operationBuilder = OperationBuilder( operationTagsBuilder = OperationTagsBuilder(pluginConfigData),