diff --git a/gradle.properties b/gradle.properties index ccec52fc..cd97b428 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ projectDeveloperUrl=https://github.com/SMILEY4 # dependency versions versionKtor=2.3.11 versionSwaggerUI=5.9.0 -versionSwaggerParser=2.1.19 +versionSwaggerParser=2.1.22 versionSchemaKenerator=0.4.0 versionKotlinLogging=3.0.5 versionKotest=5.8.0 diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteConfig.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteConfig.kt index 627a1c8d..f6312db9 100644 --- a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteConfig.kt +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteConfig.kt @@ -14,6 +14,7 @@ import io.github.smiley4.schemakenerator.swagger.compileReferencingRoot import io.github.smiley4.schemakenerator.swagger.data.TitleType import io.github.smiley4.schemakenerator.swagger.generateSwaggerSchema import io.github.smiley4.schemakenerator.swagger.withAutoTitle +import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.server.application.Application import io.ktor.server.application.call @@ -30,12 +31,16 @@ fun main() { embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) } +class Greeting( + val name: String +) /** * A (nearly) complete - and mostly nonsensical - plugin configuration */ private fun Application.myModule() { + install(SwaggerUI) { info { title = "Example API" @@ -50,6 +55,7 @@ private fun Application.myModule() { license { name = "Example License" url = "example.com" + identifier = "Apache-2.0" } } externalDocs { @@ -59,10 +65,20 @@ private fun Application.myModule() { server { url = "localhost" description = "local dev-server" + variable("version") { + default = "1.0" + enum = setOf("1.0", "2.0", "3.0") + description = "the version of the server api" + } } server { url = "example.com" description = "productive server" + variable("version") { + default = "1.0" + enum = setOf("1.0", "2.0") + description = "the version of the server api" + } } swagger { displayOperationId = true @@ -138,15 +154,53 @@ private fun Application.myModule() { // a documented route get("hello", { - description = "A Hello-World route" + operationId = "hello" + summary = "hello world route" + description = "A Hello-World route as an example." + tags = setOf("hello", "example") + specId = PluginConfigDsl.DEFAULT_SPEC_ID + deprecated = false + hidden = false + protected = false + securitySchemeNames = emptyList() + externalDocs { + url = "example.com/hello" + description = "external documentation of 'hello'-route" + } request { queryParameter("name") { description = "the name to greet" + example("Josh") { + value = "Josh" + summary = "Example name" + description = "An example name for this query parameter" + } } + body() } response { HttpStatusCode.OK to { description = "successful request - always returns 'Hello World!'" + header("x-random") { + description = "A header with some random number" + required = true + deprecated = false + explode = false + } + body { + description = "the greeting object with the name of the person to greet." + mediaTypes = setOf(ContentType.Application.Json) + required = true + } + } + } + server { + url = "example.com" + description = "productive server for 'hello'-route" + variable("version") { + default = "1.0" + enum = setOf("1.0", "2.0") + description = "the version of the server api" } } }) { diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index 8c3dd7e6..dae653b2 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -75,7 +75,7 @@ val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration 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) + val specName = route.documentation.specId ?: config.specAssigner(route.path, route.documentation.tags.toList()) computeIfAbsent(specName) { mutableListOf() }.add(route) } } @@ -157,6 +157,8 @@ private fun builder( config = config ), securityRequirementsBuilder = SecurityRequirementsBuilder(config), + externalDocumentationBuilder = ExternalDocumentationBuilder(), + serverBuilder = ServerBuilder() ) ) ), diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt index 964f9354..63330eaa 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt @@ -14,6 +14,8 @@ class HeaderBuilder( it.required = header.required it.deprecated = header.deprecated it.schema = header.type?.let { t -> schemaContext.getSchema(t) } + it.explode = header.explode +// it.example = TODO() } } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt index bfc03dc2..0fb0e99e 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt @@ -20,6 +20,7 @@ class InfoBuilder( info.license?.also { license -> it.license = licenseBuilder.build(license) } + it.summary = info.summary } } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt index 30f727c0..056662bb 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt @@ -9,6 +9,7 @@ class LicenseBuilder { License().also { it.name = license.name it.url = license.url + it.identifier = license.identifier } } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt index fc734820..b0830ab9 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt @@ -5,6 +5,7 @@ import io.github.smiley4.ktorswaggerui.data.PluginConfigData import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.SpecVersion class OpenApiBuilder( private val config: PluginConfigData, @@ -20,6 +21,8 @@ class OpenApiBuilder( fun build(routes: Collection): OpenAPI { return OpenAPI().also { + it.specVersion = SpecVersion.V31 + it.openapi = "3.1.0" it.info = infoBuilder.build(config.info) it.externalDocs = externalDocumentationBuilder.build(config.externalDocs) it.servers = config.servers.map { server -> serverBuilder.build(server) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt index dab2bbe2..339faedc 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt @@ -8,7 +8,9 @@ class OperationBuilder( private val parameterBuilder: ParameterBuilder, private val requestBodyBuilder: RequestBodyBuilder, private val responsesBuilder: ResponsesBuilder, - private val securityRequirementsBuilder: SecurityRequirementsBuilder + private val securityRequirementsBuilder: SecurityRequirementsBuilder, + private val externalDocumentationBuilder: ExternalDocumentationBuilder, + private val serverBuilder: ServerBuilder ) { fun build(route: RouteMeta): Operation = @@ -30,6 +32,10 @@ class OperationBuilder( } } } + it.externalDocs = route.documentation.externalDocs?.let { docs -> externalDocumentationBuilder.build(docs) } + if (route.documentation.servers.isNotEmpty()) { + it.servers = route.documentation.servers.map { server -> serverBuilder.build(server) } + } } } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt index ff5d1d0f..221b33d9 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt @@ -24,9 +24,10 @@ class ParameterBuilder( it.deprecated = parameter.deprecated it.allowEmptyValue = parameter.allowEmptyValue it.explode = parameter.explode - it.example = parameter.example?.let { e -> exampleContext.getExample(e).value } + it.example = parameter.example?.let { e -> exampleContext.getExample(e).value } // todo: example"S" ? it.allowReserved = parameter.allowReserved it.schema = schemaContext.getSchema(parameter.type) + it.style = parameter.style } } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt index 5878483f..dfe2ba6b 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt @@ -2,6 +2,8 @@ package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.data.ServerData import io.swagger.v3.oas.models.servers.Server +import io.swagger.v3.oas.models.servers.ServerVariable +import io.swagger.v3.oas.models.servers.ServerVariables class ServerBuilder { @@ -9,6 +11,17 @@ class ServerBuilder { Server().also { it.url = server.url it.description = server.description + if (server.variables.isNotEmpty()) { + it.variables = ServerVariables().also { variables -> + server.variables.forEach { entry -> + variables.addServerVariable(entry.name, ServerVariable().also { variable -> + variable.enum = entry.enum.toList() + variable.default = entry.default + variable.description = entry.description + }) + } + } + } } } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt index fe8e1642..c8ee0256 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt @@ -4,18 +4,20 @@ data class InfoData( val title: String, val version: String?, val description: String?, + val summary: String?, val termsOfService: String?, val contact: ContactData?, - val license: LicenseData? + val license: LicenseData?, ) { companion object { val DEFAULT = InfoData( title = "API", version = null, description = null, + summary = null, termsOfService = null, contact = null, - license = null + license = null, ) } } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt index 154be47b..5ae2002d 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt @@ -3,11 +3,13 @@ package io.github.smiley4.ktorswaggerui.data data class LicenseData( val name: String?, val url: String?, + val identifier: String? ) { companion object { val DEFAULT = LicenseData( name = null, url = null, + identifier = null ) } } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiHeaderData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiHeaderData.kt index a734189c..08eb7383 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiHeaderData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiHeaderData.kt @@ -5,4 +5,5 @@ data class OpenApiHeaderData( val type: TypeDescriptor?, val required: Boolean, val deprecated: Boolean, + val explode: Boolean?, ) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRequestParameterData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRequestParameterData.kt index c947e66d..6946d1cd 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRequestParameterData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRequestParameterData.kt @@ -1,5 +1,7 @@ package io.github.smiley4.ktorswaggerui.data +import io.swagger.v3.oas.models.parameters.Parameter + data class OpenApiRequestParameterData( val name: String, val type: TypeDescriptor, @@ -10,5 +12,6 @@ data class OpenApiRequestParameterData( val deprecated: Boolean, val allowEmptyValue: Boolean, val explode: Boolean, - val allowReserved: Boolean + val allowReserved: Boolean, + val style: Parameter.StyleEnum?, ) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRouteData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRouteData.kt index 8b980a8a..1add5d68 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRouteData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRouteData.kt @@ -2,7 +2,7 @@ package io.github.smiley4.ktorswaggerui.data data class OpenApiRouteData( val specId: String?, - val tags: List, + val tags: Set, val summary: String?, val description: String?, val operationId: String?, @@ -12,4 +12,6 @@ data class OpenApiRouteData( val protected: Boolean?, val request: OpenApiRequestData, val responses: List, + val externalDocs: ExternalDocsData?, + val servers: List, ) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt index a98ca613..8fec5417 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt @@ -3,12 +3,14 @@ package io.github.smiley4.ktorswaggerui.data data class ServerData( val url: String, val description: String?, + val variables: List ) { companion object { val DEFAULT = ServerData( url = "/", - description = null + description = null, + variables = emptyList() ) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerVariableData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerVariableData.kt new file mode 100644 index 00000000..415c98e0 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerVariableData.kt @@ -0,0 +1,8 @@ +package io.github.smiley4.ktorswaggerui.data + +data class ServerVariableData( + val name: String, + val enum: Set, + val default: String, + val description: String? +) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiInfo.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiInfo.kt index 84e2387f..83923bdb 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiInfo.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiInfo.kt @@ -27,6 +27,11 @@ class OpenApiInfo { */ var description: String? = null + /** + * A short summary of the API + */ + var summary: String? = null + /** * A URL to the Terms of Service for the API. MUST be in the format of a URL. @@ -62,7 +67,8 @@ class OpenApiInfo { 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 + license = license?.build(base.license ?: LicenseData.DEFAULT) ?: base.license, + summary = merge(base.summary, this.summary) ) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiLicense.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiLicense.kt index 66fef89a..3d05b853 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiLicense.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiLicense.kt @@ -22,8 +22,14 @@ class OpenApiLicense { var url: String? = LicenseData.DEFAULT.url + /** + * An SPDX (https://spdx.org/licenses/) license expression for the API. The identifier field is mutually exclusive of the url field. + */ + var identifier: String? = LicenseData.DEFAULT.identifier + fun build(base: LicenseData) = LicenseData( name = DataUtils.merge(base.name, name), url = DataUtils.merge(base.url, url), + identifier = DataUtils.merge(base.identifier, identifier) ) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServer.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServer.kt index da249d17..5c667d62 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServer.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServer.kt @@ -22,10 +22,23 @@ class OpenApiServer { */ var description: String? = ServerData.DEFAULT.description + private val variables = mutableMapOf() + + + /** + * Adds a new server variable with the given name + */ + fun variable(name: String, block: OpenApiServerVariable.() -> Unit) { + variables[name] = OpenApiServerVariable(name).apply(block) + } fun build(base: ServerData) = ServerData( url = mergeDefault(base.url, url, ServerData.DEFAULT.url), - description = merge(base.description, description) + description = merge(base.description, description), + variables = buildMap { + base.variables.forEach { this[it.name] = it } + variables.values.map { it.build() }.forEach { this[it.name] = it } + }.values.toList() ) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServerVariable.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServerVariable.kt new file mode 100644 index 00000000..dd2fc39d --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServerVariable.kt @@ -0,0 +1,41 @@ +package io.github.smiley4.ktorswaggerui.dsl.config + +import io.github.smiley4.ktorswaggerui.data.ServerVariableData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker + +/** + * An object representing a Server Variable for server URL template substitution. + */ +@OpenApiDslMarker +class OpenApiServerVariable( + /** + * The name of this variable + */ + private val name: String +) { + + /** + * An enumeration of string values to be used if the substitution options are from a limited set. Must not be empty. + */ + var enum: Collection = emptyList() + + + /** + * The default value to use for substitution. Must be in the list of enums. + */ + var default: String? = null + + + /** + * An optional description for this server variable. + */ + var description: String? = null + + fun build() = ServerVariableData( + name = name, + enum = enum.toSet(), + default = (default ?: enum.firstOrNull()) ?: "", + description = description + ) + +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiBaseBody.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiBaseBody.kt index 20f846c0..20adb16f 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiBaseBody.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiBaseBody.kt @@ -23,7 +23,7 @@ sealed class OpenApiBaseBody { /** * Allowed Media Types for this body. If none specified, a media type will be chosen automatically based on the provided schema */ - var mediaTypes: Set = emptySet() + var mediaTypes: Collection = emptySet() abstract fun build(): OpenApiBaseBodyData } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiHeader.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiHeader.kt index 1a2b6ef2..73227c79 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiHeader.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiHeader.kt @@ -62,11 +62,13 @@ class OpenApiHeader { */ var deprecated: Boolean? = null + var explode: Boolean? = null fun build() = OpenApiHeaderData( description = description, type = type, required = required ?: false, - deprecated = deprecated ?: false + deprecated = deprecated ?: false, + explode = explode, ) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiMultipartBody.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiMultipartBody.kt index e1ee012d..fc89dee3 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiMultipartBody.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiMultipartBody.kt @@ -50,7 +50,7 @@ class OpenApiMultipartBody : OpenApiBaseBody() { override fun build() = OpenApiMultipartBodyData( description = description, required = required ?: false, - mediaTypes = mediaTypes, + mediaTypes = mediaTypes.toSet(), parts = parts.map { it.build() } ) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequest.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequest.kt index d307b075..d078eddf 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequest.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequest.kt @@ -19,6 +19,7 @@ class OpenApiRequest { */ val parameters = mutableListOf() + /** * A path parameters that is applicable for this operation */ @@ -33,18 +34,21 @@ class OpenApiRequest { fun pathParameter(name: String, type: TypeDescriptor, block: OpenApiRequestParameter.() -> Unit = {}) = parameter(ParameterLocation.PATH, name, type, block) + /** * A path parameters that is applicable for this operation */ fun pathParameter(name: String, type: Schema<*>, block: OpenApiRequestParameter.() -> Unit = {}) = parameter(ParameterLocation.PATH, name, SwaggerTypeDescriptor(type), block) + /** * A path parameters that is applicable for this operation */ fun pathParameter(name: String, type: KType, block: OpenApiRequestParameter.() -> Unit = {}) = parameter(ParameterLocation.PATH, name, KTypeDescriptor(type), block) + /** * A path parameters that is applicable for this operation */ @@ -58,18 +62,21 @@ class OpenApiRequest { fun queryParameter(name: String, type: TypeDescriptor, block: OpenApiRequestParameter.() -> Unit = {}) = parameter(ParameterLocation.QUERY, name, type, block) + /** * A query parameters that is applicable for this operation */ fun queryParameter(name: String, type: Schema<*>, block: OpenApiRequestParameter.() -> Unit = {}) = parameter(ParameterLocation.QUERY, name, SwaggerTypeDescriptor(type), block) + /** * A query parameters that is applicable for this operation */ fun queryParameter(name: String, type: KType, block: OpenApiRequestParameter.() -> Unit = {}) = parameter(ParameterLocation.QUERY, name, KTypeDescriptor(type), block) + /** * A query parameters that is applicable for this operation */ @@ -83,18 +90,21 @@ class OpenApiRequest { fun headerParameter(name: String, type: TypeDescriptor, block: OpenApiRequestParameter.() -> Unit = {}) = parameter(ParameterLocation.HEADER, name, type, block) + /** * A header parameters that is applicable for this operation */ fun headerParameter(name: String, type: Schema<*>, block: OpenApiRequestParameter.() -> Unit = {}) = parameter(ParameterLocation.HEADER, name, SwaggerTypeDescriptor(type), block) + /** * A header parameters that is applicable for this operation */ fun headerParameter(name: String, type: KType, block: OpenApiRequestParameter.() -> Unit = {}) = parameter(ParameterLocation.HEADER, name, KTypeDescriptor(type), block) + /** * A header parameters that is applicable for this operation */ @@ -111,19 +121,25 @@ class OpenApiRequest { * The body returned with this request */ fun body(type: TypeDescriptor, block: OpenApiSimpleBody.() -> Unit = {}) { - body = OpenApiSimpleBody(type).apply(block) + val result = OpenApiSimpleBody(type).apply(block) + if (!result.isEmptyBody()) { + body = result + } } + /** * The body returned with this request */ fun body(type: Schema<*>, block: OpenApiSimpleBody.() -> Unit = {}) = body(SwaggerTypeDescriptor(type), block) + /** * The body returned with this request */ fun body(type: KType, block: OpenApiSimpleBody.() -> Unit = {}) = body(KTypeDescriptor(type), block) + /** * The body returned with this request */ @@ -150,4 +166,14 @@ class OpenApiRequest { body = body?.build() ) + private fun OpenApiBaseBody.isEmptyBody(): Boolean { + return when (this) { + is OpenApiSimpleBody -> when (type) { + is KTypeDescriptor -> type.type == typeOf() + else -> false + } + is OpenApiMultipartBody -> false + } + } + } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequestParameter.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequestParameter.kt index d828998e..93f822ee 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequestParameter.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequestParameter.kt @@ -8,6 +8,7 @@ import io.github.smiley4.ktorswaggerui.data.TypeDescriptor import io.github.smiley4.ktorswaggerui.data.ValueExampleDescriptor import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker import io.swagger.v3.oas.models.examples.Example +import io.swagger.v3.oas.models.parameters.Parameter @OpenApiDslMarker @@ -99,6 +100,11 @@ class OpenApiRequestParameter( var allowReserved: Boolean? = null + /** + * Describes how the parameter value will be serialized depending on the type of the parameter value. + */ + var style: Parameter.StyleEnum? = null + fun build() = OpenApiRequestParameterData( name = name, type = type, @@ -110,6 +116,7 @@ class OpenApiRequestParameter( allowEmptyValue = allowEmptyValue ?: true, explode = explode ?: false, allowReserved = allowReserved ?: true, + style = style ) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRoute.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRoute.kt index 90fa050c..2b2def45 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRoute.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRoute.kt @@ -1,7 +1,11 @@ package io.github.smiley4.ktorswaggerui.dsl.routes +import io.github.smiley4.ktorswaggerui.data.ExternalDocsData import io.github.smiley4.ktorswaggerui.data.OpenApiRouteData +import io.github.smiley4.ktorswaggerui.data.ServerData import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.github.smiley4.ktorswaggerui.dsl.config.OpenApiExternalDocs +import io.github.smiley4.ktorswaggerui.dsl.config.OpenApiServer @OpenApiDslMarker class OpenApiRoute { @@ -15,7 +19,7 @@ class OpenApiRoute { /** * A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources or any other qualifier */ - var tags: List = emptyList() + var tags: Collection = emptyList() /** @@ -49,14 +53,6 @@ class OpenApiRoute { var hidden: Boolean = false - /** - * A declaration of which security mechanisms can be used for this operation (i.e. any of the specified ones). - * If none is specified, defaultSecuritySchemeName (global plugin config) will be used. - * Only applied to [protected] operations. - */ - var securitySchemeNames: Collection? = null - - /** * Specifies whether this operation is protected. * If not specified, the authentication state of the Ktor route will be used @@ -64,6 +60,14 @@ class OpenApiRoute { */ var protected: Boolean? = null + + /** + * A declaration of which security mechanisms can be used for this operation (i.e. any of the specified ones). + * If none is specified, defaultSecuritySchemeName (global plugin config) will be used. + * Only applied to [protected] operations. + */ + var securitySchemeNames: Collection? = null + private val request = OpenApiRequest() @@ -89,9 +93,29 @@ class OpenApiRoute { fun getResponses() = responses + /** + * OpenAPI external docs configuration - link and description of an external documentation + */ + fun externalDocs(block: OpenApiExternalDocs.() -> Unit) { + externalDocs = OpenApiExternalDocs().apply(block) + } + + private var externalDocs: OpenApiExternalDocs? = null + + + /** + * 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 build() = OpenApiRouteData( specId = specId, - tags = tags, + tags = tags.toSet(), summary = summary, description = description, operationId = operationId, @@ -101,6 +125,8 @@ class OpenApiRoute { protected = protected, request = request.build(), responses = responses.getResponses().map { it.build() }, + externalDocs = externalDocs?.build(ExternalDocsData.DEFAULT), + servers = servers.map { it.build(ServerData.DEFAULT) } ) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiSimpleBody.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiSimpleBody.kt index e32c5c32..517dc52e 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiSimpleBody.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiSimpleBody.kt @@ -72,7 +72,7 @@ class OpenApiSimpleBody( override fun build() = OpenApiSimpleBodyData( description = description, required = required ?: false, - mediaTypes = mediaTypes, + mediaTypes = mediaTypes.toSet(), type = type, examples = examples, ) diff --git a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/InfoBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/InfoBuilderTest.kt index d2f215c8..efa98ce6 100644 --- a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/InfoBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/InfoBuilderTest.kt @@ -29,6 +29,7 @@ class InfoBuilderTest : StringSpec({ buildInfoObject { title = "Test Api" version = "1.0" + summary = "testing api" description = "Api for testing" termsOfService = "test-tos" contact { @@ -40,10 +41,12 @@ class InfoBuilderTest : StringSpec({ license { name = "Test License" url = "example.com" + identifier = "Example" } }.also { info -> info.title shouldBe "Test Api" info.version shouldBe "1.0" + info.summary shouldBe "testing api" info.description shouldBe "Api for testing" info.termsOfService shouldBe "test-tos" info.contact @@ -58,9 +61,9 @@ class InfoBuilderTest : StringSpec({ ?.also { license -> license.name shouldBe "Test License" license.url shouldBe "example.com" + license.identifier shouldBe "Example" } info.extensions shouldBe null - info.summary shouldBe null } } diff --git a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt index fa359f71..b805c6d7 100644 --- a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt @@ -33,6 +33,7 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.maps.shouldHaveSize +import io.kotest.matchers.should import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.swagger.v3.oas.models.OpenAPI @@ -67,10 +68,16 @@ class OpenApiBuilderTest : StringSpec({ } buildOpenApiObject(emptyList(), config).also { openapi -> openapi.servers shouldHaveSize 2 - openapi.servers.map { it.url } shouldContainExactlyInAnyOrder listOf( - "http://localhost:8080", - "https://127.0.0.1" - ) + openapi.servers.find { it.url == "http://localhost:8080" }!!.also { server -> + server.url shouldBe "http://localhost:8080" + server.description shouldBe "Development Server" + server.variables shouldBe null + } + openapi.servers.find { it.url == "https://127.0.0.1" }!!.also { server -> + server.url shouldBe "https://127.0.0.1" + server.description shouldBe "Production Server" + server.variables shouldBe null + } } } @@ -160,6 +167,8 @@ class OpenApiBuilderTest : StringSpec({ config = pluginConfigData ), securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfigData), + externalDocumentationBuilder = ExternalDocumentationBuilder(), + serverBuilder = ServerBuilder() ) ) ), diff --git a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt index 005bd773..f35e0724 100644 --- a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt @@ -1,9 +1,9 @@ package io.github.smiley4.ktorswaggerui.builder -import com.fasterxml.jackson.databind.ObjectMapper import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextImpl import io.github.smiley4.ktorswaggerui.builder.openapi.ContentBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ExternalDocumentationBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.HeaderBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.OperationBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.OperationTagsBuilder @@ -12,6 +12,7 @@ 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.ServerBuilder import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextImpl @@ -1001,6 +1002,8 @@ class OperationBuilderTest : StringSpec({ config = pluginConfigData ), securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfigData), + externalDocumentationBuilder = ExternalDocumentationBuilder(), + serverBuilder = ServerBuilder() ).build(route) } diff --git a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt index f31cb745..f55f7675 100644 --- a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt @@ -3,6 +3,7 @@ package io.github.smiley4.ktorswaggerui.builder import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextImpl import io.github.smiley4.ktorswaggerui.builder.openapi.ContentBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ExternalDocumentationBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.HeaderBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.OperationBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.OperationTagsBuilder @@ -13,6 +14,7 @@ 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.ServerBuilder import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextImpl @@ -133,6 +135,8 @@ class PathsBuilderTest : StringSpec({ config = pluginConfigData ), securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfigData), + externalDocumentationBuilder = ExternalDocumentationBuilder(), + serverBuilder = ServerBuilder() ) ) ).build(routes) diff --git a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/ServersBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/ServersBuilderTest.kt index aefda0d9..afa7dec7 100644 --- a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/ServersBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/ServersBuilderTest.kt @@ -4,6 +4,8 @@ import io.github.smiley4.ktorswaggerui.builder.openapi.ServerBuilder import io.github.smiley4.ktorswaggerui.data.ServerData import io.github.smiley4.ktorswaggerui.dsl.config.OpenApiServer import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.should import io.kotest.matchers.shouldBe import io.swagger.v3.oas.models.servers.Server @@ -15,7 +17,6 @@ class ServersBuilderTest : StringSpec({ server.description shouldBe null server.variables shouldBe null server.extensions shouldBe null - } } @@ -23,10 +24,30 @@ class ServersBuilderTest : StringSpec({ buildServerObject { url = "Test URL" description = "Test Description" + variable("version") { + description = "the version of the api" + default = "2" + enum = setOf("1", "2", "3") + } + variable("region") { + description = "the region of the api" + default = "somewhere" + enum = setOf("somewhere", "else") + } }.also { server -> server.url shouldBe "Test URL" server.description shouldBe "Test Description" - server.variables shouldBe null + server.variables.keys shouldContainExactlyInAnyOrder listOf("version", "region") + server.variables["version"]!!.also { variable -> + variable.description shouldBe "the version of the api" + variable.default shouldBe "2" + variable.enum shouldContainExactlyInAnyOrder listOf("1", "2", "3") + } + server.variables["region"]!!.also { variable -> + variable.description shouldBe "the region of the api" + variable.default shouldBe "somewhere" + variable.enum shouldContainExactlyInAnyOrder listOf("somewhere", "else") + } server.extensions shouldBe null } }