diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java index 2a84a4fb6cd..534c5248293 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java @@ -16,6 +16,7 @@ package org.openapitools.codegen.languages; +import com.google.common.collect.Sets; import io.swagger.v3.core.util.Json; import io.swagger.v3.oas.models.media.*; import io.swagger.v3.oas.models.media.ArraySchema; @@ -879,7 +880,7 @@ public class PythonClientCodegen extends PythonLegacyClientCodegen { public String toExampleValue(Schema schema, Object objExample) { String modelName = getModelName(schema); - return toExampleValueRecursive(modelName, schema, objExample, 1, "", 0); + return toExampleValueRecursive(modelName, schema, objExample, 1, "", 0, Sets.newHashSet()); } private Boolean simpleStringSchema(Schema schema) { @@ -925,9 +926,12 @@ public class PythonClientCodegen extends PythonLegacyClientCodegen { * ModelName( line 0 * some_property='some_property_example' line 1 * ) line 2 + * @param seenSchemas This set contains all the schemas passed into the recursive function. It is used to check + * if a schema was already passed into the function and breaks the infinite recursive loop. The + * only schemas that are not added are ones that contain $ref != null * @return the string example */ - private String toExampleValueRecursive(String modelName, Schema schema, Object objExample, int indentationLevel, String prefix, Integer exampleLine) { + private String toExampleValueRecursive(String modelName, Schema schema, Object objExample, int indentationLevel, String prefix, Integer exampleLine, Set seenSchemas) { final String indentionConst = " "; String currentIndentation = ""; String closingIndentation = ""; @@ -951,6 +955,27 @@ public class PythonClientCodegen extends PythonLegacyClientCodegen { if (objExample != null) { example = objExample.toString(); } + // checks if the current schema has already been passed in. If so, breaks the current recursive pass + if (seenSchemas.contains(schema)){ + if (modelName != null) { + return fullPrefix + modelName + closeChars; + } else { + // this is a recursive schema + // need to add a reasonable example to avoid + // infinite recursion + if(ModelUtils.isNullable(schema)) { + // if the schema is nullable, then 'None' is a valid value + return fullPrefix + "None" + closeChars; + } else if(ModelUtils.isArraySchema(schema)) { + // the schema is an array, add an empty array + return fullPrefix + "[]" + closeChars; + } else { + // the schema is an object, make an empty object + return fullPrefix + "{}" + closeChars; + } + } + } + if (null != schema.get$ref()) { Map allDefinitions = ModelUtils.getSchemas(this.openAPI); String ref = ModelUtils.getSimpleRef(schema.get$ref()); @@ -960,7 +985,7 @@ public class PythonClientCodegen extends PythonLegacyClientCodegen { return fullPrefix + "None" + closeChars; } String refModelName = getModelName(schema); - return toExampleValueRecursive(refModelName, refSchema, objExample, indentationLevel, prefix, exampleLine); + return toExampleValueRecursive(refModelName, refSchema, objExample, indentationLevel, prefix, exampleLine, seenSchemas); } else if (ModelUtils.isNullType(schema) || isAnyTypeSchema(schema)) { // The 'null' type is allowed in OAS 3.1 and above. It is not supported by OAS 3.0.x, // though this tooling supports it. @@ -1058,7 +1083,8 @@ public class PythonClientCodegen extends PythonLegacyClientCodegen { ArraySchema arrayschema = (ArraySchema) schema; Schema itemSchema = arrayschema.getItems(); String itemModelName = getModelName(itemSchema); - example = fullPrefix + "[" + "\n" + toExampleValueRecursive(itemModelName, itemSchema, objExample, indentationLevel + 1, "", exampleLine + 1) + ",\n" + closingIndentation + "]" + closeChars; + seenSchemas.add(schema); + example = fullPrefix + "[" + "\n" + toExampleValueRecursive(itemModelName, itemSchema, objExample, indentationLevel + 1, "", exampleLine + 1, seenSchemas) + ",\n" + closingIndentation + "]" + closeChars; return example; } else if (ModelUtils.isMapSchema(schema)) { if (modelName == null) { @@ -1080,7 +1106,8 @@ public class PythonClientCodegen extends PythonLegacyClientCodegen { addPropPrefix = ensureQuotes(key) + ": "; } String addPropsModelName = getModelName(addPropsSchema); - example = fullPrefix + "\n" + toExampleValueRecursive(addPropsModelName, addPropsSchema, addPropsExample, indentationLevel + 1, addPropPrefix, exampleLine + 1) + ",\n" + closingIndentation + closeChars; + seenSchemas.add(schema); + example = fullPrefix + "\n" + toExampleValueRecursive(addPropsModelName, addPropsSchema, addPropsExample, indentationLevel + 1, addPropPrefix, exampleLine + 1, seenSchemas) + ",\n" + closingIndentation + closeChars; } else { example = fullPrefix + closeChars; } @@ -1103,7 +1130,12 @@ public class PythonClientCodegen extends PythonLegacyClientCodegen { return fullPrefix + closeChars; } } - return exampleForObjectModel(schema, fullPrefix, closeChars, null, indentationLevel, exampleLine, closingIndentation); + // Adds schema to seenSchemas before running example model function. romoves schema after running + // the function. It also doesnt keep track of any schemas within the ObjectModel. + seenSchemas.add(schema); + String exampleForObjectModel = exampleForObjectModel(schema, fullPrefix, closeChars, null, indentationLevel, exampleLine, closingIndentation, seenSchemas); + seenSchemas.remove(schema); + return exampleForObjectModel; } else if (ModelUtils.isComposedSchema(schema)) { // TODO add examples for composed schema models without discriminators @@ -1117,7 +1149,12 @@ public class PythonClientCodegen extends PythonLegacyClientCodegen { CodegenProperty cp = new CodegenProperty(); cp.setName(disc.getPropertyName()); cp.setExample(discPropNameValue); - return exampleForObjectModel(modelSchema, fullPrefix, closeChars, cp, indentationLevel, exampleLine, closingIndentation); + // Adds schema to seenSchemas before running example model function. romoves schema after running + // the function. It also doesnt keep track of any schemas within the ObjectModel. + seenSchemas.add(modelSchema); + String exampleForObjectModel = exampleForObjectModel(modelSchema, fullPrefix, closeChars, cp, indentationLevel, exampleLine, closingIndentation, seenSchemas); + seenSchemas.remove(modelSchema); + return exampleForObjectModel; } else { return fullPrefix + closeChars; } @@ -1130,7 +1167,7 @@ public class PythonClientCodegen extends PythonLegacyClientCodegen { return example; } - private String exampleForObjectModel(Schema schema, String fullPrefix, String closeChars, CodegenProperty discProp, int indentationLevel, int exampleLine, String closingIndentation) { + private String exampleForObjectModel(Schema schema, String fullPrefix, String closeChars, CodegenProperty discProp, int indentationLevel, int exampleLine, String closingIndentation, Set seenSchemas) { Map requiredAndOptionalProps = schema.getProperties(); if (requiredAndOptionalProps == null || requiredAndOptionalProps.isEmpty()) { return fullPrefix + closeChars; @@ -1150,7 +1187,7 @@ public class PythonClientCodegen extends PythonLegacyClientCodegen { propModelName = getModelName(propSchema); propExample = exampleFromStringOrArraySchema(propSchema, null, propName); } - example += toExampleValueRecursive(propModelName, propSchema, propExample, indentationLevel + 1, propName + "=", exampleLine + 1) + ",\n"; + example += toExampleValueRecursive(propModelName, propSchema, propExample, indentationLevel + 1, propName + "=", exampleLine + 1, seenSchemas) + ",\n"; } // TODO handle additionalProperties also example += closingIndentation + closeChars; diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientTest.java index 683d4aa4b02..6c46b6b6117 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientTest.java @@ -15,6 +15,18 @@ */ package org.openapitools.codegen.python; +import com.google.common.io.Resources; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.parameters.RequestBody; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import javax.validation.constraints.AssertTrue; +import org.apache.commons.io.IOUtils; import org.openapitools.codegen.config.CodegenConfigurator; import com.google.common.collect.Sets; @@ -33,6 +45,7 @@ import org.openapitools.codegen.*; import org.openapitools.codegen.languages.PythonClientCodegen; import org.openapitools.codegen.utils.ModelUtils; import org.testng.Assert; +import org.testng.TestNGAntTask.Mode; import org.testng.annotations.Test; @SuppressWarnings("static-method") @@ -425,4 +438,30 @@ public class PythonClientTest { final CodegenModel model = codegen.fromModel(modelName, modelSchema); Assert.assertEquals((int) model.getMinProperties(), 1); } + + @Test(description = "tests RecursiveToExample") + public void testRecursiveToExample() throws IOException { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/issue_8052_recursive_model.yaml"); + final PythonClientCodegen codegen = new PythonClientCodegen(); + 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_8052_recursive_model_expected_value.txt"), + StandardCharsets.UTF_8); + + + Assert.assertEquals(expectedValue.trim(), exampleValue.trim()); + + } + } diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_8052_recursive_model.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_8052_recursive_model.yaml new file mode 100644 index 00000000000..6d833b29211 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue_8052_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_8052_recursive_model_expected_value.txt b/modules/openapi-generator/src/test/resources/3_0/issue_8052_recursive_model_expected_value.txt new file mode 100644 index 00000000000..98ef62651fb --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue_8052_recursive_model_expected_value.txt @@ -0,0 +1,9 @@ +GeoJsonGeometry( + type="GeometryCollection", + geometries=[ + GeoJsonGeometry( + type="GeometryCollection", + geometries=[], + ), + ], + ) \ No newline at end of file