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