Skip to content

Commit 11f3ce7

Browse files
committed
Properly read multiple nested/composed @APIResponse annotations
Signed-off-by: Michael Edgar <michael@xlate.io>
1 parent 5cbd9e6 commit 11f3ce7

File tree

6 files changed

+141
-30
lines changed

6 files changed

+141
-30
lines changed

core/src/main/java/io/smallrye/openapi/runtime/io/responses/APIResponseIO.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.eclipse.microprofile.openapi.models.responses.APIResponse;
1111
import org.eclipse.microprofile.openapi.models.responses.APIResponses;
1212
import org.jboss.jandex.AnnotationInstance;
13+
import org.jboss.jandex.AnnotationTarget;
1314
import org.jboss.jandex.DotName;
1415
import org.jboss.jandex.Type;
1516

@@ -40,6 +41,11 @@ public APIResponseIO(IOContext<V, A, O, AB, OB> context) {
4041
super(context, Names.API_RESPONSE, DotName.createSimple(APIResponse.class));
4142
}
4243

44+
@Override
45+
public Map<String, APIResponse> readMap(AnnotationTarget target) {
46+
return readMap(target, this::responseCode);
47+
}
48+
4349
@Override
4450
public APIResponse read(AnnotationInstance annotation) {
4551
IoLogging.logger.singleAnnotation("@APIResponse");

core/src/main/java/io/smallrye/openapi/runtime/io/responses/APIResponsesIO.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.smallrye.openapi.runtime.io.responses;
22

33
import java.util.Arrays;
4-
import java.util.Collections;
54
import java.util.Map;
65
import java.util.Optional;
76

@@ -23,15 +22,21 @@ public APIResponsesIO(IOContext<V, A, O, AB, OB> context) {
2322
super(context, Names.API_RESPONSES, Names.create(APIResponses.class));
2423
}
2524

26-
public Map<String, APIResponse> readSingle(AnnotationTarget target) {
27-
return Optional.ofNullable(apiResponseIO().getAnnotation(target))
28-
.map(Collections::singleton)
29-
.map(annotations -> apiResponseIO().readMap(annotations, apiResponseIO()::responseCode))
25+
@Override
26+
public APIResponses read(AnnotationTarget target) {
27+
Map<String, APIResponse> responseMap = apiResponseIO().readMap(target);
28+
Map<String, Object> extensions = Optional.ofNullable(getAnnotation(target))
29+
.map(extensionIO()::readExtensible)
3030
.orElse(null);
31-
}
3231

33-
public Map<String, APIResponse> readAll(AnnotationTarget target) {
34-
return apiResponseIO().readMap(target, apiResponseIO()::responseCode);
32+
if (!responseMap.isEmpty() || extensions != null) {
33+
APIResponses responses = OASFactory.createAPIResponses();
34+
responseMap.forEach(responses::addAPIResponse);
35+
responses.setExtensions(extensions);
36+
return responses;
37+
}
38+
39+
return null;
3540
}
3641

3742
@Override

core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -376,25 +376,22 @@ default void processResponse(final AnnotationScannerContext context, final Class
376376

377377
setJsonViewContext(context, context.annotations().getAnnotationValue(method, JacksonConstants.JSON_VIEW));
378378

379-
Optional<APIResponses> classResponses = Optional.ofNullable(context.io().apiResponsesIO().read(resourceClass));
380-
Map<String, APIResponse> classResponse = context.io().apiResponsesIO().readSingle(resourceClass);
381-
addResponses(operation, classResponses, classResponse, false);
379+
// Add responses from the class
380+
addResponses(operation, context.io().apiResponsesIO().read(resourceClass), false);
382381

383382
// Method annotations override class annotations
384-
Optional<APIResponses> methodResponses = Optional.ofNullable(context.io().apiResponsesIO().read(method));
385-
Map<String, APIResponse> methodResponse = context.io().apiResponsesIO().readSingle(method);
386-
addResponses(operation, methodResponses, methodResponse, true);
383+
addResponses(operation, context.io().apiResponsesIO().read(method), true);
387384

388385
context.io().apiResponsesIO().readResponseSchema(method)
389386
.ifPresent(responseSchema -> addApiReponseSchemaFromAnnotation(responseSchema, method, operation));
390387

391388
/*
392389
* If there is no response from annotations, try to create one from the method return value.
393-
* Do not generate a response if the app has used an empty @ApiResponses annotation. This
390+
* Do not generate a response if the app has used an empty @APIResponses annotation. This
394391
* provides a way for the application to indicate that responses will be supplied some other
395392
* way (i.e. static file).
396393
*/
397-
if (methodResponses.isPresent() || context.io().apiResponsesIO().getAnnotation(method) == null) {
394+
if (!hasEmptyAPIResponsesAnnotation(context, method)) {
398395
createResponseFromRestMethod(context, method, operation);
399396
}
400397

@@ -418,16 +415,25 @@ default void processResponse(final AnnotationScannerContext context, final Class
418415
clearJsonViewContext(context);
419416
}
420417

421-
default void addResponses(Operation operation, Optional<APIResponses> responses, Map<String, APIResponse> singleResponse,
422-
boolean includeExtensions) {
423-
responses.ifPresent(resp -> {
424-
resp.getAPIResponses().forEach(ModelUtil.responses(operation)::addAPIResponse);
425-
if (includeExtensions && resp.getExtensions() != null) {
426-
resp.getExtensions().forEach(ModelUtil.responses(operation)::addExtension);
418+
private boolean hasEmptyAPIResponsesAnnotation(AnnotationScannerContext context, MethodInfo method) {
419+
AnnotationInstance responsesAnno = context.io().apiResponsesIO().getAnnotation(method);
420+
421+
if (responsesAnno == null) {
422+
return false;
423+
}
424+
425+
AnnotationInstance[] responsesAnnoValue = context.annotations().value(responsesAnno);
426+
return (responsesAnnoValue == null || responsesAnnoValue.length == 0);
427+
}
428+
429+
private void addResponses(Operation operation, APIResponses responses, boolean includeExtensions) {
430+
if (responses != null) {
431+
APIResponses operationResponses = ModelUtil.responses(operation);
432+
responses.getAPIResponses().forEach(operationResponses::addAPIResponse);
433+
434+
if (includeExtensions) {
435+
operationResponses.setExtensions(responses.getExtensions());
427436
}
428-
});
429-
if (singleResponse != null) {
430-
singleResponse.forEach(ModelUtil.responses(operation)::addAPIResponse);
431437
}
432438
}
433439

extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ private Map<DotName, Map<String, APIResponse>> processExceptionMappers() {
323323
MethodInfo mapperMethod = mapperAnnotation.target().asMethod();
324324
if (mapperMethod.parametersCount() == 1) {
325325
DotName exceptionName = mapperMethod.parameterType(0).name();
326-
exceptionMappers.put(exceptionName, context.io().apiResponsesIO().readAll(mapperMethod));
326+
exceptionMappers.put(exceptionName, context.io().apiResponseIO().readMap(mapperMethod));
327327
}
328328
}
329329

@@ -349,14 +349,14 @@ private Map<DotName, Map<String, APIResponse>> exceptionResponseAnnotations(Clas
349349
.of(classInfo.method(JaxRsConstants.TO_RESPONSE_METHOD_NAME, exceptionType))
350350
.filter(Objects::nonNull)
351351
.flatMap(m -> context.io()
352-
.apiResponsesIO()
353-
.readAll(m)
352+
.apiResponseIO()
353+
.readMap(m)
354354
.entrySet()
355355
.stream());
356356

357357
Stream<Entry<String, APIResponse>> classAnnotations = context.io()
358-
.apiResponsesIO()
359-
.readAll(classInfo)
358+
.apiResponseIO()
359+
.readMap(classInfo)
360360
.entrySet()
361361
.stream();
362362

extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ApiResponseTests.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package io.smallrye.openapi.runtime.scanner;
22

33
import java.io.IOException;
4+
import java.lang.annotation.ElementType;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
48
import java.util.Collection;
59
import java.util.HashMap;
610
import java.util.Iterator;
@@ -11,6 +15,7 @@
1115
import org.eclipse.microprofile.openapi.annotations.media.Schema;
1216
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
1317
import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema;
18+
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
1419
import org.json.JSONException;
1520
import org.junit.jupiter.api.Test;
1621
import org.junit.jupiter.params.ParameterizedTest;
@@ -454,4 +459,48 @@ public jakarta.ws.rs.core.Response handle() {
454459
(java.io.InputStream) null,
455460
DataApi.class));
456461
}
462+
463+
@Target(ElementType.METHOD)
464+
@Retention(RetentionPolicy.RUNTIME)
465+
@APIResponses({ // NOSONAR - both @APIResponses + @APIResponse wanted directly
466+
@APIResponse(responseCode = "400", description = "Really Bad Request"),
467+
@APIResponse(responseCode = "406", description = "Not Acceptable")
468+
})
469+
@APIResponse(responseCode = "404", description = "Not found")
470+
@interface WithClientErrors {
471+
}
472+
473+
@Target(ElementType.METHOD)
474+
@Retention(RetentionPolicy.RUNTIME)
475+
@APIResponse(responseCode = "500", description = "Internal server error")
476+
@APIResponses({ // NOSONAR - both @APIResponses + @APIResponse wanted directly
477+
@APIResponse(responseCode = "502", description = "Bad Gateway"),
478+
@APIResponse(responseCode = "504", description = "Gateway Timeout")
479+
})
480+
@interface WithServerErrors {
481+
}
482+
483+
@Test
484+
void testAnnotationComposition() throws IOException, JSONException {
485+
486+
@jakarta.ws.rs.Path("hello")
487+
class HelloApi {
488+
489+
@jakarta.ws.rs.GET
490+
@jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.TEXT_PLAIN)
491+
@APIResponses({ // NOSONAR - both @APIResponses + @APIResponse wanted directly on method
492+
@APIResponse(responseCode = "200", description = "OK"),
493+
@APIResponse(responseCode = "301", description = "See Other")
494+
})
495+
@WithClientErrors
496+
@WithServerErrors
497+
@APIResponse(responseCode = "403", description = "Not Authorized")
498+
public String sayHello() {
499+
return "Goodbye";
500+
}
501+
}
502+
503+
assertJsonEquals("responses.multi-location-composition.json", HelloApi.class, WithClientErrors.class,
504+
WithServerErrors.class);
505+
}
457506
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"openapi" : "3.1.0",
3+
"paths" : {
4+
"/hello" : {
5+
"get" : {
6+
"responses" : {
7+
"403" : {
8+
"description" : "Not Authorized"
9+
},
10+
"404" : {
11+
"description" : "Not found"
12+
},
13+
"500" : {
14+
"description" : "Internal server error"
15+
},
16+
"200" : {
17+
"description" : "OK",
18+
"content" : {
19+
"text/plain" : {
20+
"schema" : {
21+
"type" : "string"
22+
}
23+
}
24+
}
25+
},
26+
"301" : {
27+
"description" : "See Other"
28+
},
29+
"400" : {
30+
"description" : "Really Bad Request"
31+
},
32+
"406" : {
33+
"description" : "Not Acceptable"
34+
},
35+
"502" : {
36+
"description" : "Bad Gateway"
37+
},
38+
"504" : {
39+
"description" : "Gateway Timeout"
40+
}
41+
}
42+
}
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)