Skip to content

Commit 738e147

Browse files
committed
Merge branch 'improve-support-for-typesafe-routing' into pre-release-5-0-0
2 parents 66b804f + 3c038ca commit 738e147

File tree

23 files changed

+390
-67
lines changed

23 files changed

+390
-67
lines changed

examples/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ plugins {
99
}
1010

1111
repositories {
12+
mavenLocal() // todo: remove after releasing schema-kenerator 1.7.0
1213
mavenCentral()
1314
}
1415

@@ -17,13 +18,16 @@ dependencies {
1718
implementation(project(":ktor-swagger-ui"))
1819
implementation(project(":ktor-redoc"))
1920

21+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
22+
2023
val versionKtor: String by project
2124
implementation("io.ktor:ktor-server-netty-jvm:$versionKtor")
2225
implementation("io.ktor:ktor-server-content-negotiation:$versionKtor")
2326
implementation("io.ktor:ktor-serialization-jackson:$versionKtor")
2427
implementation("io.ktor:ktor-server-auth:$versionKtor")
2528
implementation("io.ktor:ktor-server-call-logging:$versionKtor")
2629
implementation("io.ktor:ktor-server-test-host:$versionKtor")
30+
implementation("io.ktor:ktor-server-resources:$versionKtor")
2731

2832
val versionSchemaKenerator: String by project
2933
implementation("io.github.smiley4:schema-kenerator-core:$versionSchemaKenerator")

examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/Petstore.kt

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@ fun main() {
2727
*/
2828
private fun Application.myModule() {
2929

30+
data class Pet(
31+
val id: Long,
32+
val name: String,
33+
val tag: String
34+
)
35+
36+
data class NewPet(
37+
val name: String,
38+
val tag: String
39+
)
40+
41+
data class ErrorModel(
42+
val message: String
43+
)
44+
3045
install(OpenApi) {
3146
info {
3247
title = "Swagger Petstore"
@@ -247,19 +262,4 @@ private fun Application.myModule() {
247262

248263
}
249264

250-
}
251-
252-
private data class Pet(
253-
val id: Long,
254-
val name: String,
255-
val tag: String
256-
)
257-
258-
private data class NewPet(
259-
val name: String,
260-
val tag: String
261-
)
262-
263-
private data class ErrorModel(
264-
val message: String
265-
)
265+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package io.github.smiley4.ktoropenapi.examples
2+
3+
import io.github.smiley4.ktoropenapi.OpenApi
4+
import io.github.smiley4.ktoropenapi.openApi
5+
import io.github.smiley4.ktoropenapi.resources.delete
6+
import io.github.smiley4.ktoropenapi.resources.get
7+
import io.github.smiley4.ktoropenapi.resources.post
8+
import io.github.smiley4.ktorredoc.redoc
9+
import io.github.smiley4.ktorswaggerui.swaggerUI
10+
import io.github.smiley4.schemakenerator.serialization.processKotlinxSerialization
11+
import io.github.smiley4.schemakenerator.swagger.compileReferencingRoot
12+
import io.github.smiley4.schemakenerator.swagger.data.TitleType
13+
import io.github.smiley4.schemakenerator.swagger.generateSwaggerSchema
14+
import io.github.smiley4.schemakenerator.swagger.handleCoreAnnotations
15+
import io.github.smiley4.schemakenerator.swagger.withTitle
16+
import io.ktor.http.HttpStatusCode
17+
import io.ktor.resources.Resource
18+
import io.ktor.server.application.Application
19+
import io.ktor.server.application.install
20+
import io.ktor.server.engine.embeddedServer
21+
import io.ktor.server.netty.Netty
22+
import io.ktor.server.resources.Resources
23+
import io.ktor.server.response.respond
24+
import io.ktor.server.routing.route
25+
import io.ktor.server.routing.routing
26+
import kotlinx.serialization.Serializable
27+
28+
fun main() {
29+
embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true)
30+
}
31+
32+
private fun Application.myModule() {
33+
34+
install(Resources)
35+
36+
install(OpenApi) {
37+
// enable automatically extracting documentation from resources-routes
38+
autoDocumentResourcesRoutes = true
39+
// schema-generator must use kotlinx-serialization to be compatible with resources
40+
schemas {
41+
generator = { type ->
42+
type
43+
.processKotlinxSerialization()
44+
.generateSwaggerSchema()
45+
.withTitle(TitleType.SIMPLE)
46+
.handleCoreAnnotations()
47+
.compileReferencingRoot()
48+
}
49+
}
50+
}
51+
52+
routing {
53+
54+
// add the routes for the api-spec, swagger-ui and redoc
55+
route("api.json") {
56+
openApi()
57+
}
58+
route("swagger") {
59+
swaggerUI("/api.json")
60+
}
61+
route("redoc") {
62+
redoc("/api.json")
63+
}
64+
65+
// query and path parameters are picked up automatically and added to the schema with the correct name and type
66+
get<PetsRoute.All> { request ->
67+
println("..${request.tags}, ${request.limit}")
68+
call.respond(HttpStatusCode.NotImplemented, Unit)
69+
}
70+
71+
// additional information can be added to the route manually as usual.
72+
// automatically added information can also be overwritten this way.
73+
get<PetsRoute.Id>({
74+
request {
75+
pathParameter<Long>("id") {
76+
description = "the id of the pet"
77+
}
78+
}
79+
}) { request ->
80+
println("..${request.id}")
81+
call.respond(HttpStatusCode.NotImplemented, Unit)
82+
}
83+
84+
delete<PetsRoute.Id.Delete> { request ->
85+
println("..${request.parent.id}")
86+
call.respond(HttpStatusCode.NotImplemented, Unit)
87+
}
88+
89+
post<PetsRoute.Id.New> { request ->
90+
println("..${request.parent.id}")
91+
call.respond(HttpStatusCode.NotImplemented, Unit)
92+
}
93+
94+
}
95+
96+
}
97+
98+
99+
@Resource("/pets")
100+
class PetsRoute {
101+
102+
@Resource("/")
103+
class All(
104+
val parent: PetsRoute,
105+
val tags: List<String>?,
106+
val limit: Int = 100
107+
)
108+
109+
110+
@Resource("{id}")
111+
class Id(
112+
val parent: PetsRoute,
113+
val id: Long
114+
) {
115+
116+
@Resource("/")
117+
class Delete(
118+
val parent: Id
119+
)
120+
121+
122+
@Resource("/")
123+
class New(
124+
val parent: Id,
125+
)
126+
127+
}
128+
129+
}
130+
131+
132+
@Serializable
133+
data class Pet(
134+
val name: String,
135+
val tag: String
136+
)

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ projectDeveloperUrl=https://github.com/SMILEY4
1616
versionKtor=3.0.0
1717
versionSwaggerUI=5.17.11
1818
versionSwaggerParser=2.1.24
19-
versionSchemaKenerator=1.6.3
19+
versionSchemaKenerator=1.7.0
2020
versionKotlinLogging=7.0.0
2121
versionKotest=5.8.0
2222
versionKotlinTest=2.0.21

ktor-openapi/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ plugins {
1919

2020
repositories {
2121
mavenCentral()
22+
mavenLocal() // todo: remove after releasing schema-kenerator 1.7.0
2223
}
2324

2425
dependencies {
@@ -43,6 +44,7 @@ dependencies {
4344
val versionSchemaKenerator: String by project
4445
implementation("io.github.smiley4:schema-kenerator-core:$versionSchemaKenerator")
4546
implementation("io.github.smiley4:schema-kenerator-reflection:$versionSchemaKenerator")
47+
implementation("io.github.smiley4:schema-kenerator-serialization:$versionSchemaKenerator")
4648
implementation("io.github.smiley4:schema-kenerator-swagger:$versionSchemaKenerator")
4749

4850
val versionKotlinLogging: String by project

ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/OpenApiPlugin.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import io.github.smiley4.ktoropenapi.config.OpenApiPluginConfig
99
import io.ktor.http.ContentType
1010
import io.ktor.http.HttpStatusCode
1111
import io.ktor.server.application.Application
12+
import io.ktor.server.application.ApplicationPlugin
1213
import io.ktor.server.application.ApplicationStarted
1314
import io.ktor.server.application.createApplicationPlugin
1415
import io.ktor.server.application.hooks.MonitoringEvent
@@ -20,7 +21,7 @@ import io.ktor.server.routing.get
2021

2122
private val logger = KotlinLogging.logger {}
2223

23-
val OpenApi = createApplicationPlugin(name = "OpenApi", createConfiguration = ::OpenApiPluginConfig) {
24+
val OpenApi: ApplicationPlugin<OpenApiPluginConfig> = createApplicationPlugin(name = "OpenApi", createConfiguration = ::OpenApiPluginConfig) {
2425
OpenApiPlugin.config = pluginConfig.build(OpenApiPluginData.DEFAULT, getRootPath(application))
2526
on(MonitoringEvent(ApplicationStarted)) { application ->
2627
try {

ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/OperationBuilder.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ internal class OperationBuilder(
2424
it.operationId = route.documentation.operationId
2525
it.deprecated = route.documentation.deprecated
2626
it.tags = operationTagsBuilder.build(route)
27-
it.parameters = route.documentation.request.parameters.map { param -> parameterBuilder.build(param) }
27+
it.parameters = route.documentation.request.parameters
28+
.filter { param -> !param.hidden }
29+
.map { param -> parameterBuilder.build(param) }
2830
route.documentation.request.body?.let { body ->
2931
it.requestBody = requestBodyBuilder.build(body)
3032
}

ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/ResponsesBuilder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ internal class ResponsesBuilder(
1717
fun build(responses: List<ResponseData>, isProtected: Boolean): ApiResponses =
1818
ApiResponses().also {
1919
responses
20+
.filter { !it.hidden }
2021
.map { response -> responseBuilder.build(response) }
2122
.forEach { (name, response) -> it.addApiResponse(name, response) }
2223
if (shouldAddUnauthorized(responses, isProtected)) {
@@ -29,6 +30,7 @@ internal class ResponsesBuilder(
2930
private fun shouldAddUnauthorized(responses: List<ResponseData>, isProtected: Boolean): Boolean {
3031
val unauthorizedCode = HttpStatusCode.Unauthorized.value.toString()
3132
return config.securityConfig.defaultUnauthorizedResponse != null
33+
&& !config.securityConfig.defaultUnauthorizedResponse.hidden
3234
&& isProtected
3335
&& responses.count { it.statusCode == unauthorizedCode } == 0
3436
}

ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/route/RouteCollector.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package io.github.smiley4.ktoropenapi.builder.route
22

3-
import io.github.smiley4.ktoropenapi.data.OpenApiPluginData
4-
import io.github.smiley4.ktoropenapi.config.RouteConfig
53
import io.github.smiley4.ktoropenapi.DocumentedRouteSelector
4+
import io.github.smiley4.ktoropenapi.config.RouteConfig
5+
import io.github.smiley4.ktoropenapi.data.OpenApiPluginData
66
import io.ktor.http.HttpMethod
77
import io.ktor.server.auth.AuthenticationRouteSelector
88
import io.ktor.server.routing.ConstantParameterRouteSelector
@@ -49,6 +49,13 @@ internal class RouteCollector {
4949
.toList()
5050
}
5151

52+
private fun unroll(route: RoutingNode): List<Pair<RoutingNode, RouteSelector>> {
53+
return if (route.parent != null) {
54+
unroll(route.parent!!) + listOf(route to route.selector)
55+
} else {
56+
emptyList()
57+
}
58+
}
5259

5360
private fun getDocumentation(route: RoutingNode, base: RouteConfig): RouteConfig {
5461
var documentation = base

ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/route/RouteDocumentationMerger.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.github.smiley4.ktoropenapi.builder.route
22

3+
import io.github.smiley4.ktoropenapi.config.RequestConfig
4+
import io.github.smiley4.ktoropenapi.config.RequestParameterConfig
35
import io.github.smiley4.ktoropenapi.config.RouteConfig
46

57
internal class RouteDocumentationMerger {
@@ -10,7 +12,7 @@ internal class RouteDocumentationMerger {
1012
fun merge(a: RouteConfig, b: RouteConfig): RouteConfig {
1113
return RouteConfig().apply {
1214
specName = a.specName ?: b.specName
13-
tags = mutableListOf<String>().also {
15+
tags = mutableSetOf<String>().also {
1416
it.addAll(a.tags)
1517
it.addAll(b.tags)
1618
}
@@ -25,15 +27,17 @@ internal class RouteDocumentationMerger {
2527
hidden = a.hidden || b.hidden
2628
protected = a.protected ?: b.protected
2729
request {
28-
parameters.also {
29-
it.addAll(a.getRequest().parameters)
30-
it.addAll(b.getRequest().parameters)
31-
}
30+
buildMap {
31+
b.getRequest().parameters.forEach { this[it.name] = it }
32+
a.getRequest().parameters.forEach { this[it.name] = it }
33+
}.values.forEach { parameters.add(it) }
3234
setBody(a.getRequest().getBody() ?: b.getRequest().getBody())
3335
}
3436
response {
35-
b.getResponses().getResponses().forEach { response -> addResponse(response) }
36-
a.getResponses().getResponses().forEach { response -> addResponse(response) }
37+
buildMap {
38+
b.getResponses().getResponses().forEach { this[it.statusCode] = it }
39+
a.getResponses().getResponses().forEach { this[it.statusCode] = it }
40+
}.values.forEach { addResponse(it) }
3741
}
3842
}
3943
}

0 commit comments

Comments
 (0)