Fixes issue 8052: Stackoverflow in toExampleValue() for python client (#8326)

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 <frank.levine@blacklynx.tech>
This commit is contained in:
fbl100 2021-01-05 12:45:43 -05:00 committed by GitHub
parent fc22de0522
commit 04dfff83e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 178 additions and 9 deletions

View File

@ -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<Schema> 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<String, Schema> 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<Schema> seenSchemas) {
Map<String, Schema> 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;

View File

@ -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,31 @@ 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);
expectedValue = expectedValue.replaceAll("\\r\\n", "\n");
Assert.assertEquals(expectedValue.trim(), exampleValue.trim());
}
}

View File

@ -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'

View File

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