Skip to content

Commit a32f634

Browse files
authored
Merge pull request #43 from AlbRoehm/add-support-for-empty-response
feat: add empty response body support
2 parents 298bb25 + 2e88a3d commit a32f634

File tree

7 files changed

+161
-14
lines changed

7 files changed

+161
-14
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ fun Route.ordersRouting() {
146146

147147
### Responses
148148
Defining response schemas and their corresponding HTTP status codes are done via `@KtorResponds` annotation on an endpoint.
149+
`Nothing` is treated specially and will result in empty response body.
149150

150151
```kotlin
151152
@GenerateOpenApi
@@ -154,6 +155,7 @@ fun Route.ordersRouting() {
154155
@KtorResponds(
155156
[
156157
ResponseEntry("200", Order::class, description = "Created order"),
158+
ResponseEntry("204", Nothing::class),
157159
ResponseEntry("400", ErrorResponseSample::class, description = "Invalid order payload")
158160
]
159161
)

create-plugin/src/main/kotlin/io/github/tabilzad/ktor/FileUtils.kt

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal fun OpenApiSpec.serializeAndWriteTo(configuration: PluginConfiguration)
2525
val sorted = new.copy(
2626
components = new.components.copy(
2727
schemas = new.components.schemas.toSortedMap()
28+
.filter { (key, value) -> key != "kotlin.Nothing" }
2829
.mapValues {
2930
it.value.copy(properties = it.value.properties?.toSortedMap())
3031
}

create-plugin/src/main/kotlin/io/github/tabilzad/ktor/k2/ExpressionsVisitorK2.kt

+19-13
Original file line numberDiff line numberDiff line change
@@ -268,38 +268,44 @@ internal class ExpressionsVisitorK2(
268268
type = kotlinType.toString().toSwaggerType()
269269
)
270270
} else {
271-
272271
val typeRef = response.type?.generateTypeAndVisitMemberDescriptors()
273272
OpenApiSpec.SchemaType(
274273
`$ref` = "${typeRef?.contentBodyRef}"
275274
)
276275
}
277276
if (!response.isCollection) {
278-
response.status to
277+
if (kotlinType?.isNothing ?: false) {
278+
response.status to OpenApiSpec.ResponseDetails(
279+
response.descr,
280+
null,
281+
)
282+
} else {
283+
response.status to
279284
OpenApiSpec.ResponseDetails(
280-
response.descr ?: "",
285+
response.descr,
281286
mapOf(
282287
ContentType.APPLICATION_JSON to mapOf(
283288
"schema" to schema
284289
)
285290
)
286291
)
292+
}
287293
} else {
288294
response.status to
289-
OpenApiSpec.ResponseDetails(
290-
response.descr ?: "",
291-
mapOf(
292-
ContentType.APPLICATION_JSON to mapOf(
293-
"schema" to OpenApiSpec.SchemaType(
294-
type = "array",
295-
items = OpenApiSpec.SchemaRef(
296-
type = schema.type,
297-
`$ref` = schema.`$ref`
298-
)
295+
OpenApiSpec.ResponseDetails(
296+
response.descr,
297+
mapOf(
298+
ContentType.APPLICATION_JSON to mapOf(
299+
"schema" to OpenApiSpec.SchemaType(
300+
type = "array",
301+
items = OpenApiSpec.SchemaRef(
302+
type = schema.type,
303+
`$ref` = schema.`$ref`
299304
)
300305
)
301306
)
302307
)
308+
)
303309
}
304310
}
305311

create-plugin/src/main/kotlin/io/github/tabilzad/ktor/output/OpenApiSpec.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ data class OpenApiSpec(
8585

8686
data class ResponseDetails(
8787
val description: String,
88-
val content: BodyContent
88+
val content: BodyContent?
8989
)
9090

9191
data class OpenApiComponents(

create-plugin/src/test/kotlin/io/github/tabilzad/ktor/K2StabilityTest.kt

+12
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,18 @@ class K2StabilityTest {
310310
result.assertWith(expected)
311311
}
312312

313+
@Test
314+
fun `should correctly resolve Nothing class into empty response annotations`() {
315+
val (source, expected) = loadSourceAndExpected("ResponseBodyEmpty")
316+
generateCompilerTest(
317+
testFile,
318+
source,
319+
PluginConfiguration.createDefault(hideTransients = false, hidePrivateFields = false)
320+
)
321+
val result = testFile.readText()
322+
result.assertWith(expected)
323+
}
324+
313325
@Test
314326
fun `should handle abstract or sealed schema definitions`() {
315327
val (source, expected) = loadSourceAndExpected("Abstractions")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
{
2+
"openapi" : "3.1.0",
3+
"info" : {
4+
"title" : "Open API Specification",
5+
"description" : "test",
6+
"version" : "1.0.0"
7+
},
8+
"paths" : {
9+
"/v1/noResponseBody" : {
10+
"post" : {
11+
"responses" : {
12+
"200" : {
13+
"description" : "Success"
14+
},
15+
"500" : {
16+
"description" : "Failure",
17+
"content" : {
18+
"application/json" : {
19+
"schema" : {
20+
"$ref" : "#/components/schemas/sources.requests.PrivateBodyRequest"
21+
}
22+
}
23+
}
24+
}
25+
},
26+
"requestBody" : {
27+
"required" : true,
28+
"content" : {
29+
"application/json" : {
30+
"schema" : {
31+
"$ref" : "#/components/schemas/sources.requests.SimpleRequest"
32+
}
33+
}
34+
}
35+
}
36+
}
37+
},
38+
"/v1/implicitArgNames" : {
39+
"post" : {
40+
"responses" : {
41+
"204" : {
42+
"description" : ""
43+
}
44+
},
45+
"requestBody" : {
46+
"required" : true,
47+
"content" : {
48+
"application/json" : {
49+
"schema" : {
50+
"$ref" : "#/components/schemas/sources.requests.SimpleRequest"
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
},
58+
"components" : {
59+
"schemas" : {
60+
"sources.requests.PrivateBodyRequest" : {
61+
"type" : "object",
62+
"properties" : {
63+
"invisible" : {
64+
"type" : "string"
65+
},
66+
"transientFieldInvisible" : {
67+
"type" : "integer"
68+
},
69+
"visible" : {
70+
"type" : "boolean"
71+
}
72+
},
73+
"required" : [ "visible", "invisible", "transientFieldInvisible" ]
74+
},
75+
"sources.requests.SimpleRequest" : {
76+
"type" : "object",
77+
"properties" : {
78+
"float" : {
79+
"type" : "number"
80+
},
81+
"integer" : {
82+
"type" : "integer"
83+
},
84+
"string" : {
85+
"type" : "string"
86+
}
87+
},
88+
"required" : [ "string", "integer", "float" ]
89+
}
90+
}
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package sources
2+
3+
import io.github.tabilzad.ktor.annotations.GenerateOpenApi
4+
import io.github.tabilzad.ktor.annotations.KtorResponds
5+
import io.github.tabilzad.ktor.annotations.ResponseEntry
6+
import io.ktor.http.*
7+
import io.ktor.server.application.*
8+
import io.ktor.server.request.*
9+
import io.ktor.server.response.*
10+
import io.ktor.server.routing.*
11+
import sources.requests.PrivateBodyRequest
12+
import sources.requests.SimpleRequest
13+
14+
@GenerateOpenApi
15+
fun Application.responseBodySample() {
16+
routing {
17+
route("/v1") {
18+
@KtorResponds(
19+
mapping = [
20+
ResponseEntry("200", Nothing::class, description = "Success"),
21+
ResponseEntry("500", PrivateBodyRequest::class, description = "Failure")
22+
]
23+
)
24+
post("/noResponseBody") {
25+
call.receive<SimpleRequest>()
26+
}
27+
28+
@KtorResponds([ResponseEntry("204", Nothing::class)])
29+
post("/implicitArgNames") {
30+
call.receive<SimpleRequest>()
31+
}
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)