Add cycle detection (#7532) (#11500)

* Add cycle detection (#7532)

* Review feedback

* Including ContextAwareNodes to detect cycles more accurately.

* Add test

* Add forest to test.

* No longer need ContextAwareNode

* Review feedback

* Update samples
This commit is contained in:
Ethan Keller
2022-02-06 13:21:17 -05:00
committed by GitHub
parent fcce44ab9b
commit 9f5422d688
11 changed files with 166 additions and 43 deletions
@@ -33,6 +33,7 @@ import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.openapitools.codegen.utils.StringUtils.camelize;
import static org.openapitools.codegen.utils.StringUtils.underscore;
@@ -292,7 +293,11 @@ public abstract class AbstractPythonCodegen extends DefaultCodegen implements Co
return toExampleValueRecursive(schema, new ArrayList<>(), 5);
}
private String toExampleValueRecursive(Schema schema, List<String> includedSchemas, int indentation) {
private String toExampleValueRecursive(Schema schema, List<Schema> includedSchemas, int indentation) {
boolean cycleFound = includedSchemas.stream().filter(s->schema.equals(s)).count() > 1;
if (cycleFound) {
return "";
}
String indentationString = "";
for (int i = 0; i < indentation; i++) indentationString += " ";
String example = null;
@@ -347,7 +352,7 @@ public abstract class AbstractPythonCodegen extends DefaultCodegen implements Co
refSchema.setTitle(ref);
}
if (StringUtils.isNotBlank(schema.getTitle()) && !"null".equals(schema.getTitle())) {
includedSchemas.add(schema.getTitle());
includedSchemas.add(schema);
}
return toExampleValueRecursive(refSchema, includedSchemas, indentation);
}
@@ -412,13 +417,13 @@ public abstract class AbstractPythonCodegen extends DefaultCodegen implements Co
example = "True";
} else if (ModelUtils.isArraySchema(schema)) {
if (StringUtils.isNotBlank(schema.getTitle()) && !"null".equals(schema.getTitle())) {
includedSchemas.add(schema.getTitle());
includedSchemas.add(schema);
}
ArraySchema arrayschema = (ArraySchema) schema;
example = "[\n" + indentationString + toExampleValueRecursive(arrayschema.getItems(), includedSchemas, indentation + 1) + "\n" + indentationString + "]";
} else if (ModelUtils.isMapSchema(schema)) {
if (StringUtils.isNotBlank(schema.getTitle()) && !"null".equals(schema.getTitle())) {
includedSchemas.add(schema.getTitle());
includedSchemas.add(schema);
}
Object additionalObject = schema.getAdditionalProperties();
if (additionalObject instanceof Schema) {
@@ -464,13 +469,13 @@ public abstract class AbstractPythonCodegen extends DefaultCodegen implements Co
if (toExclude != null && reqs.contains(toExclude)) {
reqs.remove(toExclude);
}
for (String toRemove : includedSchemas) {
for (String toRemove : includedSchemas.stream().map(Schema::getTitle).collect(Collectors.toList())) {
if (reqs.contains(toRemove)) {
reqs.remove(toRemove);
}
}
if (StringUtils.isNotBlank(schema.getTitle()) && !"null".equals(schema.getTitle())) {
includedSchemas.add(schema.getTitle());
includedSchemas.add(schema);
}
if (null != schema.getRequired()) for (Object toAdd : schema.getRequired()) {
reqs.add((String) toAdd);
@@ -21,6 +21,7 @@ import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;
import org.apache.commons.lang3.tuple.Triple;
import org.openapitools.codegen.api.TemplatePathLocator;
import org.openapitools.codegen.ignore.CodegenIgnoreProcessor;
import org.openapitools.codegen.templating.*;
@@ -51,6 +52,7 @@ import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.openapitools.codegen.utils.OnceLogger.once;
import static org.openapitools.codegen.utils.StringUtils.underscore;
@@ -1352,7 +1354,7 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
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, new ArrayList<>());
}
private Boolean simpleStringSchema(Schema schema) {
@@ -1398,9 +1400,17 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
* ModelName( line 0
* some_property='some_property_example' line 1
* ) line 2
* @param includedSchemas are a list of schemas that we have moved through to get here. If the new schemas that we
* are looking at is in includedSchemas then we have hit a cycle.
* @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, List<Schema> includedSchemas) {
boolean couldHaveCycle = includedSchemas.size() > 0 && potentiallySelfReferencingSchema(schema);
// If we have seen the ContextAwareSchemaNode more than once before, we must be in a cycle.
boolean cycleFound = false;
if (couldHaveCycle) {
cycleFound = includedSchemas.subList(0, includedSchemas.size()-1).stream().anyMatch(s -> schema.equals(s));
}
final String indentionConst = " ";
String currentIndentation = "";
String closingIndentation = "";
@@ -1433,7 +1443,7 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
return fullPrefix + "None" + closeChars;
}
String refModelName = getModelName(schema);
return toExampleValueRecursive(refModelName, refSchema, objExample, indentationLevel, prefix, exampleLine);
return toExampleValueRecursive(refModelName, refSchema, objExample, indentationLevel, prefix, exampleLine, includedSchemas);
} 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.
@@ -1531,8 +1541,13 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
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;
return example;
includedSchemas.add(schema);
String itemExample = toExampleValueRecursive(itemModelName, itemSchema, objExample, indentationLevel + 1, "", exampleLine + 1, includedSchemas);
if (StringUtils.isEmpty(itemExample) || cycleFound) {
return fullPrefix + "[]";
} else {
return fullPrefix + "[" + "\n" + itemExample + "\n" + closingIndentation + "]" + closeChars;
}
} else if (ModelUtils.isMapSchema(schema)) {
if (modelName == null) {
fullPrefix += "dict(";
@@ -1540,7 +1555,7 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
}
Object addPropsObj = schema.getAdditionalProperties();
// TODO handle true case for additionalProperties
if (addPropsObj instanceof Schema) {
if (addPropsObj instanceof Schema && !cycleFound) {
Schema addPropsSchema = (Schema) addPropsObj;
String key = "key";
Object addPropsExample = getObjectExample(addPropsSchema);
@@ -1553,7 +1568,8 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
addPropPrefix = ensureQuotes(key) + ": ";
}
String addPropsModelName = getModelName(addPropsSchema);
example = fullPrefix + "\n" + toExampleValueRecursive(addPropsModelName, addPropsSchema, addPropsExample, indentationLevel + 1, addPropPrefix, exampleLine + 1) + ",\n" + closingIndentation + closeChars;
includedSchemas.add(schema);
example = fullPrefix + "\n" + toExampleValueRecursive(addPropsModelName, addPropsSchema, addPropsExample, indentationLevel + 1, addPropPrefix, exampleLine + 1, includedSchemas) + ",\n" + closingIndentation + closeChars;
} else {
example = fullPrefix + closeChars;
}
@@ -1563,6 +1579,9 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
fullPrefix += "dict(";
closeChars = ")";
}
if (cycleFound) {
return fullPrefix + closeChars;
}
CodegenDiscriminator disc = createDiscriminator(modelName, schema, openAPI);
if (disc != null) {
MappedModel mm = getDiscriminatorMappedModel(disc);
@@ -1576,8 +1595,11 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
return fullPrefix + closeChars;
}
}
return exampleForObjectModel(schema, fullPrefix, closeChars, null, indentationLevel, exampleLine, closingIndentation);
return exampleForObjectModel(schema, fullPrefix, closeChars, null, indentationLevel, exampleLine, closingIndentation, includedSchemas);
} else if (ModelUtils.isComposedSchema(schema)) {
if (cycleFound) {
return fullPrefix + closeChars;
}
// TODO add examples for composed schema models without discriminators
CodegenDiscriminator disc = createDiscriminator(modelName, schema, openAPI);
@@ -1590,7 +1612,7 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
CodegenProperty cp = new CodegenProperty();
cp.setName(disc.getPropertyName());
cp.setExample(discPropNameValue);
return exampleForObjectModel(modelSchema, fullPrefix, closeChars, cp, indentationLevel, exampleLine, closingIndentation);
return exampleForObjectModel(modelSchema, fullPrefix, closeChars, cp, indentationLevel, exampleLine, closingIndentation, includedSchemas);
} else {
return fullPrefix + closeChars;
}
@@ -1603,7 +1625,11 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
return example;
}
private String exampleForObjectModel(Schema schema, String fullPrefix, String closeChars, CodegenProperty discProp, int indentationLevel, int exampleLine, String closingIndentation) {
private boolean potentiallySelfReferencingSchema(Schema schema) {
return null != schema.get$ref() || ModelUtils.isArraySchema(schema) || ModelUtils.isMapSchema(schema) || ModelUtils.isObjectSchema(schema) || ModelUtils.isComposedSchema(schema);
}
private String exampleForObjectModel(Schema schema, String fullPrefix, String closeChars, CodegenProperty discProp, int indentationLevel, int exampleLine, String closingIndentation, List<Schema> includedSchemas) {
Map<String, Schema> requiredAndOptionalProps = schema.getProperties();
if (requiredAndOptionalProps == null || requiredAndOptionalProps.isEmpty()) {
return fullPrefix + closeChars;
@@ -1623,7 +1649,8 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
propModelName = getModelName(propSchema);
propExample = exampleFromStringOrArraySchema(propSchema, null, propName);
}
example += toExampleValueRecursive(propModelName, propSchema, propExample, indentationLevel + 1, propName + "=", exampleLine + 1) + ",\n";
includedSchemas.add(schema);
example += toExampleValueRecursive(propModelName, propSchema, propExample, indentationLevel + 1, propName + "=", exampleLine + 1, includedSchemas) + ",\n";
}
// TODO handle additionalProperties also
example += closingIndentation + closeChars;
@@ -37,6 +37,7 @@ import java.util.Map;
import org.openapitools.codegen.*;
import org.openapitools.codegen.languages.PythonClientCodegen;
import org.openapitools.codegen.languages.PythonExperimentalClientCodegen;
import org.openapitools.codegen.utils.ModelUtils;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -487,4 +488,20 @@ public class PythonClientTest {
}
}
@Test(description = "tests RecursiveExampleValueWithCycle")
public void testRecursiveExampleValueWithCycle() throws Exception {
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/issue_7532.yaml");
final PythonExperimentalClientCodegen codegen = new PythonExperimentalClientCodegen();
codegen.setOpenAPI(openAPI);
Schema schemaWithCycleInTreesProperty = openAPI.getComponents().getSchemas().get("Forest");
String exampleValue = codegen.toExampleValue(schemaWithCycleInTreesProperty, null);
String expectedValue = Resources.toString(
Resources.getResource("3_0/issue_7532_tree_example_value_expected.txt"),
StandardCharsets.UTF_8);
expectedValue = expectedValue.replaceAll("\\r\\n", "\n");
Assert.assertEquals(exampleValue.trim(), expectedValue.trim());
}
}
@@ -0,0 +1,61 @@
openapi: 3.0.0
info:
version: 1.0.0
title: Test swagger file
paths:
/tree:
post:
description: Create
operationId: createTree
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Tree'
description: The tree to create
required: true
tags:
- Test
responses:
'200':
description: Successfully created the tree
content:
'*/*':
schema:
$ref: '#/components/schemas/Tree'
'400':
description: Bad request
components:
schemas:
Tree:
type: object
required:
- id
- name
- children
properties:
id:
type: integer
name:
type: string
description:
type: string
children:
type: array
items:
$ref: '#/components/schemas/Tree'
parent:
$ref: '#/components/schemas/Tree'
forest:
$ref: '#/components/schemas/Forest'
additional:
type: object
additionalProperties:
$ref: '#/components/schemas/Tree'
Forest:
type: object
properties:
trees:
$ref: '#/components/schemas/Tree'
@@ -0,0 +1,15 @@
dict(
trees=Tree(
id=1,
name="name_example",
description="description_example",
children=[
Tree()
],
parent=Tree(),
forest=Forest(),
additional=dict(
"key": Tree(),
),
),
)
@@ -1 +1 @@
5.4.0-SNAPSHOT
6.0.0-SNAPSHOT
@@ -1 +1 @@
5.4.0-SNAPSHOT
6.0.0-SNAPSHOT
@@ -1 +1 @@
5.4.0-SNAPSHOT
6.0.0-SNAPSHOT
@@ -57,7 +57,7 @@ with petstore_api.ApiClient(configuration) as api_client:
# example passing only optional values
body = AdditionalPropertiesWithArrayOfEnums(
key=[
EnumClass("-efg"),
EnumClass("-efg")
],
)
try:
@@ -144,7 +144,7 @@ with petstore_api.ApiClient(configuration) as api_client:
# example passing only optional values
body = AnimalFarm([
Animal(),
Animal()
])
try:
api_response = api_instance.array_model(
@@ -227,7 +227,7 @@ with petstore_api.ApiClient(configuration) as api_client:
# example passing only optional values
body = ArrayOfEnums([
StringEnum("placed"),
StringEnum("placed")
])
try:
# Array of Enums
@@ -317,9 +317,7 @@ with petstore_api.ApiClient(configuration) as api_client:
source_uri="source_uri_example",
),
files=[
File(
source_uri="source_uri_example",
),
File()
],
)
try:
@@ -974,7 +972,7 @@ with petstore_api.ApiClient(configuration) as api_client:
# example passing only optional values
query_params = {
'enum_query_string_array': [
"$",
"$"
],
'enum_query_string': "-efg",
'enum_query_integer': 1,
@@ -982,13 +980,13 @@ with petstore_api.ApiClient(configuration) as api_client:
}
header_params = {
'enum_header_string_array': [
"$",
"$"
],
'enum_header_string': "-efg",
}
body = dict(
enum_form_string_array=[
"$",
"$"
],
enum_form_string="-efg",
)
@@ -2203,19 +2201,19 @@ with petstore_api.ApiClient(configuration) as api_client:
# example passing only required values which don't have defaults set
query_params = {
'pipe': [
"pipe_example",
"pipe_example"
],
'ioutil': [
"ioutil_example",
"ioutil_example"
],
'http': [
"http_example",
"http_example"
],
'url': [
"url_example",
"url_example"
],
'context': [
"context_example",
"context_example"
],
'refParam': StringWithValidation("refParam_example"),
}
@@ -2671,7 +2669,7 @@ with petstore_api.ApiClient(configuration) as api_client:
# example passing only optional values
body = dict(
files=[
open('/path/to/file', 'rb'),
open('/path/to/file', 'rb')
],
)
try:
@@ -119,13 +119,13 @@ with petstore_api.ApiClient(configuration) as api_client:
),
name="doggie",
photo_urls=[
"photo_urls_example",
"photo_urls_example"
],
tags=[
Tag(
id=1,
name="name_example",
),
)
],
status="available",
)
@@ -415,7 +415,7 @@ with petstore_api.ApiClient(configuration) as api_client:
# example passing only required values which don't have defaults set
query_params = {
'status': [
"available",
"available"
],
}
try:
@@ -593,7 +593,7 @@ with petstore_api.ApiClient(configuration) as api_client:
# example passing only required values which don't have defaults set
query_params = {
'tags': [
"tags_example",
"tags_example"
],
}
try:
@@ -898,13 +898,13 @@ with petstore_api.ApiClient(configuration) as api_client:
),
name="doggie",
photo_urls=[
"photo_urls_example",
"photo_urls_example"
],
tags=[
Tag(
id=1,
name="name_example",
),
)
],
status="available",
)
@@ -140,7 +140,7 @@ with petstore_api.ApiClient(configuration) as api_client:
object_with_no_declared_props_nullable=dict(),
any_type_prop=None,
any_type_prop_nullable=None,
),
)
]
try:
# Creates list of users with given input array
@@ -229,7 +229,7 @@ with petstore_api.ApiClient(configuration) as api_client:
object_with_no_declared_props_nullable=dict(),
any_type_prop=None,
any_type_prop_nullable=None,
),
)
]
try:
# Creates list of users with given input array