Skip to content

Commit ba7fb8b

Browse files
fix(python): validate string enum values excluded by not (#25070)
* fix(python): validate string enum values excluded by not * fix(python): emit not enum validator once * test(python): verify not enum validator registration
1 parent 20b7db2 commit ba7fb8b

5 files changed

Lines changed: 107 additions & 0 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,10 @@ private ModelsMap postProcessModelsMap(ModelsMap objs) {
11221122
readOnlyFields.add(cp.name);
11231123
}
11241124

1125+
if (cp.vendorExtensions.containsKey("x-python-not-string-enum-values")) {
1126+
moduleImports.add(PYDANTIC, "field_validator");
1127+
}
1128+
11251129
String typing = pydantic.generatePythonType(cp);
11261130
cp.vendorExtensions.put(X_PY_TYPING, typing);
11271131

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,20 @@ public void postProcessParameter(CodegenParameter parameter) {
10481048
@Override
10491049
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
10501050
super.postProcessModelProperty(model, property);
1051+
1052+
if (property.getComposedSchemas() != null
1053+
&& property.getComposedSchemas().getNot() != null) {
1054+
CodegenProperty notProperty = property.getComposedSchemas().getNot();
1055+
if (notProperty.isEnum
1056+
&& notProperty.isString
1057+
&& notProperty.allowableValues != null
1058+
&& notProperty.allowableValues.get("values") != null) {
1059+
property.vendorExtensions.put(
1060+
"x-python-not-string-enum-values",
1061+
notProperty.allowableValues.get("values"));
1062+
}
1063+
}
1064+
10511065
if (hasOneOf(model) || hasAnyOf(model)) {
10521066
return;
10531067
}

‎modules/openapi-generator/src/main/resources/python/model_generic.mustache‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,26 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
133133
{{/isContainer}}
134134
return value
135135
{{/isEnum}}
136+
{{#vendorExtensions.x-python-not-string-enum-values.size}}
137+
@field_validator('{{{name}}}')
138+
def {{{name}}}_validate_not_enum(cls, value):
139+
"""Validates values excluded by not"""
140+
{{^required}}
141+
if value is None:
142+
return value
143+
144+
{{/required}}
145+
{{#required}}
146+
{{#isNullable}}
147+
if value is None:
148+
return value
149+
150+
{{/isNullable}}
151+
{{/required}}
152+
if value in set([{{#vendorExtensions.x-python-not-string-enum-values}}{{#lambda.pythonStringLiteral}}{{{.}}}{{/lambda.pythonStringLiteral}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-python-not-string-enum-values}}]):
153+
raise ValueError("must not be one of excluded enum values")
154+
return value
155+
{{/vendorExtensions.x-python-not-string-enum-values.size}}
136156
{{/vars}}
137157

138158
{{#vendorExtensions.x-py-preprocesses-input-names}}

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,39 @@ public void testInitialConfigValues() throws Exception {
198198
Assert.assertNull(codegen.additionalProperties().get(CodegenConstants.SOURCE_FOLDER));
199199
}
200200

201+
@Test
202+
public void testOneOfNotEnumValidation() throws IOException {
203+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
204+
output.deleteOnExit();
205+
206+
final CodegenConfigurator configurator = new CodegenConfigurator()
207+
.setGeneratorName("python")
208+
.setInputSpec("src/test/resources/3_1/python/oneof-not-enum.yaml")
209+
.setOutputDir(output.getAbsolutePath());
210+
211+
DefaultGenerator generator = new DefaultGenerator();
212+
List<File> files = generator.opts(configurator.toClientOptInput()).generate();
213+
files.forEach(File::deleteOnExit);
214+
215+
Path other = Paths.get(
216+
output.getAbsolutePath(),
217+
"openapi_client",
218+
"models",
219+
"other.py");
220+
221+
String otherContent = Files.readString(other);
222+
223+
Assert.assertTrue(otherContent.contains(
224+
"if value in set([\"known\", \"reserved\"]):"));
225+
Assert.assertTrue(otherContent.contains(
226+
"raise ValueError(\"must not be one of excluded enum values\")"));
227+
Assert.assertTrue(otherContent.contains("@field_validator('kind')"));
228+
229+
Assert.assertEquals(
230+
otherContent.split("def kind_validate_not_enum", -1).length - 1,
231+
1);
232+
}
233+
201234
@Test
202235
public void testSettersForConfigValues() throws Exception {
203236
final PythonClientCodegen codegen = new PythonClientCodegen();
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
openapi: 3.1.0
2+
3+
info:
4+
title: oneOf not enum repro
5+
version: 1.0.0
6+
7+
paths: {}
8+
9+
components:
10+
schemas:
11+
Known:
12+
type: object
13+
required:
14+
- kind
15+
properties:
16+
kind:
17+
type: string
18+
enum:
19+
- known
20+
21+
Other:
22+
type: object
23+
required:
24+
- kind
25+
properties:
26+
kind:
27+
type: string
28+
not:
29+
enum:
30+
- known
31+
- reserved
32+
33+
TaggedUnion:
34+
oneOf:
35+
- $ref: '#/components/schemas/Known'
36+
- $ref: '#/components/schemas/Other'

0 commit comments

Comments
 (0)