diff --git a/HowToRelease.md b/HowToRelease.md new file mode 100644 index 00000000..afa95656 --- /dev/null +++ b/HowToRelease.md @@ -0,0 +1,9 @@ +# How to release + +https://vanniktech.github.io/gradle-maven-publish-plugin/central/ + +1. Credentials should be configured in a gradle.properties file (in user home) + +2. `./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache` + +3. `./gradlew closeAndReleaseRepository` \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 32b8f281..0bca512d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ import io.gitlab.arturbosch.detekt.Detekt object Meta { const val groupId = "io.github.smiley4" const val artifactId = "ktor-swagger-ui" - const val version = "2.5.0" + const val version = "2.6.0" const val name = "Ktor Swagger-UI" const val description = "Ktor plugin to document routes and provide Swagger UI" const val licenseName = "The Apache License, Version 2.0" @@ -68,7 +68,7 @@ dependencies { val versionMockk = "1.12.7" testImplementation("io.mockk:mockk:$versionMockk") - val versionKotest = "5.4.2" + val versionKotest = "5.7.2" testImplementation("io.kotest:kotest-runner-junit5:$versionKotest") testImplementation("io.kotest:kotest-assertions-core:$versionKotest") diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerController.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerController.kt deleted file mode 100644 index d5f4fcda..00000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerController.kt +++ /dev/null @@ -1,104 +0,0 @@ -package io.github.smiley4.ktorswaggerui - -import io.github.smiley4.ktorswaggerui.dsl.SwaggerUIDsl -import io.github.smiley4.ktorswaggerui.dsl.SwaggerUiSort -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.http.content.OutgoingContent -import io.ktor.http.withCharset -import io.ktor.server.application.ApplicationCall -import io.ktor.server.response.respond -import io.ktor.server.response.respondText -import java.net.URL - -class SwaggerController( - private val swaggerWebjarVersion: String, - private val apiSpecUrl: String, - private val jsonSpecProvider: () -> String, - private val swaggerUiConfig: SwaggerUIDsl -) { - - suspend fun serveOpenApiSpec(call: ApplicationCall) { - call.respondText(ContentType.Application.Json, HttpStatusCode.OK, jsonSpecProvider) - } - - suspend fun serverSwaggerUI(call: ApplicationCall) { - when (val filename = call.parameters["filename"]!!) { - "swagger-initializer.js" -> serveSwaggerInitializer(call) - else -> serveStaticResource(filename, call) - } - } - - private suspend fun serveSwaggerInitializer(call: ApplicationCall) { - val propValidatorUrl = swaggerUiConfig.getSpecValidatorUrl()?.let { "validatorUrl: \"$it\"" } ?: "validatorUrl: false" - val propDisplayOperationId = "displayOperationId: ${swaggerUiConfig.displayOperationId}" - val propFilter = "filter: ${swaggerUiConfig.showTagFilterInput}" - val propSort = "operationsSorter: " + if (swaggerUiConfig.sort == SwaggerUiSort.NONE) "undefined" else - "\"${swaggerUiConfig.sort.value}\"" - val propSyntaxHighlight = "syntaxHighlight: { theme: \"${swaggerUiConfig.syntaxHighlight.value}\" }" - // see https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md for reference - val content = """ - window.onload = function() { - // - window.ui = SwaggerUIBundle({ - url: "$apiSpecUrl", - dom_id: '#swagger-ui', - deepLinking: true, - presets: [ - SwaggerUIBundle.presets.apis, - SwaggerUIStandalonePreset - ], - plugins: [ - SwaggerUIBundle.plugins.DownloadUrl - ], - layout: "StandaloneLayout", - $propValidatorUrl, - $propDisplayOperationId, - $propFilter, - $propSort, - $propSyntaxHighlight - }); - // - }; - """.trimIndent() - call.respondText(ContentType.Application.JavaScript, HttpStatusCode.OK) { content } - } - - - private suspend fun serveStaticResource(filename: String, call: ApplicationCall) { - val resource = this::class.java.getResource("/META-INF/resources/webjars/swagger-ui/$swaggerWebjarVersion/$filename") - if (resource == null) { - call.respond(HttpStatusCode.NotFound, "$filename could not be found") - } else { - call.respond(ResourceContent(resource)) - } - } - -} - - -private class ResourceContent(val resource: URL) : OutgoingContent.ByteArrayContent() { - private val bytes by lazy { resource.readBytes() } - - override val contentType: ContentType? by lazy { - val extension = resource.file.substring(resource.file.lastIndexOf('.') + 1) - contentTypes[extension] ?: ContentType.Text.Html - } - - override val contentLength: Long? by lazy { - bytes.size.toLong() - } - - override fun bytes(): ByteArray = bytes - - override fun toString() = "ResourceContent \"$resource\"" -} - - -private val contentTypes = mapOf( - "html" to ContentType.Text.Html, - "css" to ContentType.Text.CSS, - "js" to ContentType.Application.JavaScript, - "json" to ContentType.Application.Json.withCharset(Charsets.UTF_8), - "png" to ContentType.Image.PNG -) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index f019555b..372f874c 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -1,20 +1,51 @@ package io.github.smiley4.ktorswaggerui import com.fasterxml.jackson.databind.ObjectMapper -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContext -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContextBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.* -import io.github.smiley4.ktorswaggerui.spec.route.RouteCollector -import io.github.smiley4.ktorswaggerui.spec.route.RouteDocumentationMerger -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContextBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.TypeOverwrites -import io.ktor.server.application.* -import io.ktor.server.application.hooks.* -import io.ktor.server.routing.* -import io.ktor.server.webjars.* +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl +import io.github.smiley4.ktorswaggerui.routing.ForwardRouteController +import io.github.smiley4.ktorswaggerui.routing.SwaggerController +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ComponentsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ContactBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ContentBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ExampleBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ExternalDocumentationBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.HeaderBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.InfoBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.LicenseBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OAuthFlowsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OpenApiBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OperationBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OperationTagsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ParameterBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.PathBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.PathsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.RequestBodyBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ResponseBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ResponsesBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.SecurityRequirementsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.SecuritySchemesBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ServerBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.TagBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.TagExternalDocumentationBuilder +import io.github.smiley4.ktorswaggerui.builder.route.RouteCollector +import io.github.smiley4.ktorswaggerui.builder.route.RouteDocumentationMerger +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextBuilder +import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationStarted +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.application.hooks.MonitoringEvent +import io.ktor.server.application.install +import io.ktor.server.application.plugin +import io.ktor.server.application.pluginOrNull +import io.ktor.server.routing.Routing +import io.ktor.server.webjars.Webjars import io.swagger.v3.core.util.Json import mu.KotlinLogging @@ -25,56 +56,96 @@ internal const val SWAGGER_UI_WEBJARS_VERSION = "4.15.0" private val logger = KotlinLogging.logger {} -val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration = ::SwaggerUIPluginConfig) { - var apiSpecJson = "{}" +val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration = ::PluginConfigDsl) { + + val config = pluginConfig.build(PluginConfigData.DEFAULT) + on(MonitoringEvent(ApplicationStarted)) { application -> + if (application.pluginOrNull(Webjars) == null) { application.install(Webjars) } + + val apiSpecsJson = mutableMapOf() try { - val routes = routes(application, pluginConfig) - val schemaContext = schemaContext(pluginConfig, routes) - val exampleContext = exampleContext(pluginConfig, routes) - val openApi = builder(pluginConfig, schemaContext, exampleContext).build(routes) - apiSpecJson = Json.pretty(openApi) + val routes = routes(application, config) + apiSpecsJson.putAll(buildOpenApiSpecs(config, routes)) } catch (e: Exception) { - logger.error("Error during openapi-generation", e) + logger.error("Error during application startup in swagger-ui-plugin", e) + } + + apiSpecsJson.forEach { (specId, json) -> + val specConfig = config.specConfigs[specId] ?: config + SwaggerController( + applicationConfig!!, + specConfig, + SWAGGER_UI_WEBJARS_VERSION, + if (apiSpecsJson.size > 1) specId else null, + json + ).setup(application) + } + + if (apiSpecsJson.size == 1 && config.swaggerUI.forwardRoot) { + ForwardRouteController(applicationConfig!!, config).setup(application) } + } - SwaggerRouting( - pluginConfig.getSwaggerUI(), - application.environment.config, - SWAGGER_UI_WEBJARS_VERSION, - ) { apiSpecJson }.setup(application) } -private fun routes(application: Application, pluginConfig: SwaggerUIPluginConfig): List { +private fun buildOpenApiSpecs(config: PluginConfigData, routes: List): Map { + val routesBySpec = buildMap> { + routes.forEach { route -> + val specName = route.documentation.specId ?: config.specAssigner(route.path, route.documentation.tags) + computeIfAbsent(specName) { mutableListOf() }.add(route) + } + } + return buildMap { + routesBySpec.forEach { (specName, routes) -> + val specConfig = config.specConfigs[specName] ?: config + this[specName] = buildOpenApiSpec(specConfig, routes) + } + } +} + +private fun buildOpenApiSpec(pluginConfig: PluginConfigData, routes: List): String { + return try { + val schemaContext = schemaContext(pluginConfig, routes) + val exampleContext = exampleContext(pluginConfig, routes) + val openApi = builder(pluginConfig, schemaContext, exampleContext).build(routes) + Json.pretty(openApi) + } catch (e: Exception) { + logger.error("Error during openapi-generation", e) + "{}" + } +} + +private fun routes(application: Application, config: PluginConfigData): List { return RouteCollector(RouteDocumentationMerger()) - .collectRoutes({ application.plugin(Routing) }, pluginConfig) + .collectRoutes({ application.plugin(Routing) }, config) .toList() } -private fun schemaContext(pluginConfig: SwaggerUIPluginConfig, routes: List): SchemaContext { +private fun schemaContext(config: PluginConfigData, routes: List): SchemaContext { return SchemaContextBuilder( - config = pluginConfig, + config = config, schemaBuilder = SchemaBuilder( - definitionsField = pluginConfig.encodingConfig.schemaDefinitionsField, - schemaEncoder = pluginConfig.encodingConfig.getSchemaEncoder(), + definitionsField = config.encoding.schemaDefsField, + schemaEncoder = config.encoding.schemaEncoder, ObjectMapper(), TypeOverwrites.get() ), ).build(routes.toList()) } -private fun exampleContext(pluginConfig: SwaggerUIPluginConfig, routes: List): ExampleContext { +private fun exampleContext(config: PluginConfigData, routes: List): ExampleContext { return ExampleContextBuilder( exampleBuilder = ExampleBuilder( - config = pluginConfig + config = config ) ).build(routes.toList()) } -private fun builder(config: SwaggerUIPluginConfig, schemaContext: SchemaContext, exampleContext: ExampleContext): OpenApiBuilder { +private fun builder(config: PluginConfigData, schemaContext: SchemaContext, exampleContext: ExampleContext): OpenApiBuilder { return OpenApiBuilder( config = config, schemaContext = schemaContext, diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerRouting.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerRouting.kt deleted file mode 100644 index a3ed89bd..00000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerRouting.kt +++ /dev/null @@ -1,85 +0,0 @@ -package io.github.smiley4.ktorswaggerui - -import io.github.smiley4.ktorswaggerui.dsl.SwaggerUIDsl -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.auth.authenticate -import io.ktor.server.config.ApplicationConfig -import io.ktor.server.response.respondRedirect -import io.ktor.server.routing.Route -import io.ktor.server.routing.get -import io.ktor.server.routing.route -import io.ktor.server.routing.routing -import mu.KotlinLogging - -/** - * Registers and handles routes required for the swagger-ui - */ -class SwaggerRouting( - private val swaggerUiConfig: SwaggerUIDsl, - appConfig: ApplicationConfig, - swaggerWebjarVersion: String, - jsonSpecProvider: () -> String -) { - - private val logger = KotlinLogging.logger {} - - private val controller = SwaggerController( - swaggerWebjarVersion = swaggerWebjarVersion, - apiSpecUrl = getApiSpecUrl(appConfig), - jsonSpecProvider = jsonSpecProvider, - swaggerUiConfig = swaggerUiConfig - ) - - private fun getApiSpecUrl(appConfig: ApplicationConfig): String { - val rootPath = appConfig.propertyOrNull("ktor.deployment.rootPath")?.getString()?.let { "/${dropSlashes(it)}" } ?: "" - return "$rootPath${swaggerUiConfig.rootHostPath}/${dropSlashes(swaggerUiConfig.swaggerUrl)}/api.json" - } - - private fun dropSlashes(str: String): String { - var value = str - value = if (value.startsWith("/")) value.substring(1) else value - value = if (value.endsWith("/")) value.substring(0, value.length - 1) else value - return value - } - - /** - * registers the required routes - */ - fun setup(app: Application) { - val swaggerUrl = swaggerUiConfig.swaggerUrl - val forwardRoot = swaggerUiConfig.forwardRoot - val authentication = swaggerUiConfig.authentication - logger.info("Registering routes for swagger-ui: $swaggerUrl (forwardRoot=$forwardRoot)") - app.routing { - if (forwardRoot) { - get("/") { - call.respondRedirect("${swaggerUiConfig.rootHostPath}/${dropSlashes(swaggerUrl)}/index.html") - } - } - if (authentication == null) { - setupSwaggerRoutes() - } else { - authenticate(authentication) { - setupSwaggerRoutes() - } - } - } - } - - private fun Route.setupSwaggerRoutes() { - val swaggerUrl = swaggerUiConfig.swaggerUrl - route(swaggerUrl) { - get { - call.respondRedirect("${swaggerUiConfig.rootHostPath}/${dropSlashes(swaggerUrl)}/index.html") - } - get("api.json") { - controller.serveOpenApiSpec(call) - } - get("{filename}") { - controller.serverSwaggerUI(call) - } - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt deleted file mode 100644 index 9059928b..00000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt +++ /dev/null @@ -1,164 +0,0 @@ -package io.github.smiley4.ktorswaggerui - -import io.github.smiley4.ktorswaggerui.dsl.* -import io.ktor.http.* -import io.ktor.server.routing.* -import kotlin.reflect.KClass - -/** - * Main-Configuration of the "SwaggerUI"-Plugin - */ -@OpenApiDslMarker -class SwaggerUIPluginConfig { - - /** - * Default response to automatically add to each protected route for the "Unauthorized"-Response-Code. - * Generated response can be overwritten with custom response. - */ - fun defaultUnauthorizedResponse(block: OpenApiResponse.() -> Unit) { - defaultUnauthorizedResponse = OpenApiResponse(HttpStatusCode.Unauthorized.value.toString()).apply(block) - } - - private var defaultUnauthorizedResponse: OpenApiResponse? = null - - fun getDefaultUnauthorizedResponse() = defaultUnauthorizedResponse - - - /** - * The name of the security scheme to use for the protected paths - */ - var defaultSecuritySchemeName: String? = null - - - /** - * The names of the security schemes available for use for the protected paths - */ - var defaultSecuritySchemeNames: Collection? = null - - - /** - * Automatically add tags to the route with the given url. - * The returned (non-null) tags will be added to the tags specified in the route-specific documentation. - */ - fun generateTags(generator: TagGenerator) { - tagGenerator = generator - } - - private var tagGenerator: TagGenerator = { emptyList() } - - fun getTagGenerator() = tagGenerator - - - /** - * Filter to apply to all routes. Return 'false' for routes to not include them in the OpenApi-Spec and Swagger-UI. - * The url of the paths are already split at '/'. - */ - var pathFilter: ((method: HttpMethod, url: List) -> Boolean)? = null - - - /** - * Swagger-UI configuration - */ - fun swagger(block: SwaggerUIDsl.() -> Unit) { - swaggerUI = SwaggerUIDsl().apply(block) - } - - private var swaggerUI = SwaggerUIDsl() - - fun getSwaggerUI() = swaggerUI - - - /** - * OpenAPI info configuration - provides metadata about the API - */ - fun info(block: OpenApiInfo.() -> Unit) { - info = OpenApiInfo().apply(block) - } - - private var info = OpenApiInfo() - - fun getInfo() = info - - - /** - * OpenAPI server configuration - an array of servers, which provide connectivity information to a target server - */ - fun server(block: OpenApiServer.() -> Unit) { - servers.add(OpenApiServer().apply(block)) - } - - private val servers = mutableListOf() - - fun getServers(): List = servers - - - /** - * OpenAPI external docs configuration - link and description of an external documentation - */ - fun externalDocs(block: OpenApiExternalDocs.() -> Unit) { - externalDocs = OpenApiExternalDocs().apply(block) - } - - private var externalDocs = OpenApiExternalDocs() - - fun getExternalDocs() = externalDocs - - - /** - * Defines security schemes that can be used by operations - */ - fun securityScheme(name: String, block: OpenApiSecurityScheme.() -> Unit) { - securitySchemes.add(OpenApiSecurityScheme(name).apply(block)) - } - - private val securitySchemes = mutableListOf() - - fun getSecuritySchemes(): List = securitySchemes - - - /** - * Tags used by the specification with additional metadata. Not all tags that are used must be declared - */ - fun tag(name: String, block: OpenApiTag.() -> Unit) { - tags.add(OpenApiTag(name).apply(block)) - } - - private val tags = mutableListOf() - - fun getTags(): List = tags - - - /** - * Custom schemas to reference via [io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef] - */ - fun customSchemas(block: CustomSchemas.() -> Unit) { - this.customSchemas = CustomSchemas().apply(block) - } - - private var customSchemas = CustomSchemas() - - fun getCustomSchemas() = customSchemas - - - /** - * customize the behaviour of different encoders (examples, schemas, ...) - */ - fun encoding(block: EncodingConfig.() -> Unit) { - block(encodingConfig) - } - - val encodingConfig: EncodingConfig = EncodingConfig() - - - /** - * List of all [RouteSelector] types in that should be ignored in the resulting url of any route. - */ - var ignoredRouteSelectors: List> = listOf() - -} - -/** - * url - the parts of the route-url split at all `/`. - * return a collection of tags. "Null"-entries will be ignored. - */ -typealias TagGenerator = (url: List) -> Collection diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/example/ExampleContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt similarity index 94% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/example/ExampleContext.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt index 05dd8d5f..8c031c32 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/example/ExampleContext.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.spec.example +package io.github.smiley4.ktorswaggerui.builder.example import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/example/ExampleContextBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextBuilder.kt similarity index 61% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/example/ExampleContextBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextBuilder.kt index 426ca98d..608eef52 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/example/ExampleContextBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextBuilder.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.spec.example +package io.github.smiley4.ktorswaggerui.builder.example import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample @@ -7,8 +7,14 @@ import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody import io.github.smiley4.ktorswaggerui.dsl.SchemaType import io.github.smiley4.ktorswaggerui.dsl.getSchemaType -import io.github.smiley4.ktorswaggerui.spec.openapi.ExampleBuilder -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.builder.openapi.ExampleBuilder +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta +import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.CollectionBodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.CustomRefBodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.EmptyBodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.OneOfBodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.SchemaBodyTypeDescriptor import io.swagger.v3.oas.models.examples.Example class ExampleContextBuilder( @@ -42,7 +48,18 @@ class ExampleContextBuilder( private fun handle(ctx: ExampleContext, body: OpenApiSimpleBody) { body.getExamples().forEach { (name, value) -> - ctx.addExample(body, name, createExample(body.type ?: getSchemaType(), value)) + val bodyType = getRelevantSchemaType(body.type, getSchemaType()) + ctx.addExample(body, name, createExample(bodyType, value)) + } + } + + private fun getRelevantSchemaType(typeDescriptor: BodyTypeDescriptor, fallback: SchemaType): SchemaType { + return when(typeDescriptor) { + is EmptyBodyTypeDescriptor -> fallback + is SchemaBodyTypeDescriptor -> typeDescriptor.schemaType + is CollectionBodyTypeDescriptor -> getRelevantSchemaType(typeDescriptor.schemaType, fallback) + is OneOfBodyTypeDescriptor -> fallback + is CustomRefBodyTypeDescriptor -> fallback } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ComponentsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ComponentsBuilder.kt similarity index 67% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ComponentsBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ComponentsBuilder.kt index 3847063a..7d09097b 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ComponentsBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ComponentsBuilder.kt @@ -1,12 +1,12 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.data.PluginConfigData import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.examples.Example import io.swagger.v3.oas.models.media.Schema class ComponentsBuilder( - private val config: SwaggerUIPluginConfig, + private val config: PluginConfigData, private val securitySchemesBuilder: SecuritySchemesBuilder ) { @@ -14,8 +14,8 @@ class ComponentsBuilder( return Components().also { it.schemas = schemas it.examples = examples - if (config.getSecuritySchemes().isNotEmpty()) { - it.securitySchemes = securitySchemesBuilder.build(config.getSecuritySchemes()) + if (config.securitySchemes.isNotEmpty()) { + it.securitySchemes = securitySchemesBuilder.build(config.securitySchemes) } } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContactBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContactBuilder.kt similarity index 57% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContactBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContactBuilder.kt index 2d88df06..5fc0e483 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContactBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContactBuilder.kt @@ -1,11 +1,11 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.dsl.OpenApiContact +import io.github.smiley4.ktorswaggerui.data.ContactData import io.swagger.v3.oas.models.info.Contact class ContactBuilder { - fun build(contact: OpenApiContact): Contact = + fun build(contact: ContactData): Contact = Contact().also { it.name = contact.name it.email = contact.email diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt similarity index 73% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt index 34896aa1..ee2bd7d5 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt @@ -1,29 +1,23 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.CollectionBodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.CustomRefBodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.EmptyBodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.OneOfBodyTypeDescriptor import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartPart -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContext -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody +import io.github.smiley4.ktorswaggerui.dsl.SchemaBodyTypeDescriptor import io.ktor.http.ContentType import io.swagger.v3.oas.models.media.Content import io.swagger.v3.oas.models.media.Encoding import io.swagger.v3.oas.models.media.MediaType import io.swagger.v3.oas.models.media.Schema -import kotlin.collections.Map -import kotlin.collections.MutableMap -import kotlin.collections.associateWith -import kotlin.collections.filter -import kotlin.collections.flatMap -import kotlin.collections.forEach -import kotlin.collections.ifEmpty -import kotlin.collections.isNotEmpty -import kotlin.collections.joinToString -import kotlin.collections.mapValues -import kotlin.collections.mutableMapOf import kotlin.collections.set -import kotlin.collections.setOf class ContentBuilder( private val schemaContext: SchemaContext, @@ -106,22 +100,37 @@ class ContentBuilder( } private fun getSchema(body: OpenApiSimpleBody): Schema<*>? { - return if (body.customSchema != null) { - schemaContext.getSchema(body.customSchema!!) - } else if (body.type != null) { - schemaContext.getSchema(body.type) - } else { - null - } + return getSchema(body.type) } private fun getSchema(part: OpenApiMultipartPart): Schema<*>? { - return if (part.customSchema != null) { - schemaContext.getSchema(part.customSchema!!) - } else if (part.type != null) { - schemaContext.getSchema(part.type) - } else { - null + return getSchema(part.type) + } + + private fun getSchema(typeDescriptor: BodyTypeDescriptor): Schema<*>? { + return when (typeDescriptor) { + is EmptyBodyTypeDescriptor -> { + null + } + is SchemaBodyTypeDescriptor -> { + schemaContext.getSchema(typeDescriptor.schemaType) + } + is OneOfBodyTypeDescriptor -> { + Schema().also { schema -> + typeDescriptor.elements.forEach { + schema.addOneOfItem(getSchema(it)) + } + } + } + is CollectionBodyTypeDescriptor -> { + Schema().also { schema -> + schema.type = "array" + schema.items = getSchema(typeDescriptor.schemaType) + } + } + is CustomRefBodyTypeDescriptor -> { + schemaContext.getSchema(typeDescriptor.customSchemaId) + } } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExampleBuilder.kt similarity index 67% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExampleBuilder.kt index 864678c6..2b452fa1 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExampleBuilder.kt @@ -1,12 +1,12 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.data.PluginConfigData import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample import io.github.smiley4.ktorswaggerui.dsl.SchemaType import io.swagger.v3.oas.models.examples.Example class ExampleBuilder( - private val config: SwaggerUIPluginConfig + private val config: PluginConfigData ) { fun build(type: SchemaType?, example: OpenApiExample): Example = @@ -17,7 +17,7 @@ class ExampleBuilder( } fun buildExampleValue(type: SchemaType?, value: Any): String { - return config.encodingConfig.getExampleEncoder()(type, value) ?: value.toString() + return config.encoding.exampleEncoder(type, value) ?: value.toString() } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExternalDocumentationBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExternalDocumentationBuilder.kt similarity index 55% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExternalDocumentationBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExternalDocumentationBuilder.kt index 7c463e4f..49f4fd2e 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExternalDocumentationBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExternalDocumentationBuilder.kt @@ -1,11 +1,11 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.dsl.OpenApiExternalDocs +import io.github.smiley4.ktorswaggerui.data.ExternalDocsData import io.swagger.v3.oas.models.ExternalDocumentation class ExternalDocumentationBuilder { - fun build(externalDocs: OpenApiExternalDocs): ExternalDocumentation = + fun build(externalDocs: ExternalDocsData): ExternalDocumentation = ExternalDocumentation().also { it.url = externalDocs.url it.description = externalDocs.description diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/HeaderBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt similarity index 79% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/HeaderBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt index b9f2e11c..4c0fd1a7 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/HeaderBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt @@ -1,7 +1,7 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.dsl.OpenApiHeader -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext import io.swagger.v3.oas.models.headers.Header class HeaderBuilder( diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/InfoBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt similarity index 67% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/InfoBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt index 3c2b7a9f..bfc03dc2 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/InfoBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt @@ -1,6 +1,6 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo +import io.github.smiley4.ktorswaggerui.data.InfoData import io.swagger.v3.oas.models.info.Info class InfoBuilder( @@ -8,16 +8,16 @@ class InfoBuilder( private val licenseBuilder: LicenseBuilder ) { - fun build(info: OpenApiInfo): Info = + fun build(info: InfoData): Info = Info().also { it.title = info.title it.version = info.version it.description = info.description it.termsOfService = info.termsOfService - info.getContact()?.also { contact -> + info.contact?.also { contact -> it.contact = contactBuilder.build(contact) } - info.getLicense()?.also { license -> + info.license?.also { license -> it.license = licenseBuilder.build(license) } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/LicenseBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt similarity index 52% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/LicenseBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt index 968ea2fb..30f727c0 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/LicenseBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt @@ -1,11 +1,11 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.dsl.OpenApiLicense +import io.github.smiley4.ktorswaggerui.data.LicenseData import io.swagger.v3.oas.models.info.License class LicenseBuilder { - fun build(license: OpenApiLicense): License = + fun build(license: LicenseData): License = License().also { it.name = license.name it.url = license.url diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OAuthFlowsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OAuthFlowsBuilder.kt new file mode 100644 index 00000000..c8f8c965 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OAuthFlowsBuilder.kt @@ -0,0 +1,35 @@ +package io.github.smiley4.ktorswaggerui.builder.openapi + +import io.github.smiley4.ktorswaggerui.data.OpenIdOAuthFlowData +import io.github.smiley4.ktorswaggerui.data.OpenIdOAuthFlowsData +import io.swagger.v3.oas.models.security.OAuthFlow +import io.swagger.v3.oas.models.security.OAuthFlows +import io.swagger.v3.oas.models.security.Scopes + +class OAuthFlowsBuilder { + + fun build(flows: OpenIdOAuthFlowsData): OAuthFlows { + return OAuthFlows().apply { + implicit = flows.implicit?.let { build(it) } + password = flows.password?.let { build(it) } + clientCredentials = flows.clientCredentials?.let { build(it) } + authorizationCode = flows.authorizationCode?.let { build(it) } + } + } + + private fun build(flow: OpenIdOAuthFlowData): OAuthFlow { + return OAuthFlow().apply { + authorizationUrl = flow.authorizationUrl + tokenUrl = flow.tokenUrl + refreshUrl = flow.refreshUrl + scopes = flow.scopes?.let { buildScopes(it) } + } + } + + private fun buildScopes(scopes: Map): Scopes { + return Scopes().apply { + scopes.forEach { (k, v) -> addString(k, v) } + } + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt similarity index 57% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt index 0f1c6946..f3adac85 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt @@ -1,13 +1,13 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContext -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext import io.swagger.v3.oas.models.OpenAPI class OpenApiBuilder( - private val config: SwaggerUIPluginConfig, + private val config: PluginConfigData, private val schemaContext: SchemaContext, private val exampleContext: ExampleContext, private val infoBuilder: InfoBuilder, @@ -20,10 +20,10 @@ class OpenApiBuilder( fun build(routes: Collection): OpenAPI { return OpenAPI().also { - it.info = infoBuilder.build(config.getInfo()) - it.externalDocs = externalDocumentationBuilder.build(config.getExternalDocs()) - it.servers = config.getServers().map { server -> serverBuilder.build(server) } - it.tags = config.getTags().map { tag -> tagBuilder.build(tag) } + it.info = infoBuilder.build(config.info) + it.externalDocs = externalDocumentationBuilder.build(config.externalDocs) + it.servers = config.servers.map { server -> serverBuilder.build(server) } + it.tags = config.tags.map { tag -> tagBuilder.build(tag) } it.paths = pathsBuilder.build(routes) it.components = componentsBuilder.build(schemaContext.getComponentsSection(), exampleContext.getComponentsSection()) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt similarity index 92% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt index 0b2df038..3e48ce88 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt @@ -1,6 +1,6 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta import io.swagger.v3.oas.models.Operation class OperationBuilder( diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationTagsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationTagsBuilder.kt new file mode 100644 index 00000000..962ae049 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationTagsBuilder.kt @@ -0,0 +1,21 @@ +package io.github.smiley4.ktorswaggerui.builder.openapi + +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta + +class OperationTagsBuilder( + private val config: PluginConfigData +) { + + fun build(route: RouteMeta): List { + return mutableSetOf().also { tags -> + tags.addAll(getGeneratedTags(route)) + tags.addAll(getRouteTags(route)) + }.filterNotNull() + } + + private fun getRouteTags(route: RouteMeta) = route.documentation.tags + + private fun getGeneratedTags(route: RouteMeta) = config.tagGenerator(route.path.split("/").filter { it.isNotEmpty() }) + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ParameterBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt similarity index 84% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ParameterBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt index 8b7a811b..bbf76039 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ParameterBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt @@ -1,8 +1,8 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContext -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext import io.swagger.v3.oas.models.parameters.Parameter class ParameterBuilder( diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathBuilder.kt similarity index 86% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathBuilder.kt index a77b865f..a6c5c93e 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathBuilder.kt @@ -1,6 +1,6 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta import io.ktor.http.HttpMethod import io.swagger.v3.oas.models.PathItem diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathsBuilder.kt similarity index 91% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathsBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathsBuilder.kt index 615d45d3..46a3c6eb 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathsBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathsBuilder.kt @@ -1,6 +1,6 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta import io.swagger.v3.oas.models.PathItem import io.swagger.v3.oas.models.Paths diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/RequestBodyBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/RequestBodyBuilder.kt similarity index 88% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/RequestBodyBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/RequestBodyBuilder.kt index 189fada8..f5a6383b 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/RequestBodyBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/RequestBodyBuilder.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody import io.swagger.v3.oas.models.parameters.RequestBody diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponseBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponseBuilder.kt similarity index 91% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponseBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponseBuilder.kt index 66cf191d..7e59eec0 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponseBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponseBuilder.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse import io.swagger.v3.oas.models.responses.ApiResponse diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponsesBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponsesBuilder.kt similarity index 79% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponsesBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponsesBuilder.kt index 8a95d268..bd148dd7 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponsesBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponsesBuilder.kt @@ -1,13 +1,13 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.data.PluginConfigData import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponses import io.ktor.http.HttpStatusCode import io.swagger.v3.oas.models.responses.ApiResponses class ResponsesBuilder( private val responseBuilder: ResponseBuilder, - private val config: SwaggerUIPluginConfig + private val config: PluginConfigData ) { fun build(responses: OpenApiResponses, isProtected: Boolean): ApiResponses = @@ -16,7 +16,7 @@ class ResponsesBuilder( .map { response -> responseBuilder.build(response) } .forEach { (name, response) -> it.addApiResponse(name, response) } if (shouldAddUnauthorized(responses, isProtected)) { - config.getDefaultUnauthorizedResponse() + config.defaultUnauthorizedResponse ?.let { response -> responseBuilder.build(response) } ?.also { (name, response) -> it.addApiResponse(name, response) } } @@ -24,7 +24,7 @@ class ResponsesBuilder( private fun shouldAddUnauthorized(responses: OpenApiResponses, isProtected: Boolean): Boolean { val unauthorizedCode = HttpStatusCode.Unauthorized.value.toString(); - return config.getDefaultUnauthorizedResponse() != null + return config.defaultUnauthorizedResponse != null && isProtected && responses.getResponses().count { it.statusCode == unauthorizedCode } == 0 } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecurityRequirementsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecurityRequirementsBuilder.kt similarity index 61% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecurityRequirementsBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecurityRequirementsBuilder.kt index 473bdef7..3b1df677 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecurityRequirementsBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecurityRequirementsBuilder.kt @@ -1,11 +1,11 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta import io.swagger.v3.oas.models.security.SecurityRequirement class SecurityRequirementsBuilder( - private val config: SwaggerUIPluginConfig + private val config: PluginConfigData ) { fun build(route: RouteMeta): List { @@ -14,8 +14,7 @@ class SecurityRequirementsBuilder( route.documentation.securitySchemeNames?.also { schemes.addAll(it) } } if (securitySchemes.isEmpty()) { - config.defaultSecuritySchemeName?.also { securitySchemes.add(it) } - config.defaultSecuritySchemeNames?.also { securitySchemes.addAll(it) } + config.defaultSecuritySchemeNames.also { securitySchemes.addAll(it) } } return securitySchemes.map { SecurityRequirement().apply { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecuritySchemesBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecuritySchemesBuilder.kt similarity index 66% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecuritySchemesBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecuritySchemesBuilder.kt index 017720bf..9216f0fe 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecuritySchemesBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecuritySchemesBuilder.kt @@ -1,13 +1,13 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme +import io.github.smiley4.ktorswaggerui.data.SecuritySchemeData import io.swagger.v3.oas.models.security.SecurityScheme class SecuritySchemesBuilder( private val oAuthFlowsBuilder: OAuthFlowsBuilder ) { - fun build(securitySchemes: List): Map { + fun build(securitySchemes: List): Map { return mutableMapOf().apply { securitySchemes.forEach { put(it.name, build(it)) @@ -15,7 +15,7 @@ class SecuritySchemesBuilder( } } - private fun build(securityScheme: OpenApiSecurityScheme): SecurityScheme { + private fun build(securityScheme: SecuritySchemeData): SecurityScheme { return SecurityScheme().apply { description = securityScheme.description name = securityScheme.name @@ -23,7 +23,7 @@ class SecuritySchemesBuilder( `in` = securityScheme.location?.swaggerType scheme = securityScheme.scheme?.swaggerType bearerFormat = securityScheme.bearerFormat - flows = securityScheme.getFlows()?.let { f -> oAuthFlowsBuilder.build(f) } + flows = securityScheme.flows?.let { f -> oAuthFlowsBuilder.build(f) } openIdConnectUrl = securityScheme.openIdConnectUrl } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ServerBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt similarity index 54% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ServerBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt index 84196d42..5878483f 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ServerBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt @@ -1,11 +1,11 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer +import io.github.smiley4.ktorswaggerui.data.ServerData import io.swagger.v3.oas.models.servers.Server class ServerBuilder { - fun build(server: OpenApiServer): Server = + fun build(server: ServerData): Server = Server().also { it.url = server.url it.description = server.description diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/TagBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagBuilder.kt similarity index 67% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/TagBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagBuilder.kt index 60284d77..d550e6a3 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/TagBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagBuilder.kt @@ -1,18 +1,18 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.dsl.OpenApiTag +import io.github.smiley4.ktorswaggerui.data.TagData import io.swagger.v3.oas.models.tags.Tag class TagBuilder( private val tagExternalDocumentationBuilder: TagExternalDocumentationBuilder ) { - fun build(tag: OpenApiTag): Tag = + fun build(tag: TagData): Tag = Tag().also { it.name = tag.name it.description = tag.description if(tag.externalDocUrl != null && tag.externalDocDescription != null) { - it.externalDocs = tagExternalDocumentationBuilder.build(tag.externalDocUrl!!, tag.externalDocDescription!!) + it.externalDocs = tagExternalDocumentationBuilder.build(tag.externalDocUrl, tag.externalDocDescription) } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/TagExternalDocumentationBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagExternalDocumentationBuilder.kt similarity index 83% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/TagExternalDocumentationBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagExternalDocumentationBuilder.kt index 805cd313..4e53c08b 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/TagExternalDocumentationBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagExternalDocumentationBuilder.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi +package io.github.smiley4.ktorswaggerui.builder.openapi import io.swagger.v3.oas.models.ExternalDocumentation diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteCollector.kt similarity index 76% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteCollector.kt index 4fac4256..040af3cc 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteCollector.kt @@ -1,6 +1,6 @@ -package io.github.smiley4.ktorswaggerui.spec.route +package io.github.smiley4.ktorswaggerui.builder.route -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.data.PluginConfigData import io.github.smiley4.ktorswaggerui.dsl.DocumentedRouteSelector import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute import io.ktor.http.HttpMethod @@ -19,7 +19,7 @@ class RouteCollector( /** * Collect all routes from the given application */ - fun collectRoutes(routeProvider: () -> Route, config: SwaggerUIPluginConfig): Sequence { + fun collectRoutes(routeProvider: () -> Route, config: PluginConfigData): Sequence { return allRoutes(routeProvider()) .asSequence() .map { route -> @@ -31,25 +31,10 @@ class RouteCollector( protected = documentation.protected ?: isProtected(route) ) } - .filter { removeLeadingSlash(it.path) != removeLeadingSlash(config.getSwaggerUI().swaggerUrl) } - .filter { removeLeadingSlash(it.path) != removeLeadingSlash("${config.getSwaggerUI().swaggerUrl}/api.json") } - .filter { removeLeadingSlash(it.path) != removeLeadingSlash("${config.getSwaggerUI().swaggerUrl}/{filename}") } - .filter { !config.getSwaggerUI().forwardRoot || it.path != "/" } .filter { !it.documentation.hidden } - .filter { path -> - config.pathFilter - ?.let { it(path.method, path.path.split("/").filter { it.isNotEmpty() }) } - ?: true - } + .filter { path -> config.pathFilter(path.method, path.path.split("/").filter { it.isNotEmpty() }) } } - private fun removeLeadingSlash(str: String): String = - if (str.startsWith("/")) { - str.substring(1) - } else { - str - } - private fun getDocumentation(route: Route, base: OpenApiRoute): OpenApiRoute { var documentation = base if (route.selector is DocumentedRouteSelector) { @@ -67,7 +52,7 @@ class RouteCollector( return (route.selector as HttpMethodRouteSelector).method } - private fun getPath(route: Route, config: SwaggerUIPluginConfig): String { + private fun getPath(route: Route, config: PluginConfigData): String { val selector = route.selector return if (isIgnoredSelector(selector, config)) { route.parent?.let { getPath(it, config) } ?: "" @@ -83,7 +68,7 @@ class RouteCollector( } } - private fun isIgnoredSelector(selector: RouteSelector, config: SwaggerUIPluginConfig): Boolean { + private fun isIgnoredSelector(selector: RouteSelector, config: PluginConfigData): Boolean { return when (selector) { is TrailingSlashRouteSelector -> false is RootRouteSelector -> false diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteDocumentationMerger.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteDocumentationMerger.kt similarity index 87% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteDocumentationMerger.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteDocumentationMerger.kt index 6c4cf509..f0509b9b 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteDocumentationMerger.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteDocumentationMerger.kt @@ -1,11 +1,15 @@ -package io.github.smiley4.ktorswaggerui.spec.route +package io.github.smiley4.ktorswaggerui.builder.route import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute class RouteDocumentationMerger { + /** + * Merges "a" with "b" and returns the result as a new [OpenApiRoute]. "a" has priority over "b". + */ fun merge(a: OpenApiRoute, b: OpenApiRoute): OpenApiRoute { return OpenApiRoute().apply { + specId = a.specId ?: b.specId tags = mutableListOf().also { it.addAll(a.tags) it.addAll(b.tags) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteMeta.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteMeta.kt similarity index 83% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteMeta.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteMeta.kt index e7975984..21452920 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteMeta.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteMeta.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.spec.route +package io.github.smiley4.ktorswaggerui.builder.route import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute import io.ktor.http.HttpMethod diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaBuilder.kt similarity index 98% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaBuilder.kt index e7f50baa..5d85e608 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaBuilder.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.spec.schema +package io.github.smiley4.ktorswaggerui.builder.schema import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContext.kt similarity index 93% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContext.kt index 4fb81fdf..ab0f9a56 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContext.kt @@ -1,12 +1,10 @@ -package io.github.smiley4.ktorswaggerui.spec.schema +package io.github.smiley4.ktorswaggerui.builder.schema -import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef import io.github.smiley4.ktorswaggerui.dsl.SchemaType import io.github.smiley4.ktorswaggerui.dsl.getSchemaType import io.github.smiley4.ktorswaggerui.dsl.getSimpleArrayElementTypeName import io.github.smiley4.ktorswaggerui.dsl.getSimpleTypeName import io.swagger.v3.oas.models.media.Schema -import java.lang.IllegalArgumentException import kotlin.collections.set @@ -52,22 +50,24 @@ class SchemaContext { } - fun addSchema(ref: CustomSchemaRef, schema: SchemaDefinitions) { - schemasCustom[ref.schemaId] = schema + fun addSchema(customSchemaId: String, schema: SchemaDefinitions) { + schemasCustom[customSchemaId] = schema } fun getComponentsSection(): Map> = componentsSection - fun getSchema(type: SchemaType) = getSchemaOrNull(type) ?: throw NoSuchElementException ("No schema for type '$type'!") + fun getSchema(type: SchemaType) = getSchemaOrNull(type) + ?: throw NoSuchElementException("No schema for type '$type'!") fun getSchemaOrNull(type: SchemaType) = inlineSchemas[type] - fun getSchema(ref: CustomSchemaRef) = getSchemaOrNull(ref) ?: throw NoSuchElementException("No schema for ref '$ref'!") + fun getSchema(customSchemaId: String) = getSchemaOrNull(customSchemaId) + ?: throw NoSuchElementException("No schema for ref '$customSchemaId'!") - fun getSchemaOrNull(ref: CustomSchemaRef) = inlineSchemasCustom[ref.schemaId] + fun getSchemaOrNull(customSchemaId: String) = inlineSchemasCustom[customSchemaId] fun finalize() { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContextBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContextBuilder.kt similarity index 59% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContextBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContextBuilder.kt index 923526a9..3dbea962 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContextBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContextBuilder.kt @@ -1,24 +1,27 @@ -package io.github.smiley4.ktorswaggerui.spec.schema - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.BaseCustomSchema -import io.github.smiley4.ktorswaggerui.dsl.CustomArraySchemaRef -import io.github.smiley4.ktorswaggerui.dsl.CustomJsonSchema -import io.github.smiley4.ktorswaggerui.dsl.CustomOpenApiSchema -import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef +package io.github.smiley4.ktorswaggerui.builder.schema + +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta +import io.github.smiley4.ktorswaggerui.data.BaseCustomSchema +import io.github.smiley4.ktorswaggerui.data.CustomJsonSchema +import io.github.smiley4.ktorswaggerui.data.CustomOpenApiSchema +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.data.RemoteSchema +import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.CollectionBodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.CustomRefBodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.EmptyBodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.OneOfBodyTypeDescriptor import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -import io.github.smiley4.ktorswaggerui.dsl.RemoteSchema +import io.github.smiley4.ktorswaggerui.dsl.SchemaBodyTypeDescriptor import io.github.smiley4.ktorswaggerui.dsl.SchemaType -import io.github.smiley4.ktorswaggerui.dsl.obj -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.swagger.v3.oas.models.media.Schema class SchemaContextBuilder( - private val config: SwaggerUIPluginConfig, + private val config: PluginConfigData, private val schemaBuilder: SchemaBuilder ) { @@ -26,9 +29,9 @@ class SchemaContextBuilder( return SchemaContext() .also { ctx -> routes.forEach { handle(ctx, it) } } .also { ctx -> - if (config.getCustomSchemas().includeAll) { - config.getCustomSchemas().getSchemas().forEach { (id, schema) -> - ctx.addSchema(obj(id), createSchema(schema, false)) + if (config.includeAllCustomSchemas) { + config.customSchemas.forEach { (id, schema) -> + ctx.addSchema(id, createSchema(schema)) } } } @@ -62,20 +65,29 @@ class SchemaContextBuilder( private fun handle(ctx: SchemaContext, body: OpenApiSimpleBody) { - if (body.customSchema != null) { - body.customSchema?.also { ctx.addSchema(it, createSchema(it)) } - } else { - body.type?.also { ctx.addSchema(it, createSchema(it)) } - } + addSchemas(ctx, body.type) } - private fun handle(ctx: SchemaContext, body: OpenApiMultipartBody) { body.getParts().forEach { part -> - if (part.customSchema != null) { - part.customSchema?.also { ctx.addSchema(it, createSchema(it)) } - } else { - part.type?.also { ctx.addSchema(it, createSchema(it)) } + part.type.also { addSchemas(ctx, part.type) } + } + } + + private fun addSchemas(ctx: SchemaContext, typeDescriptor: BodyTypeDescriptor) { + when (typeDescriptor) { + is EmptyBodyTypeDescriptor -> Unit + is SchemaBodyTypeDescriptor -> { + ctx.addSchema(typeDescriptor.schemaType, createSchema(typeDescriptor.schemaType)) + } + is CollectionBodyTypeDescriptor -> { + addSchemas(ctx, typeDescriptor.schemaType) + } + is OneOfBodyTypeDescriptor -> { + typeDescriptor.elements.forEach { addSchemas(ctx, it) } + } + is CustomRefBodyTypeDescriptor -> { + ctx.addSchema(typeDescriptor.customSchemaId, createSchema(typeDescriptor.customSchemaId)) } } } @@ -91,20 +103,20 @@ class SchemaContextBuilder( } - private fun createSchema(customSchemaRef: CustomSchemaRef): SchemaDefinitions { - val customSchema = config.getCustomSchemas().getSchema(customSchemaRef.schemaId) + private fun createSchema(customSchemaId: String): SchemaDefinitions { + val customSchema = config.customSchemas[customSchemaId] return if (customSchema == null) { SchemaDefinitions( root = Schema(), definitions = emptyMap() ) } else { - createSchema(customSchema, customSchemaRef is CustomArraySchemaRef) + createSchema(customSchema) } } - private fun createSchema(customSchema: BaseCustomSchema, isArray: Boolean): SchemaDefinitions { + private fun createSchema(customSchema: BaseCustomSchema): SchemaDefinitions { return when (customSchema) { is CustomJsonSchema -> { schemaBuilder.create(customSchema.provider()) @@ -125,18 +137,6 @@ class SchemaContextBuilder( definitions = emptyMap() ) } - }.let { schemaDefinitions -> - if (isArray) { - SchemaDefinitions( - root = Schema().apply { - type = "array" - items = schemaDefinitions.root - }, - definitions = schemaDefinitions.definitions - ) - } else { - schemaDefinitions - } } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaTypeAttributeOverride.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaTypeAttributeOverride.kt similarity index 95% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaTypeAttributeOverride.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaTypeAttributeOverride.kt index 0fc80fca..f160b369 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaTypeAttributeOverride.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaTypeAttributeOverride.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.spec.schema +package io.github.smiley4.ktorswaggerui.builder.schema import com.fasterxml.jackson.databind.node.ObjectNode import com.github.victools.jsonschema.generator.FieldScope diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/TypeOverwrites.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/TypeOverwrites.kt similarity index 88% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/TypeOverwrites.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/TypeOverwrites.kt index 12bbae0f..c74646f3 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/TypeOverwrites.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/TypeOverwrites.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.spec.schema +package io.github.smiley4.ktorswaggerui.builder.schema import io.github.smiley4.ktorswaggerui.dsl.getSchemaType import java.io.File diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthKeyLocation.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthKeyLocation.kt new file mode 100644 index 00000000..d94ab76a --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthKeyLocation.kt @@ -0,0 +1,9 @@ +package io.github.smiley4.ktorswaggerui.data + +import io.swagger.v3.oas.models.security.SecurityScheme + +enum class AuthKeyLocation(val swaggerType: SecurityScheme.In) { + QUERY(SecurityScheme.In.QUERY), + HEADER(SecurityScheme.In.HEADER), + COOKIE(SecurityScheme.In.COOKIE) +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthScheme.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthScheme.kt new file mode 100644 index 00000000..e90593d8 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthScheme.kt @@ -0,0 +1,14 @@ +package io.github.smiley4.ktorswaggerui.data + +// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml +enum class AuthScheme(val swaggerType: String) { + BASIC("Basic"), + BEARER("Bearer"), + DIGEST("Digest"), + HOBA("HOBA"), + MUTUAL("Mutual"), + OAUTH("OAuth"), + SCRAM_SHA_1("SCRAM-SHA-1"), + SCRAM_SHA_256("SCRAM-SHA-256"), + VAPID("vapid") +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthType.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthType.kt new file mode 100644 index 00000000..897ae471 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthType.kt @@ -0,0 +1,11 @@ +package io.github.smiley4.ktorswaggerui.data + +import io.swagger.v3.oas.models.security.SecurityScheme + +enum class AuthType(val swaggerType: SecurityScheme.Type) { + API_KEY(SecurityScheme.Type.APIKEY), + HTTP(SecurityScheme.Type.HTTP), + OAUTH2(SecurityScheme.Type.OAUTH2), + OPENID_CONNECT(SecurityScheme.Type.OPENIDCONNECT), + MUTUAL_TLS(SecurityScheme.Type.MUTUALTLS) +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/BaseCustomSchema.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/BaseCustomSchema.kt new file mode 100644 index 00000000..5be9f7fa --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/BaseCustomSchema.kt @@ -0,0 +1,11 @@ +package io.github.smiley4.ktorswaggerui.data + +import io.swagger.v3.oas.models.media.Schema + +sealed class BaseCustomSchema + +class CustomJsonSchema(val provider: () -> String) : BaseCustomSchema() + +class CustomOpenApiSchema(val provider: () -> Schema) : BaseCustomSchema() + +class RemoteSchema(val url: String) : BaseCustomSchema() diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ContactData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ContactData.kt new file mode 100644 index 00000000..4a5f6968 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ContactData.kt @@ -0,0 +1,15 @@ +package io.github.smiley4.ktorswaggerui.data + +data class ContactData( + val name: String?, + val url: String?, + val email: String? +) { + companion object { + val DEFAULT = ContactData( + name = null, + url = null, + email = null + ) + } +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/DataUtils.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/DataUtils.kt new file mode 100644 index 00000000..0a59d037 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/DataUtils.kt @@ -0,0 +1,11 @@ +package io.github.smiley4.ktorswaggerui.data + +object DataUtils { + + fun mergeBoolean(base: Boolean, value: Boolean) = if (value) true else base + + fun mergeDefault(base: T, value: T, default: T) = if (value != default) value else base + + fun merge(base: T?, value: T?) = value ?: base + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/EncodingData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/EncodingData.kt new file mode 100644 index 00000000..a87c9245 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/EncodingData.kt @@ -0,0 +1,99 @@ +package io.github.smiley4.ktorswaggerui.data + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.github.victools.jsonschema.generator.Option +import com.github.victools.jsonschema.generator.OptionPreset +import com.github.victools.jsonschema.generator.SchemaGenerator +import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder +import com.github.victools.jsonschema.generator.SchemaVersion +import com.github.victools.jsonschema.module.jackson.JacksonModule +import com.github.victools.jsonschema.module.swagger2.Swagger2Module +import io.github.smiley4.ktorswaggerui.dsl.ExampleEncoder +import io.github.smiley4.ktorswaggerui.dsl.SchemaEncoder +import io.github.smiley4.ktorswaggerui.dsl.SchemaType +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaTypeAttributeOverride +import kotlin.reflect.jvm.javaType + +data class EncodingData( + val exampleEncoder: ExampleEncoder, + val schemaEncoder: SchemaEncoder, + val schemaDefsField: String +) { + + companion object { + val DEFAULT = EncodingData( + exampleEncoder = defaultExampleEncoder(), + schemaEncoder = defaultSchemaEncoder(), + schemaDefsField = "\$defs" + ) + + + /** + * The default jackson object mapper used for encoding examples to json. + */ + var DEFAULT_EXAMPLE_OBJECT_MAPPER = jacksonObjectMapper() + + + /** + * The default [SchemaGenerator] used to encode types to json-schema. + * See https://victools.github.io/jsonschema-generator/#generator-options for more information. + */ + var DEFAULT_SCHEMA_GENERATOR = SchemaGenerator(schemaGeneratorConfigBuilder().build()) + + + /** + * The default [ExampleEncoder] + */ + fun defaultExampleEncoder(): ExampleEncoder { + return { _, value -> encodeExample(value) } + } + + + /** + * encode the given value to a json string + */ + fun encodeExample(value: Any?): String { + return if (value is String) { + value + } else { + DEFAULT_EXAMPLE_OBJECT_MAPPER.writeValueAsString(value) + } + } + + + /** + * The default [SchemaEncoder] + */ + fun defaultSchemaEncoder(): SchemaEncoder { + return { type -> encodeSchema(type) } + } + + + /** + * encode the given type to a json-schema + */ + fun encodeSchema(type: SchemaType): String { + return DEFAULT_SCHEMA_GENERATOR.generateSchema(type.javaType).toPrettyString() + } + + + /** + * The default [SchemaGeneratorConfigBuilder] + */ + fun schemaGeneratorConfigBuilder(): SchemaGeneratorConfigBuilder = + SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) + .with(JacksonModule()) + .with(Swagger2Module()) + .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) + .with(Option.ALLOF_CLEANUP_AT_THE_END) + .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) + .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) + .without(Option.INLINE_ALL_SCHEMAS) + .also { + it.forTypesInGeneral() + .withTypeAttributeOverride(SchemaTypeAttributeOverride()) + } + + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExternalDocsData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExternalDocsData.kt new file mode 100644 index 00000000..8ef5db07 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExternalDocsData.kt @@ -0,0 +1,15 @@ +package io.github.smiley4.ktorswaggerui.data + +data class ExternalDocsData( + val url: String, + val description: String?, +) { + + companion object { + val DEFAULT = ExternalDocsData( + url = "/", + description = null + ) + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt new file mode 100644 index 00000000..fe8e1642 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt @@ -0,0 +1,21 @@ +package io.github.smiley4.ktorswaggerui.data + +data class InfoData( + val title: String, + val version: String?, + val description: String?, + val termsOfService: String?, + val contact: ContactData?, + val license: LicenseData? +) { + companion object { + val DEFAULT = InfoData( + title = "API", + version = null, + description = null, + termsOfService = null, + contact = null, + license = null + ) + } +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt new file mode 100644 index 00000000..154be47b --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt @@ -0,0 +1,13 @@ +package io.github.smiley4.ktorswaggerui.data + +data class LicenseData( + val name: String?, + val url: String?, +) { + companion object { + val DEFAULT = LicenseData( + name = null, + url = null, + ) + } +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowData.kt new file mode 100644 index 00000000..3072dec0 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowData.kt @@ -0,0 +1,19 @@ +package io.github.smiley4.ktorswaggerui.data + +data class OpenIdOAuthFlowData( + val authorizationUrl: String? = null, + val tokenUrl: String? = null, + val refreshUrl: String? = null, + val scopes: Map? = null, +) { + + companion object { + val DEFAULT = OpenIdOAuthFlowData( + authorizationUrl = null, + tokenUrl = null, + refreshUrl = null, + scopes = null, + ) + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowsData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowsData.kt new file mode 100644 index 00000000..44acedf5 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowsData.kt @@ -0,0 +1,19 @@ +package io.github.smiley4.ktorswaggerui.data + +data class OpenIdOAuthFlowsData( + val implicit: OpenIdOAuthFlowData?, + val password: OpenIdOAuthFlowData?, + val clientCredentials: OpenIdOAuthFlowData?, + val authorizationCode: OpenIdOAuthFlowData?, +) { + + companion object { + val DEFAULT = OpenIdOAuthFlowsData( + implicit = null, + password = null, + clientCredentials = null, + authorizationCode = null, + ) + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PathFilter.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PathFilter.kt new file mode 100644 index 00000000..73a75534 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PathFilter.kt @@ -0,0 +1,6 @@ +package io.github.smiley4.ktorswaggerui.data + +import io.ktor.http.HttpMethod + + +typealias PathFilter = (method: HttpMethod, url: List) -> Boolean diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PluginConfigData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PluginConfigData.kt new file mode 100644 index 00000000..0e5a605f --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PluginConfigData.kt @@ -0,0 +1,47 @@ +package io.github.smiley4.ktorswaggerui.data + +import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl +import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse +import kotlin.reflect.KClass + +data class PluginConfigData( + val defaultUnauthorizedResponse: OpenApiResponse?, + val defaultSecuritySchemeNames: Set, + val tagGenerator: TagGenerator, + val specAssigner: SpecAssigner, + val pathFilter: PathFilter, + val ignoredRouteSelectors: Set>, + val swaggerUI: SwaggerUIData, + val info: InfoData, + val servers: List, + val externalDocs: ExternalDocsData, + val securitySchemes: List, + val tags: List, + val customSchemas: Map, + val includeAllCustomSchemas: Boolean, + val encoding: EncodingData, + val specConfigs: MutableMap +) { + + companion object { + val DEFAULT = PluginConfigData( + defaultUnauthorizedResponse = null, + defaultSecuritySchemeNames = emptySet(), + tagGenerator = { emptyList() }, + specAssigner = { _, _ -> PluginConfigDsl.DEFAULT_SPEC_ID }, + pathFilter = { _, _ -> true }, + ignoredRouteSelectors = emptySet(), + swaggerUI = SwaggerUIData.DEFAULT, + info = InfoData.DEFAULT, + servers = emptyList(), + externalDocs = ExternalDocsData.DEFAULT, + securitySchemes = emptyList(), + tags = emptyList(), + customSchemas = emptyMap(), + includeAllCustomSchemas = false, + encoding = EncodingData.DEFAULT, + specConfigs = mutableMapOf() + ) + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SecuritySchemeData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SecuritySchemeData.kt new file mode 100644 index 00000000..1e46bcaf --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SecuritySchemeData.kt @@ -0,0 +1,27 @@ +package io.github.smiley4.ktorswaggerui.data + +data class SecuritySchemeData( + val name: String, + val type: AuthType?, + val location: AuthKeyLocation?, + val scheme: AuthScheme?, + val bearerFormat: String?, + val flows: OpenIdOAuthFlowsData?, + val openIdConnectUrl: String?, + val description: String? +) { + + companion object { + val DEFAULT = SecuritySchemeData( + name = "", + type = null, + location = null, + scheme = null, + bearerFormat = null, + flows = null, + openIdConnectUrl = null, + description = null, + ) + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt new file mode 100644 index 00000000..a98ca613 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt @@ -0,0 +1,15 @@ +package io.github.smiley4.ktorswaggerui.data + +data class ServerData( + val url: String, + val description: String?, +) { + + companion object { + val DEFAULT = ServerData( + url = "/", + description = null + ) + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SpecAssigned.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SpecAssigned.kt new file mode 100644 index 00000000..28a5e840 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SpecAssigned.kt @@ -0,0 +1,7 @@ +package io.github.smiley4.ktorswaggerui.data + +/** + * url - the parts of the route-url split at all `/`. + * tags - the tags assigned to the route + */ +typealias SpecAssigner = (url: String, tags: List) -> String diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUIData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUIData.kt new file mode 100644 index 00000000..2b7014cf --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUIData.kt @@ -0,0 +1,29 @@ +package io.github.smiley4.ktorswaggerui.data + +data class SwaggerUIData( + val forwardRoot: Boolean, + val swaggerUrl: String, + val rootHostPath: String, + val authentication: String?, + val validatorUrl: String?, + val displayOperationId: Boolean, + val showTagFilterInput: Boolean, + val sort: SwaggerUiSort, + val syntaxHighlight: SwaggerUiSyntaxHighlight +) { + + companion object { + val DEFAULT = SwaggerUIData( + forwardRoot = false, + swaggerUrl = "swagger-ui", + rootHostPath = "", + authentication = null, + validatorUrl = null, + displayOperationId = false, + showTagFilterInput = false, + sort = SwaggerUiSort.NONE, + syntaxHighlight = SwaggerUiSyntaxHighlight.AGATE + ) + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSort.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSort.kt new file mode 100644 index 00000000..84a40b13 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSort.kt @@ -0,0 +1,20 @@ +package io.github.smiley4.ktorswaggerui.data + +enum class SwaggerUiSort(val value: String) { + /** + * The order returned by the server unchanged + */ + NONE("undefined"), + + + /** + * sort by paths alphanumerically + */ + ALPHANUMERICALLY("alpha"), + + + /** + * sort by HTTP method + */ + HTTP_METHOD("method") +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSyntaxHighlight.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSyntaxHighlight.kt new file mode 100644 index 00000000..749adf29 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSyntaxHighlight.kt @@ -0,0 +1,10 @@ +package io.github.smiley4.ktorswaggerui.data + +enum class SwaggerUiSyntaxHighlight(val value: String) { + AGATE("agate"), + ARTA("arta"), + MONOKAI("monokai"), + NORD("nord"), + OBSIDIAN("obsidian"), + TOMORROW_NIGHT("tomorrow-night") +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagData.kt new file mode 100644 index 00000000..29b30b6a --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagData.kt @@ -0,0 +1,18 @@ +package io.github.smiley4.ktorswaggerui.data + +data class TagData( + val name: String, + val description: String?, + val externalDocDescription: String?, + val externalDocUrl: String? +) { + + companion object { + val DEFAULT = TagData( + name = "", + description = null, + externalDocDescription = null, + externalDocUrl = null + ) + } +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagGenerator.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagGenerator.kt new file mode 100644 index 00000000..6ba542bb --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagGenerator.kt @@ -0,0 +1,7 @@ +package io.github.smiley4.ktorswaggerui.data + +/** + * url - the parts of the route-url split at all `/`. + * return a collection of tags. "Null"-entries will be ignored. + */ +typealias TagGenerator = (url: List) -> Collection diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/BodyTypeDescriptor.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/BodyTypeDescriptor.kt new file mode 100644 index 00000000..72d74f88 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/BodyTypeDescriptor.kt @@ -0,0 +1,130 @@ +package io.github.smiley4.ktorswaggerui.dsl + +import kotlin.reflect.KClass + +/** + * Describes the type/schema of a request or response body. + * [BodyTypeDescriptor]s can be nested to build more specific bodies from simple types + */ +sealed interface BodyTypeDescriptor { + + companion object { + + /** + * A [BodyTypeDescriptor] of the specific given type (or empty if type is null). + */ + fun typeOf(type: KClass<*>?) = type?.let { SchemaBodyTypeDescriptor(it.asSchemaType()) } ?: EmptyBodyTypeDescriptor() + + + /** + * A [BodyTypeDescriptor] of the specific given type (or empty if type is null). + */ + fun typeOf(type: SchemaType?) = type?.let { SchemaBodyTypeDescriptor(it) } ?: EmptyBodyTypeDescriptor() + + + /** + * A [BodyTypeDescriptor] of the specific given generic type. + */ + inline fun typeOf() = SchemaBodyTypeDescriptor(getSchemaType()) + + + /** + * Type can be any one of the given types. + */ + fun oneOf(vararg type: KClass<*>) = OneOfBodyTypeDescriptor(type.toList().map { typeOf(it.asSchemaType()) }) + + + /** + * Type can be any one of the given types. + */ + @JvmName("oneOfClass") + fun oneOf(types: Collection>) = OneOfBodyTypeDescriptor(types.map { typeOf(it.asSchemaType()) }) + + + /** + * Type can be any one of the given types. + */ + fun oneOf(vararg type: SchemaType) = OneOfBodyTypeDescriptor(type.map { typeOf(it) }) + + + /** + * Type can be any one of the given types. + */ + @JvmName("oneOfType") + fun oneOf(types: Collection) = OneOfBodyTypeDescriptor(types.map { typeOf(it) }) + + + /** + * Type can be any one of the given types. + */ + fun oneOf(vararg type: BodyTypeDescriptor) = OneOfBodyTypeDescriptor(type.toList()) + + + /** + * Type can be any one of the given types. + */ + @JvmName("oneOfDescriptor") + fun oneOf(types: Collection) = OneOfBodyTypeDescriptor(types.toList()) + + + /** + * Type is an array of the specific given type. + */ + fun multipleOf(type: KClass<*>) = CollectionBodyTypeDescriptor(typeOf(type.asSchemaType())) + + + /** + * Type is an array of the specific given type. + */ + fun multipleOf(type: SchemaType) = CollectionBodyTypeDescriptor(typeOf(type)) + + + /** + * Type is an array of the given type. + */ + fun multipleOf(type: BodyTypeDescriptor) = CollectionBodyTypeDescriptor(type) + + + /** + * A [BodyTypeDescriptor] of the specific given custom schema. + */ + fun custom(customSchemaId: String) = CustomRefBodyTypeDescriptor(customSchemaId) + + + /** + * An empty type. + */ + fun empty() = EmptyBodyTypeDescriptor() + } + +} + + +/** + * Describes an empty type + */ +class EmptyBodyTypeDescriptor : BodyTypeDescriptor + + +/** + * Describes a specific type/schema + */ +class SchemaBodyTypeDescriptor(val schemaType: SchemaType) : BodyTypeDescriptor + + +/** + * Describes any one of the given types + */ +class OneOfBodyTypeDescriptor(val elements: List) : BodyTypeDescriptor + + +/** + * Describes an array of the given type + */ +class CollectionBodyTypeDescriptor(val schemaType: BodyTypeDescriptor) : BodyTypeDescriptor + + +/** + * Describes the custom schema/type with the given id + */ +class CustomRefBodyTypeDescriptor(val customSchemaId: String) : BodyTypeDescriptor diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemaRef.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemaRef.kt index ef0c7fd2..417bef81 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemaRef.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemaRef.kt @@ -1,13 +1,14 @@ package io.github.smiley4.ktorswaggerui.dsl -sealed class CustomSchemaRef( - val schemaId: String +@Deprecated( + "Use BodyTypeDescriptor instead", + ReplaceWith("BodyTypeDescriptor.custom(schemaId)") ) +fun obj(schemaId: String) = BodyTypeDescriptor.custom(schemaId) -class CustomObjectSchemaRef(schemaId: String) : CustomSchemaRef(schemaId) -class CustomArraySchemaRef(schemaId: String) : CustomSchemaRef(schemaId) - -fun obj(schemaId: String) = CustomObjectSchemaRef(schemaId) - -fun array(schemaId: String) = CustomArraySchemaRef(schemaId) +@Deprecated( + "Use BodyTypeDescriptor instead", + ReplaceWith("BodyTypeDescriptor.multipleOf(BodyTypeDescriptor.custom(schemaId))") +) +fun array(schemaId: String) = BodyTypeDescriptor.multipleOf(BodyTypeDescriptor.custom(schemaId)) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt index 6aa07e64..11f5e8fe 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt @@ -1,5 +1,9 @@ package io.github.smiley4.ktorswaggerui.dsl +import io.github.smiley4.ktorswaggerui.data.BaseCustomSchema +import io.github.smiley4.ktorswaggerui.data.CustomJsonSchema +import io.github.smiley4.ktorswaggerui.data.CustomOpenApiSchema +import io.github.smiley4.ktorswaggerui.data.RemoteSchema import io.swagger.v3.oas.models.media.Schema @OpenApiDslMarker @@ -43,10 +47,3 @@ class CustomSchemas { } -sealed class BaseCustomSchema - -class CustomJsonSchema(val provider: () -> String) : BaseCustomSchema() - -class CustomOpenApiSchema(val provider: () -> Schema) : BaseCustomSchema() - -class RemoteSchema(val url: String) : BaseCustomSchema() diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt index 9acf46b6..1714a227 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt @@ -1,15 +1,7 @@ package io.github.smiley4.ktorswaggerui.dsl -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.github.victools.jsonschema.generator.Option -import com.github.victools.jsonschema.generator.OptionPreset -import com.github.victools.jsonschema.generator.SchemaGenerator -import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder -import com.github.victools.jsonschema.generator.SchemaVersion -import com.github.victools.jsonschema.module.jackson.JacksonModule -import com.github.victools.jsonschema.module.swagger2.Swagger2Module -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaTypeAttributeOverride -import kotlin.reflect.jvm.javaType +import io.github.smiley4.ktorswaggerui.data.DataUtils.mergeDefault +import io.github.smiley4.ktorswaggerui.data.EncodingData typealias ExampleEncoder = (type: SchemaType?, example: Any) -> String? @@ -29,9 +21,7 @@ class EncodingConfig { exampleEncoder = encoder } - private var exampleEncoder: ExampleEncoder = defaultExampleEncoder() - - fun getExampleEncoder() = exampleEncoder + private var exampleEncoder: ExampleEncoder = EncodingData.DEFAULT.exampleEncoder /** @@ -42,85 +32,19 @@ class EncodingConfig { schemaEncoder = encoder } - private var schemaEncoder: SchemaEncoder = defaultSchemaEncoder() - - fun getSchemaEncoder() = schemaEncoder + private var schemaEncoder: SchemaEncoder = EncodingData.DEFAULT.schemaEncoder /** * the name of the field (if it exists) in the json-schema containing schema-definitions. */ - var schemaDefinitionsField = "\$defs" - - companion object { - - /** - * The default jackson object mapper used for encoding examples to json. - */ - var DEFAULT_EXAMPLE_OBJECT_MAPPER = jacksonObjectMapper() - - - /** - * The default [SchemaGenerator] used to encode types to json-schema. - * See https://victools.github.io/jsonschema-generator/#generator-options for more information. - */ - var DEFAULT_SCHEMA_GENERATOR = SchemaGenerator(schemaGeneratorConfigBuilder().build()) - - - /** - * The default [ExampleEncoder] - */ - fun defaultExampleEncoder(): ExampleEncoder { - return { _, value -> encodeExample(value) } - } - + var schemaDefinitionsField = EncodingData.DEFAULT.schemaDefsField - /** - * encode the given value to a json string - */ - fun encodeExample(value: Any?): String { - return if (value is String) { - value - } else { - DEFAULT_EXAMPLE_OBJECT_MAPPER.writeValueAsString(value) - } - } - - /** - * The default [SchemaEncoder] - */ - fun defaultSchemaEncoder(): SchemaEncoder { - return { type -> encodeSchema(type) } - } - - - /** - * encode the given type to a json-schema - */ - fun encodeSchema(type: SchemaType): String { - return DEFAULT_SCHEMA_GENERATOR.generateSchema(type.javaType).toPrettyString() - } - - - /** - * The default [SchemaGeneratorConfigBuilder] - */ - fun schemaGeneratorConfigBuilder(): SchemaGeneratorConfigBuilder = - SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) - .with(JacksonModule()) - .with(Swagger2Module()) - .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) - .with(Option.ALLOF_CLEANUP_AT_THE_END) - .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) - .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .without(Option.INLINE_ALL_SCHEMAS) - .also { - it.forTypesInGeneral() - .withTypeAttributeOverride(SchemaTypeAttributeOverride()) - } - - - } + fun build(base: EncodingData) = EncodingData( + exampleEncoder = mergeDefault(base.exampleEncoder, exampleEncoder, EncodingData.DEFAULT.exampleEncoder), + schemaEncoder = mergeDefault(base.schemaEncoder, schemaEncoder, EncodingData.DEFAULT.schemaEncoder), + schemaDefsField = mergeDefault(base.schemaDefsField, schemaDefinitionsField, EncodingData.DEFAULT.schemaDefsField), + ) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiContact.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiContact.kt index 06403d20..7ab26b64 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiContact.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiContact.kt @@ -1,5 +1,8 @@ package io.github.smiley4.ktorswaggerui.dsl +import io.github.smiley4.ktorswaggerui.data.ContactData +import io.github.smiley4.ktorswaggerui.data.DataUtils.merge + /** * Contact information for the exposed API. */ @@ -9,18 +12,25 @@ class OpenApiContact { /** * The identifying name of the contact person/organization. */ - var name: String? = null + var name: String? = ContactData.DEFAULT.name /** * The URL pointing to the contact information. MUST be in the format of a URL. */ - var url: String? = null + var url: String? = ContactData.DEFAULT.url /** * The email address of the contact person/organization. MUST be in the format of an email address. */ - var email: String? = null + var email: String? = ContactData.DEFAULT.email + + + fun build(base: ContactData) = ContactData( + name = merge(base.name, name), + url = merge(base.url, url), + email = merge(base.email, email) + ) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiExternalDocs.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiExternalDocs.kt index c1ce30dd..7fd85bce 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiExternalDocs.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiExternalDocs.kt @@ -1,5 +1,8 @@ package io.github.smiley4.ktorswaggerui.dsl +import io.github.smiley4.ktorswaggerui.data.DataUtils +import io.github.smiley4.ktorswaggerui.data.ExternalDocsData + /** * An object representing external documentation. */ @@ -10,8 +13,15 @@ class OpenApiExternalDocs { */ var description: String? = null + /** * A URL to the external documentation */ var url: String = "/" + + fun build(base: ExternalDocsData) = ExternalDocsData( + url = DataUtils.mergeDefault(base.url, url, ExternalDocsData.DEFAULT.url), + description = DataUtils.merge(base.description, description) + ) + } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiInfo.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiInfo.kt index 9671e43f..98efa891 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiInfo.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiInfo.kt @@ -1,5 +1,11 @@ package io.github.smiley4.ktorswaggerui.dsl +import io.github.smiley4.ktorswaggerui.data.ContactData +import io.github.smiley4.ktorswaggerui.data.DataUtils.merge +import io.github.smiley4.ktorswaggerui.data.DataUtils.mergeDefault +import io.github.smiley4.ktorswaggerui.data.InfoData +import io.github.smiley4.ktorswaggerui.data.LicenseData + @OpenApiDslMarker class OpenApiInfo { @@ -26,9 +32,9 @@ class OpenApiInfo { */ var termsOfService: String? = null - private var contact: OpenApiContact? = null + /** * The contact information for the exposed API. */ @@ -36,11 +42,10 @@ class OpenApiInfo { contact = OpenApiContact().apply(block) } - fun getContact() = contact - private var license: OpenApiLicense? = null + /** * The license information for the exposed API. */ @@ -48,6 +53,15 @@ class OpenApiInfo { license = OpenApiLicense().apply(block) } - fun getLicense() = license + fun build(base: InfoData): InfoData { + return InfoData( + title = mergeDefault(base.title, this.title, InfoData.DEFAULT.title), + version = merge(base.version, this.version), + description = merge(base.description, this.description), + termsOfService = merge(base.termsOfService, this.termsOfService), + contact = contact?.build(base.contact ?: ContactData.DEFAULT) ?: base.contact, + license = license?.build(base.license ?: LicenseData.DEFAULT) ?: base.license + ) + } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiLicense.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiLicense.kt index b15cace3..5faf8d9f 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiLicense.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiLicense.kt @@ -1,5 +1,8 @@ package io.github.smiley4.ktorswaggerui.dsl +import io.github.smiley4.ktorswaggerui.data.DataUtils +import io.github.smiley4.ktorswaggerui.data.LicenseData + /** * License information for the exposed API. */ @@ -9,12 +12,17 @@ class OpenApiLicense { /** * The license name used for the API */ - var name: String = "?" + var name: String? = LicenseData.DEFAULT.name /** * A URL to the license used for the API. MUST be in the format of a URL. */ - var url: String? = null + var url: String? = LicenseData.DEFAULT.url + + fun build(base: LicenseData) = LicenseData( + name = DataUtils.merge(base.name, name), + url = DataUtils.merge(base.url, url), + ) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt index 67f2568b..fd5a61d2 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt @@ -18,7 +18,7 @@ class OpenApiMultipartBody : OpenApiBaseBody() { /** * One part of a multipart-body */ - fun part(name: String, type: SchemaType, block: OpenApiMultipartPart.() -> Unit) { + fun part(name: String, type: BodyTypeDescriptor, block: OpenApiMultipartPart.() -> Unit) { parts.add(OpenApiMultipartPart(name, type).apply(block)) } @@ -26,41 +26,38 @@ class OpenApiMultipartBody : OpenApiBaseBody() { /** * One part of a multipart-body */ - fun part(name: String, type: KClass<*>) = part(name, type.asSchemaType()) {} + fun part(name: String, type: BodyTypeDescriptor) = part(name, type) {} /** * One part of a multipart-body */ - inline fun part(name: String) = part(name, getSchemaType()) {} + fun part(name: String, type: SchemaType, block: OpenApiMultipartPart.() -> Unit) = part(name, BodyTypeDescriptor.typeOf(type), block) /** * One part of a multipart-body */ - inline fun part(name: String, noinline block: OpenApiMultipartPart.() -> Unit) = part(name, getSchemaType(), block) + fun part(name: String, type: KClass<*>) = part(name, type.asSchemaType()) {} /** * One part of a multipart-body */ - fun part(name: String, customSchema: CustomSchemaRef, block: OpenApiMultipartPart.() -> Unit) { - parts.add(OpenApiMultipartPart(name, null).apply(block).apply { - this.customSchema = customSchema - }) - } + inline fun part(name: String) = part(name, getSchemaType()) {} /** * One part of a multipart-body */ - fun part(name: String, customSchema: CustomSchemaRef) = part(name, customSchema) {} + inline fun part(name: String, noinline block: OpenApiMultipartPart.() -> Unit) = part(name, getSchemaType(), block) /** * One part of a multipart-body */ - fun part(name: String, customSchemaId: String, block: OpenApiMultipartPart.() -> Unit) = part(name, obj(customSchemaId), block) + fun part(name: String, customSchemaId: String, block: OpenApiMultipartPart.() -> Unit) = + part(name, BodyTypeDescriptor.custom(customSchemaId), block) /** diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt index f06e8088..60ae1f7d 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt @@ -14,15 +14,9 @@ class OpenApiMultipartPart( */ val name: String, - val type: SchemaType? + val type: BodyTypeDescriptor ) { - /** - * reference to a custom schema (alternative to 'type') - */ - var customSchema: CustomSchemaRef? = null - - /** * Set a specific content type for this part */ diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequest.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequest.kt index 0ef7ad49..0f851e7b 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequest.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequest.kt @@ -110,23 +110,21 @@ class OpenApiRequest { /** - * The request body applicable for this operation + * The body returned with this request */ - fun body(type: SchemaType?, block: OpenApiSimpleBody.() -> Unit) { - body = OpenApiSimpleBody(type).apply(block) + fun body(typeDescriptor: BodyTypeDescriptor, block: OpenApiSimpleBody.() -> Unit) { + body = OpenApiSimpleBody(typeDescriptor).apply(block) } /** - * The request body applicable for this operation + * The body returned with this request */ - fun body(type: KClass<*>, block: OpenApiSimpleBody.() -> Unit) = body(type.asSchemaType(), block) - + fun body(typeDescriptor: BodyTypeDescriptor) = body(typeDescriptor) {} /** * The request body applicable for this operation */ - @JvmName("bodyGenericType") - inline fun body(noinline block: OpenApiSimpleBody.() -> Unit) = body(getSchemaType(), block) + fun body(type: SchemaType?, block: OpenApiSimpleBody.() -> Unit) = body(BodyTypeDescriptor.typeOf(type), block) /** @@ -138,41 +136,38 @@ class OpenApiRequest { /** * The request body applicable for this operation */ - inline fun body() = body(getSchemaType()) {} + fun body(type: KClass<*>, block: OpenApiSimpleBody.() -> Unit) = body(type.asSchemaType(), block) /** * The request body applicable for this operation */ - fun body(block: OpenApiSimpleBody.() -> Unit) = body(null, block) + @JvmName("bodyGenericType") + inline fun body(noinline block: OpenApiSimpleBody.() -> Unit) = body(getSchemaType(), block) /** - * The body returned with this request + * The request body applicable for this operation */ - fun body(customSchema: CustomSchemaRef, block: OpenApiSimpleBody.() -> Unit) { - body = OpenApiSimpleBody(null).apply(block).apply { - this.customSchema = customSchema - } - } + inline fun body() = body(getSchemaType()) {} /** - * The body returned with this request + * The request body applicable for this operation */ - fun body(customSchema: CustomSchemaRef) = body(customSchema) {} + fun body(block: OpenApiSimpleBody.() -> Unit) = body(BodyTypeDescriptor.empty(), block) /** * The body returned with this request */ - fun body(customSchemaId: String, block: OpenApiSimpleBody.() -> Unit) = body(obj(customSchemaId), block) + fun body(customSchemaId: String) = body(customSchemaId) {} /** * The body returned with this request */ - fun body(customSchemaId: String) = body(customSchemaId) {} + fun body(customSchemaId: String, block: OpenApiSimpleBody.() -> Unit) = body(BodyTypeDescriptor.custom(customSchemaId), block) /** @@ -190,5 +185,4 @@ class OpenApiRequest { this.body = body } - } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponse.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponse.kt index 77b244a4..4d9c4b92 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponse.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponse.kt @@ -61,7 +61,7 @@ class OpenApiResponse(val statusCode: String) { /** * The body returned with this response */ - fun body(type: SchemaType?, block: OpenApiSimpleBody.() -> Unit) { + fun body(type: BodyTypeDescriptor, block: OpenApiSimpleBody.() -> Unit) { body = OpenApiSimpleBody(type).apply(block) } @@ -69,54 +69,49 @@ class OpenApiResponse(val statusCode: String) { /** * The body returned with this response */ - fun body(type: KClass<*>, block: OpenApiSimpleBody.() -> Unit) = body(type.asSchemaType(), block) + fun body(type: BodyTypeDescriptor) = body(type) {} /** * The body returned with this response */ - @JvmName("bodyGenericType") - inline fun body(noinline block: OpenApiSimpleBody.() -> Unit) = body(getSchemaType(), block) + fun body(type: SchemaType?, block: OpenApiSimpleBody.() -> Unit) = body(BodyTypeDescriptor.typeOf(type), block) /** * The body returned with this response */ - fun body(type: KClass<*>) = body(type) {} + fun body(type: KClass<*>, block: OpenApiSimpleBody.() -> Unit) = body(type.asSchemaType(), block) /** * The body returned with this response */ - inline fun body() = body(getSchemaType()) {} + @JvmName("bodyGenericType") + inline fun body(noinline block: OpenApiSimpleBody.() -> Unit) = body(getSchemaType(), block) /** * The body returned with this response */ - fun body(block: OpenApiSimpleBody.() -> Unit) = body(null, block) + fun body(type: KClass<*>) = body(type) {} /** * The body returned with this response */ - fun body(customSchema: CustomSchemaRef, block: OpenApiSimpleBody.() -> Unit) { - body = OpenApiSimpleBody(null).apply(block).apply { - this.customSchema = customSchema - } - } + inline fun body() = body(getSchemaType()) {} /** * The body returned with this response */ - fun body(customSchema: CustomSchemaRef) = body(customSchema) {} - + fun body(block: OpenApiSimpleBody.() -> Unit) = body(null, block) /** * The body returned with this response */ - fun body(customSchemaId: String, block: OpenApiSimpleBody.() -> Unit) = body(obj(customSchemaId), block) + fun body(customSchemaId: String, block: OpenApiSimpleBody.() -> Unit) = body(BodyTypeDescriptor.custom(customSchemaId), block) /** diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRoute.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRoute.kt index e3781dfb..68dc0dbc 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRoute.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRoute.kt @@ -3,6 +3,11 @@ package io.github.smiley4.ktorswaggerui.dsl @OpenApiDslMarker class OpenApiRoute { + /** + * the id of the openapi-spec this route belongs to. 'Null' to use default spec. + */ + var specId: String? = null + /** * A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources or any other qualifier */ diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSecurityScheme.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSecurityScheme.kt index 9754230c..4a6e265d 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSecurityScheme.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSecurityScheme.kt @@ -1,35 +1,11 @@ package io.github.smiley4.ktorswaggerui.dsl -import io.swagger.v3.oas.models.security.SecurityScheme - - -enum class AuthType(val swaggerType: SecurityScheme.Type) { - API_KEY(SecurityScheme.Type.APIKEY), - HTTP(SecurityScheme.Type.HTTP), - OAUTH2(SecurityScheme.Type.OAUTH2), - OPENID_CONNECT(SecurityScheme.Type.OPENIDCONNECT), - MUTUAL_TLS(SecurityScheme.Type.MUTUALTLS) -} - -enum class AuthKeyLocation(val swaggerType: SecurityScheme.In) { - QUERY(SecurityScheme.In.QUERY), - HEADER(SecurityScheme.In.HEADER), - COOKIE(SecurityScheme.In.COOKIE) -} - - -// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml -enum class AuthScheme(val swaggerType: String) { - BASIC("Basic"), - BEARER("Bearer"), - DIGEST("Digest"), - HOBA("HOBA"), - MUTUAL("Mutual"), - OAUTH("OAuth"), - SCRAM_SHA_1("SCRAM-SHA-1"), - SCRAM_SHA_256("SCRAM-SHA-256"), - VAPID("vapid") -} +import io.github.smiley4.ktorswaggerui.data.AuthKeyLocation +import io.github.smiley4.ktorswaggerui.data.AuthScheme +import io.github.smiley4.ktorswaggerui.data.AuthType +import io.github.smiley4.ktorswaggerui.data.DataUtils.merge +import io.github.smiley4.ktorswaggerui.data.OpenIdOAuthFlowsData +import io.github.smiley4.ktorswaggerui.data.SecuritySchemeData /** @@ -83,9 +59,6 @@ class OpenApiSecurityScheme( } - fun getFlows() = flows - - /** * OpenId Connect URL to discover OAuth2 configuration values. * Required for type [AuthType.OPENID_CONNECT] @@ -98,4 +71,15 @@ class OpenApiSecurityScheme( */ var description: String? = null + + fun build(base: SecuritySchemeData) = SecuritySchemeData( + name = name, + type = merge(base.type, type), + location = merge(base.location, location), + scheme = merge(base.scheme, scheme), + bearerFormat = merge(base.bearerFormat, bearerFormat), + flows = flows?.build(base.flows ?: OpenIdOAuthFlowsData.DEFAULT) ?: base.flows, + openIdConnectUrl = merge(base.openIdConnectUrl, openIdConnectUrl), + description = merge(base.description, description), + ) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiServer.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiServer.kt index c8914097..74367b63 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiServer.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiServer.kt @@ -1,5 +1,9 @@ package io.github.smiley4.ktorswaggerui.dsl +import io.github.smiley4.ktorswaggerui.data.DataUtils.merge +import io.github.smiley4.ktorswaggerui.data.DataUtils.mergeDefault +import io.github.smiley4.ktorswaggerui.data.ServerData + /** * An object representing a Server. */ @@ -10,12 +14,17 @@ class OpenApiServer { * A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to * the location where the OpenAPI document is being served */ - var url: String = "/" + var url: String = ServerData.DEFAULT.url /** * An optional string describing the host designated by the URL */ - var description: String? = null + var description: String? = ServerData.DEFAULT.description + + fun build(base: ServerData) = ServerData( + url = mergeDefault(base.url, url, ServerData.DEFAULT.url), + description = merge(base.description, description) + ) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSimpleBody.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSimpleBody.kt index 4ea77e51..cd2de62a 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSimpleBody.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSimpleBody.kt @@ -9,15 +9,9 @@ class OpenApiSimpleBody( /** * The type defining the schema used for the body. */ - val type: SchemaType?, + val type: BodyTypeDescriptor, ) : OpenApiBaseBody() { - /** - * reference to a custom schema (alternative to 'type') - */ - var customSchema: CustomSchemaRef? = null - - /** * Examples for this body */ diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiTag.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiTag.kt index 0fcf5f6b..2d81c93b 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiTag.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiTag.kt @@ -1,5 +1,8 @@ package io.github.smiley4.ktorswaggerui.dsl +import io.github.smiley4.ktorswaggerui.data.DataUtils.merge +import io.github.smiley4.ktorswaggerui.data.TagData + /** * Adds metadata to a single tag. */ @@ -28,4 +31,12 @@ class OpenApiTag( */ var externalDocUrl: String? = null + + fun build(base: TagData) = TagData( + name = name, + description = merge(base.description, description), + externalDocDescription = merge(base.externalDocDescription, externalDocDescription), + externalDocUrl = merge(base.externalDocUrl, externalDocUrl) + ) + } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlow.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlow.kt index 2e31e198..55ececa5 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlow.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlow.kt @@ -1,4 +1,8 @@ package io.github.smiley4.ktorswaggerui.dsl + +import io.github.smiley4.ktorswaggerui.data.DataUtils.merge +import io.github.smiley4.ktorswaggerui.data.OpenIdOAuthFlowData + /** * Configuration details for a supported OAuth Flow */ @@ -28,4 +32,12 @@ class OpenIdOAuthFlow { */ var scopes: Map? = null + + fun build(base: OpenIdOAuthFlowData) = OpenIdOAuthFlowData( + authorizationUrl = merge(base.authorizationUrl, authorizationUrl), + tokenUrl = merge(base.tokenUrl, tokenUrl), + refreshUrl = merge(base.refreshUrl, refreshUrl), + scopes = merge(base.scopes, scopes), + ) + } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlows.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlows.kt index 123a4417..b13bb47b 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlows.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlows.kt @@ -1,5 +1,8 @@ package io.github.smiley4.ktorswaggerui.dsl +import io.github.smiley4.ktorswaggerui.data.OpenIdOAuthFlowData +import io.github.smiley4.ktorswaggerui.data.OpenIdOAuthFlowsData + /** * An object containing configuration information for the oauth flow types supported */ @@ -17,9 +20,6 @@ class OpenIdOAuthFlows { } - fun getImplicit() = implicit - - private var password: OpenIdOAuthFlow? = null @@ -31,9 +31,6 @@ class OpenIdOAuthFlows { } - fun getPassword() = password - - private var clientCredentials: OpenIdOAuthFlow? = null @@ -45,9 +42,6 @@ class OpenIdOAuthFlows { } - fun getClientCredentials() = clientCredentials - - private var authorizationCode: OpenIdOAuthFlow? = null @@ -59,6 +53,11 @@ class OpenIdOAuthFlows { } - fun getAuthorizationCode() = authorizationCode + fun build(base: OpenIdOAuthFlowsData) = OpenIdOAuthFlowsData( + implicit = implicit?.build(base.implicit ?: OpenIdOAuthFlowData.DEFAULT) ?: base.implicit, + password = password?.build(base.password ?: OpenIdOAuthFlowData.DEFAULT) ?: base.password, + clientCredentials = clientCredentials?.build(base.clientCredentials ?: OpenIdOAuthFlowData.DEFAULT) ?: base.clientCredentials, + authorizationCode = authorizationCode?.build(base.authorizationCode ?: OpenIdOAuthFlowData.DEFAULT) ?: base.authorizationCode, + ) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/PluginConfigDsl.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/PluginConfigDsl.kt new file mode 100644 index 00000000..b89ef3e4 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/PluginConfigDsl.kt @@ -0,0 +1,211 @@ +package io.github.smiley4.ktorswaggerui.dsl + +import io.github.smiley4.ktorswaggerui.data.DataUtils.merge +import io.github.smiley4.ktorswaggerui.data.DataUtils.mergeBoolean +import io.github.smiley4.ktorswaggerui.data.PathFilter +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.data.SecuritySchemeData +import io.github.smiley4.ktorswaggerui.data.ServerData +import io.github.smiley4.ktorswaggerui.data.SpecAssigner +import io.github.smiley4.ktorswaggerui.data.TagData +import io.github.smiley4.ktorswaggerui.data.TagGenerator +import io.ktor.http.HttpStatusCode +import io.ktor.server.routing.RouteSelector +import kotlin.reflect.KClass + +/** + * Main-Configuration of the "SwaggerUI"-Plugin + */ +@OpenApiDslMarker +class PluginConfigDsl { + + companion object { + const val DEFAULT_SPEC_ID = "api" + } + + + private val specConfigs = mutableMapOf() + + fun spec(specId: String, block: PluginConfigDsl.() -> Unit) { + specConfigs[specId] = PluginConfigDsl().apply(block) + } + + + /** + * Default response to automatically add to each protected route for the "Unauthorized"-Response-Code. + * Generated response can be overwritten with custom response. + */ + fun defaultUnauthorizedResponse(block: OpenApiResponse.() -> Unit) { + defaultUnauthorizedResponse = OpenApiResponse(HttpStatusCode.Unauthorized.value.toString()).apply(block) + } + + private var defaultUnauthorizedResponse: OpenApiResponse? = PluginConfigData.DEFAULT.defaultUnauthorizedResponse + + + /** + * The name of the security scheme to use for the protected paths + */ + var defaultSecuritySchemeName: String? = null + + + /** + * The names of the security schemes available for use for the protected paths + */ + var defaultSecuritySchemeNames: Collection? = PluginConfigData.DEFAULT.defaultSecuritySchemeNames + + + /** + * Automatically add tags to the route with the given url. + * The returned (non-null) tags will be added to the tags specified in the route-specific documentation. + */ + fun generateTags(generator: TagGenerator) { + tagGenerator = generator + } + + private var tagGenerator: TagGenerator? = PluginConfigData.DEFAULT.tagGenerator + + + /** + * Assigns routes without an [io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute.specId] to a specified openapi-spec. + */ + var specAssigner: SpecAssigner? = PluginConfigData.DEFAULT.specAssigner + + + /** + * Filter to apply to all routes. Return 'false' for routes to not include them in the OpenApi-Spec and Swagger-UI. + * The url of the paths are already split at '/'. + */ + var pathFilter: PathFilter? = PluginConfigData.DEFAULT.pathFilter + + + /** + * Swagger-UI configuration + */ + fun swagger(block: SwaggerUIDsl.() -> Unit) { + swaggerUI = SwaggerUIDsl().apply(block) + } + + private var swaggerUI = SwaggerUIDsl() + + + /** + * OpenAPI info configuration - provides metadata about the API + */ + fun info(block: OpenApiInfo.() -> Unit) { + info = OpenApiInfo().apply(block) + } + + private var info = OpenApiInfo() + + + /** + * OpenAPI server configuration - an array of servers, which provide connectivity information to a target server + */ + fun server(block: OpenApiServer.() -> Unit) { + servers.add(OpenApiServer().apply(block)) + } + + private val servers = mutableListOf() + + + /** + * OpenAPI external docs configuration - link and description of an external documentation + */ + fun externalDocs(block: OpenApiExternalDocs.() -> Unit) { + externalDocs = OpenApiExternalDocs().apply(block) + } + + private var externalDocs = OpenApiExternalDocs() + + + /** + * Defines security schemes that can be used by operations + */ + fun securityScheme(name: String, block: OpenApiSecurityScheme.() -> Unit) { + securitySchemes.add(OpenApiSecurityScheme(name).apply(block)) + } + + private val securitySchemes = mutableListOf() + + + /** + * Tags used by the specification with additional metadata. Not all tags that are used must be declared + */ + fun tag(name: String, block: OpenApiTag.() -> Unit) { + tags.add(OpenApiTag(name).apply(block)) + } + + private val tags = mutableListOf() + + + /** + * Custom schemas to reference via [io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef] + */ + fun customSchemas(block: CustomSchemas.() -> Unit) { + this.customSchemas = CustomSchemas().apply(block) + } + + private var customSchemas = CustomSchemas() + + + /** + * customize the behaviour of different encoders (examples, schemas, ...) + */ + fun encoding(block: EncodingConfig.() -> Unit) { + block(encodingConfig) + } + + val encodingConfig: EncodingConfig = EncodingConfig() + + + /** + * List of all [RouteSelector] types in that should be ignored in the resulting url of any route. + */ + var ignoredRouteSelectors: Set> = PluginConfigData.DEFAULT.ignoredRouteSelectors + + + internal fun build(base: PluginConfigData): PluginConfigData { + return PluginConfigData( + defaultUnauthorizedResponse = merge(base.defaultUnauthorizedResponse, defaultUnauthorizedResponse), + defaultSecuritySchemeNames = buildSet { + addAll(base.defaultSecuritySchemeNames) + defaultSecuritySchemeNames?.also { addAll(it) } + defaultSecuritySchemeName?.also { add(it) } + }, + tagGenerator = merge(base.tagGenerator, tagGenerator) ?: PluginConfigData.DEFAULT.tagGenerator, + specAssigner = merge(base.specAssigner, specAssigner) ?: PluginConfigData.DEFAULT.specAssigner, + pathFilter = merge(base.pathFilter, pathFilter) ?: PluginConfigData.DEFAULT.pathFilter, + ignoredRouteSelectors = buildSet { + addAll(base.ignoredRouteSelectors) + addAll(ignoredRouteSelectors) + }, + swaggerUI = swaggerUI.build(base.swaggerUI), + info = info.build(base.info), + servers = buildList { + addAll(base.servers) + addAll(servers.map { it.build(ServerData.DEFAULT) }) + }, + externalDocs = externalDocs.build(base.externalDocs), + securitySchemes = buildList { + addAll(base.securitySchemes) + addAll(securitySchemes.map { it.build(SecuritySchemeData.DEFAULT) }) + }, + tags = buildList { + addAll(base.tags) + addAll(tags.map { it.build(TagData.DEFAULT) }) + }, + customSchemas = buildMap { + putAll(base.customSchemas) + putAll(customSchemas.getSchemas()) + }, + includeAllCustomSchemas = mergeBoolean(base.includeAllCustomSchemas, customSchemas.includeAll), + encoding = encodingConfig.build(base.encoding), + specConfigs = mutableMapOf() + ).also { + specConfigs.forEach { (specId, config) -> + it.specConfigs[specId] = config.build(it) + } + } + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUIDsl.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUIDsl.kt index 16544351..0144409b 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUIDsl.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUIDsl.kt @@ -1,27 +1,37 @@ package io.github.smiley4.ktorswaggerui.dsl +import io.github.smiley4.ktorswaggerui.data.DataUtils.merge +import io.github.smiley4.ktorswaggerui.data.DataUtils.mergeBoolean +import io.github.smiley4.ktorswaggerui.data.DataUtils.mergeDefault +import io.github.smiley4.ktorswaggerui.data.SwaggerUIData + + @OpenApiDslMarker class SwaggerUIDsl { /** * Whether to forward the root-url to the swagger-url */ - var forwardRoot: Boolean = false + var forwardRoot: Boolean = SwaggerUIData.DEFAULT.forwardRoot + /** * the url to the swagger-ui */ - var swaggerUrl: String = "swagger-ui" + var swaggerUrl: String = SwaggerUIData.DEFAULT.swaggerUrl + /** * the path under which the KTOR app gets deployed. can be useful if reverse proxy is in use. */ - var rootHostPath: String = "" + var rootHostPath: String = SwaggerUIData.DEFAULT.rootHostPath + /** * The name of the authentication to use for the swagger routes. Null to not protect the swagger-ui. */ - var authentication: String? = null + var authentication: String? = SwaggerUIData.DEFAULT.authentication + /** * Swagger UI can attempt to validate specs against swagger.io's online validator. @@ -30,7 +40,7 @@ class SwaggerUIDsl { * Validation is disabled when the url of the api-spec-file contains localhost. * (see https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md#network) */ - private var validatorUrl: String? = null + private var validatorUrl: String? = SwaggerUIData.DEFAULT.validatorUrl fun disableSpecValidator() { validatorUrl = null @@ -46,50 +56,44 @@ class SwaggerUIDsl { fun getSpecValidatorUrl() = validatorUrl + /** * Whether to show the operation-id of endpoints in the list */ - var displayOperationId = false + var displayOperationId = SwaggerUIData.DEFAULT.displayOperationId + /** * Whether the top bar will show an edit box that you can use to filter the tagged operations. */ - var showTagFilterInput = false + var showTagFilterInput = SwaggerUIData.DEFAULT.showTagFilterInput - /** - * Apply a sort to the operation list of each API - */ - var sort = SwaggerUiSort.NONE /** - * Syntax coloring theme to use + * Apply a sort to the operation list of each API */ - var syntaxHighlight = SwaggerUiSyntaxHighlight.AGATE - -} + var sort = SwaggerUIData.DEFAULT.sort -enum class SwaggerUiSort(val value: String) { - /** - * The order returned by the server unchanged - */ - NONE("undefined"), /** - * sort by paths alphanumerically + * Syntax coloring theme to use */ - ALPHANUMERICALLY("alpha"), + var syntaxHighlight = SwaggerUIData.DEFAULT.syntaxHighlight + + + internal fun build(base: SwaggerUIData): SwaggerUIData { + return SwaggerUIData( + forwardRoot = mergeBoolean(base.forwardRoot, this.forwardRoot), + swaggerUrl = mergeDefault(base.swaggerUrl, this.swaggerUrl, SwaggerUIData.DEFAULT.swaggerUrl), + rootHostPath = mergeDefault(base.rootHostPath, this.rootHostPath, SwaggerUIData.DEFAULT.rootHostPath), + authentication = merge(base.authentication, this.authentication), + validatorUrl = merge(base.validatorUrl, this.validatorUrl), + displayOperationId = mergeBoolean(base.displayOperationId, this.displayOperationId), + showTagFilterInput = mergeBoolean(base.showTagFilterInput, this.showTagFilterInput), + sort = mergeDefault(base.sort, this.sort, SwaggerUIData.DEFAULT.sort), + syntaxHighlight = mergeDefault(base.syntaxHighlight, this.syntaxHighlight, SwaggerUIData.DEFAULT.syntaxHighlight) + ) + } - /** - * sort by HTTP method - */ - HTTP_METHOD("method") } -enum class SwaggerUiSyntaxHighlight(val value: String) { - AGATE("agate"), - ARTA("arta"), - MONOKAI("monokai"), - NORD("nord"), - OBSIDIAN("obsidian"), - TOMORROW_NIGHT("tomorrow-night") -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ControllerUtils.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ControllerUtils.kt new file mode 100644 index 00000000..bb990937 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ControllerUtils.kt @@ -0,0 +1,18 @@ +package io.github.smiley4.ktorswaggerui.routing + +import io.ktor.server.config.ApplicationConfig + +object ControllerUtils { + + fun getRootPath(appConfig: ApplicationConfig): String { + return appConfig.propertyOrNull("ktor.deployment.rootPath")?.getString()?.let { "/${dropSlashes(it)}" } ?: "" + } + + fun dropSlashes(str: String): String { + var value = str + value = if (value.startsWith("/")) value.substring(1) else value + value = if (value.endsWith("/")) value.substring(0, value.length - 1) else value + return value + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ForwardRouteController.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ForwardRouteController.kt new file mode 100644 index 00000000..bcaa16b0 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ForwardRouteController.kt @@ -0,0 +1,35 @@ +package io.github.smiley4.ktorswaggerui.routing + +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.config.ApplicationConfig +import io.ktor.server.response.respondRedirect +import io.ktor.server.routing.get +import io.ktor.server.routing.routing + +class ForwardRouteController( + private val appConfig: ApplicationConfig, + private val swaggerUiConfig: PluginConfigData, +) { + + fun setup(app: Application) { + app.routing { + get { + call.respondRedirect("${getRootUrl()}/index.html") + } + } + } + + private fun getRootUrl(): String { + return "/" + listOf( + ControllerUtils.getRootPath(appConfig), + swaggerUiConfig.swaggerUI.rootHostPath, + swaggerUiConfig.swaggerUI.swaggerUrl, + ) + .filter { it.isNotBlank() } + .map { ControllerUtils.dropSlashes(it) } + .joinToString("/") + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ResourceContent.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ResourceContent.kt new file mode 100644 index 00000000..9523187a --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ResourceContent.kt @@ -0,0 +1,32 @@ +package io.github.smiley4.ktorswaggerui.routing + +import io.ktor.http.ContentType +import io.ktor.http.content.OutgoingContent +import io.ktor.http.withCharset +import java.net.URL + +class ResourceContent(private val resource: URL) : OutgoingContent.ByteArrayContent() { + + private val contentTypes = mapOf( + "html" to ContentType.Text.Html, + "css" to ContentType.Text.CSS, + "js" to ContentType.Application.JavaScript, + "json" to ContentType.Application.Json.withCharset(Charsets.UTF_8), + "png" to ContentType.Image.PNG + ) + + private val bytes by lazy { resource.readBytes() } + + override val contentType: ContentType? by lazy { + val extension = resource.file.substring(resource.file.lastIndexOf('.') + 1) + contentTypes[extension] ?: ContentType.Text.Html + } + + override val contentLength: Long? by lazy { + bytes.size.toLong() + } + + override fun bytes(): ByteArray = bytes + + override fun toString() = "ResourceContent \"$resource\"" +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/SwaggerController.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/SwaggerController.kt new file mode 100644 index 00000000..9e991f9e --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/SwaggerController.kt @@ -0,0 +1,126 @@ +package io.github.smiley4.ktorswaggerui.routing + +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.data.SwaggerUiSort +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.call +import io.ktor.server.auth.authenticate +import io.ktor.server.config.ApplicationConfig +import io.ktor.server.request.uri +import io.ktor.server.response.respond +import io.ktor.server.response.respondRedirect +import io.ktor.server.response.respondText +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import io.ktor.server.routing.route +import io.ktor.server.routing.routing + +class SwaggerController( + private val appConfig: ApplicationConfig, + private val pluginConfig: PluginConfigData, + private val swaggerWebjarVersion: String, + private val specName: String?, + private val jsonSpec: String, +) { + + companion object { + const val DEFAULT_SPEC_NAME: String = "api" + } + + fun setup(app: Application) { + app.routing { + if (pluginConfig.swaggerUI.authentication == null) { + setup() + } else { + authenticate(pluginConfig.swaggerUI.authentication) { + setup() + } + } + } + } + + private fun Route.setup() { + route(getSubUrl()) { + get { + call.respondRedirect("${call.request.uri}/index.html") + } + get("{filename}") { + serveStaticResource(call.parameters["filename"]!!, call) + } + get("swagger-initializer.js") { + serveSwaggerInitializer(call) + } + get("${specName ?: DEFAULT_SPEC_NAME}.json") { + serveOpenApiSpec(call) + } + } + } + + private suspend fun serveSwaggerInitializer(call: ApplicationCall) { + val swaggerUiConfig = pluginConfig.swaggerUI + // see https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md for reference + val propValidatorUrl = swaggerUiConfig.validatorUrl?.let { "validatorUrl: \"$it\"" } ?: "validatorUrl: false" + val propDisplayOperationId = "displayOperationId: ${swaggerUiConfig.displayOperationId}" + val propFilter = "filter: ${swaggerUiConfig.showTagFilterInput}" + val propSort = "operationsSorter: " + + if (swaggerUiConfig.sort == SwaggerUiSort.NONE) "undefined" + else "\"${swaggerUiConfig.sort.value}\"" + val propSyntaxHighlight = "syntaxHighlight: { theme: \"${swaggerUiConfig.syntaxHighlight.value}\" }" + val content = """ + window.onload = function() { + window.ui = SwaggerUIBundle({ + url: "${getRootUrl(appConfig)}/${specName ?: DEFAULT_SPEC_NAME}.json", + dom_id: '#swagger-ui', + deepLinking: true, + presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIStandalonePreset + ], + plugins: [ + SwaggerUIBundle.plugins.DownloadUrl + ], + layout: "StandaloneLayout", + $propValidatorUrl, + $propDisplayOperationId, + $propFilter, + $propSort, + $propSyntaxHighlight + }); + }; + """.trimIndent() + call.respondText(ContentType.Application.JavaScript, HttpStatusCode.OK) { content } + } + + private suspend fun serveOpenApiSpec(call: ApplicationCall) { + call.respondText(ContentType.Application.Json, HttpStatusCode.OK) { jsonSpec } + } + + private suspend fun serveStaticResource(filename: String, call: ApplicationCall) { + val resource = this::class.java.getResource("/META-INF/resources/webjars/swagger-ui/$swaggerWebjarVersion/$filename") + if (resource != null) { + call.respond(ResourceContent(resource)) + } else { + call.respond(HttpStatusCode.NotFound, "$filename could not be found") + } + } + + private fun getRootUrl(appConfig: ApplicationConfig): String { + return "${ControllerUtils.getRootPath(appConfig)}${getSubUrl()}" + } + + private fun getSubUrl(): String { + return "/" + listOf( + pluginConfig.swaggerUI.rootHostPath, + pluginConfig.swaggerUI.swaggerUrl, + specName + ) + .filter { !it.isNullOrBlank() } + .map { ControllerUtils.dropSlashes(it!!) } + .joinToString("/") + } + + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OAuthFlowsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OAuthFlowsBuilder.kt deleted file mode 100644 index 1fc2ddfe..00000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OAuthFlowsBuilder.kt +++ /dev/null @@ -1,35 +0,0 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi - -import io.github.smiley4.ktorswaggerui.dsl.OpenIdOAuthFlow -import io.github.smiley4.ktorswaggerui.dsl.OpenIdOAuthFlows -import io.swagger.v3.oas.models.security.OAuthFlow -import io.swagger.v3.oas.models.security.OAuthFlows -import io.swagger.v3.oas.models.security.Scopes - -class OAuthFlowsBuilder { - - fun build(flows: OpenIdOAuthFlows): OAuthFlows { - return OAuthFlows().apply { - implicit = flows.getImplicit()?.let { build(it) } - password = flows.getPassword()?.let { build(it) } - clientCredentials = flows.getClientCredentials()?.let { build(it) } - authorizationCode = flows.getAuthorizationCode()?.let { build(it) } - } - } - - private fun build(flow: OpenIdOAuthFlow): OAuthFlow { - return OAuthFlow().apply { - authorizationUrl = flow.authorizationUrl - tokenUrl = flow.tokenUrl - refreshUrl = flow.refreshUrl - scopes = flow.scopes?.let { buildScopes(it) } - } - } - - private fun buildScopes(scopes: Map): Scopes { - return Scopes().apply { - scopes.forEach { (k, v) -> addString(k, v) } - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt deleted file mode 100644 index 30fa43e6..00000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt +++ /dev/null @@ -1,21 +0,0 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta - -class OperationTagsBuilder( - private val config: SwaggerUIPluginConfig -) { - - fun build(route: RouteMeta): List { - return mutableSetOf().also { tags -> - tags.addAll(getGeneratedTags(route)) - tags.addAll(getRouteTags(route)) - }.filterNotNull() - } - - private fun getRouteTags(route: RouteMeta) = route.documentation.tags - - private fun getGeneratedTags(route: RouteMeta) = config.getTagGenerator()(route.path.split("/").filter { it.isNotEmpty() }) - -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/AuthExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/AuthExample.kt index 09943daa..79410f64 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/AuthExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/AuthExample.kt @@ -1,8 +1,8 @@ package io.github.smiley4.ktorswaggerui.examples import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.AuthScheme -import io.github.smiley4.ktorswaggerui.dsl.AuthType +import io.github.smiley4.ktorswaggerui.data.AuthScheme +import io.github.smiley4.ktorswaggerui.data.AuthType import io.github.smiley4.ktorswaggerui.dsl.get import io.ktor.http.HttpStatusCode import io.ktor.server.application.Application diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt index 539b90d6..70f21ba3 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt @@ -3,11 +3,12 @@ package io.github.smiley4.ktorswaggerui.examples import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.AuthScheme -import io.github.smiley4.ktorswaggerui.dsl.AuthType +import io.github.smiley4.ktorswaggerui.data.AuthScheme +import io.github.smiley4.ktorswaggerui.data.AuthType +import io.github.smiley4.ktorswaggerui.data.EncodingData +import io.github.smiley4.ktorswaggerui.data.SwaggerUiSort +import io.github.smiley4.ktorswaggerui.data.SwaggerUiSyntaxHighlight import io.github.smiley4.ktorswaggerui.dsl.EncodingConfig -import io.github.smiley4.ktorswaggerui.dsl.SwaggerUiSort -import io.github.smiley4.ktorswaggerui.dsl.SwaggerUiSyntaxHighlight import io.ktor.server.application.Application import io.ktor.server.application.install import io.ktor.server.engine.embeddedServer @@ -98,7 +99,7 @@ private fun Application.myModule() { } encoding { schemaEncoder { type -> - SchemaGenerator(EncodingConfig.schemaGeneratorConfigBuilder().build()) + SchemaGenerator(EncodingData.schemaGeneratorConfigBuilder().build()) .generateSchema(type.javaType) .toPrettyString() } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomSchemaExample.kt similarity index 73% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomSchemaExample.kt index 23b3ffdc..73b71f7e 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomSchemaExample.kt @@ -1,9 +1,11 @@ package io.github.smiley4.ktorswaggerui.examples import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.array +import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.custom +import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.multipleOf +import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.oneOf +import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.typeOf import io.github.smiley4.ktorswaggerui.dsl.get -import io.github.smiley4.ktorswaggerui.dsl.obj import io.ktor.http.HttpStatusCode import io.ktor.server.application.Application import io.ktor.server.application.call @@ -34,6 +36,20 @@ private fun Application.myModule() { val someNumber: Long ) + data class Rectangle( + val width: Int, + val height: Int + ) + + data class Circle( + val radius: Int + ) + + data class Point( + val x: Int, + val y: Int + ) + install(SwaggerUI) { // don't show the test-routes providing json-schemas pathFilter = { _, url -> url.firstOrNull() != "schema" } @@ -64,12 +80,12 @@ private fun Application.myModule() { get("something", { request { // body referencing the custom schema with id 'myRequestData' - body(obj("myRequestData")) + body("myRequestData") } response { HttpStatusCode.OK to { // body referencing the custom schema with id 'myResponseData' - body(obj("myResponseData")) + body("myResponseData") } } }) { @@ -80,12 +96,12 @@ private fun Application.myModule() { get("something/many", { request { // body referencing the custom schema with id 'myRequestData' - body(array("myRequestData")) + body(multipleOf(custom("myRequestData"))) } response { HttpStatusCode.OK to { // body referencing the custom schema with id 'myResponseData' - body(array("myResponseData")) + body(multipleOf(custom("myResponseData"))) } } }) { @@ -93,6 +109,23 @@ private fun Application.myModule() { call.respond(HttpStatusCode.OK, MyResponseData(text, 42)) } + get("oneof/shapes", { + request { + // body allowing a mixed list of rectangles, circles and points + body( + multipleOf( + oneOf( + typeOf(Rectangle::class), + typeOf(Circle::class), + typeOf(Point::class), + ) + ) + ) + } + }) { + call.respond(HttpStatusCode.OK, Unit) + } + // (external) endpoint providing a json-schema get("schema/myResponseData") { call.respondText( diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MultipleSpecsExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MultipleSpecsExample.kt new file mode 100644 index 00000000..9b712d48 --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MultipleSpecsExample.kt @@ -0,0 +1,102 @@ +package io.github.smiley4.ktorswaggerui.examples + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.get +import io.github.smiley4.ktorswaggerui.dsl.route +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.auth.Authentication +import io.ktor.server.auth.UserIdPrincipal +import io.ktor.server.auth.basic +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respondText +import io.ktor.server.routing.routing + +/** + * An example showcasing multiple openapi-specs in a single application + * - localhost:8080/swagger-ui/v1/index.html + * * /v1/hello + * - localhost:8080/swagger-ui/v2/index.html + * * /v2/hello + * * /hi + */ +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + + install(Authentication) { + basic("auth-swagger") { + realm = "Access to the Swagger UI" + validate { credentials -> + if (credentials.name == "user" && credentials.password == "pass") { + UserIdPrincipal(credentials.name) + } else { + null + } + } + } + } + + install(SwaggerUI) { + // general configuration + info { + title = "Example API" + } + specAssigner = { _, _ -> "v2" } // assign all unassigned routes to spec "v2" (here e.g. '/hi') + + // configuration specific for spec "v1" + spec("v1") { + info { + version = "1.0" + } + } + + // configuration specific for spec "v2" + spec("v2") { + info { + version = "2.0" + } + swagger { + authentication = "auth-swagger" + } + } + } + + + routing { + + // version 1.0 routes + route("v1", { + specId = "v1" // assign all sub-routes to spec "v1" + }) { + get("hello", { + description = "Simple version 1 'Hello World'-Route" + }) { + call.respondText("Hello World!") + } + } + + // version 2.0 routes + route("v2", { + specId = "v2" // assign all sub-routes to spec "v2" + }) { + get("hello", { + description = "Simple version 2 'Hello World'-Route" + }) { + call.respondText("Improved Hello World!") + } + } + + // other routes + get("hi", { + description = "Alternative version of 'Hello World'-Route" + }) { + call.respondText("Alternative Hello World!") + } + + } +} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ApplicationTests.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ApplicationTests.kt index 43197f74..e9c7fe79 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ApplicationTests.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ApplicationTests.kt @@ -1,9 +1,10 @@ package io.github.smiley4.ktorswaggerui.tests import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl import io.github.smiley4.ktorswaggerui.dsl.get import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldNotBeEmpty import io.ktor.client.request.get import io.ktor.client.statement.bodyAsText @@ -24,141 +25,443 @@ import kotlin.test.Test class ApplicationTests { - private fun ApplicationTestBuilder.setupTestApplication(pluginConfig: SwaggerUIPluginConfig.() -> Unit) { - application { - install(Authentication) { - basic("my-auth") { - validate { credentials -> - if (credentials.name == "user" && credentials.password == "pass") { - UserIdPrincipal(credentials.name) - } else { - null - } - } - } - } - install(SwaggerUI) { pluginConfig() } - routing { - get("hello", { - description = "Simple 'Hello World'- Route" - response { - HttpStatusCode.OK to { - description = "Successful Response" - } - } - }) { - call.respondText("Hello Test") - } - } + @Test + fun minimal() = swaggerUITestApplication { + get("hello").also { + it.status shouldBe HttpStatusCode.OK + it.body shouldBe "Hello Test" + } + get("/").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/swagger-ui").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/swagger-ui/index.html").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/swagger-ui/swagger-initializer.js").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Application.JavaScript.withCharset(Charsets.UTF_8) + it.body shouldContain "url: \"/swagger-ui/api.json\"" + } + get("/swagger-ui/api.json").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Application.Json.withCharset(Charsets.UTF_8) + it.body.shouldNotBeEmpty() } } @Test - fun testDefaultSwaggerUi() = testApplication { - setupTestApplication {} - client.get("/hello").bodyAsText() shouldBe "Hello Test" - client.get("/").status shouldBe HttpStatusCode.NotFound - client.get("/swagger-ui").let { + fun customRootHost() = swaggerUITestApplication({ + swagger { + rootHostPath = "my-root" + } + }) { + get("hello").also { it.status shouldBe HttpStatusCode.OK - it.contentType() shouldBe ContentType.Text.Html - it.bodyAsText().shouldNotBeEmpty() + it.body shouldBe "Hello Test" } - client.get("/swagger-ui/api.json").let { + get("/").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("my-root/swagger-ui").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("my-root/swagger-ui/index.html").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("my-root/swagger-ui/swagger-initializer.js").also { it.status shouldBe HttpStatusCode.OK - it.contentType() shouldBe ContentType.Application.Json.withCharset(Charsets.UTF_8) - it.bodyAsText().shouldNotBeEmpty() + it.contentType shouldBe ContentType.Application.JavaScript.withCharset(Charsets.UTF_8) + it.body shouldContain "url: \"/my-root/swagger-ui/api.json\"" + + } + get("my-root/swagger-ui/api.json").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Application.Json.withCharset(Charsets.UTF_8) + it.body.shouldNotBeEmpty() } } + @Test - fun testSwaggerUiForwardRoot() = testApplication { - setupTestApplication { - swagger { - forwardRoot = true - } + fun forwardRoot() = swaggerUITestApplication({ + swagger { + forwardRoot = true + } + }) { + get("hello").also { + it.status shouldBe HttpStatusCode.OK + it.body shouldBe "Hello Test" + } + get("/").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() } - client.get("/hello").bodyAsText() shouldBe "Hello Test" - client.get("/").let { + get("/swagger-ui").also { it.status shouldBe HttpStatusCode.OK - it.contentType() shouldBe ContentType.Text.Html - it.bodyAsText().shouldNotBeEmpty() + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() } - client.get("/swagger-ui").let { + get("/swagger-ui/index.html").also { it.status shouldBe HttpStatusCode.OK - it.contentType() shouldBe ContentType.Text.Html - it.bodyAsText().shouldNotBeEmpty() + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() } - client.get("/swagger-ui/api.json").let { + get("/swagger-ui/swagger-initializer.js").also { it.status shouldBe HttpStatusCode.OK - it.contentType() shouldBe ContentType.Application.Json.withCharset(Charsets.UTF_8) - it.bodyAsText().shouldNotBeEmpty() + it.contentType shouldBe ContentType.Application.JavaScript.withCharset(Charsets.UTF_8) + it.body shouldContain "url: \"/swagger-ui/api.json\"" + } + get("/swagger-ui/api.json").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Application.Json.withCharset(Charsets.UTF_8) + it.body.shouldNotBeEmpty() } } + @Test - fun testSwaggerUiProtected() = testApplication { - setupTestApplication { - swagger { - authentication = "my-auth" - } + fun forwardRootWithCustomSwaggerUrl() = swaggerUITestApplication({ + swagger { + forwardRoot = true + swaggerUrl = "test-swagger" + } + }) { + get("/").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/test-swagger").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/test-swagger/index.html").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() } - client.get("/hello").bodyAsText() shouldBe "Hello Test" - client.get("/").status shouldBe HttpStatusCode.NotFound - client.get("/swagger-ui").status shouldBe HttpStatusCode.Unauthorized - client.get("/swagger-ui/api.json").status shouldBe HttpStatusCode.Unauthorized } + @Test - fun testSwaggerUiProtectedAndForwardRoot() = testApplication { - setupTestApplication { - swagger { - authentication = "my-auth" - forwardRoot = true - } + fun protectedSwaggerUI() = swaggerUITestApplication({ + swagger { + authentication = "my-auth" + } + }) { + get("hello").also { + it.status shouldBe HttpStatusCode.OK + it.body shouldBe "Hello Test" + } + get("/").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/swagger-ui").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + get("/swagger-ui/index.html").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + get("/swagger-ui/swagger-initializer.js").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + get("/swagger-ui/api.json").also { + it.status shouldBe HttpStatusCode.Unauthorized } - client.get("/hello").bodyAsText() shouldBe "Hello Test" - client.get("/").status shouldBe HttpStatusCode.Unauthorized - client.get("/swagger-ui").status shouldBe HttpStatusCode.Unauthorized - client.get("/swagger-ui/api.json").status shouldBe HttpStatusCode.Unauthorized } @Test - fun testSwaggerUiCustomRoute() = testApplication { - setupTestApplication { - swagger { - swaggerUrl = "test-swagger" - } + fun forwardRootAndProtectedSwaggerUI() = swaggerUITestApplication({ + swagger { + authentication = "my-auth" + forwardRoot = true + } + }) { + get("hello").also { + it.status shouldBe HttpStatusCode.OK + it.body shouldBe "Hello Test" + } + get("/").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + get("/swagger-ui").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + get("/swagger-ui/index.html").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + get("/swagger-ui/swagger-initializer.js").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + get("/swagger-ui/api.json").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + } + + + @Test + fun customSwaggerUrl() = swaggerUITestApplication({ + swagger { + swaggerUrl = "test-swagger" + } + }) { + get("hello").also { + it.status shouldBe HttpStatusCode.OK + it.body shouldBe "Hello Test" + } + get("/").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/swagger-ui").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/swagger-ui/index.html").also { + it.status shouldBe HttpStatusCode.NotFound } - client.get("/hello").bodyAsText() shouldBe "Hello Test" - client.get("/swagger-ui").status shouldBe HttpStatusCode.NotFound - client.get("/swagger-ui/api.json").status shouldBe HttpStatusCode.NotFound - client.get("/test-swagger").let { + get("/swagger-ui/swagger-initializer.js").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/swagger-ui/api.json").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/test-swagger").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/test-swagger/index.html").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/test-swagger/swagger-initializer.js").also { it.status shouldBe HttpStatusCode.OK - it.contentType() shouldBe ContentType.Text.Html - it.bodyAsText().shouldNotBeEmpty() + it.contentType shouldBe ContentType.Application.JavaScript.withCharset(Charsets.UTF_8) + it.body shouldContain "url: \"/test-swagger/api.json\"" + } - client.get("/test-swagger/api.json").let { + get("/test-swagger/api.json").also { it.status shouldBe HttpStatusCode.OK - it.contentType() shouldBe ContentType.Application.Json.withCharset(Charsets.UTF_8) - it.bodyAsText().shouldNotBeEmpty() + it.contentType shouldBe ContentType.Application.Json.withCharset(Charsets.UTF_8) + it.body.shouldNotBeEmpty() } } + @Test - fun testSwaggerUiCustomRouteProtected() = testApplication { - setupTestApplication { + fun customSwaggerUrlAndProtected() = swaggerUITestApplication({ + swagger { + authentication = "my-auth" + swaggerUrl = "test-swagger" + } + }) { + get("hello").also { + it.status shouldBe HttpStatusCode.OK + it.body shouldBe "Hello Test" + } + get("/").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/swagger-ui").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/swagger-ui/index.html").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/swagger-ui/swagger-initializer.js").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/swagger-ui/api.json").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/test-swagger").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + get("/test-swagger/index.html").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + get("/test-swagger/swagger-initializer.js").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + get("/test-swagger/api.json").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + } + + + @Test + fun multipleSwaggerUI() = swaggerUITestApplication({ + specAssigner = { _, tags -> tags.firstOrNull() ?: "other" } + }) { + get("hello").also { + it.status shouldBe HttpStatusCode.OK + it.body shouldBe "Hello Test" + } + get("/").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/swagger-ui/hello").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/swagger-ui/hello/index.html").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/swagger-ui/hello/swagger-initializer.js").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Application.JavaScript.withCharset(Charsets.UTF_8) + it.body shouldContain "url: \"/swagger-ui/hello/hello.json\"" + } + get("/swagger-ui/hello/hello.json").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Application.Json.withCharset(Charsets.UTF_8) + it.body.shouldNotBeEmpty() + } + get("/swagger-ui/world").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/swagger-ui/world/index.html").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/swagger-ui/world/swagger-initializer.js").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Application.JavaScript.withCharset(Charsets.UTF_8) + it.body shouldContain "url: \"/swagger-ui/world/world.json\"" + } + get("/swagger-ui/world/world.json").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Application.Json.withCharset(Charsets.UTF_8) + it.body.shouldNotBeEmpty() + } + } + + + @Test + fun multipleSwaggerUIWithDifferentAuthConfig() = swaggerUITestApplication({ + specAssigner = { _, tags -> tags.firstOrNull() ?: "other" } + spec("hello") { + swagger { + authentication = null + } + } + spec("world") { swagger { authentication = "my-auth" - swaggerUrl = "test-swagger" } } - client.get("/hello").bodyAsText() shouldBe "Hello Test" - client.get("/swagger-ui").status shouldBe HttpStatusCode.NotFound - client.get("/swagger-ui/api.json").status shouldBe HttpStatusCode.NotFound - client.get("/test-swagger").status shouldBe HttpStatusCode.Unauthorized - client.get("/test-swagger/api.json").status shouldBe HttpStatusCode.Unauthorized + }) { + get("/swagger-ui/hello/index.html").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/swagger-ui/hello/hello.json").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Application.Json.withCharset(Charsets.UTF_8) + it.body.shouldNotBeEmpty() + } + get("/swagger-ui/world/index.html").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + get("/swagger-ui/world/world.json").also { + it.status shouldBe HttpStatusCode.Unauthorized + } + } + + + private fun swaggerUITestApplication(block: suspend ApplicationTestBuilder.() -> Unit) { + swaggerUITestApplication({}, block) + } + + private fun swaggerUITestApplication(pluginConfig: PluginConfigDsl.() -> Unit, block: suspend ApplicationTestBuilder.() -> Unit) { + testApplication { + application { + install(Authentication) { + basic("my-auth") { + validate { credentials -> + if (credentials.name == "user" && credentials.password == "pass") { + UserIdPrincipal(credentials.name) + } else { + null + } + } + } + } + install(SwaggerUI, pluginConfig) + routing { + get("hello", { + tags = listOf("hello") + description = "Simple 'Hello World'- Route" + response { + HttpStatusCode.OK to { + description = "Successful Response" + } + } + }) { + call.respondText("Hello Test") + } + get("world", { + tags = listOf("world") + description = "Another simple 'Hello World'- Route" + response { + HttpStatusCode.OK to { + description = "Successful Response" + } + } + }) { + call.respondText("Hello World") + } + } + Thread.sleep(500) + } + block() + } + } + + private suspend fun ApplicationTestBuilder.get(path: String): GetResult { + return client.get(path) + .let { + GetResult( + path = path, + status = it.status, + contentType = it.contentType(), + body = it.bodyAsText() + ) + } + .also { it.print() } + } + + private data class GetResult( + val path: String, + val status: HttpStatusCode, + val contentType: ContentType?, + val body: String, + ) + + private fun GetResult.print() { + println("GET ${this.path} => ${this.status} (${this.contentType}): ${this.body}") } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/example/ExampleTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/example/ExampleTest.kt index bcb23358..7dae2e56 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/example/ExampleTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/example/ExampleTest.kt @@ -1,13 +1,14 @@ package io.github.smiley4.ktorswaggerui.tests.example -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContext -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContextBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ExampleBuilder -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ExampleBuilder +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -166,15 +167,15 @@ class ExampleTest : StringSpec({ } - private val defaultPluginConfig = SwaggerUIPluginConfig() + private val defaultPluginConfig = PluginConfigDsl() private fun exampleContext( routes: List, - pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig + pluginConfig: PluginConfigDsl = defaultPluginConfig ): ExampleContext { return ExampleContextBuilder( exampleBuilder = ExampleBuilder( - config = pluginConfig + config = pluginConfig.build(PluginConfigData.DEFAULT) ) ).build(routes.toList()) } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ExternalDocsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ExternalDocsBuilderTest.kt index 22138528..d49ab313 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ExternalDocsBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ExternalDocsBuilderTest.kt @@ -1,7 +1,8 @@ package io.github.smiley4.ktorswaggerui.tests.openapi +import io.github.smiley4.ktorswaggerui.data.ExternalDocsData import io.github.smiley4.ktorswaggerui.dsl.OpenApiExternalDocs -import io.github.smiley4.ktorswaggerui.spec.openapi.ExternalDocumentationBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ExternalDocumentationBuilder import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.swagger.v3.oas.models.ExternalDocumentation @@ -30,7 +31,7 @@ class ExternalDocsBuilderTest : StringSpec({ companion object { private fun buildExternalDocsObject(builder: OpenApiExternalDocs.() -> Unit): ExternalDocumentation { - return ExternalDocumentationBuilder().build(OpenApiExternalDocs().apply(builder)) + return ExternalDocumentationBuilder().build(OpenApiExternalDocs().apply(builder).build(ExternalDocsData.DEFAULT)) } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/InfoBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/InfoBuilderTest.kt index 96a6ccd3..51475ef2 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/InfoBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/InfoBuilderTest.kt @@ -1,9 +1,10 @@ package io.github.smiley4.ktorswaggerui.tests.openapi +import io.github.smiley4.ktorswaggerui.data.InfoData import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo -import io.github.smiley4.ktorswaggerui.spec.openapi.ContactBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.InfoBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.LicenseBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ContactBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.InfoBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.LicenseBuilder import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe @@ -72,7 +73,7 @@ class InfoBuilderTest : StringSpec({ return InfoBuilder( contactBuilder = ContactBuilder(), licenseBuilder = LicenseBuilder() - ).build(OpenApiInfo().apply(builder)) + ).build(OpenApiInfo().apply(builder).build(InfoData.DEFAULT)) } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt index b63df8ed..f682ce89 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt @@ -1,15 +1,16 @@ package io.github.smiley4.ktorswaggerui.tests.openapi import com.fasterxml.jackson.databind.ObjectMapper -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContext -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContextBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.* -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContextBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.TypeOverwrites +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.* +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextBuilder +import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.collections.shouldHaveSize @@ -36,7 +37,7 @@ class OpenApiBuilderTest : StringSpec({ } "multiple servers" { - val config = SwaggerUIPluginConfig().also { + val config = PluginConfigDsl().also { it.server { url = "http://localhost:8080" description = "Development Server" @@ -56,7 +57,7 @@ class OpenApiBuilderTest : StringSpec({ } "multiple tags" { - val config = SwaggerUIPluginConfig().also { + val config = PluginConfigDsl().also { it.tag("tag-1") { description = "first test tag" } @@ -77,33 +78,35 @@ class OpenApiBuilderTest : StringSpec({ companion object { - private val defaultPluginConfig = SwaggerUIPluginConfig() + private val defaultPluginConfig = PluginConfigDsl() - private fun schemaContext(routes: List, pluginConfig: SwaggerUIPluginConfig): SchemaContext { + private fun schemaContext(routes: List, pluginConfig: PluginConfigDsl): SchemaContext { + val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) return SchemaContextBuilder( - config = pluginConfig, + config =pluginConfigData, schemaBuilder = SchemaBuilder( - definitionsField = pluginConfig.encodingConfig.schemaDefinitionsField, - schemaEncoder = pluginConfig.encodingConfig.getSchemaEncoder(), + definitionsField = pluginConfigData.encoding.schemaDefsField, + schemaEncoder = pluginConfigData.encoding.schemaEncoder, ObjectMapper(), TypeOverwrites.get() ) ).build(routes) } - private fun exampleContext(routes: List, pluginConfig: SwaggerUIPluginConfig): ExampleContext { + private fun exampleContext(routes: List, pluginConfig: PluginConfigDsl): ExampleContext { return ExampleContextBuilder( exampleBuilder = ExampleBuilder( - config = pluginConfig + config = pluginConfig.build(PluginConfigData.DEFAULT) ) ).build(routes.toList()) } - private fun buildOpenApiObject(routes: List, pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): OpenAPI { + private fun buildOpenApiObject(routes: List, pluginConfig: PluginConfigDsl = defaultPluginConfig): OpenAPI { val schemaContext = schemaContext(routes, pluginConfig) val exampleContext = exampleContext(routes, pluginConfig) + val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) return OpenApiBuilder( - config = pluginConfig, + config = pluginConfigData, schemaContext = schemaContext, exampleContext = exampleContext, infoBuilder = InfoBuilder( @@ -118,7 +121,7 @@ class OpenApiBuilderTest : StringSpec({ pathsBuilder = PathsBuilder( pathBuilder = PathBuilder( operationBuilder = OperationBuilder( - operationTagsBuilder = OperationTagsBuilder(pluginConfig), + operationTagsBuilder = OperationTagsBuilder(pluginConfigData), parameterBuilder = ParameterBuilder( schemaContext = schemaContext, exampleContext = exampleContext @@ -139,14 +142,14 @@ class OpenApiBuilderTest : StringSpec({ headerBuilder = HeaderBuilder(schemaContext) ) ), - config = pluginConfig + config = pluginConfigData ), - securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfig), + securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfigData), ) ) ), componentsBuilder = ComponentsBuilder( - config = pluginConfig, + config = pluginConfigData, securitySchemesBuilder = SecuritySchemesBuilder( oAuthFlowsBuilder = OAuthFlowsBuilder() ) diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt index d230cabc..39184ed1 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt @@ -1,26 +1,28 @@ package io.github.smiley4.ktorswaggerui.tests.openapi import com.fasterxml.jackson.databind.ObjectMapper -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute -import io.github.smiley4.ktorswaggerui.dsl.obj -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContext -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContextBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ContentBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ExampleBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.HeaderBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.OperationBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.OperationTagsBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ParameterBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.RequestBodyBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ResponseBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContextBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.TypeOverwrites +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ContentBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ExampleBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.HeaderBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OperationBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OperationTagsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ParameterBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.RequestBodyBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ResponseBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ResponsesBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.SecurityRequirementsBuilder +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextBuilder +import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites +import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.custom import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder @@ -95,7 +97,7 @@ class OperationBuilderTest : StringSpec({ } "operation with auto-generated tags" { - val config = SwaggerUIPluginConfig().also { + val config = PluginConfigDsl().also { it.generateTags { url -> listOf(url.firstOrNull()) } } val routeA = RouteMeta( @@ -682,7 +684,7 @@ class OperationBuilderTest : StringSpec({ } "automatic unauthorized response for protected route" { - val config = SwaggerUIPluginConfig().also { + val config = PluginConfigDsl().also { it.defaultUnauthorizedResponse { description = "Default unauthorized Response" } @@ -716,7 +718,7 @@ class OperationBuilderTest : StringSpec({ } "automatic unauthorized response for unprotected route" { - val config = SwaggerUIPluginConfig().also { + val config = PluginConfigDsl().also { it.defaultUnauthorizedResponse { description = "Default unauthorized Response" } @@ -803,7 +805,7 @@ class OperationBuilderTest : StringSpec({ } "custom body schema" { - val config = SwaggerUIPluginConfig().also { + val config = PluginConfigDsl().also { it.customSchemas { openApi("myCustomSchema") { Schema().also { schema -> @@ -822,7 +824,7 @@ class OperationBuilderTest : StringSpec({ method = HttpMethod.Get, documentation = OpenApiRoute().also { route -> route.request { - body(obj("myCustomSchema")) + body(custom("myCustomSchema")) } }, protected = false @@ -860,7 +862,7 @@ class OperationBuilderTest : StringSpec({ } "custom multipart-body schema" { - val config = SwaggerUIPluginConfig().also { + val config = PluginConfigDsl().also { it.customSchemas { openApi("myCustomSchema") { Schema().also { schema -> @@ -881,7 +883,7 @@ class OperationBuilderTest : StringSpec({ route.request { multipartBody { mediaType(ContentType.MultiPart.FormData) - part("customData", obj("myCustomSchema")) + part("customData", custom("myCustomSchema")) } } }, @@ -927,17 +929,18 @@ class OperationBuilderTest : StringSpec({ val number: Int ) - private val defaultPluginConfig = SwaggerUIPluginConfig() + private val defaultPluginConfig = PluginConfigDsl() private fun schemaContext( routes: List, - pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig + pluginConfig: PluginConfigDsl = defaultPluginConfig ): SchemaContext { + val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) return SchemaContextBuilder( - config = pluginConfig, + config = pluginConfigData, schemaBuilder = SchemaBuilder( - definitionsField = pluginConfig.encodingConfig.schemaDefinitionsField, - schemaEncoder = pluginConfig.encodingConfig.getSchemaEncoder(), + definitionsField = pluginConfigData.encoding.schemaDefsField, + schemaEncoder = pluginConfigData.encoding.schemaEncoder, ObjectMapper(), TypeOverwrites.get() ) @@ -946,11 +949,11 @@ class OperationBuilderTest : StringSpec({ private fun exampleContext( routes: List, - pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig + pluginConfig: PluginConfigDsl = defaultPluginConfig ): ExampleContext { return ExampleContextBuilder( exampleBuilder = ExampleBuilder( - config = pluginConfig + config = pluginConfig.build(PluginConfigData.DEFAULT) ) ).build(routes.toList()) } @@ -960,10 +963,11 @@ class OperationBuilderTest : StringSpec({ route: RouteMeta, schemaContext: SchemaContext, exampleContext: ExampleContext, - pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig + pluginConfig: PluginConfigDsl = defaultPluginConfig ): Operation { + val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) return OperationBuilder( - operationTagsBuilder = OperationTagsBuilder(pluginConfig), + operationTagsBuilder = OperationTagsBuilder(pluginConfigData), parameterBuilder = ParameterBuilder( schemaContext = schemaContext, exampleContext = exampleContext @@ -984,9 +988,9 @@ class OperationBuilderTest : StringSpec({ headerBuilder = HeaderBuilder(schemaContext) ) ), - config = pluginConfig + config = pluginConfigData ), - securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfig), + securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfigData), ).build(route) } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt index ac6efdb0..29f69a44 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt @@ -1,27 +1,28 @@ package io.github.smiley4.ktorswaggerui.tests.openapi import com.fasterxml.jackson.databind.ObjectMapper -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContext -import io.github.smiley4.ktorswaggerui.spec.example.ExampleContextBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ContentBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ExampleBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.HeaderBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.OperationBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.OperationTagsBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ParameterBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.PathBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.PathsBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.RequestBodyBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ResponseBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContextBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.TypeOverwrites +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ContentBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ExampleBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.HeaderBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OperationBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OperationTagsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ParameterBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.PathBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.PathsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.RequestBodyBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ResponseBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ResponsesBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.SecurityRequirementsBuilder +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextBuilder +import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.maps.shouldHaveSize @@ -88,24 +89,25 @@ class PathsBuilderTest : StringSpec({ protected = false ) - private val defaultPluginConfig = SwaggerUIPluginConfig() + private val defaultPluginConfig = PluginConfigDsl() - private fun schemaContext(routes: List, pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { + private fun schemaContext(routes: List, pluginConfig: PluginConfigDsl = defaultPluginConfig): SchemaContext { + val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) return SchemaContextBuilder( - config = pluginConfig, + config = pluginConfigData, schemaBuilder = SchemaBuilder( - definitionsField = pluginConfig.encodingConfig.schemaDefinitionsField, - schemaEncoder = pluginConfig.encodingConfig.getSchemaEncoder(), + definitionsField = pluginConfigData.encoding.schemaDefsField, + schemaEncoder = pluginConfigData.encoding.schemaEncoder, ObjectMapper(), TypeOverwrites.get() ) ).build(routes) } - private fun exampleContext(routes: List, pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): ExampleContext { + private fun exampleContext(routes: List, pluginConfig: PluginConfigDsl = defaultPluginConfig): ExampleContext { return ExampleContextBuilder( exampleBuilder = ExampleBuilder( - config = pluginConfig + config = pluginConfig.build(PluginConfigData.DEFAULT) ) ).build(routes.toList()) } @@ -114,12 +116,13 @@ class PathsBuilderTest : StringSpec({ routes: Collection, schemaContext: SchemaContext, exampleContext: ExampleContext, - pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig + pluginConfig: PluginConfigDsl = defaultPluginConfig ): Paths { + val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) return PathsBuilder( pathBuilder = PathBuilder( operationBuilder = OperationBuilder( - operationTagsBuilder = OperationTagsBuilder(pluginConfig), + operationTagsBuilder = OperationTagsBuilder(pluginConfigData), parameterBuilder = ParameterBuilder( schemaContext = schemaContext, exampleContext = exampleContext @@ -140,9 +143,9 @@ class PathsBuilderTest : StringSpec({ headerBuilder = HeaderBuilder(schemaContext) ) ), - config = pluginConfig + config = pluginConfigData ), - securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfig), + securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfigData), ) ) ).build(routes) diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/SecuritySchemesBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/SecuritySchemesBuilderTest.kt index 7e8f8135..96de733c 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/SecuritySchemesBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/SecuritySchemesBuilderTest.kt @@ -1,11 +1,12 @@ package io.github.smiley4.ktorswaggerui.tests.openapi -import io.github.smiley4.ktorswaggerui.dsl.AuthKeyLocation -import io.github.smiley4.ktorswaggerui.dsl.AuthScheme -import io.github.smiley4.ktorswaggerui.dsl.AuthType +import io.github.smiley4.ktorswaggerui.data.AuthKeyLocation +import io.github.smiley4.ktorswaggerui.data.AuthScheme +import io.github.smiley4.ktorswaggerui.data.AuthType +import io.github.smiley4.ktorswaggerui.data.SecuritySchemeData import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme -import io.github.smiley4.ktorswaggerui.spec.openapi.OAuthFlowsBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.SecuritySchemesBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OAuthFlowsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.SecuritySchemesBuilder import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.maps.shouldBeEmpty @@ -191,7 +192,7 @@ class SecuritySchemesBuilderTest : StringSpec({ private fun buildSecuritySchemeObjects(builders: Map Unit>): Map { return SecuritySchemesBuilder( oAuthFlowsBuilder = OAuthFlowsBuilder() - ).build(builders.map { (name, entry) -> OpenApiSecurityScheme(name).apply(entry) }) + ).build(builders.map { (name, entry) -> OpenApiSecurityScheme(name).apply(entry).build(SecuritySchemeData.DEFAULT) }) } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ServersBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ServersBuilderTest.kt index 0c8c4487..44192f6f 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ServersBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ServersBuilderTest.kt @@ -1,7 +1,8 @@ package io.github.smiley4.ktorswaggerui.tests.openapi +import io.github.smiley4.ktorswaggerui.data.ServerData import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer -import io.github.smiley4.ktorswaggerui.spec.openapi.ServerBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ServerBuilder import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.swagger.v3.oas.models.servers.Server @@ -35,7 +36,7 @@ class ServersBuilderTest : StringSpec({ companion object { private fun buildServerObject(builder: OpenApiServer.() -> Unit): Server { - return ServerBuilder().build(OpenApiServer().apply(builder)) + return ServerBuilder().build(OpenApiServer().apply(builder).build(ServerData.DEFAULT)) } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/TagsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/TagsBuilderTest.kt index bc2f3356..54123f80 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/TagsBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/TagsBuilderTest.kt @@ -1,8 +1,9 @@ package io.github.smiley4.ktorswaggerui.tests.openapi +import io.github.smiley4.ktorswaggerui.data.TagData import io.github.smiley4.ktorswaggerui.dsl.OpenApiTag -import io.github.smiley4.ktorswaggerui.spec.openapi.TagBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.TagExternalDocumentationBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.TagBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.TagExternalDocumentationBuilder import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe @@ -46,7 +47,7 @@ class TagsBuilderTest : StringSpec({ private fun buildTagObject(name: String, builder: OpenApiTag.() -> Unit): Tag { return TagBuilder( tagExternalDocumentationBuilder = TagExternalDocumentationBuilder() - ).build(OpenApiTag(name).apply(builder)) + ).build(OpenApiTag(name).apply(builder).build(TagData.DEFAULT)) } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/route/RouteDocumentationMergerTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/route/RouteDocumentationMergerTest.kt index 9a62986c..d4c81249 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/route/RouteDocumentationMergerTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/route/RouteDocumentationMergerTest.kt @@ -1,7 +1,7 @@ package io.github.smiley4.ktorswaggerui.tests.route import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute -import io.github.smiley4.ktorswaggerui.spec.route.RouteDocumentationMerger +import io.github.smiley4.ktorswaggerui.builder.route.RouteDocumentationMerger import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder @@ -37,6 +37,7 @@ class RouteDocumentationMergerTest : StringSpec({ "merge complete routes" { merge( route { + specId = "test-spec-a" tags = listOf("a1", "a2") summary = "Summary A" description = "Description A" @@ -60,6 +61,7 @@ class RouteDocumentationMergerTest : StringSpec({ } }, route { + specId = "test-spec-b" tags = listOf("b1", "b2") summary = "Summary B" description = "Description B" @@ -83,6 +85,7 @@ class RouteDocumentationMergerTest : StringSpec({ } } ).also { route -> + route.specId shouldBe "test-spec-a" route.tags shouldContainExactlyInAnyOrder listOf("a1", "a2", "b1", "b2") route.summary shouldBe "Summary A" route.description shouldBe "Description A" diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt index 1c1fa311..44ef34d6 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt @@ -12,10 +12,10 @@ import com.github.victools.jsonschema.module.swagger2.Swagger2Module import io.github.smiley4.ktorswaggerui.dsl.Example import io.github.smiley4.ktorswaggerui.dsl.SchemaEncoder import io.github.smiley4.ktorswaggerui.dsl.getSchemaType -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaDefinitions -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaTypeAttributeOverride -import io.github.smiley4.ktorswaggerui.spec.schema.TypeOverwrites +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaDefinitions +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaTypeAttributeOverride +import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.maps.shouldHaveSize @@ -30,6 +30,35 @@ import kotlin.reflect.jvm.javaType class SchemaBuilderTest : StringSpec({ + "victools field names".config(enabled = false) { + /** + * Test-case for https://github.com/SMILEY4/ktor-swagger-ui/issues/60. + * -> victools incorrectly ignores fields with is[A-Z] + */ + createSchemaVictools(false).also { defs -> + defs.root.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "object" + schema.properties.keys shouldContainExactly setOf("flag", "getAnotherText", "text", "isSomething", "isSomeText") + schema.properties["flag"]!!.also { prop -> + prop.type shouldBe "boolean" + } + schema.properties["isSomething"]!!.also { prop -> + prop.type shouldBe "boolean" + } + schema.properties["text"]!!.also { prop -> + prop.type shouldBe "string" + } + schema.properties["getAnotherText"]!!.also { prop -> + prop.type shouldBe "string" + } + schema.properties["isSomeText"]!!.also { prop -> + prop.type shouldBe "string" + } + } + } + } + "primitive (victools, all definitions)" { createSchemaVictools(true).also { defs -> defs.definitions.keys shouldContainExactly setOf("int") @@ -403,6 +432,14 @@ class SchemaBuilderTest : StringSpec({ ) + data class WithFieldNames( + val flag: Boolean, + val isSomething: Boolean, + val text: String, + val isSomeText: String, + val getAnotherText: String + ) + inline fun createSchemaVictools(definitions: Boolean) = createSchema("\$defs", serializerVictools(definitions)) diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt index c2012226..f4040d46 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt @@ -6,17 +6,18 @@ import com.github.victools.jsonschema.generator.OptionPreset import com.github.victools.jsonschema.generator.SchemaGenerator import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder import com.github.victools.jsonschema.generator.SchemaVersion -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute -import io.github.smiley4.ktorswaggerui.dsl.array import io.github.smiley4.ktorswaggerui.dsl.asSchemaType import io.github.smiley4.ktorswaggerui.dsl.getSchemaType -import io.github.smiley4.ktorswaggerui.dsl.obj -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContextBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.TypeOverwrites +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextBuilder +import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites +import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.custom +import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.multipleOf import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder @@ -289,7 +290,7 @@ class SchemaContextTest : StringSpec({ } "custom schema object" { - val config = SwaggerUIPluginConfig().also { + val config = PluginConfigDsl().also { it.customSchemas { openApi("myCustomSchema") { Schema().also { schema -> @@ -306,12 +307,12 @@ class SchemaContextTest : StringSpec({ val routes = listOf( route { request { - body(obj("myCustomSchema")) + body(custom("myCustomSchema")) } } ) val schemaContext = schemaContext(routes, config) - schemaContext.getSchema(obj("myCustomSchema")).also { schema -> + schemaContext.getSchema("myCustomSchema").also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/myCustomSchema" } @@ -327,7 +328,7 @@ class SchemaContextTest : StringSpec({ } "custom schema array" { - val config = SwaggerUIPluginConfig().also { + val config = PluginConfigDsl().also { it.customSchemas { openApi("myCustomSchema") { Schema().also { schema -> @@ -344,19 +345,14 @@ class SchemaContextTest : StringSpec({ val routes = listOf( route { request { - body(array("myCustomSchema")) + body(multipleOf(custom("myCustomSchema"))) } } ) val schemaContext = schemaContext(routes, config) - schemaContext.getSchema(array("myCustomSchema")).also { schema -> - schema.type shouldBe "array" - schema.`$ref` shouldBe null - schema.items - .also { it shouldNotBe null } - ?.also { items -> - items.`$ref` shouldBe "#/components/schemas/myCustomSchema" - } + schemaContext.getSchema("myCustomSchema").also { schema -> + schema.type shouldBe null + schema.`$ref` shouldBe "#/components/schemas/myCustomSchema" } schemaContext.getComponentsSection().also { components -> components.keys shouldContainExactlyInAnyOrder listOf( @@ -378,7 +374,7 @@ class SchemaContextTest : StringSpec({ .with(Option.ALLOF_CLEANUP_AT_THE_END) .build() ) - val config = SwaggerUIPluginConfig().also { + val config = PluginConfigDsl().also { it.encoding { schemaEncoder { type -> generator.generateSchema(type.javaType).toString() @@ -414,7 +410,7 @@ class SchemaContextTest : StringSpec({ } "don't include unused custom schema" { - val config = SwaggerUIPluginConfig().also { + val config = PluginConfigDsl().also { it.customSchemas { includeAll = false openApi("myCustomSchema") { @@ -430,14 +426,14 @@ class SchemaContextTest : StringSpec({ } } val schemaContext = schemaContext(emptyList(), config) - schemaContext.getSchemaOrNull(obj("myCustomSchema")) shouldBe null + schemaContext.getSchemaOrNull("myCustomSchema") shouldBe null schemaContext.getComponentsSection().also { components -> components.keys shouldHaveSize 0 } } "include unused custom schema" { - val config = SwaggerUIPluginConfig().also { + val config = PluginConfigDsl().also { it.customSchemas { includeAll = true openApi("myCustomSchema") { @@ -453,7 +449,7 @@ class SchemaContextTest : StringSpec({ } } val schemaContext = schemaContext(emptyList(), config) - schemaContext.getSchema(obj("myCustomSchema")).also { schema -> + schemaContext.getSchema("myCustomSchema").also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/myCustomSchema" } @@ -498,17 +494,18 @@ class SchemaContextTest : StringSpec({ inline fun getType() = getSchemaType() - private val defaultPluginConfig = SwaggerUIPluginConfig() + private val defaultPluginConfig = PluginConfigDsl() private fun schemaContext( routes: Collection, - pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig + pluginConfig: PluginConfigDsl = defaultPluginConfig ): SchemaContext { + val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) return SchemaContextBuilder( - config = pluginConfig, + config = pluginConfigData, schemaBuilder = SchemaBuilder( - definitionsField = pluginConfig.encodingConfig.schemaDefinitionsField, - schemaEncoder = pluginConfig.encodingConfig.getSchemaEncoder(), + definitionsField = pluginConfigData.encoding.schemaDefsField, + schemaEncoder = pluginConfigData.encoding.schemaEncoder, ObjectMapper(), TypeOverwrites.get() )