Skip to content

Commit 212cea8

Browse files
committed
extract documentation from resources-routes
1 parent 2d2c4b9 commit 212cea8

File tree

8 files changed

+83
-53
lines changed

8 files changed

+83
-53
lines changed

examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/Resources.kt renamed to examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/TypesafeRouting.kt

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ package io.github.smiley4.ktoropenapi.examples
22

33
import io.github.smiley4.ktoropenapi.OpenApi
44
import io.github.smiley4.ktoropenapi.openApi
5-
import io.github.smiley4.ktoropenapi.resources.*
5+
import io.github.smiley4.ktoropenapi.resources.delete
6+
import io.github.smiley4.ktoropenapi.resources.get
7+
import io.github.smiley4.ktoropenapi.resources.post
68
import io.github.smiley4.ktorredoc.redoc
79
import io.github.smiley4.ktorswaggerui.swaggerUI
810
import io.github.smiley4.schemakenerator.serialization.processKotlinxSerialization
911
import io.github.smiley4.schemakenerator.swagger.compileReferencingRoot
1012
import io.github.smiley4.schemakenerator.swagger.data.TitleType
1113
import io.github.smiley4.schemakenerator.swagger.generateSwaggerSchema
14+
import io.github.smiley4.schemakenerator.swagger.handleCoreAnnotations
1215
import io.github.smiley4.schemakenerator.swagger.withTitle
1316
import io.ktor.http.HttpStatusCode
1417
import io.ktor.resources.Resource
@@ -31,28 +34,16 @@ private fun Application.myModule() {
3134
install(Resources)
3235

3336
install(OpenApi) {
34-
info {
35-
title = "Example API"
36-
description = "An example api to showcase basic swagger-ui functionality."
37-
}
38-
externalDocs {
39-
url = "https://github.com/SMILEY4/ktor-swagger-ui/wiki"
40-
description = "Sample external documentation"
41-
}
42-
server {
43-
url = "http://localhost:8080"
44-
description = "Development Server"
45-
}
46-
server {
47-
url = "https://www.example.com"
48-
description = "Production Server"
49-
}
37+
// enable automatically extracting documentation from resources-routes
38+
autoDocumentResourcesRoutes = true
39+
// schema-generator must use kotlinx-serialization to be compatible with resources
5040
schemas {
5141
generator = { type ->
5242
type
5343
.processKotlinxSerialization()
5444
.generateSwaggerSchema()
5545
.withTitle(TitleType.SIMPLE)
46+
.handleCoreAnnotations()
5647
.compileReferencingRoot()
5748
}
5849
}
@@ -71,7 +62,12 @@ private fun Application.myModule() {
7162
}
7263

7364
get<PetsRoute.All>({
74-
description = "custom description"
65+
description = "custom description"
66+
request {
67+
queryParameter<String>("tags") {
68+
description = "sample description"
69+
}
70+
}
7571
}) { request ->
7672
println("..${request.tags}, ${request.limit}")
7773
call.respond(HttpStatusCode.NotImplemented, Unit)
@@ -87,49 +83,51 @@ private fun Application.myModule() {
8783
call.respond(HttpStatusCode.NotImplemented, Unit)
8884
}
8985

90-
post<PetsRoute.New> { request ->
91-
println("..${request.pet}")
86+
post<PetsRoute.Id.New> { request ->
87+
println("..${request.parent.id}")
9288
call.respond(HttpStatusCode.NotImplemented, Unit)
9389
}
9490

9591
}
9692

9793
}
9894

95+
9996
@Resource("/pets")
10097
class PetsRoute {
10198

10299
@Resource("/")
103-
class All(val parent: PetsRoute = PetsRoute(), val tags: List<String> = emptyList(), val limit: Int = 100)
100+
class All(
101+
val parent: PetsRoute,
102+
val tags: List<String> = emptyList(),
103+
val limit: Int = 100
104+
)
105+
104106

105107
@Resource("{id}")
106-
class Id(val parent: PetsRoute = PetsRoute(), val id: Long) {
108+
class Id(
109+
val parent: PetsRoute,
110+
val id: Long
111+
) {
107112

108113
@Resource("/")
109-
class Delete(val parent: Id)
114+
class Delete(
115+
val parent: Id
116+
)
110117

111-
}
112118

113-
@Resource("/")
114-
class New(val parent: PetsRoute = PetsRoute(), val pet: NewPet)
119+
@Resource("/")
120+
class New(
121+
val parent: Id,
122+
)
115123

124+
}
116125

117126
}
118127

119-
@Serializable
120-
data class Pet(
121-
val id: Long,
122-
val name: String,
123-
val tag: String
124-
)
125128

126129
@Serializable
127-
data class NewPet(
130+
data class Pet(
128131
val name: String,
129132
val tag: String
130-
)
131-
132-
@Serializable
133-
data class ErrorModel(
134-
val message: String
135-
)
133+
)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import io.github.smiley4.ktoropenapi.config.TypeDescriptor
1212
import io.github.smiley4.ktoropenapi.data.MultipartBodyData
1313
import io.github.smiley4.ktoropenapi.data.SchemaConfigData
1414
import io.github.smiley4.ktoropenapi.data.SimpleBodyData
15+
import io.github.smiley4.schemakenerator.core.data.AnnotationData
1516
import io.github.smiley4.schemakenerator.core.data.KTypeInput
1617
import io.github.smiley4.schemakenerator.core.data.WildcardTypeData
1718
import io.github.smiley4.schemakenerator.serialization.SerialDescriptorInput
@@ -120,7 +121,7 @@ internal class SchemaContextImpl(private val schemaConfig: SchemaConfigData) : S
120121
return schemaConfig.generator(KTypeInput(type))
121122
}
122123

123-
private fun generateSchema(descriptor: SerialDescriptor): CompiledSwaggerSchema {
124+
private fun generateSchema(descriptor: SerialDescriptor,): CompiledSwaggerSchema {
124125
return schemaConfig.generator(SerialDescriptorInput(descriptor))
125126
}
126127

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.smiley4.ktoropenapi.config
22

33
import io.github.smiley4.ktoropenapi.data.DataUtils.merge
4+
import io.github.smiley4.ktoropenapi.data.DataUtils.mergeBoolean
45
import io.github.smiley4.ktoropenapi.data.OpenApiPluginData
56
import io.github.smiley4.ktoropenapi.data.ServerData
67
import io.ktor.server.routing.RouteSelector
@@ -143,6 +144,11 @@ class OpenApiPluginConfig {
143144
var rootPath: String? = OpenApiPluginData.DEFAULT.rootPath
144145

145146

147+
/**
148+
* Whether to automatically extract and add documentation from routes created using the resources-plugin.
149+
*/
150+
var autoDocumentResourcesRoutes: Boolean = OpenApiPluginData.DEFAULT.autoDocumentResourcesRoutes
151+
146152
/**
147153
* Build the data object for this config.
148154
* @param base the base config to "inherit" from. Values from the base should be copied, replaced or merged together.
@@ -173,7 +179,8 @@ class OpenApiPluginConfig {
173179
specConfigs = mutableMapOf(),
174180
postBuild = merge(base.postBuild, postBuild),
175181
outputFormat = outputFormat,
176-
rootPath = merge(rootPath ?: ktorRootPath, base.rootPath)
182+
rootPath = merge(rootPath ?: ktorRootPath, base.rootPath),
183+
autoDocumentResourcesRoutes = mergeBoolean(base.autoDocumentResourcesRoutes, autoDocumentResourcesRoutes),
177184
).also {
178185
specConfigs.forEach { (specName, config) ->
179186
it.specConfigs[specName] = config.build(it, ktorRootPath)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.smiley4.ktoropenapi.config
22

33
import io.github.smiley4.ktoropenapi.data.*
4+
import io.github.smiley4.schemakenerator.core.data.AnnotationData
45
import io.github.smiley4.schemakenerator.core.data.InputType
56
import io.github.smiley4.schemakenerator.swagger.data.CompiledSwaggerSchema
67
import io.swagger.v3.oas.models.media.Schema

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.smiley4.ktoropenapi.config
22

3+
import io.github.smiley4.schemakenerator.core.data.AnnotationData
34
import io.swagger.v3.oas.models.media.Schema
45
import kotlinx.serialization.descriptors.SerialDescriptor
56
import kotlin.reflect.KType

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ internal data class OpenApiPluginData(
2525
val securityConfig: SecurityData,
2626
val tagsConfig: TagsData,
2727
val outputFormat: OutputFormat,
28-
val rootPath: String?
28+
val rootPath: String?,
29+
val autoDocumentResourcesRoutes: Boolean,
2930
) {
3031

3132
companion object {
@@ -44,7 +45,8 @@ internal data class OpenApiPluginData(
4445
securityConfig = SecurityData.DEFAULT,
4546
tagsConfig = TagsData.DEFAULT,
4647
outputFormat = OutputFormat.JSON,
47-
rootPath = null
48+
rootPath = null,
49+
autoDocumentResourcesRoutes = false,
4850
)
4951
}
5052

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.github.smiley4.ktoropenapi.data
22

33
import io.github.smiley4.ktoropenapi.config.TypeDescriptor
44
import io.github.smiley4.schemakenerator.core.connectSubTypes
5+
import io.github.smiley4.schemakenerator.core.data.AnnotationData
56
import io.github.smiley4.schemakenerator.core.data.InputType
67
import io.github.smiley4.schemakenerator.core.handleNameAnnotation
78
import io.github.smiley4.schemakenerator.reflection.collectSubTypes

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

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
package io.github.smiley4.ktoropenapi.resources
44

5+
import io.github.smiley4.ktoropenapi.OpenApiPlugin
56
import io.github.smiley4.ktoropenapi.config.ParameterLocation
67
import io.github.smiley4.ktoropenapi.config.RouteConfig
78
import io.github.smiley4.ktoropenapi.config.SerialTypeDescriptor
@@ -12,36 +13,54 @@ import kotlinx.serialization.descriptors.SerialDescriptor
1213
import kotlinx.serialization.descriptors.StructureKind
1314
import kotlinx.serialization.descriptors.elementNames
1415

16+
private data class ParameterData(
17+
val name: String,
18+
val descriptor: SerialDescriptor,
19+
val optional: Boolean,
20+
val location: ParameterLocation
21+
)
22+
1523
fun <T> extractTypesafeDocumentation(serializer: KSerializer<T>, resourcesFormat: ResourcesFormat): RouteConfig.() -> Unit {
24+
if(!OpenApiPlugin.config.autoDocumentResourcesRoutes) {
25+
return {}
26+
}
27+
1628
// Note: typesafe routing only defines information about path & query parameters - no other information is available
1729
val path = resourcesFormat.encodeToPathPattern(serializer)
1830
return {
1931
request {
20-
collectParameters(serializer.descriptor).forEach { (name, parameterDescriptor) ->
21-
parameter(getLocation(name, path), name, SerialTypeDescriptor(parameterDescriptor)) {
22-
// todo: if elementDescriptor is not class, see "resourcesFormat.encodeToQueryParameters")
23-
required = parameterDescriptor.isNullable
32+
collectParameters(serializer.descriptor, path).forEach { parameter ->
33+
parameter(parameter.location, parameter.name, SerialTypeDescriptor(parameter.descriptor)) {
34+
required = !parameter.optional
2435
}
2536
}
2637
}
2738
}
2839
}
2940

3041

31-
private fun collectParameters(descriptor: SerialDescriptor): List<Pair<String, SerialDescriptor>> {
32-
val parameters = mutableListOf<Pair<String, SerialDescriptor>>()
42+
43+
private fun collectParameters(descriptor: SerialDescriptor, path: String): List<ParameterData> {
44+
val parameters = mutableListOf<ParameterData>()
3345

3446
descriptor.elementNames.forEach { name ->
3547
val index = descriptor.getElementIndex(name)
3648
val elementDescriptor = descriptor.getElementDescriptor(index)
3749
if (!elementDescriptor.isInline && elementDescriptor.kind is StructureKind.CLASS) {
38-
parameters.addAll(collectParameters(elementDescriptor))
50+
parameters.addAll(collectParameters(elementDescriptor, path))
3951
} else {
40-
parameters.add(name to elementDescriptor)
52+
parameters.add(
53+
ParameterData(
54+
name = name,
55+
descriptor = elementDescriptor,
56+
optional = path.contains("{$name?}"),
57+
location = getLocation(name, path)
58+
)
59+
)
4160
}
4261
}
4362

44-
return parameters;
63+
return parameters
4564
}
4665

4766

@@ -55,4 +74,4 @@ private fun getLocation(name: String, path: String): ParameterLocation {
5574
} else {
5675
ParameterLocation.QUERY
5776
}
58-
}
77+
}

0 commit comments

Comments
 (0)