Skip to content

Commit

Permalink
fix: ExampleGenerator correctly generates allOf composed schemas (#17499
Browse files Browse the repository at this point in the history
)

* fix: ExampleGenerator correctly generates allOf composed schemas

Changes the previous behavior of generating `null` examples for allOf composed schemas.

Fixes #17497

* fix: ExampleGenerator correctly generates anyOf and oneOf composed schemas

Changes the previous behavior of generating `null` examples for anyOf and oneOf composed schemas.

To generate a oneOf/anyOf example, we generate the example using the first valid schema available. In case of a $ref, we use the first valid reference.

Fixes #17497
  • Loading branch information
acouvreur committed Jan 10, 2024
1 parent 8bab0ce commit dd5c7e3
Show file tree
Hide file tree
Showing 30 changed files with 288 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -351,15 +351,57 @@ private Object resolveModelToExample(String name, String mediaType, Schema schem
return schema.getExample();
} else if (schema.getProperties() != null) {
LOGGER.debug("Creating example from model values");
for (Object propertyName : schema.getProperties().keySet()) {
Schema property = (Schema) schema.getProperties().get(propertyName.toString());
values.put(propertyName.toString(), resolvePropertyToExample(propertyName.toString(), mediaType, property, processedModels));
traverseSchemaProperties(mediaType, schema, processedModels, values);
schema.setExample(values);
return schema.getExample();
} else if (ModelUtils.isAllOf(schema) || ModelUtils.isAllOfWithProperties(schema)) {
LOGGER.debug("Resolving allOf model '{}' to example", name);
List<Schema> interfaces = schema.getAllOf();
for (Schema composed : interfaces) {
traverseSchemaProperties(mediaType, composed, processedModels, values);
if (composed.get$ref() != null) {
String ref = ModelUtils.getSimpleRef(composed.get$ref());
Schema resolved = ModelUtils.getSchema(openAPI, ref);
if (resolved != null) {
traverseSchemaProperties(mediaType, resolved, processedModels, values);
}
}
}
schema.setExample(values);
return schema.getExample();
} else if (ModelUtils.isAnyOf(schema) || ModelUtils.isOneOf(schema)) {
LOGGER.debug("Resolving anyOf/oneOf model '{}' using the first schema to example", name);
Optional<Schema> found = ModelUtils.getInterfaces(schema)
.stream()
.filter(this::hasValidRef)
.findFirst();

if (found.isEmpty()) {
return null;
}
return resolvePropertyToExample(name, mediaType, found.get(), processedModels);
} else {
// TODO log an error message as the model does not have any properties
return null;
}
}

private void traverseSchemaProperties(String mediaType, Schema schema, Set<String> processedModels, Map<String, Object> values) {
if (schema.getProperties() != null) {
for (Object propertyName : schema.getProperties().keySet()) {
Schema property = (Schema) schema.getProperties().get(propertyName.toString());
values.put(propertyName.toString(), resolvePropertyToExample(propertyName.toString(), mediaType, property, processedModels));
}
}
}

private boolean hasValidRef(Schema schema) {
if (schema.get$ref() != null) {
String ref = ModelUtils.getSimpleRef(schema.get$ref());
Schema resolved = ModelUtils.getSchema(openAPI, ref);
return resolved != null;
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,91 @@ public void generateFromResponseSchemaWithModel() {
assertEquals(String.format(Locale.ROOT, "{%n \"example_schema_property\" : \"example schema property value\"%n}"), examples.get(0).get("example"));
assertEquals("200", examples.get(0).get("statusCode"));
}

@Test
public void generateFromResponseSchemaWithAllOfComposedModel() {
OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/example_generator_test.yaml");

new InlineModelResolver().flatten(openAPI);

ExampleGenerator exampleGenerator = new ExampleGenerator(openAPI.getComponents().getSchemas(), openAPI);
Set<String> mediaTypeKeys = new TreeSet<>();
mediaTypeKeys.add("application/json");
List<Map<String, String>> examples = exampleGenerator.generateFromResponseSchema(
"200",
openAPI
.getPaths()
.get("/generate_from_response_schema_with_allOf_composed_model")
.getGet()
.getResponses()
.get("200")
.getContent()
.get("application/json")
.getSchema(),
mediaTypeKeys
);

assertEquals(1, examples.size());
assertEquals("application/json", examples.get(0).get("contentType"));
assertEquals(String.format(Locale.ROOT, "{%n \"example_schema_property_composed\" : \"example schema property value composed\",%n \"example_schema_property\" : \"example schema property value\"%n}"), examples.get(0).get("example"));
assertEquals("200", examples.get(0).get("statusCode"));
}

@Test
public void generateFromResponseSchemaWithOneOfComposedModel() {
OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/example_generator_test.yaml");

new InlineModelResolver().flatten(openAPI);

ExampleGenerator exampleGenerator = new ExampleGenerator(openAPI.getComponents().getSchemas(), openAPI);
Set<String> mediaTypeKeys = new TreeSet<>();
mediaTypeKeys.add("application/json");
List<Map<String, String>> examples = exampleGenerator.generateFromResponseSchema(
"200",
openAPI
.getPaths()
.get("/generate_from_response_schema_with_oneOf_composed_model")
.getGet()
.getResponses()
.get("200")
.getContent()
.get("application/json")
.getSchema(),
mediaTypeKeys
);

assertEquals(1, examples.size());
assertEquals("application/json", examples.get(0).get("contentType"));
assertEquals(String.format(Locale.ROOT, "{%n \"example_schema_property\" : \"example schema property value\"%n}"), examples.get(0).get("example"));
assertEquals("200", examples.get(0).get("statusCode"));
}

@Test
public void generateFromResponseSchemaWithAnyOfComposedModel() {
OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/example_generator_test.yaml");

new InlineModelResolver().flatten(openAPI);

ExampleGenerator exampleGenerator = new ExampleGenerator(openAPI.getComponents().getSchemas(), openAPI);
Set<String> mediaTypeKeys = new TreeSet<>();
mediaTypeKeys.add("application/json");
List<Map<String, String>> examples = exampleGenerator.generateFromResponseSchema(
"200",
openAPI
.getPaths()
.get("/generate_from_response_schema_with_anyOf_composed_model")
.getGet()
.getResponses()
.get("200")
.getContent()
.get("application/json")
.getSchema(),
mediaTypeKeys
);

assertEquals(1, examples.size());
assertEquals("application/json", examples.get(0).get("contentType"));
assertEquals(String.format(Locale.ROOT, "{%n \"example_schema_property\" : \"example schema property value\"%n}"), examples.get(0).get("example"));
assertEquals("200", examples.get(0).get("statusCode"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,44 @@ paths:
example: primitive types example value
/generate_from_response_schema_with_model:
get:
operationId: generateFromResponseSchemaWithArrayOfPrimitiveTypes
operationId: generateFromResponseSchemaWithModel
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ExampleSchema'
/generate_from_response_schema_with_allOf_composed_model:
get:
operationId: generateFromResponseSchemaWithAllOfModel
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ExampleAllOfSchema'
/generate_from_response_schema_with_anyOf_composed_model:
get:
operationId: generateFromResponseSchemaWithAnyOfModel
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ExampleAnyOfSchema'
/generate_from_response_schema_with_oneOf_composed_model:
get:
operationId: generateFromResponseSchemaWithOneOfModel
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ExampleOneOfSchema'
components:
schemas:
StringSchema:
Expand All @@ -72,3 +102,30 @@ components:
example_schema_property:
type: string
example: example schema property value
ExampleAllOfSchema:
type: object
allOf:
- $ref: '#/components/schemas/ExampleSchema'
- type: object
properties:
example_schema_property_composed:
type: string
example: example schema property value composed
ExampleAnyOfSchema:
type: object
anyOf:
- $ref: '#/components/schemas/ExampleSchema'
- type: object
properties:
example_schema_property_composed:
type: string
example: example schema property value composed
ExampleOneOfSchema:
type: object
oneOf:
- $ref: '#/components/schemas/ExampleSchema'
- type: object
properties:
example_schema_property_composed:
type: string
example: example schema property value composed
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean
Expand Down
4 changes: 4 additions & 0 deletions samples/client/petstore/java/feign/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean
Expand Down
2 changes: 1 addition & 1 deletion samples/client/petstore/java/okhttp-gson/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2411,7 +2411,7 @@ components:
description: Value object
example:
name: variable_1
value: null
value: Scalar
properties:
name:
example: variable_1
Expand Down
4 changes: 4 additions & 0 deletions samples/client/petstore/java/resteasy/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean
Expand Down
4 changes: 4 additions & 0 deletions samples/client/petstore/java/resttemplate/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean
Expand Down
4 changes: 4 additions & 0 deletions samples/client/petstore/java/vertx/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean
Expand Down
4 changes: 4 additions & 0 deletions samples/client/petstore/java/webclient/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,7 @@ components:
\ characters. The sanitization rules should make it possible to generate a\
\ language-specific classname with allowed characters in that programming\
\ language."
example:
prop2: prop2
objectType: objectType

Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ default ResponseEntity<Bar> createBar(
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "{ \"id\" : \"id\", \"fooPropB\" : \"fooPropB\", \"barPropA\" : \"barPropA\" }";
String exampleString = "{ \"foo\" : { \"fooPropA\" : \"fooPropA\", \"fooPropB\" : \"fooPropB\" }, \"id\" : \"id\", \"fooPropB\" : \"fooPropB\", \"barPropA\" : \"barPropA\" }";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ default ResponseEntity<FooRefOrValue> createFoo(
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "null";
String exampleString = "{ \"fooPropA\" : \"fooPropA\", \"fooPropB\" : \"fooPropB\" }";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
Expand Down Expand Up @@ -109,7 +109,7 @@ default ResponseEntity<List<FooRefOrValue>> getAllFoos(
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json;charset=utf-8"))) {
String exampleString = "[ null, null ]";
String exampleString = "[ { \"fooPropA\" : \"fooPropA\", \"fooPropB\" : \"fooPropB\" }, { \"fooPropA\" : \"fooPropA\", \"fooPropB\" : \"fooPropB\" } ]";
ApiUtil.setExampleResponse(request, "application/json;charset=utf-8", exampleString);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ components:
allOf:
- $ref: '#/components/schemas/Entity'
example:
foo: null
foo:
fooPropA: fooPropA
fooPropB: fooPropB
id: id
fooPropB: fooPropB
barPropA: barPropA
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1902,6 +1902,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean
Expand Down
Loading

0 comments on commit dd5c7e3

Please sign in to comment.