zod-codegen 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +5 -5
- package/.github/workflows/release.yml +1 -1
- package/.nvmrc +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/src/assets/manifest.json +2 -2
- package/dist/src/generator.js +3 -3
- package/dist/src/http/fetch-client.js +0 -1
- package/dist/src/services/code-generator.service.js +549 -58
- package/dist/src/services/file-reader.service.js +27 -14
- package/dist/src/services/import-builder.service.js +4 -11
- package/dist/src/services/type-builder.service.js +16 -9
- package/dist/tests/unit/generator.test.js +1 -0
- package/eslint.config.mjs +18 -0
- package/package.json +22 -22
- package/src/assets/manifest.json +2 -2
- package/src/generator.ts +3 -3
- package/src/http/fetch-client.ts +4 -4
- package/src/services/code-generator.service.ts +1212 -88
- package/src/services/file-reader.service.ts +26 -13
- package/src/services/import-builder.service.ts +8 -11
- package/src/services/type-builder.service.ts +19 -12
- package/tsconfig.json +4 -4
- package/scripts/update-manifest.js +0 -31
|
@@ -21,17 +21,17 @@ export class TypeScriptCodeGeneratorService {
|
|
|
21
21
|
const safeCategorySchema = SchemaProperties.safeParse(schema);
|
|
22
22
|
if (safeCategorySchema.success) {
|
|
23
23
|
const safeCategory = safeCategorySchema.data;
|
|
24
|
-
if (safeCategory
|
|
25
|
-
return this.handleLogicalOperator('anyOf', safeCategory
|
|
24
|
+
if (safeCategory['anyOf'] && Array.isArray(safeCategory['anyOf']) && safeCategory['anyOf'].length > 0) {
|
|
25
|
+
return this.handleLogicalOperator('anyOf', safeCategory['anyOf'], required);
|
|
26
26
|
}
|
|
27
|
-
if (safeCategory
|
|
28
|
-
return this.handleLogicalOperator('oneOf', safeCategory
|
|
27
|
+
if (safeCategory['oneOf'] && Array.isArray(safeCategory['oneOf']) && safeCategory['oneOf'].length > 0) {
|
|
28
|
+
return this.handleLogicalOperator('oneOf', safeCategory['oneOf'], required);
|
|
29
29
|
}
|
|
30
|
-
if (safeCategory
|
|
31
|
-
return this.handleLogicalOperator('allOf', safeCategory
|
|
30
|
+
if (safeCategory['allOf'] && Array.isArray(safeCategory['allOf']) && safeCategory['allOf'].length > 0) {
|
|
31
|
+
return this.handleLogicalOperator('allOf', safeCategory['allOf'], required);
|
|
32
32
|
}
|
|
33
|
-
if (safeCategory
|
|
34
|
-
return this.handleLogicalOperator('not', [safeCategory
|
|
33
|
+
if (safeCategory['not']) {
|
|
34
|
+
return this.handleLogicalOperator('not', [safeCategory['not']], required);
|
|
35
35
|
}
|
|
36
36
|
return this.buildProperty(safeCategory, required);
|
|
37
37
|
}
|
|
@@ -90,33 +90,103 @@ export class TypeScriptCodeGeneratorService {
|
|
|
90
90
|
return ts.factory.createMethodDeclaration([ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)], undefined, ts.factory.createPrivateIdentifier('#makeRequest'), undefined, [this.typeBuilder.createGenericType('T')], [
|
|
91
91
|
this.typeBuilder.createParameter('method', 'string'),
|
|
92
92
|
this.typeBuilder.createParameter('path', 'string'),
|
|
93
|
-
this.typeBuilder.createParameter('options', 'unknown', ts.factory.createObjectLiteralExpression([], false),
|
|
93
|
+
this.typeBuilder.createParameter('options', '{params?: Record<string, string | number | boolean>; data?: unknown; contentType?: string}', ts.factory.createObjectLiteralExpression([], false), false),
|
|
94
94
|
], ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Promise'), [
|
|
95
95
|
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('T'), undefined),
|
|
96
96
|
]), ts.factory.createBlock([
|
|
97
|
+
// Build URL with query parameters
|
|
97
98
|
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
98
|
-
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('
|
|
99
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('baseUrl'), undefined, undefined, ts.factory.createTemplateExpression(ts.factory.createTemplateHead('', ''), [
|
|
99
100
|
ts.factory.createTemplateSpan(ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createPrivateIdentifier('#baseUrl')), ts.factory.createTemplateMiddle('', '')),
|
|
100
101
|
ts.factory.createTemplateSpan(ts.factory.createIdentifier('path'), ts.factory.createTemplateTail('', '')),
|
|
101
102
|
])),
|
|
102
103
|
], ts.NodeFlags.Const)),
|
|
104
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
105
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('url'), undefined, undefined, ts.factory.createConditionalExpression(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('params')), ts.factory.createToken(ts.SyntaxKind.AmpersandAmpersandToken), ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('Object'), ts.factory.createIdentifier('keys')), undefined, [
|
|
106
|
+
ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('params')),
|
|
107
|
+
]), ts.factory.createIdentifier('length')), ts.factory.createToken(ts.SyntaxKind.GreaterThanToken), ts.factory.createNumericLiteral('0'))), undefined, (() => {
|
|
108
|
+
const urlObj = ts.factory.createNewExpression(ts.factory.createIdentifier('URL'), undefined, [
|
|
109
|
+
ts.factory.createIdentifier('baseUrl'),
|
|
110
|
+
]);
|
|
111
|
+
const forEachCall = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('Object'), ts.factory.createIdentifier('entries')), undefined, [
|
|
112
|
+
ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('params')),
|
|
113
|
+
]), ts.factory.createIdentifier('forEach')), undefined, [
|
|
114
|
+
ts.factory.createArrowFunction(undefined, undefined, [
|
|
115
|
+
ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createArrayBindingPattern([
|
|
116
|
+
ts.factory.createBindingElement(undefined, undefined, ts.factory.createIdentifier('key'), undefined),
|
|
117
|
+
ts.factory.createBindingElement(undefined, undefined, ts.factory.createIdentifier('value'), undefined),
|
|
118
|
+
]), undefined, undefined),
|
|
119
|
+
], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createBlock([
|
|
120
|
+
ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createPropertyAccessExpression(urlObj, ts.factory.createIdentifier('searchParams')), ts.factory.createIdentifier('set')), undefined, [
|
|
121
|
+
ts.factory.createIdentifier('key'),
|
|
122
|
+
ts.factory.createCallExpression(ts.factory.createIdentifier('String'), undefined, [ts.factory.createIdentifier('value')]),
|
|
123
|
+
])),
|
|
124
|
+
], false)),
|
|
125
|
+
]);
|
|
126
|
+
// Use IIFE to execute forEach and return URL string
|
|
127
|
+
return ts.factory.createCallExpression(ts.factory.createParenthesizedExpression(ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createBlock([
|
|
128
|
+
ts.factory.createExpressionStatement(forEachCall),
|
|
129
|
+
ts.factory.createReturnStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(urlObj, ts.factory.createIdentifier('toString')), undefined, [])),
|
|
130
|
+
], false))), undefined, []);
|
|
131
|
+
})(), undefined, ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createNewExpression(ts.factory.createIdentifier('URL'), undefined, [
|
|
132
|
+
ts.factory.createIdentifier('baseUrl'),
|
|
133
|
+
]), ts.factory.createIdentifier('toString')), undefined, []))),
|
|
134
|
+
], ts.NodeFlags.Const)),
|
|
135
|
+
// Build headers with dynamic Content-Type
|
|
136
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
137
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('headers'), undefined, undefined, ts.factory.createObjectLiteralExpression([
|
|
138
|
+
ts.factory.createPropertyAssignment(ts.factory.createStringLiteral('Content-Type', true), ts.factory.createConditionalExpression(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('contentType')), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createStringLiteral('application/x-www-form-urlencoded', true)), undefined, ts.factory.createStringLiteral('application/x-www-form-urlencoded', true), undefined, ts.factory.createStringLiteral('application/json', true))),
|
|
139
|
+
], true)),
|
|
140
|
+
], ts.NodeFlags.Const)),
|
|
141
|
+
// Build body with form-urlencoded support
|
|
142
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
143
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('body'), undefined, undefined, ts.factory.createConditionalExpression(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('data')), ts.factory.createToken(ts.SyntaxKind.ExclamationEqualsEqualsToken), ts.factory.createIdentifier('undefined')), undefined, ts.factory.createConditionalExpression(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('contentType')), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createStringLiteral('application/x-www-form-urlencoded', true)), undefined,
|
|
144
|
+
// Form-urlencoded: convert object to URLSearchParams
|
|
145
|
+
ts.factory.createCallExpression(ts.factory.createParenthesizedExpression(ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createBlock([
|
|
146
|
+
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
147
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('params'), undefined, undefined, ts.factory.createNewExpression(ts.factory.createIdentifier('URLSearchParams'), undefined, [])),
|
|
148
|
+
], ts.NodeFlags.Const)),
|
|
149
|
+
ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('Object'), ts.factory.createIdentifier('entries')), undefined, [
|
|
150
|
+
ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('data')),
|
|
151
|
+
]), ts.factory.createIdentifier('forEach')), undefined, [
|
|
152
|
+
ts.factory.createArrowFunction(undefined, undefined, [
|
|
153
|
+
ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createArrayBindingPattern([
|
|
154
|
+
ts.factory.createBindingElement(undefined, undefined, ts.factory.createIdentifier('key'), undefined),
|
|
155
|
+
ts.factory.createBindingElement(undefined, undefined, ts.factory.createIdentifier('value'), undefined),
|
|
156
|
+
]), undefined, undefined),
|
|
157
|
+
], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createBlock([
|
|
158
|
+
ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('params'), ts.factory.createIdentifier('set')), undefined, [
|
|
159
|
+
ts.factory.createIdentifier('key'),
|
|
160
|
+
ts.factory.createCallExpression(ts.factory.createIdentifier('String'), undefined, [ts.factory.createIdentifier('value')]),
|
|
161
|
+
])),
|
|
162
|
+
], false)),
|
|
163
|
+
])),
|
|
164
|
+
ts.factory.createReturnStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('params'), ts.factory.createIdentifier('toString')), undefined, [])),
|
|
165
|
+
], false))), undefined, []), undefined,
|
|
166
|
+
// JSON: stringify the data
|
|
167
|
+
ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('JSON'), ts.factory.createIdentifier('stringify')), undefined, [
|
|
168
|
+
ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('options'), ts.factory.createIdentifier('data')),
|
|
169
|
+
])), undefined, ts.factory.createNull())),
|
|
170
|
+
], ts.NodeFlags.Const)),
|
|
171
|
+
// Make fetch request
|
|
103
172
|
ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
104
173
|
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('response'), undefined, undefined, ts.factory.createAwaitExpression(ts.factory.createCallExpression(ts.factory.createIdentifier('fetch'), undefined, [
|
|
105
174
|
ts.factory.createIdentifier('url'),
|
|
106
175
|
ts.factory.createObjectLiteralExpression([
|
|
107
176
|
ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier('method'), undefined),
|
|
108
|
-
ts.factory.createPropertyAssignment(ts.factory.createIdentifier('headers'), ts.factory.
|
|
109
|
-
|
|
110
|
-
], true)),
|
|
177
|
+
ts.factory.createPropertyAssignment(ts.factory.createIdentifier('headers'), ts.factory.createIdentifier('headers')),
|
|
178
|
+
ts.factory.createPropertyAssignment(ts.factory.createIdentifier('body'), ts.factory.createIdentifier('body')),
|
|
111
179
|
], true),
|
|
112
180
|
]))),
|
|
113
181
|
], ts.NodeFlags.Const)),
|
|
182
|
+
// Check response status
|
|
114
183
|
ts.factory.createIfStatement(ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('response'), ts.factory.createIdentifier('ok'))), ts.factory.createThrowStatement(ts.factory.createNewExpression(ts.factory.createIdentifier('Error'), undefined, [
|
|
115
184
|
ts.factory.createTemplateExpression(ts.factory.createTemplateHead('HTTP ', 'HTTP '), [
|
|
116
185
|
ts.factory.createTemplateSpan(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('response'), ts.factory.createIdentifier('status')), ts.factory.createTemplateMiddle(': ', ': ')),
|
|
117
186
|
ts.factory.createTemplateSpan(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('response'), ts.factory.createIdentifier('statusText')), ts.factory.createTemplateTail('', '')),
|
|
118
187
|
]),
|
|
119
188
|
])), undefined),
|
|
189
|
+
// Return parsed JSON
|
|
120
190
|
ts.factory.createReturnStatement(ts.factory.createAwaitExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('response'), ts.factory.createIdentifier('json')), undefined, []))),
|
|
121
191
|
], true));
|
|
122
192
|
}
|
|
@@ -136,36 +206,260 @@ export class TypeScriptCodeGeneratorService {
|
|
|
136
206
|
}, []);
|
|
137
207
|
}
|
|
138
208
|
buildEndpointMethod(method, path, schema, schemas) {
|
|
139
|
-
const parameters = this.buildMethodParameters(schema, schemas);
|
|
209
|
+
const { parameters, pathParams, queryParams, hasRequestBody, contentType } = this.buildMethodParameters(schema, schemas);
|
|
140
210
|
const responseType = this.getResponseType(schema, schemas);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
211
|
+
const responseSchema = this.getResponseSchema(schema, schemas);
|
|
212
|
+
const statements = [];
|
|
213
|
+
// Build path with parameter substitution
|
|
214
|
+
const pathExpression = this.buildPathExpression(path, pathParams);
|
|
215
|
+
// Build query parameters object
|
|
216
|
+
const queryParamsExpression = queryParams.length > 0
|
|
217
|
+
? ts.factory.createObjectLiteralExpression(queryParams.map((param) => {
|
|
218
|
+
const paramName = this.typeBuilder.sanitizeIdentifier(param.name);
|
|
219
|
+
return ts.factory.createPropertyAssignment(ts.factory.createStringLiteral(param.name, true), ts.factory.createIdentifier(paramName));
|
|
220
|
+
}), false)
|
|
221
|
+
: undefined;
|
|
222
|
+
// Build request body
|
|
223
|
+
const requestBodyExpression = hasRequestBody
|
|
224
|
+
? ts.factory.createIdentifier('body')
|
|
225
|
+
: undefined;
|
|
226
|
+
// Build options object for makeRequest
|
|
227
|
+
const optionsProps = [];
|
|
228
|
+
if (queryParamsExpression) {
|
|
229
|
+
optionsProps.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier('params'), queryParamsExpression));
|
|
230
|
+
}
|
|
231
|
+
if (requestBodyExpression) {
|
|
232
|
+
optionsProps.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier('data'), requestBodyExpression));
|
|
233
|
+
}
|
|
234
|
+
// Add content type if it's form-urlencoded
|
|
235
|
+
if (hasRequestBody && contentType === 'application/x-www-form-urlencoded') {
|
|
236
|
+
optionsProps.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier('contentType'), ts.factory.createStringLiteral('application/x-www-form-urlencoded', true)));
|
|
237
|
+
}
|
|
238
|
+
const optionsExpression = ts.factory.createObjectLiteralExpression(optionsProps, false);
|
|
239
|
+
// Call makeRequest
|
|
240
|
+
const makeRequestCall = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createPrivateIdentifier('#makeRequest')), undefined, [ts.factory.createStringLiteral(method.toUpperCase(), true), pathExpression, optionsExpression]);
|
|
241
|
+
// Add Zod validation if we have a response schema
|
|
242
|
+
if (responseSchema) {
|
|
243
|
+
const validateCall = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(responseSchema, ts.factory.createIdentifier('parse')), undefined, [ts.factory.createAwaitExpression(makeRequestCall)]);
|
|
244
|
+
statements.push(ts.factory.createReturnStatement(validateCall));
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
statements.push(ts.factory.createReturnStatement(ts.factory.createAwaitExpression(makeRequestCall)));
|
|
248
|
+
}
|
|
249
|
+
return ts.factory.createMethodDeclaration([ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)], undefined, ts.factory.createIdentifier(String(schema.operationId)), undefined, undefined, parameters, responseType, ts.factory.createBlock(statements, true));
|
|
250
|
+
}
|
|
251
|
+
buildPathExpression(path, pathParams) {
|
|
252
|
+
// Replace {param} with ${param} for template literal
|
|
253
|
+
const pathParamNames = new Set(pathParams.map((p) => p.name));
|
|
254
|
+
const pathParamRegex = /\{([^}]+)\}/g;
|
|
255
|
+
const matches = [];
|
|
256
|
+
// Find all path parameters
|
|
257
|
+
for (const match of path.matchAll(pathParamRegex)) {
|
|
258
|
+
const paramName = match[1];
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
260
|
+
if (paramName && pathParamNames.has(paramName) && match.index !== undefined) {
|
|
261
|
+
matches.push({
|
|
262
|
+
index: match.index,
|
|
263
|
+
length: match[0].length,
|
|
264
|
+
name: paramName,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (matches.length === 0) {
|
|
269
|
+
// No path parameters, return as string literal
|
|
270
|
+
return ts.factory.createStringLiteral(path, true);
|
|
271
|
+
}
|
|
272
|
+
// Build template expression
|
|
273
|
+
const templateSpans = [];
|
|
274
|
+
let lastIndex = 0;
|
|
275
|
+
for (const [index, m] of matches.entries()) {
|
|
276
|
+
const before = path.substring(lastIndex, m.index);
|
|
277
|
+
const sanitizedName = this.typeBuilder.sanitizeIdentifier(m.name);
|
|
278
|
+
const isLast = index === matches.length - 1;
|
|
279
|
+
const after = isLast ? path.substring(m.index + m.length) : '';
|
|
280
|
+
if (isLast) {
|
|
281
|
+
templateSpans.push(ts.factory.createTemplateSpan(ts.factory.createIdentifier(sanitizedName), ts.factory.createTemplateTail(after, after)));
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
templateSpans.push(ts.factory.createTemplateSpan(ts.factory.createIdentifier(sanitizedName), ts.factory.createTemplateMiddle(before, before)));
|
|
285
|
+
}
|
|
286
|
+
lastIndex = m.index + m.length;
|
|
287
|
+
}
|
|
288
|
+
const firstMatch = matches[0];
|
|
289
|
+
if (!firstMatch) {
|
|
290
|
+
return ts.factory.createStringLiteral(path, true);
|
|
291
|
+
}
|
|
292
|
+
const head = path.substring(0, firstMatch.index);
|
|
293
|
+
return ts.factory.createTemplateExpression(ts.factory.createTemplateHead(head, head), templateSpans);
|
|
147
294
|
}
|
|
148
295
|
buildMethodParameters(schema, schemas) {
|
|
149
|
-
void schemas; // Mark as intentionally unused
|
|
150
296
|
const parameters = [];
|
|
297
|
+
const pathParams = [];
|
|
298
|
+
const queryParams = [];
|
|
299
|
+
// Extract path and query parameters
|
|
151
300
|
if (schema.parameters) {
|
|
152
|
-
schema.parameters
|
|
153
|
-
|
|
154
|
-
|
|
301
|
+
for (const param of schema.parameters) {
|
|
302
|
+
const paramName = this.typeBuilder.sanitizeIdentifier(param.name);
|
|
303
|
+
const paramType = this.getParameterType(param.schema);
|
|
304
|
+
if (param.in === 'path') {
|
|
305
|
+
pathParams.push({ name: param.name, type: paramType });
|
|
306
|
+
parameters.push(this.typeBuilder.createParameter(paramName, paramType, undefined, false));
|
|
155
307
|
}
|
|
156
|
-
|
|
308
|
+
else if (param.in === 'query') {
|
|
309
|
+
// Improve type inference for query parameters
|
|
310
|
+
const queryParamType =
|
|
311
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
312
|
+
typeof param.schema === 'object' && param.schema !== null
|
|
313
|
+
? (() => {
|
|
314
|
+
const paramSchema = param.schema;
|
|
315
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
316
|
+
if (paramSchema['type'] === 'array' && paramSchema['items']) {
|
|
317
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
318
|
+
const itemSchema = paramSchema['items'];
|
|
319
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
320
|
+
if (itemSchema['type'] === 'string') {
|
|
321
|
+
return 'string[]';
|
|
322
|
+
}
|
|
323
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
324
|
+
if (itemSchema['type'] === 'number' || itemSchema['type'] === 'integer') {
|
|
325
|
+
return 'number[]';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return paramType;
|
|
329
|
+
})()
|
|
330
|
+
: paramType;
|
|
331
|
+
queryParams.push({ name: param.name, type: queryParamType, required: param.required ?? false });
|
|
332
|
+
parameters.push(this.typeBuilder.createParameter(paramName, queryParamType, undefined, !param.required));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
157
335
|
}
|
|
158
|
-
|
|
159
|
-
|
|
336
|
+
// Add request body parameter if present
|
|
337
|
+
// Check for both application/json and application/x-www-form-urlencoded
|
|
338
|
+
const jsonContent = schema.requestBody?.content?.['application/json'];
|
|
339
|
+
const formContent = schema.requestBody?.content?.['application/x-www-form-urlencoded'];
|
|
340
|
+
const hasRequestBody = !!(jsonContent ?? formContent);
|
|
341
|
+
if (hasRequestBody) {
|
|
342
|
+
const requestBodyContent = jsonContent ?? formContent;
|
|
343
|
+
const requestBodySchema = requestBodyContent?.schema;
|
|
344
|
+
const bodyType = typeof requestBodySchema === 'object' && requestBodySchema !== null
|
|
345
|
+
? (() => {
|
|
346
|
+
const schemaObj = requestBodySchema;
|
|
347
|
+
const ref = schemaObj['$ref'];
|
|
348
|
+
if (ref && typeof ref === 'string' && ref.startsWith('#/components/schemas/')) {
|
|
349
|
+
const refName = ref.split('/').pop() ?? 'unknown';
|
|
350
|
+
return this.typeBuilder.sanitizeIdentifier(refName);
|
|
351
|
+
}
|
|
352
|
+
// Fallback to getSchemaTypeName for non-ref schemas
|
|
353
|
+
return this.getSchemaTypeName(requestBodySchema, schemas);
|
|
354
|
+
})()
|
|
355
|
+
: 'unknown';
|
|
356
|
+
parameters.push(this.typeBuilder.createParameter('body', bodyType, undefined, !schema.requestBody?.required));
|
|
357
|
+
}
|
|
358
|
+
// Determine content type for request body
|
|
359
|
+
const contentType = hasRequestBody && schema.requestBody?.content?.['application/x-www-form-urlencoded']
|
|
360
|
+
? 'application/x-www-form-urlencoded'
|
|
361
|
+
: 'application/json';
|
|
362
|
+
return { parameters, pathParams, queryParams, hasRequestBody, contentType };
|
|
160
363
|
}
|
|
161
|
-
|
|
162
|
-
|
|
364
|
+
getParameterType(schema) {
|
|
365
|
+
if (!schema || typeof schema !== 'object') {
|
|
366
|
+
return 'string';
|
|
367
|
+
}
|
|
368
|
+
const schemaObj = schema;
|
|
369
|
+
if (schemaObj.$ref) {
|
|
370
|
+
const refName = schemaObj.$ref.split('/').pop() ?? 'unknown';
|
|
371
|
+
return this.typeBuilder.sanitizeIdentifier(refName);
|
|
372
|
+
}
|
|
373
|
+
if (schemaObj.type === 'array' && schemaObj.items) {
|
|
374
|
+
const itemType = this.getParameterType(schemaObj.items);
|
|
375
|
+
return `${itemType}[]`;
|
|
376
|
+
}
|
|
377
|
+
switch (schemaObj.type) {
|
|
378
|
+
case 'integer':
|
|
379
|
+
case 'number':
|
|
380
|
+
return 'number';
|
|
381
|
+
case 'boolean':
|
|
382
|
+
return 'boolean';
|
|
383
|
+
case 'array':
|
|
384
|
+
return 'unknown[]';
|
|
385
|
+
case 'object':
|
|
386
|
+
return 'Record<string, unknown>';
|
|
387
|
+
case 'string':
|
|
388
|
+
// If it has enum values, we could generate a union type, but for simplicity, keep as string
|
|
389
|
+
// The Zod schema will handle the validation
|
|
390
|
+
return 'string';
|
|
391
|
+
default:
|
|
392
|
+
return 'string';
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
getSchemaTypeName(schema, _schemas) {
|
|
396
|
+
if (typeof schema !== 'object' || schema === null) {
|
|
397
|
+
return 'unknown';
|
|
398
|
+
}
|
|
399
|
+
const schemaObj = schema;
|
|
400
|
+
// Check for $ref using both dot notation and bracket notation
|
|
401
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
402
|
+
const ref = schemaObj['$ref'];
|
|
403
|
+
if (ref && typeof ref === 'string') {
|
|
404
|
+
const refName = ref.split('/').pop() ?? 'unknown';
|
|
405
|
+
return this.typeBuilder.sanitizeIdentifier(refName);
|
|
406
|
+
}
|
|
407
|
+
if (schemaObj.type === 'array' && schemaObj.items) {
|
|
408
|
+
const itemType = this.getSchemaTypeName(schemaObj.items, _schemas);
|
|
409
|
+
return `${itemType}[]`;
|
|
410
|
+
}
|
|
411
|
+
switch (schemaObj.type) {
|
|
412
|
+
case 'integer':
|
|
413
|
+
case 'number':
|
|
414
|
+
return 'number';
|
|
415
|
+
case 'boolean':
|
|
416
|
+
return 'boolean';
|
|
417
|
+
case 'string':
|
|
418
|
+
return 'string';
|
|
419
|
+
case 'object':
|
|
420
|
+
return 'Record<string, unknown>';
|
|
421
|
+
default:
|
|
422
|
+
return 'unknown';
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
getResponseSchema(schema, _schemas) {
|
|
426
|
+
// Try to find a 200 response first, then 201, then default
|
|
163
427
|
const response200 = schema.responses?.['200'];
|
|
164
|
-
|
|
428
|
+
const response201 = schema.responses?.['201'];
|
|
429
|
+
const responseDefault = schema.responses?.['default'];
|
|
430
|
+
const response = response200 ?? response201 ?? responseDefault;
|
|
431
|
+
if (!response?.content?.['application/json']?.schema) {
|
|
165
432
|
return undefined;
|
|
166
433
|
}
|
|
434
|
+
const responseSchema = response.content['application/json'].schema;
|
|
435
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
436
|
+
if (responseSchema !== null && typeof responseSchema === 'object' && '$ref' in responseSchema) {
|
|
437
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
438
|
+
const ref = responseSchema['$ref'];
|
|
439
|
+
if (typeof ref === 'string') {
|
|
440
|
+
const refName = ref.split('/').pop() ?? 'unknown';
|
|
441
|
+
return ts.factory.createIdentifier(this.typeBuilder.sanitizeIdentifier(refName));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// For inline schemas, we'd need to generate a schema, but for now return undefined
|
|
445
|
+
// This could be enhanced to generate inline schemas
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
getResponseType(schema, schemas) {
|
|
449
|
+
// Try to find a 200 response first, then 201, then default
|
|
450
|
+
const response200 = schema.responses?.['200'];
|
|
451
|
+
const response201 = schema.responses?.['201'];
|
|
452
|
+
const responseDefault = schema.responses?.['default'];
|
|
453
|
+
const response = response200 ?? response201 ?? responseDefault;
|
|
454
|
+
if (!response?.content?.['application/json']?.schema) {
|
|
455
|
+
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Promise'), [
|
|
456
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword),
|
|
457
|
+
]);
|
|
458
|
+
}
|
|
459
|
+
const responseSchema = response.content['application/json'].schema;
|
|
460
|
+
const typeName = this.getSchemaTypeName(responseSchema, schemas);
|
|
167
461
|
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Promise'), [
|
|
168
|
-
ts.factory.
|
|
462
|
+
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(typeName), undefined),
|
|
169
463
|
]);
|
|
170
464
|
}
|
|
171
465
|
buildBaseUrlConstant(openapi) {
|
|
@@ -210,35 +504,73 @@ export class TypeScriptCodeGeneratorService {
|
|
|
210
504
|
}
|
|
211
505
|
const prop = safeProperty.data;
|
|
212
506
|
if (this.isReference(prop)) {
|
|
213
|
-
|
|
507
|
+
const refSchema = this.buildFromReference(prop);
|
|
508
|
+
return required
|
|
509
|
+
? refSchema
|
|
510
|
+
: ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(refSchema, ts.factory.createIdentifier('optional')), undefined, []);
|
|
214
511
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return this.handleLogicalOperator('anyOf', prop.anyOf, required);
|
|
512
|
+
if (prop['anyOf'] && Array.isArray(prop['anyOf']) && prop['anyOf'].length > 0) {
|
|
513
|
+
return this.handleLogicalOperator('anyOf', prop['anyOf'], required);
|
|
218
514
|
}
|
|
219
|
-
if (prop
|
|
220
|
-
return this.handleLogicalOperator('oneOf', prop
|
|
515
|
+
if (prop['oneOf'] && Array.isArray(prop['oneOf']) && prop['oneOf'].length > 0) {
|
|
516
|
+
return this.handleLogicalOperator('oneOf', prop['oneOf'], required);
|
|
221
517
|
}
|
|
222
|
-
if (prop
|
|
223
|
-
return this.handleLogicalOperator('allOf', prop
|
|
518
|
+
if (prop['allOf'] && Array.isArray(prop['allOf']) && prop['allOf'].length > 0) {
|
|
519
|
+
return this.handleLogicalOperator('allOf', prop['allOf'], required);
|
|
224
520
|
}
|
|
225
|
-
if (prop
|
|
226
|
-
return this.handleLogicalOperator('not', [prop
|
|
521
|
+
if (prop['not']) {
|
|
522
|
+
return this.handleLogicalOperator('not', [prop['not']], required);
|
|
227
523
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
524
|
+
// Handle enum
|
|
525
|
+
if (prop['enum'] && Array.isArray(prop['enum']) && prop['enum'].length > 0) {
|
|
526
|
+
const enumValues = prop['enum'].map((val) => {
|
|
527
|
+
if (typeof val === 'string') {
|
|
528
|
+
return ts.factory.createStringLiteral(val, true);
|
|
529
|
+
}
|
|
530
|
+
if (typeof val === 'number') {
|
|
531
|
+
// Handle negative numbers correctly
|
|
532
|
+
if (val < 0) {
|
|
533
|
+
return ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, ts.factory.createNumericLiteral(String(Math.abs(val))));
|
|
534
|
+
}
|
|
535
|
+
return ts.factory.createNumericLiteral(String(val));
|
|
536
|
+
}
|
|
537
|
+
if (typeof val === 'boolean') {
|
|
538
|
+
return val ? ts.factory.createTrue() : ts.factory.createFalse();
|
|
539
|
+
}
|
|
540
|
+
return ts.factory.createStringLiteral(String(val), true);
|
|
541
|
+
});
|
|
542
|
+
const enumExpression = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('enum')), undefined, [ts.factory.createArrayLiteralExpression(enumValues, false)]);
|
|
543
|
+
return required
|
|
544
|
+
? enumExpression
|
|
545
|
+
: ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(enumExpression, ts.factory.createIdentifier('optional')), undefined, []);
|
|
546
|
+
}
|
|
547
|
+
switch (prop['type']) {
|
|
548
|
+
case 'array': {
|
|
549
|
+
const itemsSchema = prop['items'] ? this.buildProperty(prop['items'], true) : this.buildZodAST(['unknown']);
|
|
550
|
+
let arraySchema = this.buildZodAST([
|
|
231
551
|
{
|
|
232
552
|
type: 'array',
|
|
233
|
-
args:
|
|
553
|
+
args: [itemsSchema],
|
|
234
554
|
},
|
|
235
|
-
...(!required ? ['optional'] : []),
|
|
236
555
|
]);
|
|
556
|
+
// Apply array constraints
|
|
557
|
+
if (typeof prop['minItems'] === 'number') {
|
|
558
|
+
arraySchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(arraySchema, ts.factory.createIdentifier('min')), undefined, [ts.factory.createNumericLiteral(String(prop['minItems']))]);
|
|
559
|
+
}
|
|
560
|
+
if (typeof prop['maxItems'] === 'number') {
|
|
561
|
+
arraySchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(arraySchema, ts.factory.createIdentifier('max')), undefined, [ts.factory.createNumericLiteral(String(prop['maxItems']))]);
|
|
562
|
+
}
|
|
563
|
+
return required
|
|
564
|
+
? arraySchema
|
|
565
|
+
: ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(arraySchema, ts.factory.createIdentifier('optional')), undefined, []);
|
|
566
|
+
}
|
|
237
567
|
case 'object': {
|
|
238
|
-
const
|
|
568
|
+
const propObj = prop;
|
|
569
|
+
const properties = (propObj['properties'] ?? {});
|
|
570
|
+
const propRequired = (propObj['required'] ?? []);
|
|
239
571
|
const propertiesEntries = Object.entries(properties);
|
|
240
572
|
if (propertiesEntries.length > 0) {
|
|
241
|
-
|
|
573
|
+
const objectSchema = this.buildZodAST([
|
|
242
574
|
{
|
|
243
575
|
type: 'object',
|
|
244
576
|
args: [
|
|
@@ -247,8 +579,28 @@ export class TypeScriptCodeGeneratorService {
|
|
|
247
579
|
}), true),
|
|
248
580
|
],
|
|
249
581
|
},
|
|
250
|
-
...(!required ? ['optional'] : []),
|
|
251
582
|
]);
|
|
583
|
+
// Apply object constraints
|
|
584
|
+
let constrainedSchema = objectSchema;
|
|
585
|
+
if (typeof prop['minProperties'] === 'number') {
|
|
586
|
+
constrainedSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(constrainedSchema, ts.factory.createIdentifier('refine')), undefined, [
|
|
587
|
+
ts.factory.createArrowFunction(undefined, undefined, [ts.factory.createParameterDeclaration(undefined, undefined, 'obj', undefined, undefined, undefined)], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('Object'), ts.factory.createIdentifier('keys')), undefined, [ts.factory.createIdentifier('obj')]), ts.factory.createIdentifier('length')), ts.factory.createToken(ts.SyntaxKind.GreaterThanEqualsToken), ts.factory.createNumericLiteral(String(prop['minProperties'])))),
|
|
588
|
+
ts.factory.createObjectLiteralExpression([
|
|
589
|
+
ts.factory.createPropertyAssignment(ts.factory.createIdentifier('message'), ts.factory.createStringLiteral(`Object must have at least ${String(prop['minProperties'])} properties`)),
|
|
590
|
+
]),
|
|
591
|
+
]);
|
|
592
|
+
}
|
|
593
|
+
if (typeof prop['maxProperties'] === 'number') {
|
|
594
|
+
constrainedSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(constrainedSchema, ts.factory.createIdentifier('refine')), undefined, [
|
|
595
|
+
ts.factory.createArrowFunction(undefined, undefined, [ts.factory.createParameterDeclaration(undefined, undefined, 'obj', undefined, undefined, undefined)], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('Object'), ts.factory.createIdentifier('keys')), undefined, [ts.factory.createIdentifier('obj')]), ts.factory.createIdentifier('length')), ts.factory.createToken(ts.SyntaxKind.LessThanEqualsToken), ts.factory.createNumericLiteral(String(prop['maxProperties'])))),
|
|
596
|
+
ts.factory.createObjectLiteralExpression([
|
|
597
|
+
ts.factory.createPropertyAssignment(ts.factory.createIdentifier('message'), ts.factory.createStringLiteral(`Object must have at most ${String(prop['maxProperties'])} properties`)),
|
|
598
|
+
]),
|
|
599
|
+
]);
|
|
600
|
+
}
|
|
601
|
+
return required
|
|
602
|
+
? constrainedSchema
|
|
603
|
+
: ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(constrainedSchema, ts.factory.createIdentifier('optional')), undefined, []);
|
|
252
604
|
}
|
|
253
605
|
return this.buildZodAST([
|
|
254
606
|
{
|
|
@@ -257,20 +609,158 @@ export class TypeScriptCodeGeneratorService {
|
|
|
257
609
|
},
|
|
258
610
|
]);
|
|
259
611
|
}
|
|
260
|
-
case 'integer':
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
612
|
+
case 'integer': {
|
|
613
|
+
let numberSchema = this.buildZodAST(['number', 'int']);
|
|
614
|
+
// Apply number constraints
|
|
615
|
+
if (prop['minimum'] !== undefined && typeof prop['minimum'] === 'number') {
|
|
616
|
+
const minValue = prop['exclusiveMinimum'] && typeof prop['exclusiveMinimum'] === 'boolean'
|
|
617
|
+
? prop['minimum'] + 1
|
|
618
|
+
: prop['minimum'];
|
|
619
|
+
numberSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(numberSchema, ts.factory.createIdentifier(prop['exclusiveMinimum'] && typeof prop['exclusiveMinimum'] === 'boolean' ? 'gt' : 'gte')), undefined, [ts.factory.createNumericLiteral(String(minValue))]);
|
|
620
|
+
}
|
|
621
|
+
if (prop['maximum'] !== undefined && typeof prop['maximum'] === 'number') {
|
|
622
|
+
const maxValue = prop['exclusiveMaximum'] && typeof prop['exclusiveMaximum'] === 'boolean'
|
|
623
|
+
? prop['maximum'] - 1
|
|
624
|
+
: prop['maximum'];
|
|
625
|
+
numberSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(numberSchema, ts.factory.createIdentifier(prop['exclusiveMaximum'] ? 'lt' : 'lte')), undefined, [ts.factory.createNumericLiteral(String(maxValue))]);
|
|
626
|
+
}
|
|
627
|
+
if (typeof prop['multipleOf'] === 'number') {
|
|
628
|
+
const refineFunction = ts.factory.createArrowFunction(undefined, undefined, [ts.factory.createParameterDeclaration(undefined, undefined, 'val', undefined, undefined, undefined)], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createBinaryExpression(ts.factory.createBinaryExpression(ts.factory.createIdentifier('val'), ts.factory.createToken(ts.SyntaxKind.PercentToken), ts.factory.createNumericLiteral(String(prop['multipleOf']))), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createNumericLiteral('0')));
|
|
629
|
+
const refineOptions = ts.factory.createObjectLiteralExpression([
|
|
630
|
+
ts.factory.createPropertyAssignment(ts.factory.createIdentifier('message'), ts.factory.createStringLiteral(`Number must be a multiple of ${String(prop['multipleOf'])}`)),
|
|
631
|
+
]);
|
|
632
|
+
numberSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(numberSchema, ts.factory.createIdentifier('refine')), undefined, [refineFunction, refineOptions]);
|
|
633
|
+
}
|
|
634
|
+
return required
|
|
635
|
+
? numberSchema
|
|
636
|
+
: ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(numberSchema, ts.factory.createIdentifier('optional')), undefined, []);
|
|
637
|
+
}
|
|
638
|
+
case 'number': {
|
|
639
|
+
let numberSchema = this.buildZodAST(['number']);
|
|
640
|
+
// Apply number constraints
|
|
641
|
+
if (prop['minimum'] !== undefined && typeof prop['minimum'] === 'number') {
|
|
642
|
+
const minValue = prop['exclusiveMinimum'] && typeof prop['exclusiveMinimum'] === 'boolean'
|
|
643
|
+
? prop['minimum'] + 1
|
|
644
|
+
: prop['minimum'];
|
|
645
|
+
numberSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(numberSchema, ts.factory.createIdentifier(prop['exclusiveMinimum'] && typeof prop['exclusiveMinimum'] === 'boolean' ? 'gt' : 'gte')), undefined, [ts.factory.createNumericLiteral(String(minValue))]);
|
|
646
|
+
}
|
|
647
|
+
if (prop['maximum'] !== undefined && typeof prop['maximum'] === 'number') {
|
|
648
|
+
const maxValue = prop['exclusiveMaximum'] && typeof prop['exclusiveMaximum'] === 'boolean'
|
|
649
|
+
? prop['maximum'] - 1
|
|
650
|
+
: prop['maximum'];
|
|
651
|
+
numberSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(numberSchema, ts.factory.createIdentifier(prop['exclusiveMaximum'] ? 'lt' : 'lte')), undefined, [ts.factory.createNumericLiteral(String(maxValue))]);
|
|
652
|
+
}
|
|
653
|
+
if (typeof prop['multipleOf'] === 'number') {
|
|
654
|
+
const refineFunction = ts.factory.createArrowFunction(undefined, undefined, [ts.factory.createParameterDeclaration(undefined, undefined, 'val', undefined, undefined, undefined)], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createBinaryExpression(ts.factory.createBinaryExpression(ts.factory.createIdentifier('val'), ts.factory.createToken(ts.SyntaxKind.PercentToken), ts.factory.createNumericLiteral(String(prop['multipleOf']))), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createNumericLiteral('0')));
|
|
655
|
+
const refineOptions = ts.factory.createObjectLiteralExpression([
|
|
656
|
+
ts.factory.createPropertyAssignment(ts.factory.createIdentifier('message'), ts.factory.createStringLiteral(`Number must be a multiple of ${String(prop['multipleOf'])}`)),
|
|
657
|
+
]);
|
|
658
|
+
numberSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(numberSchema, ts.factory.createIdentifier('refine')), undefined, [refineFunction, refineOptions]);
|
|
659
|
+
}
|
|
660
|
+
return required
|
|
661
|
+
? numberSchema
|
|
662
|
+
: ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(numberSchema, ts.factory.createIdentifier('optional')), undefined, []);
|
|
663
|
+
}
|
|
664
|
+
case 'string': {
|
|
665
|
+
let stringSchema = this.buildZodAST(['string']);
|
|
666
|
+
// Apply string format
|
|
667
|
+
if (prop['format']) {
|
|
668
|
+
switch (prop['format']) {
|
|
669
|
+
case 'email':
|
|
670
|
+
stringSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('email')), undefined, []);
|
|
671
|
+
break;
|
|
672
|
+
case 'uri':
|
|
673
|
+
case 'url':
|
|
674
|
+
stringSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('url')), undefined, []);
|
|
675
|
+
break;
|
|
676
|
+
case 'uuid':
|
|
677
|
+
stringSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('uuid')), undefined, []);
|
|
678
|
+
break;
|
|
679
|
+
case 'date-time':
|
|
680
|
+
stringSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('datetime')), undefined, []);
|
|
681
|
+
break;
|
|
682
|
+
case 'date':
|
|
683
|
+
stringSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('date')), undefined, []);
|
|
684
|
+
break;
|
|
685
|
+
case 'time':
|
|
686
|
+
stringSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('time')), undefined, []);
|
|
687
|
+
break;
|
|
688
|
+
// Add more formats as needed
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// Apply string constraints
|
|
692
|
+
if (typeof prop['minLength'] === 'number') {
|
|
693
|
+
stringSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('min')), undefined, [ts.factory.createNumericLiteral(String(prop['minLength']))]);
|
|
694
|
+
}
|
|
695
|
+
if (typeof prop['maxLength'] === 'number') {
|
|
696
|
+
stringSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('max')), undefined, [ts.factory.createNumericLiteral(String(prop['maxLength']))]);
|
|
697
|
+
}
|
|
698
|
+
if (prop['pattern'] && typeof prop['pattern'] === 'string') {
|
|
699
|
+
stringSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('regex')), undefined, [
|
|
700
|
+
ts.factory.createNewExpression(ts.factory.createIdentifier('RegExp'), undefined, [
|
|
701
|
+
ts.factory.createStringLiteral(prop['pattern'], true),
|
|
702
|
+
]),
|
|
703
|
+
]);
|
|
704
|
+
}
|
|
705
|
+
// Apply default value if not required
|
|
706
|
+
if (!required && prop['default'] !== undefined) {
|
|
707
|
+
const defaultValue = this.buildDefaultValue(prop['default']);
|
|
708
|
+
stringSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('default')), undefined, [defaultValue]);
|
|
709
|
+
}
|
|
710
|
+
return required
|
|
711
|
+
? stringSchema
|
|
712
|
+
: ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('optional')), undefined, []);
|
|
713
|
+
}
|
|
714
|
+
case 'boolean': {
|
|
715
|
+
let booleanSchema = this.buildZodAST(['boolean']);
|
|
716
|
+
// Apply default value if not required
|
|
717
|
+
if (!required && prop['default'] !== undefined) {
|
|
718
|
+
const defaultValue = typeof prop['default'] === 'boolean'
|
|
719
|
+
? prop['default']
|
|
720
|
+
? ts.factory.createTrue()
|
|
721
|
+
: ts.factory.createFalse()
|
|
722
|
+
: ts.factory.createFalse();
|
|
723
|
+
booleanSchema = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(booleanSchema, ts.factory.createIdentifier('default')), undefined, [defaultValue]);
|
|
724
|
+
}
|
|
725
|
+
return required
|
|
726
|
+
? booleanSchema
|
|
727
|
+
: ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(booleanSchema, ts.factory.createIdentifier('optional')), undefined, []);
|
|
728
|
+
}
|
|
269
729
|
case 'unknown':
|
|
270
730
|
default:
|
|
271
731
|
return this.buildZodAST(['unknown', ...(!required ? ['optional'] : [])]);
|
|
272
732
|
}
|
|
273
733
|
}
|
|
734
|
+
buildDefaultValue(value) {
|
|
735
|
+
if (typeof value === 'string') {
|
|
736
|
+
return ts.factory.createStringLiteral(value, true);
|
|
737
|
+
}
|
|
738
|
+
if (typeof value === 'number') {
|
|
739
|
+
return ts.factory.createNumericLiteral(String(value));
|
|
740
|
+
}
|
|
741
|
+
if (typeof value === 'boolean') {
|
|
742
|
+
return value ? ts.factory.createTrue() : ts.factory.createFalse();
|
|
743
|
+
}
|
|
744
|
+
if (value === null) {
|
|
745
|
+
return ts.factory.createNull();
|
|
746
|
+
}
|
|
747
|
+
if (Array.isArray(value)) {
|
|
748
|
+
return ts.factory.createArrayLiteralExpression(value.map((item) => this.buildDefaultValue(item)), false);
|
|
749
|
+
}
|
|
750
|
+
if (typeof value === 'object') {
|
|
751
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
752
|
+
if (value === null) {
|
|
753
|
+
return ts.factory.createNull();
|
|
754
|
+
}
|
|
755
|
+
return ts.factory.createObjectLiteralExpression(Object.entries(value).map(([key, val]) => ts.factory.createPropertyAssignment(ts.factory.createIdentifier(key), this.buildDefaultValue(val))), true);
|
|
756
|
+
}
|
|
757
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
758
|
+
return ts.factory.createStringLiteral(String(value), true);
|
|
759
|
+
}
|
|
760
|
+
// For objects and arrays, we need to handle them differently
|
|
761
|
+
// This should not happen in practice, but we handle it for type safety
|
|
762
|
+
return ts.factory.createStringLiteral(JSON.stringify(value), true);
|
|
763
|
+
}
|
|
274
764
|
handleLogicalOperator(operator, schemas, required) {
|
|
275
765
|
const logicalExpression = this.buildLogicalOperator(operator, schemas);
|
|
276
766
|
return required
|
|
@@ -309,6 +799,7 @@ export class TypeScriptCodeGeneratorService {
|
|
|
309
799
|
}
|
|
310
800
|
buildSchemaFromLogicalOperator(schema) {
|
|
311
801
|
if (this.isReference(schema)) {
|
|
802
|
+
// In logical operators, references are always required (they're part of a union/intersection)
|
|
312
803
|
return this.buildFromReference(schema);
|
|
313
804
|
}
|
|
314
805
|
const safeSchema = SchemaProperties.safeParse(schema);
|