Skip to content

Commit 2cf6932

Browse files
authored
[python] Fix regex patterns starting with forward slash (#25413)
* Fix regex patterns starting with forward slash * Extract shared PERL_DELIMITER_PATTERN constant
1 parent 7869a6e commit 2cf6932

5 files changed

Lines changed: 112 additions & 14 deletions

File tree

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public abstract class AbstractPythonCodegen extends DefaultCodegen implements Co
6666
"finally", "for", "from", "global", "if", "import", "in", "is",
6767
"lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try",
6868
"while", "with", "yield");
69+
private static final Pattern PERL_DELIMITER_PATTERN =
70+
Pattern.compile("(?s)^/(?:\\\\.|[^/\\\\])+/[gimsuxl]*$");
6971

7072
protected String packageName = "openapi_client";
7173
@Setter protected String packageVersion = "1.0.0";
@@ -814,12 +816,14 @@ public String sanitizeTag(String tag) {
814816
}
815817

816818
public String patternCorrection(String pattern) {
819+
if (StringUtils.isEmpty(pattern)) {
820+
return "";
821+
}
817822
// Java does not recognize starting and ending forward slashes and mode modifiers
818823
// It considers them as characters with no special meaning and tries to find them in the match string
819-
boolean checkEnding = pattern.endsWith("/i") || pattern.endsWith("/g") || pattern.endsWith("/m");
820-
if (checkEnding) pattern = pattern.substring(0, pattern.length() - 2);
821-
if (pattern.endsWith("/")) pattern = pattern.substring(0, pattern.length() - 1);
822-
if (pattern.startsWith("/")) pattern = pattern.substring(1);
824+
if (PERL_DELIMITER_PATTERN.matcher(pattern).matches()) {
825+
return pattern.substring(1, pattern.lastIndexOf('/'));
826+
}
823827
return pattern;
824828
}
825829

@@ -1561,6 +1565,7 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
15611565
*/
15621566
public void postProcessPattern(String pattern, Map<String, Object> vendorExtensions) {
15631567
if (pattern != null) {
1568+
pattern = addRegularExpressionDelimiter(pattern);
15641569
int i = pattern.lastIndexOf('/');
15651570

15661571
// TODO update the check below follow python convention
@@ -1592,9 +1597,9 @@ public String addRegularExpressionDelimiter(String pattern) {
15921597
return pattern;
15931598
}
15941599

1595-
if (!pattern.matches("^/.*")) {
1596-
// Perform a negative lookbehind on each `/` to ensure that it is escaped.
1597-
return "/" + pattern.replaceAll("(?<!\\\\)\\/", "\\\\/") + "/";
1600+
if (!PERL_DELIMITER_PATTERN.matcher(pattern).matches()) {
1601+
// Escape each '/' only when preceded by an even number of backslashes.
1602+
return "/" + pattern.replaceAll("((?<!\\\\)(?:\\\\\\\\)*)/", "$1\\\\/") + "/";
15981603
}
15991604

16001605
return pattern;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,7 @@ protected void postProcessConnexionOperationMap(OperationMap operations, Map<Str
883883
@Override
884884
public void postProcessPattern(String pattern, Map<String, Object> vendorExtensions) {
885885
if (pattern != null) {
886+
pattern = addRegularExpressionDelimiter(pattern);
886887
int i = pattern.lastIndexOf('/');
887888

888889
//Must follow Perl /pattern/modifiers convention

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public abstract class AbstractPythonPydanticV1Codegen extends DefaultCodegen imp
5252
private final Logger LOGGER = LoggerFactory.getLogger(AbstractPythonPydanticV1Codegen.class);
5353

5454
public static final String MAP_NUMBER_TO = "mapNumberTo";
55+
private static final Pattern PERL_DELIMITER_PATTERN =
56+
Pattern.compile("(?s)^/(?:\\\\.|[^/\\\\])+/[gimsuxl]*$");
5557

5658
protected String packageName = "openapi_client";
5759
@Setter protected String packageVersion = "1.0.0";
@@ -628,12 +630,14 @@ public String sanitizeTag(String tag) {
628630
}
629631

630632
public String patternCorrection(String pattern) {
633+
if (StringUtils.isEmpty(pattern)) {
634+
return "";
635+
}
631636
// Java does not recognize starting and ending forward slashes and mode modifiers
632637
// It considers them as characters with no special meaning and tries to find them in the match string
633-
boolean checkEnding = pattern.endsWith("/i") || pattern.endsWith("/g") || pattern.endsWith("/m");
634-
if (checkEnding) pattern = pattern.substring(0, pattern.length() - 2);
635-
if (pattern.endsWith("/")) pattern = pattern.substring(0, pattern.length() - 1);
636-
if (pattern.startsWith("/")) pattern = pattern.substring(1);
638+
if (PERL_DELIMITER_PATTERN.matcher(pattern).matches()) {
639+
return pattern.substring(1, pattern.lastIndexOf('/'));
640+
}
637641
return pattern;
638642
}
639643

@@ -1923,6 +1927,7 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
19231927
*/
19241928
public void postProcessPattern(String pattern, Map<String, Object> vendorExtensions) {
19251929
if (pattern != null) {
1930+
pattern = addRegularExpressionDelimiter(pattern);
19261931
int i = pattern.lastIndexOf('/');
19271932

19281933
// TODO update the check below follow python convention
@@ -1954,9 +1959,9 @@ public String addRegularExpressionDelimiter(String pattern) {
19541959
return pattern;
19551960
}
19561961

1957-
if (!pattern.matches("^/.*")) {
1958-
// Perform a negative lookbehind on each `/` to ensure that it is escaped.
1959-
return "/" + pattern.replaceAll("(?<!\\\\)\\/", "\\\\/") + "/";
1962+
if (!PERL_DELIMITER_PATTERN.matcher(pattern).matches()) {
1963+
// Escape each '/' only when preceded by an even number of backslashes.
1964+
return "/" + pattern.replaceAll("((?<!\\\\)(?:\\\\\\\\)*)/", "$1\\\\/") + "/";
19601965
}
19611966

19621967
return pattern;

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,61 @@ public void testRegularExpressionOpenAPISchemaVersion3() {
342342
// added to test fix for issue #6675
343343
// removed because "/^[\\pattern\\d{3}$/i" is invalid regex because [ is not escaped and there is no closing ]
344344
// Assert.assertEquals(op.allParams.get(6).pattern, "/^[\\pattern\\d{3}$/i");
345+
// alternation_with_forward_slash '/ax$|/bx$'
346+
Assert.assertEquals(op.allParams.get(7).pattern, "/\\/ax$|\\/bx$/");
347+
Assert.assertEquals(op.allParams.get(7).vendorExtensions.get("x-regex"), "\\/ax$|\\/bx$");
348+
Assert.assertEquals(op.allParams.get(7).vendorExtensions.get("x-modifiers"), Collections.emptyList());
349+
}
350+
351+
@Test(description = "test regex patterns starting with forward slash (e.g. URL paths)")
352+
public void testRegularExpressionStartingWithSlash() {
353+
final PythonClientCodegen codegen = new PythonClientCodegen();
345354

355+
Schema<?> singleSlashSchema = new StringSchema().pattern("/v1");
356+
CodegenProperty singleSlash = codegen.fromProperty("single_slash", singleSlashSchema, false);
357+
codegen.postProcessModelProperty(null, singleSlash);
358+
Assert.assertEquals(singleSlash.pattern, "/\\/v1/");
359+
Assert.assertEquals(singleSlash.vendorExtensions.get("x-regex"), "\\/v1");
360+
Assert.assertEquals(singleSlash.vendorExtensions.get("x-modifiers"), Collections.emptyList());
361+
362+
Schema<?> pathSchema = new StringSchema().pattern("/health_question_definition/[0-9]+");
363+
CodegenProperty pathProp = codegen.fromProperty("path_pattern", pathSchema, false);
364+
codegen.postProcessModelProperty(null, pathProp);
365+
Assert.assertEquals(pathProp.pattern, "/\\/health_question_definition\\/[0-9]+/");
366+
Assert.assertEquals(pathProp.vendorExtensions.get("x-regex"), "\\/health_question_definition\\/[0-9]+");
367+
Assert.assertEquals(pathProp.vendorExtensions.get("x-modifiers"), Collections.emptyList());
368+
369+
Schema<?> multiSlashWithModifierLetters = new StringSchema().pattern("/api/v1/users");
370+
CodegenProperty multiSlashPath = codegen.fromProperty("multi_slash_path", multiSlashWithModifierLetters, false);
371+
codegen.postProcessModelProperty(null, multiSlashPath);
372+
Assert.assertEquals(multiSlashPath.pattern, "/\\/api\\/v1\\/users/");
373+
Assert.assertEquals(multiSlashPath.vendorExtensions.get("x-regex"), "\\/api\\/v1\\/users");
374+
Assert.assertEquals(multiSlashPath.vendorExtensions.get("x-modifiers"), Collections.emptyList());
375+
376+
Schema<?> perlModifierSchema = new StringSchema().pattern("/^image_\\d{1,3}$/i");
377+
CodegenProperty perlModifierProp = codegen.fromProperty("perl_modifier", perlModifierSchema, false);
378+
codegen.postProcessModelProperty(null, perlModifierProp);
379+
Assert.assertEquals(perlModifierProp.pattern, "/^image_\\d{1,3}$/i");
380+
Assert.assertEquals(perlModifierProp.vendorExtensions.get("x-regex"), "^image_\\d{1,3}$");
381+
Assert.assertEquals(perlModifierProp.vendorExtensions.get("x-modifiers"), Collections.singletonList("IGNORECASE"));
382+
383+
Schema<?> perlGlobalSchema = new StringSchema().pattern("/[0-9]{10}/g");
384+
CodegenProperty perlGlobalProp = codegen.fromProperty("perl_global", perlGlobalSchema, false);
385+
codegen.postProcessModelProperty(null, perlGlobalProp);
386+
Assert.assertEquals(perlGlobalProp.pattern, "/[0-9]{10}/g");
387+
Assert.assertEquals(perlGlobalProp.vendorExtensions.get("x-regex"), "[0-9]{10}");
388+
Assert.assertEquals(perlGlobalProp.vendorExtensions.get("x-modifiers"), Collections.emptyList());
389+
390+
Schema<?> escapedBackslashBeforeSlashSchema = new StringSchema().pattern("a\\\\/b");
391+
CodegenProperty escapedBackslashProp = codegen.fromProperty("escaped_backslash", escapedBackslashBeforeSlashSchema, false);
392+
codegen.postProcessModelProperty(null, escapedBackslashProp);
393+
Assert.assertEquals(escapedBackslashProp.pattern, "/a\\\\\\/b/");
394+
Assert.assertEquals(escapedBackslashProp.vendorExtensions.get("x-regex"), "a\\\\\\/b");
395+
Assert.assertEquals(escapedBackslashProp.vendorExtensions.get("x-modifiers"), Collections.emptyList());
396+
397+
Assert.assertEquals(codegen.patternCorrection("/health_question_definition/[0-9]+"), "/health_question_definition/[0-9]+");
398+
Assert.assertEquals(codegen.patternCorrection("/api/v1/"), "/api/v1/");
399+
Assert.assertEquals(codegen.patternCorrection("/^image_\\d{1,3}$/i"), "^image_\\d{1,3}$");
346400
}
347401

348402

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.nio.file.Paths;
3838
import java.util.ArrayList;
3939
import java.util.Arrays;
40+
import java.util.Collections;
4041
import java.util.List;
4142

4243
import static org.openapitools.codegen.TestUtils.assertFileContains;
@@ -107,7 +108,39 @@ public void testRegularExpressionOpenAPISchemaVersion3() {
107108
// added to test fix for issue #6675
108109
// removed because "/^[\\pattern\\d{3}$/i" is invalid regex because [ is not escaped and there is no closing ]
109110
// Assert.assertEquals(op.allParams.get(6).pattern, "/^[\\pattern\\d{3}$/i");
111+
// alternation_with_forward_slash '/ax$|/bx$'
112+
Assert.assertEquals(op.allParams.get(7).pattern, "/\\/ax$|\\/bx$/");
113+
Assert.assertEquals(op.allParams.get(7).vendorExtensions.get("x-regex"), "\\/ax$|\\/bx$");
114+
Assert.assertEquals(op.allParams.get(7).vendorExtensions.get("x-modifiers"), Collections.emptyList());
115+
}
116+
117+
@Test(description = "test regex patterns starting with forward slash (e.g. URL paths)")
118+
public void testRegularExpressionStartingWithSlash() {
119+
final PythonPydanticV1ClientCodegen codegen = new PythonPydanticV1ClientCodegen();
110120

121+
Schema<?> singleSlashSchema = new StringSchema().pattern("/v1");
122+
CodegenProperty singleSlash = codegen.fromProperty("single_slash", singleSlashSchema, false);
123+
codegen.postProcessModelProperty(null, singleSlash);
124+
Assert.assertEquals(singleSlash.pattern, "/\\/v1/");
125+
Assert.assertEquals(singleSlash.vendorExtensions.get("x-regex"), "\\/v1");
126+
Assert.assertEquals(singleSlash.vendorExtensions.get("x-modifiers"), Collections.emptyList());
127+
128+
Schema<?> pathSchema = new StringSchema().pattern("/health_question_definition/[0-9]+");
129+
CodegenProperty pathProp = codegen.fromProperty("path_pattern", pathSchema, false);
130+
codegen.postProcessModelProperty(null, pathProp);
131+
Assert.assertEquals(pathProp.pattern, "/\\/health_question_definition\\/[0-9]+/");
132+
Assert.assertEquals(pathProp.vendorExtensions.get("x-regex"), "\\/health_question_definition\\/[0-9]+");
133+
Assert.assertEquals(pathProp.vendorExtensions.get("x-modifiers"), Collections.emptyList());
134+
135+
Schema<?> perlModifierSchema = new StringSchema().pattern("/^image_\\d{1,3}$/i");
136+
CodegenProperty perlModifierProp = codegen.fromProperty("perl_modifier", perlModifierSchema, false);
137+
codegen.postProcessModelProperty(null, perlModifierProp);
138+
Assert.assertEquals(perlModifierProp.pattern, "/^image_\\d{1,3}$/i");
139+
Assert.assertEquals(perlModifierProp.vendorExtensions.get("x-regex"), "^image_\\d{1,3}$");
140+
Assert.assertEquals(perlModifierProp.vendorExtensions.get("x-modifiers"), Collections.singletonList("IGNORECASE"));
141+
142+
Assert.assertEquals(codegen.patternCorrection("/health_question_definition/[0-9]+"), "/health_question_definition/[0-9]+");
143+
Assert.assertEquals(codegen.patternCorrection("/api/v1/"), "/api/v1/");
111144
}
112145

113146

0 commit comments

Comments
 (0)