Skip to content

Commit d112f65

Browse files
committed
handle multi content type responses
1 parent 16befae commit d112f65

File tree

9 files changed

+302
-86
lines changed

9 files changed

+302
-86
lines changed

src/main/groovy/com/github/hauner/openapi/spring/model/Endpoint.groovy

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class Endpoint {
5454
}
5555

5656
/**
57-
* tes support
57+
* test support
5858
*
5959
* @param status the response status
6060
* @return first response of status
@@ -73,52 +73,59 @@ class Endpoint {
7373
}
7474

7575
/**
76-
* return the first response assuming there is only a single successful response.
76+
* checks if the endpoint has multiple success responses with different content types.
7777
*
78-
* @return the first response
78+
* @return true if condition is met, otherwise false.
7979
*/
80-
Response getSingleResponse () {
81-
if (hasMultiStatusResponses ()) {
82-
println "warning: Endpoint::getSingleResponse() called on a multi status response!"
83-
}
84-
85-
responses
86-
.values ()
87-
.first ()
88-
.first ()
89-
}
90-
91-
Set<String> getResponseImports () {
92-
responses
93-
.values ()
94-
.flatten ()
95-
.collect { it.imports }
96-
.flatten () as Set<String>
80+
boolean hasMultipleEndpointResponses () {
81+
endpointResponses.size () > 1
9782
}
9883

9984
/**
100-
* checks if the endpoint contains multiple http status with responses, e.g. for status 200 and
101-
* default (or a specific error code).
85+
* creates groups from the responses.
10286
*
103-
* @return true if condition is met, else false
87+
* if the endpoint does provide its result in multiple content types it will create one entry
88+
* for each response kind (main response). if error responses are defined they are added as
89+
* error responses.
90+
*
91+
* this is used to create one controller method for each (successful) response definition.
92+
*
93+
* @return list of method responses
10494
*/
105-
boolean hasMultiStatusResponses () {
106-
responses.size () > 1
95+
List<EndpointResponse> getEndpointResponses () {
96+
List<Response> oks = successResponses
97+
List<Response> errors = errorResponses
98+
oks.collect {
99+
new EndpointResponse(main: it, errors: errors)
100+
}
107101
}
108102

109-
boolean hasResponseContentTypes () {
110-
!responseContentTypes.empty
103+
/**
104+
* finds the success responses
105+
*/
106+
private List<Response> getSuccessResponses () {
107+
def success = responses.keySet ()
108+
.findAll {
109+
it.startsWith ('2')
110+
}
111+
112+
if (success.size () == 1) {
113+
return responses."${success.first ()}"
114+
}
115+
116+
println "Endpoint: can't find successful responses (${path}/${success})"
117+
[]
111118
}
112119

113-
List<String> getResponseContentTypes () {
114-
def results = []
115-
responses.each {
116-
def contentType = it.value.first ().contentType
117-
if (contentType) {
118-
results.add (contentType)
119-
}
120+
/**
121+
* finds the error responses
122+
*/
123+
private List<Response> getErrorResponses () {
124+
responses.findAll {
125+
!it.key.startsWith ('2')
126+
}.collect {
127+
it.value.first ()
120128
}
121-
results
122129
}
123130

124131
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2019-2020 the original authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.github.hauner.openapi.spring.model
18+
19+
/**
20+
* The responses that can be returned by an endpoint method for one (successful) response.
21+
*
22+
* @author Martin Hauner
23+
*/
24+
class EndpointResponse {
25+
26+
/**
27+
* success response
28+
*/
29+
Response main
30+
31+
/**
32+
* additional (error) responses
33+
*/
34+
Set<Response> errors
35+
36+
37+
38+
String getResponseType () {
39+
main.responseType.name
40+
}
41+
42+
String getContentType () {
43+
main.contentType
44+
}
45+
46+
/**
47+
* can this response return multiple types?
48+
*
49+
* @return true if multi else false
50+
*/
51+
boolean hasMultipleResponses () {
52+
!errors.empty
53+
}
54+
55+
/**
56+
* provides the imports required for this response.
57+
*
58+
* @return list of imports
59+
*/
60+
Set<String> getResponseImports () {
61+
if (errors.empty) {
62+
def imports = [] as Set<String>
63+
64+
imports.addAll (main.imports)
65+
errors.each {
66+
imports.addAll (it.imports)
67+
}
68+
69+
imports
70+
} else {
71+
// Because the response has multiple possible return types it will return "?" (any)
72+
// and no imports are required.
73+
[] as Set<String>
74+
}
75+
}
76+
77+
/**
78+
* returns a list with all content types.
79+
*/
80+
List<String> getContentTypes () {
81+
def result = []
82+
83+
if (main != null && main.contentType) {
84+
result.add (main.contentType)
85+
}
86+
87+
errors.each {
88+
result.addAll (it.contentType)
89+
}
90+
result
91+
}
92+
93+
}

src/main/groovy/com/github/hauner/openapi/spring/writer/InterfaceWriter.groovy

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import com.github.hauner.openapi.spring.model.Interface
2424
* Writer for Java interfaces.
2525
*
2626
* @author Martin Hauner
27-
* @authro Bastian Wilhelm
27+
* @author Bastian Wilhelm
2828
*/
2929
class InterfaceWriter {
3030
ApiOptions apiOptions
@@ -44,15 +44,17 @@ class InterfaceWriter {
4444

4545
target.write ("public interface ${itf.interfaceName} {\n\n")
4646

47-
itf.endpoints.each {
48-
methodWriter.write(target, it)
49-
target.write ("\n")
47+
itf.endpoints.each { ep ->
48+
ep.endpointResponses.each { er ->
49+
methodWriter.write(target, ep, er)
50+
target.write ("\n")
51+
}
5052
}
5153

5254
target.write ("}\n")
5355
}
5456

55-
List<String> collectImports(String packageName, List<Endpoint> endpoints) {
57+
List<String> collectImports (String packageName, List<Endpoint> endpoints) {
5658
Set<String> imports = []
5759

5860
imports.add ('org.springframework.http.ResponseEntity')
@@ -80,8 +82,12 @@ class InterfaceWriter {
8082
}
8183
}
8284

83-
// may add unnecessary imports with multiple status responses
84-
imports.addAll (ep.responseImports)
85+
ep.endpointResponses.each { mr ->
86+
def responseImports = mr.responseImports
87+
if (!responseImports.empty) {
88+
imports.addAll (responseImports)
89+
}
90+
}
8591
}
8692

8793
new ImportFilter ()

src/main/groovy/com/github/hauner/openapi/spring/writer/MethodWriter.groovy

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.github.hauner.openapi.spring.writer
1818

1919
import com.github.hauner.openapi.spring.converter.ApiOptions
2020
import com.github.hauner.openapi.spring.model.Endpoint
21+
import com.github.hauner.openapi.spring.model.EndpointResponse
2122
import com.github.hauner.openapi.spring.model.RequestBody
2223
import com.github.hauner.openapi.spring.model.parameters.Parameter
2324
import com.github.hauner.openapi.support.Identifier
@@ -33,22 +34,22 @@ class MethodWriter {
3334
ApiOptions apiOptions
3435
BeanValidationFactory beanValidationFactory
3536

36-
void write (Writer target, Endpoint endpoint) {
37+
void write (Writer target, Endpoint endpoint, EndpointResponse endpointResponse) {
3738
target.write ("""\
38-
${createMappingAnnotation (endpoint)}
39-
ResponseEntity<${getResponseEntityType(endpoint)}> ${createMethodName (endpoint)}(${createParameters(endpoint)});
39+
${createMappingAnnotation (endpoint, endpointResponse)}
40+
ResponseEntity<${getResponseEntityType(endpointResponse)}> ${createMethodName (endpoint, endpointResponse)}(${createParameters(endpoint)});
4041
""")
4142
}
4243

43-
private String getResponseEntityType (Endpoint endpoint) {
44-
if (endpoint.hasMultiStatusResponses ()) {
44+
private String getResponseEntityType (EndpointResponse endpointResponse) {
45+
if (endpointResponse.hasMultipleResponses ()) {
4546
'?'
4647
} else {
47-
endpoint.singleResponse.responseType.name
48+
endpointResponse.responseType
4849
}
4950
}
5051

51-
private String createMappingAnnotation (Endpoint endpoint) {
52+
private String createMappingAnnotation (Endpoint endpoint, EndpointResponse endpointResponse) {
5253
String mapping = "${endpoint.method.mappingAnnotation}"
5354
mapping += "("
5455
mapping += 'path = ' + quote(endpoint.path)
@@ -58,15 +59,15 @@ class MethodWriter {
5859
mapping += 'consumes = {' + quote(endpoint.requestBody.contentType) + '}'
5960
}
6061

61-
if (endpoint.hasResponseContentTypes ()) {
62+
def contentTypes = endpointResponse.contentTypes
63+
if (!contentTypes.empty) {
6264
mapping += ", "
6365
mapping += 'produces = {'
6466

65-
mapping += endpoint.getResponseContentTypes ()
66-
.collect {
67-
quote (it)
68-
}
69-
.join (', ')
67+
mapping += contentTypes.collect {
68+
quote (it)
69+
}.join (', ')
70+
7071
mapping += '}'
7172
}
7273

@@ -110,8 +111,13 @@ class MethodWriter {
110111
param
111112
}
112113

113-
private String createMethodName (Endpoint endpoint) {
114+
private String createMethodName (Endpoint endpoint, EndpointResponse endpointResponse) {
114115
def tokens = endpoint.path.tokenize ('/')
116+
117+
if (endpoint.hasMultipleEndpointResponses ()) {
118+
tokens += endpointResponse.contentType.tokenize ('/')
119+
}
120+
115121
tokens = tokens.collect { Identifier.toCamelCase (it).capitalize () }
116122
def name = tokens.join ('')
117123
"${endpoint.method.method}${name}"
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2020 the original authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.github.hauner.openapi.spring.model
18+
19+
import com.github.hauner.openapi.spring.model.datatypes.CollectionDataType
20+
import com.github.hauner.openapi.spring.model.datatypes.StringDataType
21+
import spock.lang.Specification
22+
23+
class EndpointMethodResponseSpec extends Specification {
24+
25+
void "creates single success/other content type groups" () {
26+
def endpoint = new Endpoint (path: '/foo', method: HttpMethod.GET, responses: [
27+
'200' : [
28+
new Response (contentType: 'application/json',
29+
responseType: new CollectionDataType (item: new StringDataType ()))
30+
]
31+
])
32+
33+
when:
34+
def result = endpoint.endpointResponses
35+
36+
then:
37+
result.size () == 1
38+
result[0].main.contentType == 'application/json'
39+
result[0].errors as List == []
40+
}
41+
42+
void "groups response content types to multiple success/other content type groups" () {
43+
def endpoint = new Endpoint (path: '/foo', method: HttpMethod.GET, responses: [
44+
'200' : [
45+
new Response (contentType: 'application/json',
46+
responseType: new CollectionDataType (item: new StringDataType ())),
47+
new Response (contentType: 'application/xml',
48+
responseType: new CollectionDataType (item: new StringDataType ()))
49+
],
50+
'default': [
51+
new Response (contentType: 'text/plain',
52+
responseType: new CollectionDataType (item: new StringDataType ()))
53+
]
54+
])
55+
56+
when:
57+
def result = endpoint.endpointResponses
58+
59+
then:
60+
result.size () == 2
61+
result[0].main.contentType == 'application/json'
62+
result[0].errors.collect {it.contentType} == ['text/plain']
63+
result[1].main.contentType == 'application/xml'
64+
result[1].errors.collect {it.contentType} == ['text/plain']
65+
}
66+
67+
}

0 commit comments

Comments
 (0)