Skip to content

Commit 9f3a73f

Browse files
martin-mfgGlobeDaBoarderwing328
authored
[spring]addition: api response examples + spring generator generates response examples (#17610, #16051) (#20933)
* addition: api response examples; spring's api.mustache generates response examples * update samples * added 2 sample configs; adsjuted to only generate examples for application/json * added test * remove accidentally added files * small cleanup * add new samples to workflow --------- Co-authored-by: GlobeDaBoarder <glebivashyn@gmail.com> Co-authored-by: William Cheng <wing328hk@gmail.com>
1 parent 35ba041 commit 9f3a73f

File tree

129 files changed

+5949
-4
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

129 files changed

+5949
-4
lines changed

.github/workflows/samples-spring-jdk17.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,20 @@ on:
55
paths:
66
- samples/openapi3/client/petstore/spring-cloud-3-with-optional
77
- samples/openapi3/server/petstore/springboot-3
8+
- samples/server/petstore/springboot-api-response-examples
89
- samples/server/petstore/springboot-lombok-data
910
- samples/server/petstore/springboot-lombok-tostring
1011
- samples/server/petstore/springboot-file-delegate-optional
12+
- samples/server/petstore/springboot-petstore-with-api-response-examples
1113
pull_request:
1214
paths:
1315
- samples/openapi3/client/petstore/spring-cloud-3-with-optional
1416
- samples/openapi3/server/petstore/springboot-3
17+
- samples/server/petstore/springboot-api-response-examples
1518
- samples/server/petstore/springboot-lombok-data
1619
- samples/server/petstore/springboot-lombok-tostring
1720
- samples/server/petstore/springboot-file-delegate-optional
21+
- samples/server/petstore/springboot-petstore-with-api-response-examples
1822
jobs:
1923
build:
2024
name: Build Java Spring (JDK17)
@@ -27,9 +31,11 @@ jobs:
2731
- samples/openapi3/client/petstore/spring-cloud-3-with-optional
2832
# servers
2933
- samples/openapi3/server/petstore/springboot-3
34+
- samples/server/petstore/springboot-api-response-examples
3035
- samples/server/petstore/springboot-lombok-data
3136
- samples/server/petstore/springboot-lombok-tostring
3237
- samples/server/petstore/springboot-file-delegate-optional
38+
- samples/server/petstore/springboot-petstore-with-api-response-examples
3339
steps:
3440
- uses: actions/checkout@v4
3541
- uses: actions/setup-java@v4
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
generatorName: spring
2+
outputDir: samples/server/petstore/springboot-api-response-examples
3+
library: spring-boot
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/api-response-examples_issue17610.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
6+
additionalProperties:
7+
artifactId: springboot-api-response-examples
8+
documentationProvider: springdoc
9+
useSpringBoot3: true
10+
java8: true
11+
delegatePattern: true
12+
useBeanValidation: true
13+
hideGenerationTimestamp: "true"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
generatorName: spring
2+
outputDir: samples/server/petstore/springboot-petstore-with-api-response-examples
3+
library: spring-boot
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore_with_api_response_examples.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
6+
additionalProperties:
7+
artifactId: springboot-petstore-with-api-response-examples
8+
documentationProvider: springdoc
9+
useSpringBoot3: true
10+
java8: true
11+
delegatePattern: true
12+
useBeanValidation: true
13+
hideGenerationTimestamp: "true"

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import org.openapitools.codegen.serializer.SerializerUtils;
6666
import org.openapitools.codegen.templating.MustacheEngineAdapter;
6767
import org.openapitools.codegen.templating.mustache.*;
68+
import org.openapitools.codegen.utils.ExamplesUtils;
6869
import org.openapitools.codegen.utils.ModelUtils;
6970
import org.openapitools.codegen.utils.OneOfImplementorAdditionalData;
7071
import org.slf4j.Logger;
@@ -2338,6 +2339,10 @@ public Schema unaliasSchema(Schema schema) {
23382339
return ModelUtils.unaliasSchema(this.openAPI, schema, schemaMapping);
23392340
}
23402341

2342+
private List<Map<String, Object>> unaliasExamples(Map<String, Example> examples){
2343+
return ExamplesUtils.unaliasExamples(this.openAPI, examples);
2344+
}
2345+
23412346
/**
23422347
* Return a string representation of the schema type, resolving aliasing and references if necessary.
23432348
*
@@ -4921,9 +4926,13 @@ public CodegenResponse fromResponse(String responseCode, ApiResponse response) {
49214926
}
49224927
r.schema = responseSchema;
49234928
r.message = escapeText(response.getDescription());
4924-
// TODO need to revise and test examples in responses
4925-
// ApiResponse does not support examples at the moment
4926-
//r.examples = toExamples(response.getExamples());
4929+
4930+
// adding examples to API responses
4931+
Map<String, Example> examples = ExamplesUtils.getExamplesFromResponse(openAPI, response);
4932+
4933+
if (examples != null && !examples.isEmpty())
4934+
r.examples = unaliasExamples(examples);
4935+
49274936
r.jsonSchema = Json.pretty(response);
49284937
if (response.getExtensions() != null && !response.getExtensions().isEmpty()) {
49294938
r.vendorExtensions.putAll(response.getExtensions());
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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.responses.ApiResponse;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
import java.util.*;
13+
14+
import static org.openapitools.codegen.utils.OnceLogger.once;
15+
16+
public class ExamplesUtils {
17+
private static final Logger LOGGER = LoggerFactory.getLogger(ExamplesUtils.class);
18+
19+
/**
20+
* Return examples of API response.
21+
*
22+
* @param openAPI OpenAPI spec.
23+
* @param response ApiResponse of the operation
24+
* @return examples of API response
25+
*/
26+
public static Map<String, Example> getExamplesFromResponse(OpenAPI openAPI, ApiResponse response) {
27+
ApiResponse result = ModelUtils.getReferencedApiResponse(openAPI, response);
28+
if (result == null) {
29+
return Collections.emptyMap();
30+
} else {
31+
return getExamplesFromContent(result.getContent());
32+
}
33+
}
34+
35+
private static Map<String, Example> getExamplesFromContent(Content content) {
36+
if (content == null || content.isEmpty())
37+
return Collections.emptyMap();
38+
39+
if (content.containsKey("application/json")) {
40+
Map<String, Example> examples = content.get("application/json").getExamples();
41+
if (content.size() > 1 && examples != null && !examples.isEmpty()) {
42+
once(LOGGER).warn("More than one content media type found in response. Only response examples of the application/json will be taken for codegen.");
43+
}
44+
45+
return examples;
46+
}
47+
48+
once(LOGGER).warn("No application/json content media type found in response. Response examples can currently only be generated for application/json media type.");
49+
50+
return Collections.emptyMap();
51+
}
52+
53+
54+
/**
55+
* Return actual examples objects of API response with values and processed from references (unaliased)
56+
*
57+
* @param openapi OpenAPI spec.
58+
* @param apiRespExamples examples of API response
59+
* @return unaliased examples of API response
60+
*/
61+
public static List<Map<String, Object>> unaliasExamples(OpenAPI openapi, Map<String, Example> apiRespExamples) {
62+
Map<String, Example> actualComponentsExamples = getAllExamples(openapi);
63+
64+
List<Map<String, Object>> result = new ArrayList<>();
65+
for (Map.Entry<String, Example> example : apiRespExamples.entrySet()) {
66+
try {
67+
Map<String, Object> exampleRepr = new LinkedHashMap<>();
68+
String exampleName = ModelUtils.getSimpleRef(example.getValue().get$ref());
69+
70+
// api response example can both be a reference and specified directly in the code
71+
// if the reference is null, we get the value directly from the example -- no unaliasing is needed
72+
// if it isn't, we get the value from the components examples
73+
Object exampleValue;
74+
if(example.getValue().get$ref() != null) {
75+
exampleValue = actualComponentsExamples.get(exampleName).getValue();
76+
LOGGER.debug("Unaliased example value from components examples: {}", exampleValue);
77+
} else {
78+
exampleValue = example.getValue().getValue();
79+
LOGGER.debug("Retrieved example value directly from the api response example definition: {}", exampleValue);
80+
}
81+
82+
exampleRepr.put("exampleName", exampleName);
83+
exampleRepr.put("exampleValue", new ObjectMapper().writeValueAsString(exampleValue)
84+
.replace("\"", "\\\""));
85+
86+
result.add(exampleRepr);
87+
} catch (JsonProcessingException e) {
88+
LOGGER.error("Failed to serialize example value", e);
89+
throw new RuntimeException(e);
90+
}
91+
}
92+
93+
return result;
94+
}
95+
96+
private static Map<String, Example> getAllExamples(OpenAPI openapi) {
97+
return openapi.getComponents().getExamples();
98+
}
99+
}

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.*;
@@ -167,7 +168,19 @@ public interface {{classname}} {
167168
{{#responses}}
168169
@ApiResponse(responseCode = {{#isDefault}}"default"{{/isDefault}}{{^isDefault}}"{{{code}}}"{{/isDefault}}, description = "{{{message}}}"{{#baseType}}, content = {
169170
{{#produces}}
170-
@Content(mediaType = "{{{mediaType}}}", {{#isArray}}array = @ArraySchema({{/isArray}}schema = @Schema(implementation = {{{baseType}}}.class){{#isArray}}){{/isArray}}){{^-last}},{{/-last}}
171+
@Content(mediaType = "{{{mediaType}}}", {{#isArray}}array = @ArraySchema({{/isArray}}schema = @Schema(implementation = {{{baseType}}}.class){{#isArray}}){{/isArray}}{{^isJson}}){{^-last}},{{/-last}}{{/isJson}}{{#isJson}}{{^examples.0}}){{^-last}},{{/-last}}{{/examples.0}}{{#examples.0}}, examples = {
172+
{{#examples}}
173+
@ExampleObject(
174+
name = "{{{exampleName}}}",
175+
value = "{{{exampleValue}}}"
176+
){{^-last}},{{/-last}}
177+
{{/examples}}
178+
{{#-last}}
179+
})
180+
{{/-last}}
181+
{{^-last}}
182+
}),
183+
{{/-last}}{{/examples.0}}{{/isJson}}
171184
{{/produces}}
172185
}{{/baseType}}){{^-last}},{{/-last}}
173186
{{/responses}}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.openapitools.codegen.java.assertions;
22

3+
import com.github.javaparser.ast.Node;
34
import com.github.javaparser.ast.expr.AnnotationExpr;
45
import com.github.javaparser.ast.expr.MarkerAnnotationExpr;
56
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
@@ -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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5383,6 +5383,7 @@ public void testEnumWithImplements() {
53835383

53845384
JavaFileAssert.assertThat(files.get("Type.java")).fileContains("Type implements java.io.Serializable {");
53855385
}
5386+
53865387
@Test
53875388
public void shouldEnableBuiltInValidationOptionWhenSetToTrue() throws IOException {
53885389
final SpringCodegen codegen = new SpringCodegen();
@@ -5433,6 +5434,30 @@ public void shouldDisableBuiltInValidationOptionByDefault() throws IOException {
54335434
.containsWithName("Validated");
54345435
}
54355436

5437+
@Test
5438+
public void testExampleAnnotationGeneration_issue17610() throws IOException {
5439+
final Map<String, File> generatedCodeFiles = generateFromContract("src/test/resources/3_0/spring/api-response-examples_issue17610.yaml", SPRING_BOOT);
5440+
5441+
JavaFileAssert.assertThat(generatedCodeFiles.get("DogsApi.java"))
5442+
.assertMethod("createDog")
5443+
.assertMethodAnnotations()
5444+
.recursivelyContainsWithName("ExampleObject");
5445+
}
5446+
5447+
@Test
5448+
public void testExampleAnnotationGeneration_issue17610_2() throws IOException {
5449+
final Map<String, File> generatedCodeFiles = generateFromContract("src/test/resources/3_0/spring/petstore_with_api_response_examples.yaml", SPRING_BOOT);
5450+
5451+
JavaFileAssert.assertThat(generatedCodeFiles.get("PetApi.java"))
5452+
.assertMethod("addPet")
5453+
.assertMethodAnnotations()
5454+
.recursivelyContainsWithName("ExampleObject")
5455+
.toMethod().toFileAssert()
5456+
.assertMethod("findPetsByStatus")
5457+
.assertMethodAnnotations()
5458+
.recursivelyContainsWithName("ExampleObject");
5459+
}
5460+
54365461
@Test
54375462
public void testEnumFieldShouldBeFinal_issue21018() throws IOException {
54385463
SpringCodegen codegen = new SpringCodegen();
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)