Skip to content

Commit fd02bc3

Browse files
fbl100Frank Levine
andauthored
Fixes issue 8052: Stackoverflow in toExampleValue() for python client (#8054)
Added a Set<String> in toExampleValueRecursive() to keep track of which models we have generated to avoid an infinite recursion for recursive models. An example of a recursive model would be a GeoJson GeometryCollection. Co-authored-by: Frank Levine <[email protected]>
1 parent 9889e5d commit fd02bc3

4 files changed

Lines changed: 177 additions & 9 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.openapitools.codegen.languages;
1818

19+
import com.google.common.collect.Sets;
1920
import io.swagger.v3.core.util.Json;
2021
import io.swagger.v3.oas.models.media.*;
2122
import io.swagger.v3.oas.models.media.ArraySchema;
@@ -879,7 +880,7 @@ private String ensureQuotes(String in) {
879880

880881
public String toExampleValue(Schema schema, Object objExample) {
881882
String modelName = getModelName(schema);
882-
return toExampleValueRecursive(modelName, schema, objExample, 1, "", 0);
883+
return toExampleValueRecursive(modelName, schema, objExample, 1, "", 0, Sets.newHashSet());
883884
}
884885

885886
private Boolean simpleStringSchema(Schema schema) {
@@ -925,9 +926,12 @@ private MappedModel getDiscriminatorMappedModel(CodegenDiscriminator disc) {
925926
* ModelName( line 0
926927
* some_property='some_property_example' line 1
927928
* ) line 2
929+
* @param seenSchemas This set contains all the schemas passed into the recursive function. It is used to check
930+
* if a schema was already passed into the function and breaks the infinite recursive loop. The
931+
* only schemas that are not added are ones that contain $ref != null
928932
* @return the string example
929933
*/
930-
private String toExampleValueRecursive(String modelName, Schema schema, Object objExample, int indentationLevel, String prefix, Integer exampleLine) {
934+
private String toExampleValueRecursive(String modelName, Schema schema, Object objExample, int indentationLevel, String prefix, Integer exampleLine, Set<Schema> seenSchemas) {
931935
final String indentionConst = " ";
932936
String currentIndentation = "";
933937
String closingIndentation = "";
@@ -951,6 +955,27 @@ private String toExampleValueRecursive(String modelName, Schema schema, Object o
951955
if (objExample != null) {
952956
example = objExample.toString();
953957
}
958+
// checks if the current schema has already been passed in. If so, breaks the current recursive pass
959+
if (seenSchemas.contains(schema)){
960+
if (modelName != null) {
961+
return fullPrefix + modelName + closeChars;
962+
} else {
963+
// this is a recursive schema
964+
// need to add a reasonable example to avoid
965+
// infinite recursion
966+
if(ModelUtils.isNullable(schema)) {
967+
// if the schema is nullable, then 'None' is a valid value
968+
return fullPrefix + "None" + closeChars;
969+
} else if(ModelUtils.isArraySchema(schema)) {
970+
// the schema is an array, add an empty array
971+
return fullPrefix + "[]" + closeChars;
972+
} else {
973+
// the schema is an object, make an empty object
974+
return fullPrefix + "{}" + closeChars;
975+
}
976+
}
977+
}
978+
954979
if (null != schema.get$ref()) {
955980
Map<String, Schema> allDefinitions = ModelUtils.getSchemas(this.openAPI);
956981
String ref = ModelUtils.getSimpleRef(schema.get$ref());
@@ -960,7 +985,7 @@ private String toExampleValueRecursive(String modelName, Schema schema, Object o
960985
return fullPrefix + "None" + closeChars;
961986
}
962987
String refModelName = getModelName(schema);
963-
return toExampleValueRecursive(refModelName, refSchema, objExample, indentationLevel, prefix, exampleLine);
988+
return toExampleValueRecursive(refModelName, refSchema, objExample, indentationLevel, prefix, exampleLine, seenSchemas);
964989
} else if (ModelUtils.isNullType(schema) || isAnyTypeSchema(schema)) {
965990
// The 'null' type is allowed in OAS 3.1 and above. It is not supported by OAS 3.0.x,
966991
// though this tooling supports it.
@@ -1058,7 +1083,8 @@ private String toExampleValueRecursive(String modelName, Schema schema, Object o
10581083
ArraySchema arrayschema = (ArraySchema) schema;
10591084
Schema itemSchema = arrayschema.getItems();
10601085
String itemModelName = getModelName(itemSchema);
1061-
example = fullPrefix + "[" + "\n" + toExampleValueRecursive(itemModelName, itemSchema, objExample, indentationLevel + 1, "", exampleLine + 1) + ",\n" + closingIndentation + "]" + closeChars;
1086+
seenSchemas.add(schema);
1087+
example = fullPrefix + "[" + "\n" + toExampleValueRecursive(itemModelName, itemSchema, objExample, indentationLevel + 1, "", exampleLine + 1, seenSchemas) + ",\n" + closingIndentation + "]" + closeChars;
10621088
return example;
10631089
} else if (ModelUtils.isMapSchema(schema)) {
10641090
if (modelName == null) {
@@ -1080,7 +1106,8 @@ private String toExampleValueRecursive(String modelName, Schema schema, Object o
10801106
addPropPrefix = ensureQuotes(key) + ": ";
10811107
}
10821108
String addPropsModelName = getModelName(addPropsSchema);
1083-
example = fullPrefix + "\n" + toExampleValueRecursive(addPropsModelName, addPropsSchema, addPropsExample, indentationLevel + 1, addPropPrefix, exampleLine + 1) + ",\n" + closingIndentation + closeChars;
1109+
seenSchemas.add(schema);
1110+
example = fullPrefix + "\n" + toExampleValueRecursive(addPropsModelName, addPropsSchema, addPropsExample, indentationLevel + 1, addPropPrefix, exampleLine + 1, seenSchemas) + ",\n" + closingIndentation + closeChars;
10841111
} else {
10851112
example = fullPrefix + closeChars;
10861113
}
@@ -1103,7 +1130,12 @@ private String toExampleValueRecursive(String modelName, Schema schema, Object o
11031130
return fullPrefix + closeChars;
11041131
}
11051132
}
1106-
return exampleForObjectModel(schema, fullPrefix, closeChars, null, indentationLevel, exampleLine, closingIndentation);
1133+
// Adds schema to seenSchemas before running example model function. romoves schema after running
1134+
// the function. It also doesnt keep track of any schemas within the ObjectModel.
1135+
seenSchemas.add(schema);
1136+
String exampleForObjectModel = exampleForObjectModel(schema, fullPrefix, closeChars, null, indentationLevel, exampleLine, closingIndentation, seenSchemas);
1137+
seenSchemas.remove(schema);
1138+
return exampleForObjectModel;
11071139
} else if (ModelUtils.isComposedSchema(schema)) {
11081140
// TODO add examples for composed schema models without discriminators
11091141

@@ -1117,7 +1149,12 @@ private String toExampleValueRecursive(String modelName, Schema schema, Object o
11171149
CodegenProperty cp = new CodegenProperty();
11181150
cp.setName(disc.getPropertyName());
11191151
cp.setExample(discPropNameValue);
1120-
return exampleForObjectModel(modelSchema, fullPrefix, closeChars, cp, indentationLevel, exampleLine, closingIndentation);
1152+
// Adds schema to seenSchemas before running example model function. romoves schema after running
1153+
// the function. It also doesnt keep track of any schemas within the ObjectModel.
1154+
seenSchemas.add(modelSchema);
1155+
String exampleForObjectModel = exampleForObjectModel(modelSchema, fullPrefix, closeChars, cp, indentationLevel, exampleLine, closingIndentation, seenSchemas);
1156+
seenSchemas.remove(modelSchema);
1157+
return exampleForObjectModel;
11211158
} else {
11221159
return fullPrefix + closeChars;
11231160
}
@@ -1130,7 +1167,7 @@ private String toExampleValueRecursive(String modelName, Schema schema, Object o
11301167
return example;
11311168
}
11321169

1133-
private String exampleForObjectModel(Schema schema, String fullPrefix, String closeChars, CodegenProperty discProp, int indentationLevel, int exampleLine, String closingIndentation) {
1170+
private String exampleForObjectModel(Schema schema, String fullPrefix, String closeChars, CodegenProperty discProp, int indentationLevel, int exampleLine, String closingIndentation, Set<Schema> seenSchemas) {
11341171
Map<String, Schema> requiredAndOptionalProps = schema.getProperties();
11351172
if (requiredAndOptionalProps == null || requiredAndOptionalProps.isEmpty()) {
11361173
return fullPrefix + closeChars;
@@ -1150,7 +1187,7 @@ private String exampleForObjectModel(Schema schema, String fullPrefix, String cl
11501187
propModelName = getModelName(propSchema);
11511188
propExample = exampleFromStringOrArraySchema(propSchema, null, propName);
11521189
}
1153-
example += toExampleValueRecursive(propModelName, propSchema, propExample, indentationLevel + 1, propName + "=", exampleLine + 1) + ",\n";
1190+
example += toExampleValueRecursive(propModelName, propSchema, propExample, indentationLevel + 1, propName + "=", exampleLine + 1, seenSchemas) + ",\n";
11541191
}
11551192
// TODO handle additionalProperties also
11561193
example += closingIndentation + closeChars;

modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515
*/
1616

1717
package org.openapitools.codegen.python;
18+
import com.google.common.io.Resources;
19+
import io.swagger.v3.oas.models.PathItem;
20+
import io.swagger.v3.oas.models.Paths;
21+
import io.swagger.v3.oas.models.parameters.RequestBody;
22+
import java.io.FileOutputStream;
23+
import java.io.IOException;
24+
import java.io.InputStream;
25+
import java.io.OutputStream;
26+
import java.io.PrintWriter;
27+
import java.nio.charset.StandardCharsets;
28+
import javax.validation.constraints.AssertTrue;
29+
import org.apache.commons.io.IOUtils;
1830
import org.openapitools.codegen.config.CodegenConfigurator;
1931

2032
import com.google.common.collect.Sets;
@@ -33,6 +45,7 @@
3345
import org.openapitools.codegen.languages.PythonClientCodegen;
3446
import org.openapitools.codegen.utils.ModelUtils;
3547
import org.testng.Assert;
48+
import org.testng.TestNGAntTask.Mode;
3649
import org.testng.annotations.Test;
3750

3851
@SuppressWarnings("static-method")
@@ -425,4 +438,30 @@ public void testObjectWithValidations() {
425438
final CodegenModel model = codegen.fromModel(modelName, modelSchema);
426439
Assert.assertEquals((int) model.getMinProperties(), 1);
427440
}
441+
442+
@Test(description = "tests RecursiveToExample")
443+
public void testRecursiveToExample() throws IOException {
444+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/issue_8052_recursive_model.yaml");
445+
final PythonClientCodegen codegen = new PythonClientCodegen();
446+
codegen.setOpenAPI(openAPI);
447+
448+
final Operation operation = openAPI.getPaths().get("/geojson").getPost();
449+
Schema schema = ModelUtils.getSchemaFromRequestBody(operation.getRequestBody());
450+
String exampleValue = codegen.toExampleValue(schema, null);
451+
452+
// uncomment if you need to regenerate the expected value
453+
// PrintWriter printWriter = new PrintWriter("src/test/resources/3_0/issue_8052_recursive_model_expected_value.txt");
454+
// printWriter.write(exampleValue);
455+
// printWriter.close();
456+
// org.junit.Assert.assertTrue(false);
457+
458+
String expectedValue = Resources.toString(
459+
Resources.getResource("3_0/issue_8052_recursive_model_expected_value.txt"),
460+
StandardCharsets.UTF_8);
461+
462+
463+
Assert.assertEquals(expectedValue.trim(), exampleValue.trim());
464+
465+
}
466+
428467
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
openapi: 3.0.0
2+
info:
3+
version: 01.01.00
4+
title: APITest API documentation.
5+
termsOfService: http://api.apitest.com/party/tos/
6+
servers:
7+
- url: https://api.apitest.com/v1
8+
paths:
9+
/geojson:
10+
post:
11+
summary: Add a GeoJson Object
12+
operationId: post-geojson
13+
responses:
14+
'201':
15+
description: Created
16+
content:
17+
application/json:
18+
schema:
19+
type: string
20+
description: GeoJson ID
21+
'400':
22+
description: Bad Request
23+
requestBody:
24+
content:
25+
application/json:
26+
schema:
27+
$ref: '#/components/schemas/GeoJsonGeometry'
28+
parameters: []
29+
components:
30+
schemas:
31+
GeoJsonGeometry:
32+
title: GeoJsonGeometry
33+
description: GeoJSON geometry
34+
oneOf:
35+
- $ref: '#/components/schemas/Point'
36+
- $ref: '#/components/schemas/GeometryCollection'
37+
discriminator:
38+
propertyName: type
39+
mapping:
40+
Point: '#/components/schemas/Point'
41+
GeometryCollection: '#/components/schemas/GeometryCollection'
42+
externalDocs:
43+
url: http://geojson.org/geojson-spec.html#geometry-objects
44+
Point:
45+
title: Point
46+
type: object
47+
description: GeoJSON geometry
48+
externalDocs:
49+
url: http://geojson.org/geojson-spec.html#id2
50+
properties:
51+
coordinates:
52+
title: Point3D
53+
type: array
54+
description: Point in 3D space
55+
externalDocs:
56+
url: http://geojson.org/geojson-spec.html#id2
57+
minItems: 2
58+
maxItems: 3
59+
items:
60+
type: number
61+
format: double
62+
type:
63+
type: string
64+
default: Point
65+
required:
66+
- type
67+
GeometryCollection:
68+
title: GeometryCollection
69+
type: object
70+
description: GeoJSon geometry collection
71+
required:
72+
- type
73+
- geometries
74+
externalDocs:
75+
url: http://geojson.org/geojson-spec.html#geometrycollection
76+
properties:
77+
type:
78+
type: string
79+
default: GeometryCollection
80+
geometries:
81+
type: array
82+
items:
83+
$ref: '#/components/schemas/GeoJsonGeometry'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
GeoJsonGeometry(
2+
type="GeometryCollection",
3+
geometries=[
4+
GeoJsonGeometry(
5+
type="GeometryCollection",
6+
geometries=[],
7+
),
8+
],
9+
)

0 commit comments

Comments
 (0)