Skip to content

Commit cb514a9

Browse files
addition: api response examples; spring's api.mustache generates response examples
1 parent 64c763b commit cb514a9

6 files changed

Lines changed: 239 additions & 5 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
package org.openapitools.codegen;
1919

20-
import com.fasterxml.jackson.annotation.JsonCreator;
2120
import com.github.benmanes.caffeine.cache.Cache;
2221
import com.github.benmanes.caffeine.cache.Caffeine;
2322
import com.github.benmanes.caffeine.cache.Ticker;
@@ -64,6 +63,7 @@
6463
import org.openapitools.codegen.serializer.SerializerUtils;
6564
import org.openapitools.codegen.templating.MustacheEngineAdapter;
6665
import org.openapitools.codegen.templating.mustache.*;
66+
import org.openapitools.codegen.utils.ExamplesUtils;
6767
import org.openapitools.codegen.utils.ModelUtils;
6868
import org.openapitools.codegen.utils.OneOfImplementorAdditionalData;
6969
import org.slf4j.Logger;
@@ -2433,6 +2433,10 @@ public Schema unaliasSchema(Schema schema) {
24332433
return ModelUtils.unaliasSchema(this.openAPI, schema, schemaMapping);
24342434
}
24352435

2436+
private List<Map<String, Object>> unaliasExamples(Map<String, Example> examples){
2437+
return ExamplesUtils.unaliasExamples(this.openAPI, examples);
2438+
}
2439+
24362440
/**
24372441
* Return a string representation of the schema type, resolving aliasing and references if necessary.
24382442
*
@@ -4913,9 +4917,13 @@ public CodegenResponse fromResponse(String responseCode, ApiResponse response) {
49134917
}
49144918
r.schema = responseSchema;
49154919
r.message = escapeText(response.getDescription());
4916-
// TODO need to revise and test examples in responses
4917-
// ApiResponse does not support examples at the moment
4918-
//r.examples = toExamples(response.getExamples());
4920+
4921+
// adding examples to API responses
4922+
Map<String, Example> examples = ExamplesUtils.getExamplesFromResponse(openAPI, response);
4923+
4924+
if (examples != null && !examples.isEmpty())
4925+
r.examples = unaliasExamples(examples);
4926+
49194927
r.jsonSchema = Json.pretty(response);
49204928
if (response.getExtensions() != null && !response.getExtensions().isEmpty()) {
49214929
r.vendorExtensions.putAll(response.getExtensions());
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package org.openapitools.codegen.utils;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import io.swagger.v3.oas.models.OpenAPI;
6+
import io.swagger.v3.oas.models.examples.Example;
7+
import io.swagger.v3.oas.models.media.Content;
8+
import io.swagger.v3.oas.models.media.MediaType;
9+
import io.swagger.v3.oas.models.responses.ApiResponse;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
13+
import java.util.*;
14+
15+
import static org.openapitools.codegen.utils.OnceLogger.once;
16+
17+
public class ExamplesUtils {
18+
private static final Logger LOGGER = LoggerFactory.getLogger(ExamplesUtils.class);
19+
20+
/**
21+
* Return examples of API response.
22+
*
23+
* @param openAPI OpenAPI spec.
24+
* @param response ApiResponse of the operation
25+
* @return examples of API response
26+
*/
27+
public static Map<String, Example> getExamplesFromResponse(OpenAPI openAPI, ApiResponse response) {
28+
ApiResponse result = ModelUtils.getReferencedApiResponse(openAPI, response);
29+
if (result == null) {
30+
return Collections.emptyMap();
31+
} else {
32+
return getExamplesFromContent(result.getContent());
33+
}
34+
}
35+
36+
private static Map<String, Example> getExamplesFromContent(Content content) {
37+
if (content == null || content.isEmpty()) {
38+
return Collections.emptyMap();
39+
}
40+
Map.Entry<String, MediaType> entry = content.entrySet().iterator().next();
41+
if (content.size() > 1) {
42+
once(LOGGER).debug("Multiple API response examples found in the OAS 'content' section, returning only the first one ({})",
43+
entry.getKey());
44+
}
45+
return entry.getValue().getExamples();
46+
}
47+
48+
49+
/**
50+
* Return actual examples objects of API response with values and processed from references (unaliased)
51+
*
52+
* @param openapi OpenAPI spec.
53+
* @param apiRespExamples examples of API response
54+
* @return unaliased examples of API response
55+
*/
56+
public static List<Map<String, Object>> unaliasExamples(OpenAPI openapi, Map<String, Example> apiRespExamples) {
57+
Map<String, Example> actualComponentsExamples = getAllExamples(openapi);
58+
59+
List<Map<String, Object>> result = new ArrayList<>();
60+
for (Map.Entry<String, Example> example : apiRespExamples.entrySet()) {
61+
try {
62+
Map<String, Object> exampleRepr = new LinkedHashMap<>();
63+
String exampleName = ModelUtils.getSimpleRef(example.getValue().get$ref());
64+
65+
// api response example can both be a reference and specified directly in the code
66+
// if the reference is null, we get the value directly from the example -- no unaliasing is needed
67+
// if it isn't, we get the value from the components examples
68+
Object exampleValue;
69+
if(example.getValue().get$ref() != null){
70+
exampleValue = actualComponentsExamples.get(exampleName).getValue();
71+
LOGGER.trace("Unaliased example value from components examples: {}", exampleValue);
72+
} else {
73+
exampleValue = example.getValue().getValue();
74+
LOGGER.trace("Retrieved example value directly from the api response example definition: {}", exampleValue);
75+
}
76+
77+
exampleRepr.put("exampleName", exampleName);
78+
exampleRepr.put("exampleValue", new ObjectMapper().writeValueAsString(exampleValue)
79+
.replace("\"", "\\\""));
80+
81+
result.add(exampleRepr);
82+
} catch (JsonProcessingException e) {
83+
LOGGER.error("Failed to serialize example value", e);
84+
throw new RuntimeException(e);
85+
}
86+
}
87+
88+
return result;
89+
}
90+
91+
private static Map<String, Example> getAllExamples(OpenAPI openapi) {
92+
return openapi.getComponents().getExamples();
93+
}
94+
}

modules/openapi-generator/src/main/resources/JavaSpring/api.mustache

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
1919
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
2020
import io.swagger.v3.oas.annotations.tags.Tag;
2121
import io.swagger.v3.oas.annotations.enums.ParameterIn;
22+
import io.swagger.v3.oas.annotations.media.ExampleObject;
2223
{{/swagger2AnnotationLibrary}}
2324
{{#swagger1AnnotationLibrary}}
2425
import io.swagger.annotations.*;
@@ -171,7 +172,19 @@ public interface {{classname}} {
171172
{{#responses}}
172173
@ApiResponse(responseCode = {{#isDefault}}"default"{{/isDefault}}{{^isDefault}}"{{{code}}}"{{/isDefault}}, description = "{{{message}}}"{{#baseType}}, content = {
173174
{{#produces}}
174-
@Content(mediaType = "{{{mediaType}}}", {{#isArray}}array = @ArraySchema({{/isArray}}schema = @Schema(implementation = {{{baseType}}}.class){{#isArray}}){{/isArray}}){{^-last}},{{/-last}}
175+
@Content(mediaType = "{{{mediaType}}}", {{#isArray}}array = @ArraySchema({{/isArray}}schema = @Schema(implementation = {{{baseType}}}.class){{#isArray}}){{/isArray}}{{^examples.0}}{{#-last}}){{/-last}}{{^-last}}),{{/-last}}{{/examples.0}}{{#examples.0}}, examples = {
176+
{{#examples}}
177+
@ExampleObject(
178+
name = "{{{exampleName}}}",
179+
value = "{{{exampleValue}}}"
180+
){{^-last}},{{/-last}}
181+
{{/examples}}
182+
{{#-last}}
183+
})
184+
{{/-last}}
185+
{{^-last}}
186+
}),
187+
{{/-last}}{{/examples.0}}
175188
{{/produces}}
176189
}{{/baseType}}){{^-last}},{{/-last}}
177190
{{/responses}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationAssert.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.Objects;
77
import java.util.stream.Collectors;
88

9+
import com.github.javaparser.ast.Node;
910
import org.assertj.core.api.ListAssert;
1011
import org.assertj.core.util.CanIgnoreReturnValue;
1112

@@ -72,4 +73,32 @@ private static boolean hasAttributes(final AnnotationExpr annotation, final Map<
7273
private ACTUAL myself() {
7374
return (ACTUAL) this;
7475
}
76+
77+
public ACTUAL recursivelyContainsWithName(String name) {
78+
super
79+
.withFailMessage("Should have annotation with name: " + name)
80+
.anyMatch(annotation -> containsSpecificAnnotationName(annotation, name));
81+
82+
return myself();
83+
}
84+
85+
private boolean containsSpecificAnnotationName(Node node, String name) {
86+
if (node == null || name == null)
87+
return false;
88+
89+
if (node instanceof AnnotationExpr) {
90+
AnnotationExpr annotation = (AnnotationExpr) node;
91+
92+
if(annotation.getNameAsString().equals(name))
93+
return true;
94+
95+
}
96+
97+
for(Node child: node.getChildNodes()){
98+
if(containsSpecificAnnotationName(child, name))
99+
return true;
100+
}
101+
102+
return false;
103+
}
75104
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4427,4 +4427,14 @@ public void testMultiInheritanceParentRequiredParams_issue15796() throws IOExcep
44274427
.hasParameter("race").toConstructor()
44284428
;
44294429
}
4430+
4431+
@Test
4432+
public void testExampleAnnotationGeneration_issue17610() throws IOException {
4433+
final Map<String, File> generatedCodeFiles = generateFromContract("src/test/resources/3_0/spring/api-response-examples_issue17610.yaml", SPRING_BOOT);
4434+
4435+
JavaFileAssert.assertThat(generatedCodeFiles.get("DogsApi.java"))
4436+
.assertMethod("createDog")
4437+
.assertMethodAnnotations()
4438+
.recursivelyContainsWithName("ExampleObject");
4439+
}
44304440
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
openapi: 3.0.3
2+
info:
3+
title: No examples in annotation example API
4+
description: No examples in annotation example API
5+
version: 1.0.0
6+
servers:
7+
- url: 'https://localhost:8080'
8+
paths:
9+
/dogs:
10+
post:
11+
summary: Create a dog
12+
operationId: createDog
13+
requestBody:
14+
content:
15+
application/json:
16+
schema:
17+
$ref: '#/components/schemas/Dog'
18+
responses:
19+
'200':
20+
description: OK
21+
content:
22+
application/json:
23+
schema:
24+
$ref: '#/components/schemas/Dog'
25+
'400':
26+
description: Bad Request
27+
content:
28+
application/json:
29+
schema:
30+
$ref: '#/components/schemas/Error'
31+
examples:
32+
dog name length:
33+
$ref: '#/components/examples/DogNameBiggerThan50Error'
34+
dog name contains numbers:
35+
$ref: '#/components/examples/DogNameContainsNumbersError'
36+
dog age negative:
37+
$ref: '#/components/examples/DogAgeNegativeError'
38+
39+
components:
40+
schemas:
41+
Dog:
42+
type: object
43+
properties:
44+
name:
45+
type: string
46+
maxLength: 50
47+
pattern: '^[a-zA-Z]+$'
48+
x-pattern-message: Name must contain only letters
49+
example: 'Rex'
50+
age:
51+
type: integer
52+
format: int32
53+
minimum: 0
54+
example: 5
55+
# NOTE: not picked up by the generator
56+
# TODO: consider adding support for this
57+
# example:
58+
# name: 'Rex'
59+
# age: 5
60+
Error:
61+
type: object
62+
properties:
63+
code:
64+
type: integer
65+
format: int32
66+
message:
67+
type: string
68+
examples:
69+
DogNameBiggerThan50Error:
70+
value:
71+
code: 400
72+
message: name size must be between 0 and 50
73+
DogNameContainsNumbersError:
74+
value:
75+
code: 400
76+
message: Name must contain only letters
77+
DogAgeNegativeError:
78+
value:
79+
code: 400
80+
message: age must be greater than or equal to 0

0 commit comments

Comments
 (0)