Skip to content

Commit a2415d8

Browse files
authored
Merge pull request #87 from hauner/multi-content-response
multi content response
2 parents 16befae + d37ce66 commit a2415d8

File tree

9 files changed

+303
-91
lines changed

9 files changed

+303
-91
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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
if (hasMultipleResponses ()) {
40+
'?'
41+
} else {
42+
main.responseType.name
43+
}
44+
}
45+
46+
String getContentType () {
47+
main.contentType
48+
}
49+
50+
/**
51+
* can this response return multiple types?
52+
*
53+
* @return true if multi else false
54+
*/
55+
boolean hasMultipleResponses () {
56+
!errors.empty
57+
}
58+
59+
/**
60+
* provides the imports required for this response.
61+
*
62+
* @return list of imports
63+
*/
64+
Set<String> getResponseImports () {
65+
if (errors.empty) {
66+
def imports = [] as Set<String>
67+
68+
imports.addAll (main.imports)
69+
errors.each {
70+
imports.addAll (it.imports)
71+
}
72+
73+
imports
74+
} else {
75+
// Because the response has multiple possible return types it will return "?" (any)
76+
// and no imports are required.
77+
[] as Set<String>
78+
}
79+
}
80+
81+
/**
82+
* returns a list with all content types.
83+
*/
84+
List<String> getContentTypes () {
85+
def result = []
86+
87+
if (main != null && main.contentType) {
88+
result.add (main.contentType)
89+
}
90+
91+
errors.each {
92+
result.addAll (it.contentType)
93+
}
94+
result
95+
}
96+
97+
}

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: 17 additions & 19 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,14 @@ 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<${endpointResponse.responseType}> ${createMethodName (endpoint, endpointResponse)}(${createParameters(endpoint)});
4041
""")
4142
}
4243

43-
private String getResponseEntityType (Endpoint endpoint) {
44-
if (endpoint.hasMultiStatusResponses ()) {
45-
'?'
46-
} else {
47-
endpoint.singleResponse.responseType.name
48-
}
49-
}
50-
51-
private String createMappingAnnotation (Endpoint endpoint) {
44+
private String createMappingAnnotation (Endpoint endpoint, EndpointResponse endpointResponse) {
5245
String mapping = "${endpoint.method.mappingAnnotation}"
5346
mapping += "("
5447
mapping += 'path = ' + quote(endpoint.path)
@@ -58,15 +51,15 @@ class MethodWriter {
5851
mapping += 'consumes = {' + quote(endpoint.requestBody.contentType) + '}'
5952
}
6053

61-
if (endpoint.hasResponseContentTypes ()) {
54+
def contentTypes = endpointResponse.contentTypes
55+
if (!contentTypes.empty) {
6256
mapping += ", "
6357
mapping += 'produces = {'
6458

65-
mapping += endpoint.getResponseContentTypes ()
66-
.collect {
67-
quote (it)
68-
}
69-
.join (', ')
59+
mapping += contentTypes.collect {
60+
quote (it)
61+
}.join (', ')
62+
7063
mapping += '}'
7164
}
7265

@@ -110,8 +103,13 @@ class MethodWriter {
110103
param
111104
}
112105

113-
private String createMethodName (Endpoint endpoint) {
106+
private String createMethodName (Endpoint endpoint, EndpointResponse endpointResponse) {
114107
def tokens = endpoint.path.tokenize ('/')
108+
109+
if (endpoint.hasMultipleEndpointResponses ()) {
110+
tokens += endpointResponse.contentType.tokenize ('/')
111+
}
112+
115113
tokens = tokens.collect { Identifier.toCamelCase (it).capitalize () }
116114
def name = tokens.join ('')
117115
"${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)