Skip to content

Commit ff9b3f1

Browse files
authored
fix(python-flask): import UUID for nested model properties (#24939)
Add recursive property traversal across containers, object properties, and composition branches to detect UUID imports. Cover nested UUID schemas, shared properties, and cyclic graphs in tests.
1 parent 994a842 commit ff9b3f1

5 files changed

Lines changed: 216 additions & 0 deletions

File tree

‎modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
import java.util.function.BiFunction;
8888
import java.util.function.Consumer;
8989
import java.util.function.Function;
90+
import java.util.function.Predicate;
9091
import java.util.regex.Matcher;
9192
import java.util.regex.Pattern;
9293
import java.util.stream.Collectors;
@@ -4677,6 +4678,44 @@ protected Boolean isPropertyInnerMostEnum(CodegenProperty property) {
46774678
return currentProperty != null && currentProperty.isEnum;
46784679
}
46794680

4681+
/**
4682+
* Search a property and its items, additional properties, named properties, and composition branches.
4683+
* This traversal is independent of the generator's supported features and visits each property
4684+
* instance at most once, so shared or cyclic property graphs are safe to search.
4685+
*
4686+
* @param property the root property, or null
4687+
* @param predicate the condition to match
4688+
* @return true as soon as a matching property is found
4689+
*/
4690+
protected boolean anyPropertyMatches(CodegenProperty property, Predicate<CodegenProperty> predicate) {
4691+
Set<CodegenProperty> visited = Collections.newSetFromMap(new IdentityHashMap<>());
4692+
return anyPropertyMatches(property, predicate, visited);
4693+
}
4694+
4695+
private boolean anyPropertyMatches(CodegenProperty property, Predicate<CodegenProperty> predicate,
4696+
Set<CodegenProperty> visited) {
4697+
if (property == null || !visited.add(property)) {
4698+
return false;
4699+
}
4700+
if (predicate.test(property)
4701+
|| anyPropertyMatches(property.items, predicate, visited)
4702+
|| anyPropertyMatches(property.additionalProperties, predicate, visited)
4703+
|| anyPropertyMatches(property.vars, predicate, visited)) {
4704+
return true;
4705+
}
4706+
4707+
CodegenComposedSchemas composed = property.getComposedSchemas();
4708+
return composed != null && (anyPropertyMatches(composed.getAllOf(), predicate, visited)
4709+
|| anyPropertyMatches(composed.getOneOf(), predicate, visited)
4710+
|| anyPropertyMatches(composed.getAnyOf(), predicate, visited)
4711+
|| anyPropertyMatches(composed.getNot(), predicate, visited));
4712+
}
4713+
4714+
private boolean anyPropertyMatches(List<CodegenProperty> properties, Predicate<CodegenProperty> predicate,
4715+
Set<CodegenProperty> visited) {
4716+
return properties != null && properties.stream().anyMatch(property -> anyPropertyMatches(property, predicate, visited));
4717+
}
4718+
46804719
protected CodegenProperty getMostInnerItems(CodegenProperty property) {
46814720
CodegenProperty currentProperty = property;
46824721
while (currentProperty != null && (currentProperty.isMap

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,9 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
624624
if (StringUtils.isNotEmpty(property.pattern)) {
625625
addImport(model, "import re");
626626
}
627+
if (anyPropertyMatches(property, p -> p.isUuid)) {
628+
addImport(model, "from uuid import UUID");
629+
}
627630
postProcessPattern(property.pattern, property.vendorExtensions);
628631
}
629632

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,48 @@ public void testBuildEnumVarsRejectsMismatchedRawValues() {
8989
new DefaultCodegen().buildEnumVars(Collections.singletonList("value"), "string", Collections.emptyList());
9090
}
9191

92+
@Test
93+
public void testAnyPropertyMatchesHandlesCyclesAndSharedProperties() {
94+
final DefaultCodegen codegen = new DefaultCodegen();
95+
final CodegenProperty root = new CodegenProperty();
96+
final CodegenProperty child = new CodegenProperty();
97+
root.items = child;
98+
root.additionalProperties = child;
99+
child.items = root;
100+
final List<CodegenProperty> visited = new ArrayList<>();
101+
102+
assertFalse(codegen.anyPropertyMatches(root, property -> {
103+
visited.add(property);
104+
return property.isUuid;
105+
}));
106+
assertEquals(2, visited.size());
107+
108+
final CodegenProperty uuid = new CodegenProperty();
109+
uuid.isUuid = true;
110+
child.vars.add(uuid);
111+
assertTrue(codegen.anyPropertyMatches(root, property -> property.isUuid));
112+
}
113+
114+
@Test
115+
public void testAnyPropertyMatchesUsesIdentityAndStopsAtMatch() {
116+
final DefaultCodegen codegen = new DefaultCodegen();
117+
final CodegenProperty root = new CodegenProperty();
118+
final CodegenProperty first = new CodegenProperty();
119+
final CodegenProperty second = new CodegenProperty();
120+
assertEquals(first, second);
121+
root.vars = Arrays.asList(first, second);
122+
123+
assertTrue(codegen.anyPropertyMatches(root, property -> property == second));
124+
assertTrue(codegen.anyPropertyMatches(root, property -> {
125+
assertSame(root, property);
126+
return true;
127+
}));
128+
assertFalse(codegen.anyPropertyMatches(null, property -> {
129+
fail("A null root must not invoke the predicate");
130+
return true;
131+
}));
132+
}
133+
92134
@Test
93135
public void testDeeplyNestedAdditionalPropertiesImports() {
94136
final DefaultCodegen codegen = new DefaultCodegen();

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@
22

33
import io.swagger.parser.OpenAPIParser;
44
import io.swagger.v3.oas.models.OpenAPI;
5+
import io.swagger.v3.oas.models.media.ArraySchema;
6+
import io.swagger.v3.oas.models.media.ComposedSchema;
7+
import io.swagger.v3.oas.models.media.MapSchema;
8+
import io.swagger.v3.oas.models.media.ObjectSchema;
9+
import io.swagger.v3.oas.models.media.Schema;
10+
import io.swagger.v3.oas.models.media.StringSchema;
11+
import io.swagger.v3.oas.models.media.UUIDSchema;
512
import io.swagger.v3.parser.core.models.ParseOptions;
613
import org.openapitools.codegen.ClientOptInput;
14+
import org.openapitools.codegen.CodegenModel;
15+
import org.openapitools.codegen.CodegenProperty;
716
import org.openapitools.codegen.DefaultCodegen;
817
import org.openapitools.codegen.DefaultGenerator;
918
import org.openapitools.codegen.languages.PythonFlaskConnexionServerCodegen;
1019
import org.openapitools.codegen.languages.features.CXFServerFeatures;
1120
import org.testng.Assert;
21+
import org.testng.annotations.DataProvider;
1222
import org.testng.annotations.Test;
1323

1424
import java.io.File;
@@ -20,6 +30,7 @@
2030

2131
import static org.openapitools.codegen.TestUtils.assertFileContains;
2232
import static org.openapitools.codegen.TestUtils.assertFileExists;
33+
import static org.openapitools.codegen.TestUtils.assertFileNotContains;
2334

2435
public class PythonFlaskConnexionServerCodegenTest {
2536

@@ -45,6 +56,70 @@ static private String generateFiles(DefaultCodegen codegen, String filePath) thr
4556
}
4657

4758

59+
@Test(description = "UUID model properties require the standard-library UUID import (issue #23897)")
60+
public void testUuidModelImport() throws IOException {
61+
final DefaultCodegen codegen = new PythonFlaskConnexionServerCodegen();
62+
final String outputPath = generateFiles(codegen, "src/test/resources/bugs/issue_23897.yaml");
63+
64+
final Path model = Paths.get(outputPath, "openapi_server/models/pushnotification.py");
65+
assertFileExists(model);
66+
assertFileContains(model, "from uuid import UUID", "'id': UUID", "def id(self) -> UUID:");
67+
68+
final String[][] uuidModels = {
69+
{"uuid_list.py", "List[UUID]"},
70+
{"uuid_map.py", "Dict[str, UUID]"},
71+
{"nested_uuid_containers.py", "List[Dict[str, List[UUID]]]"},
72+
{"all_of_uuid.py", "UUID"}
73+
};
74+
for (String[] uuidModel : uuidModels) {
75+
final Path generatedModel = Paths.get(outputPath, "openapi_server/models", uuidModel[0]);
76+
assertFileExists(generatedModel);
77+
assertFileContains(generatedModel, "from uuid import UUID", "'ids': " + uuidModel[1],
78+
"def ids(self) -> " + uuidModel[1] + ":");
79+
}
80+
81+
final Path stringList = Paths.get(outputPath, "openapi_server/models/string_list.py");
82+
assertFileContains(stringList, "'ids': List[str]");
83+
assertFileNotContains(stringList, "from uuid import UUID");
84+
}
85+
86+
@DataProvider
87+
public Object[][] uuidSchemas() {
88+
return new Object[][] {
89+
{"direct UUID", new UUIDSchema(), true},
90+
{"array items", new ArraySchema().items(new UUIDSchema()), true},
91+
{"map values", new MapSchema().additionalProperties(new UUIDSchema()), true},
92+
{"UUID beside another object", new ObjectSchema()
93+
.addProperties("id", new UUIDSchema())
94+
.addProperties("tail", new ObjectSchema().addProperties("name", new StringSchema())), true},
95+
{"allOf branch", new ComposedSchema().addAllOfItem(new StringSchema())
96+
.addAllOfItem(new UUIDSchema()), true},
97+
{"oneOf branch", new ComposedSchema().addOneOfItem(new StringSchema())
98+
.addOneOfItem(new UUIDSchema()), true},
99+
{"anyOf branch", new ComposedSchema().addAnyOfItem(new StringSchema())
100+
.addAnyOfItem(new UUIDSchema()), true},
101+
{"not branch", new Schema().not(new UUIDSchema()), true},
102+
{"mixed nesting", new ArraySchema().items(new ObjectSchema().addProperties("value",
103+
new ComposedSchema().addAnyOfItem(new StringSchema())
104+
.addAnyOfItem(new MapSchema().additionalProperties(new UUIDSchema())))), true},
105+
{"no UUID", new ArraySchema().items(new ObjectSchema().addProperties("value",
106+
new ComposedSchema().addAnyOfItem(new StringSchema())
107+
.addAnyOfItem(new MapSchema().additionalProperties(new StringSchema())))), false}
108+
};
109+
}
110+
111+
@Test(dataProvider = "uuidSchemas", description = "Find UUIDs in every schema branch regardless of feature support")
112+
public void testUuidImportInNestedSchemas(String description, Schema schema, boolean expectedImport) {
113+
final PythonFlaskConnexionServerCodegen codegen = new PythonFlaskConnexionServerCodegen();
114+
codegen.setOpenAPI(new OpenAPI());
115+
final CodegenProperty property = codegen.fromProperty("value", schema, false);
116+
final CodegenModel model = new CodegenModel();
117+
118+
codegen.postProcessModelProperty(model, property);
119+
120+
Assert.assertEquals(model.imports.contains("from uuid import UUID"), expectedImport, description);
121+
}
122+
48123
@Test(description = "test requestBody")
49124
public void testRequestBody() throws IOException {
50125
final DefaultCodegen codegen = new PythonFlaskConnexionServerCodegen();
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Flask UUID missing import reproduction
4+
version: 1.0.0
5+
paths: {}
6+
components:
7+
schemas:
8+
Pushnotification:
9+
type: object
10+
properties:
11+
id:
12+
type: string
13+
format: uuid
14+
UuidList:
15+
type: object
16+
properties:
17+
ids:
18+
type: array
19+
items:
20+
type: string
21+
format: uuid
22+
UuidMap:
23+
type: object
24+
properties:
25+
ids:
26+
type: object
27+
additionalProperties:
28+
type: string
29+
format: uuid
30+
NestedUuidContainers:
31+
type: object
32+
properties:
33+
ids:
34+
type: array
35+
items:
36+
type: object
37+
additionalProperties:
38+
type: array
39+
items:
40+
type: string
41+
format: uuid
42+
AllOfUuid:
43+
type: object
44+
properties:
45+
ids:
46+
allOf:
47+
- type: string
48+
format: uuid
49+
- type: string
50+
maxLength: 36
51+
StringList:
52+
type: object
53+
properties:
54+
ids:
55+
type: array
56+
items:
57+
type: string

0 commit comments

Comments
 (0)