Skip to content

Commit 6bbf762

Browse files
committed
improve handling of different response types (openapi-processor/openapi-processor-spring#328)
1 parent 49e40c7 commit 6bbf762

File tree

5 files changed

+177
-23
lines changed

5 files changed

+177
-23
lines changed

openapi-processor-core/src/main/kotlin/io/openapiprocessor/core/model/Endpoint.kt

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ class Endpoint(
2626
// grouped responses
2727
val endpointResponses: List<EndpointResponse> = createEndpointResponses()
2828

29-
// hmmm
3029
fun getRequestBody(): RequestBody {
3130
return requestBodies.first ()
3231
}
@@ -140,32 +139,41 @@ class Endpoint(
140139
private fun createEndpointResponses(): List<EndpointResponse> {
141140
val successes = getSuccessResponses()
142141
val errors = getErrorResponses()
143-
return successes.map {
144-
EndpointResponse(it, errors)
142+
143+
return successes.map { (key, responses) ->
144+
EndpointResponse(responses.first(), errors, responses)
145145
}
146146
}
147147

148-
private fun getSuccessResponses(): Set<Response> {
149-
val result = mutableMapOf<String, Response>()
148+
private fun getSuccessResponses(): MutableMap<String, MutableList<Response>> {
149+
val result = mutableMapOf<String, MutableList<Response>>()
150150

151151
// prefer responses with content type.
152152
filterSuccessResponses()
153153
.filter { hasContentType(it) }
154154
.forEach {
155-
result[it.contentType] = it
155+
var contentValues = result[it.contentType]
156+
if (contentValues == null) {
157+
contentValues = mutableListOf()
158+
result[it.contentType] = contentValues
159+
}
160+
contentValues += it
156161
}
157162

158163
// check for responses without content type (e.g. 204) to generate a void method.
159164
if (result.isEmpty()) {
160165
filterSuccessResponses()
161166
.forEach {
162-
result[it.contentType] = it
167+
var contentValues = result[it.contentType]
168+
if (contentValues == null) {
169+
contentValues = mutableListOf()
170+
result[it.contentType] = contentValues
171+
}
172+
contentValues += it
163173
}
164174
}
165175

166176
return result
167-
.values
168-
.toSet()
169177
}
170178

171179
private fun getErrorResponses(): Set<Response> {

openapi-processor-core/src/main/kotlin/io/openapiprocessor/core/model/EndpointResponse.kt

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ class EndpointResponse(
2222
/**
2323
* additional (error) responses
2424
*/
25-
private val errors: Set<Response>
25+
private val errors: Set<Response>,
2626

27+
/**
28+
* todo replace main
29+
* success responses
30+
*/
31+
private val successes: List<Response> = listOf()
2732
) {
2833

2934
val contentType: String
@@ -51,6 +56,10 @@ class EndpointResponse(
5156
if (style == ResultStyle.ALL && errors.isNotEmpty())
5257
return getMultiResponseTypeName()
5358

59+
val distinct = getDistinctResponseTypes()
60+
if (distinct.size > 1)
61+
return getMultiResponseTypeName()
62+
5463
return getSingleResponseTypeName()
5564
}
5665

@@ -79,6 +88,10 @@ class EndpointResponse(
7988
if (style == ResultStyle.ALL && errors.isNotEmpty())
8089
return getImportsMulti()
8190

91+
val distinct = getDistinctResponseTypes()
92+
if (distinct.size > 1)
93+
return getImportsMulti()
94+
8295
return getImportsSingle()
8396
}
8497

@@ -113,7 +126,16 @@ class EndpointResponse(
113126
return "Object"
114127
}
115128

116-
private fun getSingleResponseTypeName(): String = main.responseType.getTypeName()
129+
private fun getSingleResponseTypeName(): String {
130+
val types = getDistinctResponseTypes()
131+
.map { r -> r.responseType.getTypeName() }
132+
133+
if(types.size != 1) {
134+
throw IllegalStateException("ambiguous response types: $types")
135+
}
136+
137+
return types.first()
138+
}
117139

118140
private fun getImportsMulti(): Set<String> {
119141
val rt = main.responseType
@@ -128,4 +150,7 @@ class EndpointResponse(
128150
return main.imports
129151
}
130152

153+
private fun getDistinctResponseTypes(): List<Response> {
154+
return successes.distinctBy { response -> response.responseType.getTypeName() }
155+
}
131156
}

openapi-processor-core/src/test/kotlin/io/openapiprocessor/core/model/EndpointResponseSpec.kt

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,31 +35,39 @@ class EndpointResponseSpec: StringSpec({
3535
}
3636

3737
"result style all without errors uses single response" {
38-
val er = EndpointResponse(
39-
Response("", ObjectDataType(DataTypeName("Foo"), "pkg", linkedMapOf(
38+
val response = Response("", ObjectDataType(DataTypeName("Foo"),
39+
"pkg",
40+
linkedMapOf(
4041
"bar" to PropertyDataType(
4142
readOnly = false,
4243
writeOnly = false,
4344
dataType = StringDataType(),
44-
documentation = Documentation())
45-
))),
46-
emptySet()
45+
documentation = Documentation()))))
46+
47+
val er = EndpointResponse(
48+
response,
49+
emptySet(),
50+
listOf(response)
4751
)
4852

4953
er.getResponseType(ResultStyle.ALL) shouldBe "Foo"
5054
er.getResponseImports(ResultStyle.ALL) shouldBe setOf("pkg.Foo")
5155
}
5256

5357
"result style single uses single response" {
54-
val er = EndpointResponse(
55-
Response("", ObjectDataType(DataTypeName("Foo"), "pkg", linkedMapOf(
58+
val response = Response("", ObjectDataType(DataTypeName("Foo"),
59+
"pkg",
60+
linkedMapOf(
5661
"bar" to PropertyDataType(
5762
readOnly = false,
5863
writeOnly = false,
5964
dataType = StringDataType(),
60-
documentation = Documentation())
61-
))),
62-
setOf(Response("text/plain", StringDataType()))
65+
documentation = Documentation()))))
66+
67+
val er = EndpointResponse(
68+
response,
69+
setOf(Response("text/plain", StringDataType())),
70+
listOf(response)
6371
)
6472

6573
er.getResponseType(ResultStyle.SUCCESS) shouldBe "Foo"
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2025 https://github.com/openapi-processor/openapi-processor-core
3+
* PDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.openapiprocessor.core.model
7+
8+
import io.kotest.core.spec.style.StringSpec
9+
import io.kotest.matchers.collections.shouldHaveSize
10+
import io.kotest.matchers.shouldBe
11+
import io.openapiprocessor.core.builder.api.endpoint
12+
import io.openapiprocessor.core.model.datatypes.DataTypeName
13+
import io.openapiprocessor.core.model.datatypes.ObjectDataType
14+
import io.openapiprocessor.core.model.datatypes.PropertyDataType
15+
import io.openapiprocessor.core.model.datatypes.StringDataType
16+
import io.openapiprocessor.core.parser.HttpMethod
17+
import io.openapiprocessor.core.processor.mapping.v2.ResultStyle
18+
19+
class EndpointSuccessResponseSpec: StringSpec({
20+
21+
"groups multiple success responses" {
22+
val ep = endpoint("/foo", HttpMethod.GET) {
23+
responses {
24+
status("200") {
25+
response("application/json", ObjectDataType(
26+
DataTypeName("Foo200Json"), "pkg", linkedMapOf(
27+
"foo" to PropertyDataType(
28+
readOnly = false,
29+
writeOnly = false,
30+
dataType = StringDataType(),
31+
documentation = Documentation()
32+
)))) {}
33+
response("application/xml", ObjectDataType(
34+
DataTypeName("Foo200Xml"), "pkg", linkedMapOf(
35+
"foo" to PropertyDataType(
36+
readOnly = false,
37+
writeOnly = false,
38+
dataType = StringDataType(),
39+
documentation = Documentation()
40+
)))) {}
41+
}
42+
status("202") {
43+
response("application/json", ObjectDataType(
44+
DataTypeName("Foo202Json"), "pkg", linkedMapOf(
45+
"foo" to PropertyDataType(
46+
readOnly = false,
47+
writeOnly = false,
48+
dataType = StringDataType(),
49+
documentation = Documentation()
50+
)))) {}
51+
}
52+
status("204") {
53+
empty()
54+
}
55+
status("default") {
56+
response("text/plain", StringDataType()) {}
57+
}
58+
}
59+
}
60+
61+
val result = ep.endpointResponses
62+
63+
result shouldHaveSize 2
64+
65+
result[0].contentType shouldBe "application/json"
66+
result[0].getResponseType(ResultStyle.SUCCESS) shouldBe "Object"
67+
68+
result[1].contentType shouldBe "application/xml"
69+
result[1].getResponseType(ResultStyle.SUCCESS) shouldBe "Foo200Xml"
70+
}
71+
72+
"groups multiple success responses - no content type" {
73+
val ep = endpoint("/foo", HttpMethod.GET) {
74+
responses {
75+
status("200") {
76+
empty()
77+
}
78+
status("202") {
79+
empty()
80+
}
81+
status("204") {
82+
empty()
83+
}
84+
status("default") {
85+
response("text/plain", StringDataType()) {}
86+
}
87+
}
88+
}
89+
90+
val result = ep.endpointResponses
91+
92+
result shouldHaveSize 1
93+
result.first().getResponseType(ResultStyle.SUCCESS) shouldBe "void"
94+
}
95+
96+
"groups multiple success responses - same content type" {
97+
val ep = endpoint("/foo", HttpMethod.GET) {
98+
responses {
99+
status("201") {
100+
response("text/plain", StringDataType()) {}
101+
}
102+
status("202") {
103+
response("text/plain", StringDataType()) {}
104+
}
105+
}
106+
}
107+
108+
val result = ep.endpointResponses
109+
110+
result shouldHaveSize 1
111+
result.first().getResponseType(ResultStyle.SUCCESS) shouldBe "String"
112+
}
113+
})

openapi-processor-core/src/testInt/resources/tests/response-result-multiple/outputs/api/Api.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
public interface Api {
88

99
@Mapping("/fooBar")
10-
Object getFooBarpplicationJson();
10+
Object getFooBarApplicationJson();
1111

1212
@Mapping("/fooBar")
13-
Object getFooBarTextPlain();
13+
String getFooBarTextPlain();
1414

1515
}

0 commit comments

Comments
 (0)