From 210ef955aee7827ada8b506502ab10a03251ee03 Mon Sep 17 00:00:00 2001 From: Frank Levine <frank.levine@gmail.com> Date: Sun, 31 Jul 2022 17:17:23 -0400 Subject: [PATCH] [python experimental] Issue 13043: fixes toExampleValueRecursive stackoverflow Fixes an issue in python-experimental that was causing a stackoverflow error. * Fixed by adding composed schemas to the list of 'includedSchemas' * Fixed an additional issue that was causing a schema to be added to the 'includedSchemas' list * Added a unit test with a minimal GeoJson spec to confirm results --- .../PythonExperimentalClientCodegen.java | 30 ++++++- .../python/PythonExperimentalClientTest.java | 27 ++++++ .../3_0/issue_13043_recursive_model.yaml | 83 +++++++++++++++++++ ...e_13043_recursive_model_expected_value.txt | 4 + 4 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/issue_13043_recursive_model.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/issue_13043_recursive_model_expected_value.txt diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonExperimentalClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonExperimentalClientCodegen.java index dac8c99bc56..d17480ee029 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonExperimentalClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonExperimentalClientCodegen.java @@ -1679,6 +1679,23 @@ 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 * @@ -1753,9 +1770,13 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen { TODO generate examples for some of these use cases in the future like only oneOf without a discriminator */ + if (cycleFound) { + return ""; + } Boolean hasProperties = (schema.getProperties() != null && !schema.getProperties().isEmpty()); CodegenDiscriminator disc = createDiscriminator(modelName, schema, openAPI); if (ModelUtils.isComposedSchema(schema)) { + addSchemaToIncludedSchemaList(includedSchemas, schema); // complex composed object type schemas not yet handled and the code returns early if (hasProperties) { // what if this composed schema defined properties + allOf? @@ -1920,7 +1941,7 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen { ArraySchema arrayschema = (ArraySchema) schema; Schema itemSchema = arrayschema.getItems(); String itemModelName = getModelName(itemSchema); - includedSchemas.add(schema); + addSchemaToIncludedSchemaList(includedSchemas, schema); String itemExample = toExampleValueRecursive(itemModelName, itemSchema, objExample, indentationLevel + 1, "", exampleLine + 1, includedSchemas); if (StringUtils.isEmpty(itemExample) || cycleFound) { return fullPrefix + "[]" + closeChars; @@ -1997,7 +2018,7 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen { addPropPrefix = ensureQuotes(key) + ": "; } String addPropsModelName = getModelName(addPropsSchema); - includedSchemas.add(schema); + addSchemaToIncludedSchemaList(includedSchemas, schema); example = fullPrefix + "\n" + toExampleValueRecursive(addPropsModelName, addPropsSchema, addPropsExample, indentationLevel + 1, addPropPrefix, exampleLine + 1, includedSchemas) + ",\n" + closingIndentation + closeChars; } else { example = fullPrefix + closeChars; @@ -2020,6 +2041,9 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen { } String example = fullPrefix + "\n"; + + addSchemaToIncludedSchemaList(includedSchemas, schema); + for (Map.Entry<String, Schema> entry : requiredAndOptionalProps.entrySet()) { String propName = entry.getKey(); Schema propSchema = entry.getValue(); @@ -2033,7 +2057,7 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen { propModelName = getModelName(propSchema); propExample = exampleFromStringOrArraySchema(propSchema, null, propName); } - includedSchemas.add(schema); + example += toExampleValueRecursive(propModelName, propSchema, propExample, indentationLevel + 1, propName + "=", exampleLine + 1, includedSchemas) + ",\n"; } // TODO handle additionalProperties also diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonExperimentalClientTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonExperimentalClientTest.java index 1c1c69f8307..9337f4fe652 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonExperimentalClientTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonExperimentalClientTest.java @@ -18,12 +18,15 @@ package org.openapitools.codegen.python; 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 org.openapitools.codegen.*; import org.openapitools.codegen.languages.PythonExperimentalClientCodegen; +import org.openapitools.codegen.utils.ModelUtils; import org.testng.Assert; import org.testng.annotations.Test; +import java.io.IOException; import java.nio.charset.StandardCharsets; @SuppressWarnings("static-method") @@ -71,4 +74,28 @@ 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"); + final PythonExperimentalClientCodegen codegen = new PythonExperimentalClientCodegen(); + codegen.setOpenAPI(openAPI); + + final Operation operation = openAPI.getPaths().get("/geojson").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); + + String expectedValue = Resources.toString( + Resources.getResource("3_0/issue_13043_recursive_model_expected_value.txt"), + StandardCharsets.UTF_8); + expectedValue = expectedValue.replaceAll("\\r\\n", "\n"); + Assert.assertEquals(exampleValue.trim(), expectedValue.trim()); + + } + } diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_13043_recursive_model.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_13043_recursive_model.yaml new file mode 100644 index 00000000000..6d833b29211 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue_13043_recursive_model.yaml @@ -0,0 +1,83 @@ +openapi: 3.0.0 +info: + version: 01.01.00 + title: APITest API documentation. + termsOfService: http://api.apitest.com/party/tos/ +servers: + - url: https://api.apitest.com/v1 +paths: + /geojson: + post: + summary: Add a GeoJson Object + operationId: post-geojson + 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/GeoJsonGeometry' + parameters: [] +components: + schemas: + GeoJsonGeometry: + title: GeoJsonGeometry + description: GeoJSON geometry + oneOf: + - $ref: '#/components/schemas/Point' + - $ref: '#/components/schemas/GeometryCollection' + discriminator: + propertyName: type + mapping: + Point: '#/components/schemas/Point' + GeometryCollection: '#/components/schemas/GeometryCollection' + externalDocs: + url: http://geojson.org/geojson-spec.html#geometry-objects + Point: + title: Point + type: object + description: GeoJSON geometry + externalDocs: + url: http://geojson.org/geojson-spec.html#id2 + properties: + coordinates: + title: Point3D + type: array + description: Point in 3D space + externalDocs: + url: http://geojson.org/geojson-spec.html#id2 + minItems: 2 + maxItems: 3 + items: + type: number + format: double + type: + type: string + default: Point + required: + - type + GeometryCollection: + title: GeometryCollection + type: object + description: GeoJSon geometry collection + required: + - type + - geometries + externalDocs: + url: http://geojson.org/geojson-spec.html#geometrycollection + properties: + type: + type: string + default: GeometryCollection + geometries: + type: array + items: + $ref: '#/components/schemas/GeoJsonGeometry' diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_13043_recursive_model_expected_value.txt b/modules/openapi-generator/src/test/resources/3_0/issue_13043_recursive_model_expected_value.txt new file mode 100644 index 00000000000..137db22fa97 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue_13043_recursive_model_expected_value.txt @@ -0,0 +1,4 @@ +GeoJsonGeometry( + type="GeometryCollection", + geometries=[], + ) \ No newline at end of file -- GitLab