Skip to content

Commit 53d02cc

Browse files
RhysOnlyBetterRhysmarkerikson
authored
feat: (codegen) output regex patterns for generated schemas (#5146)
* feat: output regex patterns for generated schemas * Add outputRegexConstants docs * Flesh out tests --------- Co-authored-by: Rhys <[email protected]> Co-authored-by: Mark Erikson <[email protected]>
1 parent 96ca56f commit 53d02cc

File tree

8 files changed

+219
-6
lines changed

8 files changed

+219
-6
lines changed

docs/rtk-query/usage/code-generation.mdx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ interface SimpleUsage {
121121
endpointOverrides?: EndpointOverrides[]
122122
flattenArg?: boolean
123123
useEnumType?: boolean
124+
outputRegexConstants?: boolean
124125
httpResolverOptions?: SwaggerParser.HTTPResolverOptions
125126
}
126127

@@ -192,6 +193,47 @@ const withOverride: ConfigFile = {
192193

193194
Setting `hooks: true` will generate `useQuery` and `useMutation` hook exports. If you also want `useLazyQuery` hooks generated or more granular control, you can also pass an object in the shape of: `{ queries: boolean; lazyQueries: boolean; mutations: boolean }`.
194195

196+
#### Generating regex constants for schema patterns
197+
198+
If your OpenAPI schema uses the [`pattern` keyword](https://swagger.io/docs/specification/data-models/data-types/#pattern) to specify regex validation on string properties, you can export these patterns as JavaScript regex constants by setting `outputRegexConstants: true`.
199+
200+
```ts no-transpile title="openapi-config.ts"
201+
const config: ConfigFile = {
202+
schemaFile: 'https://petstore3.swagger.io/api/v3/openapi.json',
203+
apiFile: './src/store/emptyApi.ts',
204+
outputFile: './src/store/petApi.ts',
205+
outputRegexConstants: true,
206+
}
207+
```
208+
209+
For a schema with pattern-validated properties like:
210+
211+
```yaml
212+
User:
213+
type: object
214+
properties:
215+
email:
216+
type: string
217+
pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
218+
phone:
219+
type: string
220+
pattern: '^\+?[1-9]\d{1,14}$'
221+
```
222+
223+
The codegen will generate:
224+
225+
```ts no-transpile title="Generated output"
226+
export const userEmailPattern =
227+
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
228+
export const userPhonePattern = /^\+?[1-9]\d{1,14}$/
229+
```
230+
231+
These constants can be used for client-side validation to ensure consistency with API expectations.
232+
233+
:::note
234+
Only string-type properties with non-empty `pattern` values will generate constants. The constant name follows the format `{typeName}{propertyName}Pattern` in camelCase.
235+
:::
236+
195237
#### Multiple output files
196238

197239
```ts no-transpile title="openapi-config.ts"

packages/rtk-query-codegen-openapi/src/generate.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import camelCase from 'lodash.camelcase';
22
import path from 'node:path';
33
import ApiGenerator, {
44
getOperationName as _getOperationName,
5-
getReferenceName,
6-
isReference,
7-
supportDeepObjects,
85
createPropertyAssignment,
96
createQuestionToken,
7+
getReferenceName,
8+
isReference,
109
isValidIdentifier,
1110
keywordType,
11+
supportDeepObjects,
1212
} from 'oazapfts/generate';
1313
import type { OpenAPIV3 } from 'openapi-types';
1414
import ts from 'typescript';
@@ -87,6 +87,56 @@ function withQueryComment<T extends ts.Node>(node: T, def: QueryArgDefinition, h
8787
return node;
8888
}
8989

90+
function getPatternFromProperty(
91+
property: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
92+
apiGen: ApiGenerator
93+
): string | null {
94+
const resolved = apiGen.resolve(property);
95+
if (!resolved || typeof resolved !== 'object' || !('pattern' in resolved)) return null;
96+
if (resolved.type !== 'string') return null;
97+
const pattern = resolved.pattern;
98+
return typeof pattern === 'string' && pattern.length > 0 ? pattern : null;
99+
}
100+
101+
function generateRegexConstantsForType(
102+
typeName: string,
103+
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
104+
apiGen: ApiGenerator
105+
): ts.VariableStatement[] {
106+
const resolvedSchema = apiGen.resolve(schema);
107+
if (!resolvedSchema || !('properties' in resolvedSchema) || !resolvedSchema.properties) return [];
108+
109+
const constants: ts.VariableStatement[] = [];
110+
111+
for (const [propertyName, property] of Object.entries(resolvedSchema.properties)) {
112+
const pattern = getPatternFromProperty(property, apiGen);
113+
if (!pattern) continue;
114+
115+
const constantName = camelCase(`${typeName} ${propertyName} Pattern`);
116+
const escapedPattern = pattern.replaceAll('/', String.raw`\/`);
117+
const regexLiteral = factory.createRegularExpressionLiteral(`/${escapedPattern}/`);
118+
119+
constants.push(
120+
factory.createVariableStatement(
121+
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
122+
factory.createVariableDeclarationList(
123+
[
124+
factory.createVariableDeclaration(
125+
factory.createIdentifier(constantName),
126+
undefined,
127+
undefined,
128+
regexLiteral
129+
),
130+
],
131+
ts.NodeFlags.Const
132+
)
133+
)
134+
);
135+
}
136+
137+
return constants;
138+
}
139+
90140
export function getOverrides(
91141
operation: OperationDefinition,
92142
endpointOverrides?: EndpointOverrides[]
@@ -119,6 +169,7 @@ export async function generateApi(
119169
httpResolverOptions,
120170
useUnknown = false,
121171
esmExtensions = false,
172+
outputRegexConstants = false,
122173
}: GenerationOptions
123174
) {
124175
const v3Doc = (v3DocCache[spec] ??= await getV3Doc(spec, httpResolverOptions));
@@ -206,7 +257,18 @@ export async function generateApi(
206257
undefined
207258
),
208259
...Object.values(interfaces),
209-
...apiGen.aliases,
260+
...(outputRegexConstants
261+
? apiGen.aliases.flatMap((alias) => {
262+
if (!ts.isInterfaceDeclaration(alias) && !ts.isTypeAliasDeclaration(alias)) return [alias];
263+
264+
const typeName = alias.name.escapedText.toString();
265+
const schema = v3Doc.components?.schemas?.[typeName];
266+
if (!schema) return [alias];
267+
268+
const regexConstants = generateRegexConstantsForType(typeName, schema, apiGen);
269+
return regexConstants.length > 0 ? [alias, ...regexConstants] : [alias];
270+
})
271+
: apiGen.aliases),
210272
...apiGen.enumAliases,
211273
...(hooks
212274
? [

packages/rtk-query-codegen-openapi/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ export interface CommonOptions {
126126
* Will generate imports with file extension matching the expected compiled output of the api file
127127
*/
128128
esmExtensions?: boolean;
129+
/**
130+
* @default false
131+
* Will generate regex constants for pattern keywords in the schema
132+
*/
133+
outputRegexConstants?: boolean;
129134
}
130135

131136
export type TextMatcher = string | RegExp | (string | RegExp)[];

packages/rtk-query-codegen-openapi/test/__snapshots__/cli.test.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export type User = {
241241
email?: string;
242242
password?: string;
243243
phone?: string;
244+
website?: string;
244245
/** User Status */
245246
userStatus?: number;
246247
};
@@ -488,6 +489,7 @@ export type User = {
488489
email?: string;
489490
password?: string;
490491
phone?: string;
492+
website?: string;
491493
/** User Status */
492494
userStatus?: number;
493495
};

packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export type User = {
241241
email?: string;
242242
password?: string;
243243
phone?: string;
244+
website?: string;
244245
/** User Status */
245246
userStatus?: number;
246247
};
@@ -488,6 +489,7 @@ export type User = {
488489
email?: string;
489490
password?: string;
490491
phone?: string;
492+
website?: string;
491493
/** User Status */
492494
userStatus?: number;
493495
};
@@ -791,6 +793,7 @@ export type User = {
791793
email?: string | undefined;
792794
password?: string | undefined;
793795
phone?: string | undefined;
796+
website?: string | undefined;
794797
/** User Status */
795798
userStatus?: number | undefined;
796799
};
@@ -1196,6 +1199,7 @@ export type User = {
11961199
email?: string | undefined;
11971200
password?: string | undefined;
11981201
phone?: string | undefined;
1202+
website?: string | undefined;
11991203
/** User Status */
12001204
userStatus?: number | undefined;
12011205
};
@@ -1477,6 +1481,7 @@ export type User = {
14771481
email?: string | undefined;
14781482
password?: string | undefined;
14791483
phone?: string | undefined;
1484+
website?: string | undefined;
14801485
/** User Status */
14811486
userStatus?: number | undefined;
14821487
};
@@ -1776,6 +1781,7 @@ export type User = {
17761781
email?: string | undefined;
17771782
password?: string | undefined;
17781783
phone?: string | undefined;
1784+
website?: string | undefined;
17791785
/** User Status */
17801786
userStatus?: number | undefined;
17811787
};
@@ -2057,6 +2063,7 @@ export type User = {
20572063
email?: string | undefined;
20582064
password?: string | undefined;
20592065
phone?: string | undefined;
2066+
website?: string | undefined;
20602067
/** User Status */
20612068
userStatus?: number | undefined;
20622069
};
@@ -2329,6 +2336,7 @@ export type User = {
23292336
email?: string | undefined;
23302337
password?: string | undefined;
23312338
phone?: string | undefined;
2339+
website?: string | undefined;
23322340
/** User Status */
23332341
userStatus?: number | undefined;
23342342
};
@@ -2886,6 +2894,7 @@ export type User = {
28862894
email?: string | undefined;
28872895
password?: string | undefined;
28882896
phone?: string | undefined;
2897+
website?: string | undefined;
28892898
/** User Status */
28902899
userStatus?: number | undefined;
28912900
};
@@ -3513,6 +3522,7 @@ export type User = {
35133522
email?: string | undefined;
35143523
password?: string | undefined;
35153524
phone?: string | undefined;
3525+
website?: string | undefined;
35163526
/** User Status */
35173527
userStatus?: number | undefined;
35183528
};

packages/rtk-query-codegen-openapi/test/fixtures/petstore.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,18 +1015,27 @@
10151015
},
10161016
"email": {
10171017
"type": "string",
1018+
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
10181019
"example": "[email protected]"
10191020
},
10201021
"password": {
10211022
"type": "string",
1022-
"example": "12345"
1023+
"example": "12345",
1024+
"pattern": ""
10231025
},
10241026
"phone": {
10251027
"type": "string",
1028+
"pattern": "^\\+?[1-9]\\d{1,14}$",
10261029
"example": "12345"
10271030
},
1031+
"website": {
1032+
"type": "string",
1033+
"pattern": "^https?://[^\\s]+$",
1034+
"example": "https://example.com"
1035+
},
10281036
"userStatus": {
10291037
"type": "integer",
1038+
"pattern": "^[1-9]\\d{0,2}$",
10301039
"description": "User Status",
10311040
"format": "int32",
10321041
"example": 1
@@ -1044,7 +1053,8 @@
10441053
"format": "int64"
10451054
},
10461055
"name": {
1047-
"type": "string"
1056+
"type": "string",
1057+
"pattern": "^\\S+$"
10481058
}
10491059
},
10501060
"xml": {

packages/rtk-query-codegen-openapi/test/fixtures/petstore.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,15 +695,23 @@ components:
695695
example: James
696696
email:
697697
type: string
698+
pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
698699
699700
password:
700701
type: string
702+
pattern: ''
701703
example: '12345'
702704
phone:
703705
type: string
706+
pattern: '^\+?[1-9]\d{1,14}$'
704707
example: '12345'
708+
website:
709+
type: string
710+
pattern: '^https?://[^\s]+$'
711+
example: 'https://example.com'
705712
userStatus:
706713
type: integer
714+
pattern: '^[1-9]\d{0,2}$'
707715
description: User Status
708716
format: int32
709717
example: 1
@@ -717,6 +725,7 @@ components:
717725
format: int64
718726
name:
719727
type: string
728+
pattern: '^\S+$'
720729
xml:
721730
name: tag
722731
Pet:

0 commit comments

Comments
 (0)