Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Examples regenerate #36

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions bin/configs/spring-boot-api-response-examples.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
generatorName: spring
outputDir: samples/server/petstore/springboot-api-response-examples
library: spring-boot
inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/api-response-examples_issue17610.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
additionalProperties:
artifactId: springboot-api-response-examples
documentationProvider: springdoc
useSpringBoot3: true
java8: true
delegatePattern: true
useBeanValidation: true
hideGenerationTimestamp: "true"
13 changes: 13 additions & 0 deletions bin/configs/spring-boot-petstore-with-api-response-examples.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
generatorName: spring
outputDir: samples/server/petstore/springboot-petstore-with-api-response-examples
library: spring-boot
inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore_with_api_response_examples.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
additionalProperties:
artifactId: springboot-petstore-with-api-response-examples
documentationProvider: springdoc
useSpringBoot3: true
java8: true
delegatePattern: true
useBeanValidation: true
hideGenerationTimestamp: "true"
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

package org.openapitools.codegen;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Ticker;
Expand Down Expand Up @@ -64,6 +63,7 @@
import org.openapitools.codegen.serializer.SerializerUtils;
import org.openapitools.codegen.templating.MustacheEngineAdapter;
import org.openapitools.codegen.templating.mustache.*;
import org.openapitools.codegen.utils.ExamplesUtils;
import org.openapitools.codegen.utils.ModelUtils;
import org.openapitools.codegen.utils.OneOfImplementorAdditionalData;
import org.slf4j.Logger;
Expand Down Expand Up @@ -2433,6 +2433,10 @@ public Schema unaliasSchema(Schema schema) {
return ModelUtils.unaliasSchema(this.openAPI, schema, schemaMapping);
}

private List<Map<String, Object>> unaliasExamples(Map<String, Example> examples){
return ExamplesUtils.unaliasExamples(this.openAPI, examples);
}

/**
* Return a string representation of the schema type, resolving aliasing and references if necessary.
*
Expand Down Expand Up @@ -4913,9 +4917,13 @@ public CodegenResponse fromResponse(String responseCode, ApiResponse response) {
}
r.schema = responseSchema;
r.message = escapeText(response.getDescription());
// TODO need to revise and test examples in responses
// ApiResponse does not support examples at the moment
//r.examples = toExamples(response.getExamples());

// adding examples to API responses
Map<String, Example> examples = ExamplesUtils.getExamplesFromResponse(openAPI, response);

if (examples != null && !examples.isEmpty())
r.examples = unaliasExamples(examples);

r.jsonSchema = Json.pretty(response);
if (response.getExtensions() != null && !response.getExtensions().isEmpty()) {
r.vendorExtensions.putAll(response.getExtensions());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.openapitools.codegen.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.responses.ApiResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

import static org.openapitools.codegen.utils.OnceLogger.once;

public class ExamplesUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(ExamplesUtils.class);

/**
* Return examples of API response.
*
* @param openAPI OpenAPI spec.
* @param response ApiResponse of the operation
* @return examples of API response
*/
public static Map<String, Example> getExamplesFromResponse(OpenAPI openAPI, ApiResponse response) {
ApiResponse result = ModelUtils.getReferencedApiResponse(openAPI, response);
if (result == null) {
return Collections.emptyMap();
} else {
return getExamplesFromContent(result.getContent());
}
}

private static Map<String, Example> getExamplesFromContent(Content content) {
if (content == null || content.isEmpty())
return Collections.emptyMap();

if (content.containsKey("application/json")) {
Map<String, Example> examples = content.get("application/json").getExamples();
if (content.size() > 1 && examples != null && !examples.isEmpty())
once(LOGGER).warn("More than one content media types found in response. Only response examples of the application/json will be taken for codegen.");

return examples;
}

once(LOGGER).warn("No application/json content media type found in response. Response examples can only be generated for application/json media type.");

return Collections.emptyMap();
}


/**
* Return actual examples objects of API response with values and processed from references (unaliased)
*
* @param openapi OpenAPI spec.
* @param apiRespExamples examples of API response
* @return unaliased examples of API response
*/
public static List<Map<String, Object>> unaliasExamples(OpenAPI openapi, Map<String, Example> apiRespExamples) {
Map<String, Example> actualComponentsExamples = getAllExamples(openapi);

List<Map<String, Object>> result = new ArrayList<>();
for (Map.Entry<String, Example> example : apiRespExamples.entrySet()) {
try {
Map<String, Object> exampleRepr = new LinkedHashMap<>();
String exampleName = ModelUtils.getSimpleRef(example.getValue().get$ref());

// api response example can both be a reference and specified directly in the code
// if the reference is null, we get the value directly from the example -- no unaliasing is needed
// if it isn't, we get the value from the components examples
Object exampleValue;
if(example.getValue().get$ref() != null){
exampleValue = actualComponentsExamples.get(exampleName).getValue();
LOGGER.debug("Unaliased example value from components examples: {}", exampleValue);
} else {
exampleValue = example.getValue().getValue();
LOGGER.debug("Retrieved example value directly from the api response example definition: {}", exampleValue);
}

exampleRepr.put("exampleName", exampleName);
exampleRepr.put("exampleValue", new ObjectMapper().writeValueAsString(exampleValue)
.replace("\"", "\\\""));

result.add(exampleRepr);
} catch (JsonProcessingException e) {
LOGGER.error("Failed to serialize example value", e);
throw new RuntimeException(e);
}
}

return result;
}

private static Map<String, Example> getAllExamples(OpenAPI openapi) {
return openapi.getComponents().getExamples();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ExampleObject;
{{/swagger2AnnotationLibrary}}
{{#swagger1AnnotationLibrary}}
import io.swagger.annotations.*;
Expand Down Expand Up @@ -171,7 +172,19 @@ public interface {{classname}} {
{{#responses}}
@ApiResponse(responseCode = {{#isDefault}}"default"{{/isDefault}}{{^isDefault}}"{{{code}}}"{{/isDefault}}, description = "{{{message}}}"{{#baseType}}, content = {
{{#produces}}
@Content(mediaType = "{{{mediaType}}}", {{#isArray}}array = @ArraySchema({{/isArray}}schema = @Schema(implementation = {{{baseType}}}.class){{#isArray}}){{/isArray}}){{^-last}},{{/-last}}
@Content(mediaType = "{{{mediaType}}}", {{#isArray}}array = @ArraySchema({{/isArray}}schema = @Schema(implementation = {{{baseType}}}.class){{#isArray}}){{/isArray}}{{^isJson}}{{#-last}}){{/-last}}{{^-last}}),{{/-last}}{{/isJson}}{{#isJson}}{{^examples.0}}{{#-last}}){{/-last}}{{^-last}}),{{/-last}}{{/examples.0}}{{#examples.0}}, examples = {
{{#examples}}
@ExampleObject(
name = "{{{exampleName}}}",
value = "{{{exampleValue}}}"
){{^-last}},{{/-last}}
{{/examples}}
{{#-last}}
})
{{/-last}}
{{^-last}}
}),
{{/-last}}{{/examples.0}}{{/isJson}}
{{/produces}}
}{{/baseType}}){{^-last}},{{/-last}}
{{/responses}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Objects;
import java.util.stream.Collectors;

import com.github.javaparser.ast.Node;
import org.assertj.core.api.ListAssert;
import org.assertj.core.util.CanIgnoreReturnValue;

Expand Down Expand Up @@ -72,4 +73,32 @@ private static boolean hasAttributes(final AnnotationExpr annotation, final Map<
private ACTUAL myself() {
return (ACTUAL) this;
}

public ACTUAL recursivelyContainsWithName(String name) {
super
.withFailMessage("Should have annotation with name: " + name)
.anyMatch(annotation -> containsSpecificAnnotationName(annotation, name));

return myself();
}

private boolean containsSpecificAnnotationName(Node node, String name) {
if (node == null || name == null)
return false;

if (node instanceof AnnotationExpr) {
AnnotationExpr annotation = (AnnotationExpr) node;

if(annotation.getNameAsString().equals(name))
return true;

}

for(Node child: node.getChildNodes()){
if(containsSpecificAnnotationName(child, name))
return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4427,4 +4427,28 @@ public void testMultiInheritanceParentRequiredParams_issue15796() throws IOExcep
.hasParameter("race").toConstructor()
;
}

@Test
public void testExampleAnnotationGeneration_issue17610() throws IOException {
final Map<String, File> generatedCodeFiles = generateFromContract("src/test/resources/3_0/spring/api-response-examples_issue17610.yaml", SPRING_BOOT);

JavaFileAssert.assertThat(generatedCodeFiles.get("DogsApi.java"))
.assertMethod("createDog")
.assertMethodAnnotations()
.recursivelyContainsWithName("ExampleObject");
}

@Test
public void testExampleAnnotationGeneration_issue17610_2() throws IOException {
final Map<String, File> generatedCodeFiles = generateFromContract("src/test/resources/3_0/spring/petstore_with_api_response_examples.yaml", SPRING_BOOT);

JavaFileAssert.assertThat(generatedCodeFiles.get("PetApi.java"))
.assertMethod("addPet")
.assertMethodAnnotations()
.recursivelyContainsWithName("ExampleObject")
.toMethod().toFileAssert()
.assertMethod("findPetsByStatus")
.assertMethodAnnotations()
.recursivelyContainsWithName("ExampleObject");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
openapi: 3.0.3
info:
title: No examples in annotation example API
description: No examples in annotation example API
version: 1.0.0
servers:
- url: 'https://localhost:8080'
paths:
/dogs:
post:
summary: Create a dog
operationId: createDog
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Dog'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Dog'
'400':
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
dog name length:
$ref: '#/components/examples/DogNameBiggerThan50Error'
dog name contains numbers:
$ref: '#/components/examples/DogNameContainsNumbersError'
dog age negative:
$ref: '#/components/examples/DogAgeNegativeError'

components:
schemas:
Dog:
type: object
properties:
name:
type: string
maxLength: 50
pattern: '^[a-zA-Z]+$'
x-pattern-message: Name must contain only letters
example: 'Rex'
age:
type: integer
format: int32
minimum: 0
example: 5
# NOTE: not picked up by the generator
# TODO: consider adding support for this
# example:
# name: 'Rex'
# age: 5
Error:
type: object
properties:
code:
type: integer
format: int32
message:
type: string
examples:
DogNameBiggerThan50Error:
value:
code: 400
message: name size must be between 0 and 50
DogNameContainsNumbersError:
value:
code: 400
message: Name must contain only letters
DogAgeNegativeError:
value:
code: 400
message: age must be greater than or equal to 0
Loading
Loading