Skip to content

Commit

Permalink
Merge branch 'develop' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
SMILEY4 committed Nov 17, 2024
2 parents 0a85abe + 12cfd09 commit f0cdba6
Show file tree
Hide file tree
Showing 15 changed files with 206 additions and 36 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ dependencies {
```


## Ktor compatibility

- Ktor 2.x: ktor-swagger-ui up to 3.x
- Ktor 3.x: ktor-swagger-ui starting with 4.0


## Examples

Runnable examples can be found in [ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples](https://github.com/SMILEY4/ktor-swagger-ui/tree/release/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples).
Expand Down
5 changes: 3 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ kotlin.code.style=official
# project id
projectGroupId=io.github.smiley4
projectArtifactIdBase=ktor-swagger-ui
projectVersion=4.0.0
projectVersion=4.1.0

# publishing information
projectNameBase=Ktor Swagger UI
Expand All @@ -19,9 +19,10 @@ projectDeveloperUrl=https://github.com/SMILEY4
versionKtor=3.0.0
versionSwaggerUI=5.17.11
versionSwaggerParser=2.1.22
versionSchemaKenerator=1.5.0
versionSchemaKenerator=1.6.0
versionKotlinLogging=7.0.0
versionKotest=5.8.0
versionKotlinTest=2.0.21
versionMockk=1.13.12
versionLogback=1.5.6
versionJackson=2.18.1
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ private fun Application.myModule() {

// Install and configure the "SwaggerUI"-Plugin
install(SwaggerUI) {
schemas { }
security {
// configure a basic-auth security scheme
securityScheme("MySecurityScheme") {
Expand All @@ -53,6 +54,7 @@ private fun Application.myModule() {
// if no other response is documented for "401 Unauthorized", this information is used instead
defaultUnauthorizedResponse {
description = "Username or password is invalid"
body<AuthRequired>()
}
}
}
Expand All @@ -74,6 +76,12 @@ private fun Application.myModule() {
}) {
call.respondText("Hello World!")
}

get("protected2", {
// response for "401 Unauthorized" is automatically added if configured in the plugin-config and not specified otherwise
}) {
call.respondText("Hello World!")
}
}

// route is not in an "authenticate"-block but "protected"-flag is set (e.g. because is it protected by an external reverse-proxy
Expand All @@ -96,3 +104,6 @@ private fun Application.myModule() {
}

}


class AuthRequired(val message: String)
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ private fun Application.myModule() {
specAssigner = { _, _ -> PluginConfigDsl.DEFAULT_SPEC_ID }
pathFilter = { _, url -> url.firstOrNull() != "hidden" }
ignoredRouteSelectors = emptySet()
ignoredRouteSelectorClassNames = emptySet()
postBuild = { api, name -> println("Completed api '$name': $api") }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import io.github.smiley4.schemakenerator.serialization.processKotlinxSerializati
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.github.smiley4.schemakenerator.swagger.withTitle
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
Expand All @@ -37,7 +36,7 @@ private fun Application.myModule() {
// see https://github.com/SMILEY4/schema-kenerator for more information
.processKotlinxSerialization()
.generateSwaggerSchema()
.withAutoTitle(TitleType.SIMPLE)
.withTitle(TitleType.SIMPLE)
.compileReferencingRoot()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package io.github.smiley4.ktorswaggerui.examples

import io.github.smiley4.ktorswaggerui.SwaggerUI
import io.github.smiley4.ktorswaggerui.data.kotlinxExampleEncoder
import io.github.smiley4.ktorswaggerui.dsl.routing.get
import io.github.smiley4.ktorswaggerui.routing.openApiSpec
import io.github.smiley4.ktorswaggerui.routing.swaggerUI
import io.github.smiley4.schemakenerator.serialization.processKotlinxSerialization
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.withTitle
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.server.response.respondText
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import kotlinx.serialization.Serializable

fun main() {
embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true)
}

private fun Application.myModule() {

install(SwaggerUI) {
schemas {
// configure the schema generator to use kotlinx-serializer
// (see https://github.com/SMILEY4/schema-kenerator/wiki for more information)
generator = { type ->
type
.processKotlinxSerialization()
.generateSwaggerSchema()
.withTitle(TitleType.SIMPLE)
.compileReferencingRoot()
}
}
examples {
// configure the example encoder to encode kotlin objects using kotlinx-serializer
exampleEncoder = kotlinxExampleEncoder
}
}

routing {

// add the routes for swagger-ui and api-spec
route("swagger") {
swaggerUI("/api.json")
}
route("api.json") {
openApiSpec()
}

// a documented route
get("hello", {
description = "A Hello-World route"
request {
queryParameter<String>("name") {
description = "the name to greet"
example("Name Parameter") {
value = "Mr. Example"
}
}
}
response {
code(HttpStatusCode.OK) {
description = "successful request - always returns 'Hello World!'"
body<TestResponse> {
example("Success Response") {
value = TestResponse(
name = "Mr. Example",
length = 11
)
}
}
}
}
}) {
call.respondText("Hello ${call.request.queryParameters["name"]}")
}

}

}


@Serializable
data class TestResponse(
val name: String,
val length: Int,
)
4 changes: 4 additions & 0 deletions ktor-swagger-ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies {
val versionKotest: String by project
val versionKotlinTest: String by project
val versionMockk: String by project
val versionJackson: String by project

implementation("io.ktor:ktor-server-core-jvm:$versionKtor")
implementation("io.ktor:ktor-server-auth:$versionKtor")
Expand All @@ -38,6 +39,8 @@ dependencies {

implementation("io.swagger.parser.v3:swagger-parser:$versionSwaggerParser")

implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$versionJackson")

implementation("io.github.smiley4:schema-kenerator-core:$versionSchemaKenerator")
implementation("io.github.smiley4:schema-kenerator-reflection:$versionSchemaKenerator")
implementation("io.github.smiley4:schema-kenerator-swagger:$versionSchemaKenerator")
Expand All @@ -54,6 +57,7 @@ dependencies {
testImplementation("io.kotest:kotest-assertions-core:$versionKotest")
testImplementation("org.jetbrains.kotlin:kotlin-test:$versionKotlinTest")
testImplementation("io.mockk:mockk:$versionMockk")

}

kotlin {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class RouteCollector(
is ParameterRouteSelector -> route.parent?.let { getPath(it, config) } ?: ""
is ConstantParameterRouteSelector -> route.parent?.let { getPath(it, config) } ?: ""
is OptionalParameterRouteSelector -> route.parent?.let { getPath(it, config) } ?: ""
else -> (route.parent?.let { getPath(it, config) } ?: "") + "/" + route.selector.toString()
else -> (route.parent?.let { getPath(it, config) } ?: "").dropLastWhile { it == '/' } + "/" + route.selector.toString()
}
}
}
Expand All @@ -91,7 +91,8 @@ class RouteCollector(
is ParameterRouteSelector -> true
is ConstantParameterRouteSelector -> true
is OptionalParameterRouteSelector -> true
else -> config.ignoredRouteSelectors.any { selector::class.isSubclassOf(it) }
else -> config.ignoredRouteSelectors.any { selector::class.isSubclassOf(it) } or
config.ignoredRouteSelectorClassNames.any { selector::class.java.name == it }
}
}

Expand All @@ -112,5 +113,4 @@ class RouteCollector(
return (listOf(root) + root.children.flatMap { allRoutes(it) })
.filter { it.selector is HttpMethodRouteSelector }
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,21 @@ class SchemaContextImpl(private val schemaConfig: SchemaConfigData) : SchemaCont

fun addGlobal(config: SchemaConfigData) {
config.securitySchemas.forEach { typeDescriptor ->
val schema = collapseRootRef(generateSchema(typeDescriptor))
val schema = generateSchema(typeDescriptor)
rootSchemas[typeDescriptor] = schema.swagger
schema.componentSchemas.forEach { (k, v) ->
componentSchemas[k] = v
}
}
config.schemas.forEach { (schemaId, typeDescriptor) ->
val schema = collapseRootRef(generateSchema(typeDescriptor))
val schema = generateSchema(typeDescriptor)
componentSchemas[schemaId] = schema.swagger
schema.componentSchemas.forEach { (k, v) ->
componentSchemas[k] = v
}
}
}

private fun collapseRootRef(schema: CompiledSwaggerSchema): CompiledSwaggerSchema {
if (schema.swagger.`$ref` == null) {
return schema
} else {
val referencedSchemaId = schema.swagger.`$ref`!!.replace("#/components/schemas/", "")
val referencedSchema = schema.componentSchemas[referencedSchemaId]!!
return CompiledSwaggerSchema(
typeData = schema.typeData,
swagger = referencedSchema,
componentSchemas = schema.componentSchemas.toMutableMap().also {
it.remove(referencedSchemaId)
}
)
}
}

fun add(routes: Collection<RouteMeta>) {
collectTypeDescriptor(routes).forEach { typeDescriptor ->
val schema = generateSchema(typeDescriptor)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
package io.github.smiley4.ktorswaggerui.data

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer

/**
* Encoder to produce the final example value.
* Return the unmodified example to fall back to the default encoder.
*/
typealias ExampleEncoder = (type: TypeDescriptor?, example: Any?) -> Any?

/**
* [ExampleEncoder] using kotlinx-serialization to encode example objects.
*/
val kotlinxExampleEncoder: ExampleEncoder = { type, example ->
if (type is KTypeDescriptor) {
val jsonString = Json.encodeToString(serializer(type.type), example)
val jsonObj = jacksonObjectMapper().readValue(jsonString, object : TypeReference<Any>() {})
jsonObj
} else {
example
}
}

class ExampleConfigData(
val sharedExamples: Map<String, ExampleDescriptor>,
val securityExamples: OpenApiSimpleBodyData?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class PluginConfigData(
val specAssigner: SpecAssigner,
val pathFilter: PathFilter,
val ignoredRouteSelectors: Set<KClass<*>>,
val ignoredRouteSelectorClassNames: Set<String>,
val swagger: SwaggerUIData,
val info: InfoData,
val servers: List<ServerData>,
Expand All @@ -28,6 +29,7 @@ data class PluginConfigData(
specAssigner = { _, _ -> PluginConfigDsl.DEFAULT_SPEC_ID },
pathFilter = { _, _ -> true },
ignoredRouteSelectors = emptySet(),
ignoredRouteSelectorClassNames = emptySet(),
swagger = SwaggerUIData.DEFAULT,
info = InfoData.DEFAULT,
servers = emptyList(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ class PluginConfigDsl {
*/
var ignoredRouteSelectors: Set<KClass<*>> = PluginConfigData.DEFAULT.ignoredRouteSelectors

/**
* List of all [RouteSelector] class names that should be ignored in the resulting url of any route.
*/
var ignoredRouteSelectorClassNames: Set<String> = emptySet()

/**
* The format of the generated api-spec
Expand Down Expand Up @@ -170,6 +174,10 @@ class PluginConfigDsl {
addAll(base.ignoredRouteSelectors)
addAll(ignoredRouteSelectors)
},
ignoredRouteSelectorClassNames = buildSet {
addAll(base.ignoredRouteSelectorClassNames)
addAll(ignoredRouteSelectorClassNames)
},
specConfigs = mutableMapOf(),
postBuild = merge(base.postBuild, postBuild),
outputFormat = outputFormat
Expand Down
Loading

0 comments on commit f0cdba6

Please sign in to comment.