Issue 13043: Improved handling of recursive schemas (#13109)

* Issue 13043: Improved handling of recursive schemas

Converted Exception to a warning when a Schema is doubly added to the includedSchemas list
Broke recursion in a more reasonable place when generating example values

* Addressed comments from PR
This commit is contained in:
fbl100
2022-08-08 11:39:17 -04:00
committed by GitHub
parent 032e1a42d6
commit d3dd676960
4 changed files with 86 additions and 34 deletions

View File

@@ -1679,23 +1679,6 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
return null;
}
/**
* Function to isolate the call to add schemas to the includedSchemas list that will detect when
* a schema is added multiple times.
* @param includedSchemas List of includedSchemas from toExampleValueRecursive
* @param schema the schema to add
*/
private void addSchemaToIncludedSchemaList(List<Schema> includedSchemas, Schema schema) {
// if the schema is in the list, then throw an exception.
// this should never happen
if(includedSchemas.contains(schema)) {
throw new RuntimeException("Attempted to add a schema to the includedSchemas list twice");
}
includedSchemas.add(schema);
}
/***
* Recursively generates string examples for schemas
*
@@ -1776,7 +1759,10 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
Boolean hasProperties = (schema.getProperties() != null && !schema.getProperties().isEmpty());
CodegenDiscriminator disc = createDiscriminator(modelName, schema, openAPI);
if (ModelUtils.isComposedSchema(schema)) {
addSchemaToIncludedSchemaList(includedSchemas, schema);
if(includedSchemas.contains(schema)) {
return "";
}
includedSchemas.add(schema);
// complex composed object type schemas not yet handled and the code returns early
if (hasProperties) {
// what if this composed schema defined properties + allOf?
@@ -1941,7 +1927,10 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
ArraySchema arrayschema = (ArraySchema) schema;
Schema itemSchema = arrayschema.getItems();
String itemModelName = getModelName(itemSchema);
addSchemaToIncludedSchemaList(includedSchemas, schema);
if(includedSchemas.contains(schema)) {
return "";
}
includedSchemas.add(schema);
String itemExample = toExampleValueRecursive(itemModelName, itemSchema, objExample, indentationLevel + 1, "", exampleLine + 1, includedSchemas);
if (StringUtils.isEmpty(itemExample) || cycleFound) {
return fullPrefix + "[]" + closeChars;
@@ -2018,7 +2007,11 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
addPropPrefix = ensureQuotes(key) + ": ";
}
String addPropsModelName = getModelName(addPropsSchema);
addSchemaToIncludedSchemaList(includedSchemas, schema);
if(includedSchemas.contains(schema)) {
return "";
}
includedSchemas.add(schema);
example = fullPrefix + "\n" + toExampleValueRecursive(addPropsModelName, addPropsSchema, addPropsExample, indentationLevel + 1, addPropPrefix, exampleLine + 1, includedSchemas) + ",\n" + closingIndentation + closeChars;
} else {
example = fullPrefix + closeChars;
@@ -2035,14 +2028,18 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
}
private String exampleForObjectModel(Schema schema, String fullPrefix, String closeChars, CodegenProperty discProp, int indentationLevel, int exampleLine, String closingIndentation, List<Schema> includedSchemas) {
Map<String, Schema> requiredAndOptionalProps = schema.getProperties();
if (requiredAndOptionalProps == null || requiredAndOptionalProps.isEmpty()) {
return fullPrefix + closeChars;
}
String example = fullPrefix + "\n";
if(includedSchemas.contains(schema)) {
return "";
}
includedSchemas.add(schema);
addSchemaToIncludedSchemaList(includedSchemas, schema);
String example = fullPrefix + "\n";
for (Map.Entry<String, Schema> entry : requiredAndOptionalProps.entrySet()) {
String propName = entry.getKey();
@@ -2055,14 +2052,25 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
propExample = discProp.example;
} else {
propModelName = getModelName(propSchema);
propExample = exampleFromStringOrArraySchema(propSchema, null, propName);
propExample = exampleFromStringOrArraySchema(
propSchema,
null,
propName);
}
example += toExampleValueRecursive(propModelName, propSchema, propExample, indentationLevel + 1, propName + "=", exampleLine + 1, includedSchemas) + ",\n";
example += toExampleValueRecursive(propModelName,
propSchema,
propExample,
indentationLevel + 1,
propName + "=",
exampleLine + 1,
includedSchemas) + ",\n";
}
// TODO handle additionalProperties also
example += closingIndentation + closeChars;
return example;
}
private Object exampleFromStringOrArraySchema(Schema sc, Object currentExample, String propName) {

View File

@@ -20,6 +20,7 @@ import com.google.common.io.Resources;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.*;
import java.io.PrintWriter;
import org.openapitools.codegen.*;
import org.openapitools.codegen.languages.PythonExperimentalClientCodegen;
import org.openapitools.codegen.utils.ModelUtils;
@@ -28,6 +29,7 @@ import org.testng.annotations.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.testng.reporters.jq.Model;
@SuppressWarnings("static-method")
public class PythonExperimentalClientTest {
@@ -74,24 +76,42 @@ public class PythonExperimentalClientTest {
Assert.assertFalse(openAPI.getExtensions().containsValue("x-original-swagger-version"));
}
@Test(description = "tests GeoJson Examples")
public void testRecursiveGeoJsonExample() throws IOException {
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/issue_13043_recursive_model.yaml");
@Test(description = "tests GeoJson Example for GeoJsonGeometry")
public void testRecursiveGeoJsonExampleWhenTypeIsGeoJsonGeometry() throws IOException {
testEndpointExampleValue("/geojson",
"src/test/resources/3_0/issue_13043_recursive_model.yaml",
"3_0/issue_13043_recursive_model_expected_value.txt");
}
@Test(description = "tests GeoJson Example for GeometryCollection")
public void testRecursiveGeoJsonExampleWhenTypeIsGeometryCollection() throws IOException {
testEndpointExampleValue("/geojson_geometry_collection",
"src/test/resources/3_0/issue_13043_recursive_model.yaml",
"3_0/issue_13043_geometry_collection_expected_value.txt");
}
private void testEndpointExampleValue(String endpoint, String specFilePath, String expectedAnswerPath) throws IOException {
final OpenAPI openAPI = TestUtils.parseFlattenSpec(specFilePath);
final PythonExperimentalClientCodegen codegen = new PythonExperimentalClientCodegen();
codegen.setOpenAPI(openAPI);
final Operation operation = openAPI.getPaths().get("/geojson").getPost();
final Operation operation = openAPI.getPaths().get(endpoint).getPost();
Schema schema = ModelUtils.getSchemaFromRequestBody(operation.getRequestBody());
String exampleValue = codegen.toExampleValue(schema, null);
// uncomment if you need to regenerate the expected value
// PrintWriter printWriter = new PrintWriter("src/test/resources/3_0/issue_8052_recursive_model_expected_value.txt");
// printWriter.write(exampleValue);
// printWriter.close();
// org.junit.Assert.assertTrue(false);
// uncomment if you need to regenerate the expected value
// PrintWriter printWriter = new PrintWriter("src/test/resources/" + expectedAnswerPath);
// printWriter.write(exampleValue);
// printWriter.close();
// org.junit.Assert.assertTrue(false);
String expectedValue = Resources.toString(
Resources.getResource("3_0/issue_13043_recursive_model_expected_value.txt"),
Resources.getResource(expectedAnswerPath),
StandardCharsets.UTF_8);
expectedValue = expectedValue.replaceAll("\\r\\n", "\n");
Assert.assertEquals(exampleValue.trim(), expectedValue.trim());

View File

@@ -0,0 +1,4 @@
GeometryCollection(
type="GeometryCollection",
geometries=[],
)

View File

@@ -26,6 +26,26 @@ paths:
schema:
$ref: '#/components/schemas/GeoJsonGeometry'
parameters: []
/geojson_geometry_collection:
post:
summary: Add a GeoJson GeometryCollection Object
operationId: post-geojson-geometry-collection
responses:
'201':
description: Created
content:
application/json:
schema:
type: string
description: GeoJson ID
'400':
description: Bad Request
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GeometryCollection'
parameters: []
components:
schemas:
GeoJsonGeometry: