forked from loafle/openapi-generator-original
* 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:
+11
-6
@@ -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);
|
||||
|
||||
+38
-11
@@ -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;
|
||||
|
||||
+17
@@ -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'
|
||||
|
||||
|
||||
+15
@@ -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
@@ -1 +1 @@
|
||||
5.4.0-SNAPSHOT
|
||||
6.0.0-SNAPSHOT
|
||||
+1
-1
@@ -1 +1 @@
|
||||
5.4.0-SNAPSHOT
|
||||
6.0.0-SNAPSHOT
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user