@codewithagents/openapi-server 1.3.2 → 1.5.0

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.
@@ -43,7 +43,7 @@ function deriveServiceName(spec) {
43
43
  .join('');
44
44
  if (pascal.length === 0)
45
45
  return 'ApiService';
46
- // Guard against numeric-start identifiers (e.g. '1Password Connect' '_1PasswordConnect')
46
+ // Guard against numeric-start identifiers (e.g. '1Password Connect' -> '_1PasswordConnect')
47
47
  const safePascal = /^[0-9]/.test(pascal) ? `_${pascal}` : pascal;
48
48
  if (safePascal.endsWith('Service'))
49
49
  return safePascal;
@@ -53,14 +53,14 @@ function deriveServiceName(spec) {
53
53
  * Converts a raw operationId into a valid camelCase JS identifier.
54
54
  * Handles kebab-case, snake_case, dots, spaces, parens, braces and other
55
55
  * non-alphanumeric separators found in real-world OpenAPI specs.
56
- * e.g. "post-applePay-sessions" "postApplePaySessions"
57
- * e.g. "calendar.calendars.insert" "calendarCalendarsInsert"
58
- * e.g. "Get User Profile" "getUserProfile"
59
- * e.g. "forgotPassword(oneTimeCode)" "forgotPasswordOneTimeCode"
56
+ * e.g. "post-applePay-sessions" -> "postApplePaySessions"
57
+ * e.g. "calendar.calendars.insert" -> "calendarCalendarsInsert"
58
+ * e.g. "Get User Profile" -> "getUserProfile"
59
+ * e.g. "forgotPassword(oneTimeCode)" -> "forgotPasswordOneTimeCode"
60
60
  */
61
61
  function sanitizeOperationId(id) {
62
62
  const parts = id
63
- .replace(/'/g, '') // strip apostrophes without splitting ("user's" "users")
63
+ .replace(/'/g, '') // strip apostrophes without splitting ("user's" -> "users")
64
64
  .split(/[^a-zA-Z0-9]+/) // split on any non-alphanumeric sequence
65
65
  .filter(Boolean);
66
66
  if (parts.length === 0)
@@ -150,6 +150,225 @@ function getQueryParams(operation, spec) {
150
150
  }
151
151
  return result;
152
152
  }
153
+ /**
154
+ * Map a schema format string to a Zod chain modifier.
155
+ * Returns an empty string when no specific format validation is needed.
156
+ */
157
+ function formatToZodModifier(format) {
158
+ switch (format) {
159
+ case 'uuid':
160
+ return '.uuid()';
161
+ case 'email':
162
+ return '.email()';
163
+ case 'uri':
164
+ case 'url':
165
+ return '.url()';
166
+ case 'date-time':
167
+ return '.datetime()';
168
+ default:
169
+ return '';
170
+ }
171
+ }
172
+ /**
173
+ * Build a Zod expression for a path parameter based on its schema.
174
+ * Returns undefined when the parameter does not need format validation
175
+ * (simple string with no format constraint or non-string types used as strings in URLs).
176
+ */
177
+ function pathParamZodExpr(schema) {
178
+ if (schema === undefined || isRef(schema))
179
+ return undefined;
180
+ const s = schema;
181
+ if (s.type !== 'string')
182
+ return undefined;
183
+ const format = s.format;
184
+ if (format === undefined)
185
+ return undefined;
186
+ const modifier = formatToZodModifier(format);
187
+ if (modifier === '')
188
+ return undefined;
189
+ return `z.string()${modifier}`;
190
+ }
191
+ /**
192
+ * Build a Zod expression for a query or header parameter based on its schema.
193
+ * Number/integer types use z.number() (after coercion by extraction code).
194
+ * String types use z.string(). Boolean types use z.boolean().
195
+ * Appends .optional() for non-required params.
196
+ */
197
+ function paramZodExpr(tsType, required, schema) {
198
+ let base;
199
+ if (tsType === 'number') {
200
+ base = 'z.number()';
201
+ }
202
+ else if (tsType === 'boolean') {
203
+ base = 'z.boolean()';
204
+ }
205
+ else {
206
+ // string
207
+ if (schema !== undefined && !isRef(schema)) {
208
+ const s = schema;
209
+ const format = s.format;
210
+ const modifier = format !== undefined ? formatToZodModifier(format) : '';
211
+ base = `z.string()${modifier}`;
212
+ }
213
+ else {
214
+ base = 'z.string()';
215
+ }
216
+ }
217
+ return required ? base : `${base}.optional()`;
218
+ }
219
+ /**
220
+ * Collect path parameters that have Zod format constraints.
221
+ * Only returns entries for params that need format validation (e.g. uuid).
222
+ * Simple string params with no format constraint are excluded.
223
+ */
224
+ function getPathParamValidations(operation, spec, rawPathParamNames) {
225
+ const parameters = operation.parameters;
226
+ if (parameters === undefined)
227
+ return [];
228
+ // Build a name-to-zodExpr map from path params to avoid nested loops.
229
+ const zodByName = new Map();
230
+ for (const p of parameters) {
231
+ const resolved = resolveParam(p, spec);
232
+ if (resolved === undefined || resolved.in !== 'path')
233
+ continue;
234
+ const schema = resolved.schema;
235
+ const zodExpr = pathParamZodExpr(schema);
236
+ if (zodExpr !== undefined)
237
+ zodByName.set(resolved.name, zodExpr);
238
+ }
239
+ const result = [];
240
+ for (const rawName of rawPathParamNames) {
241
+ const zodExpr = zodByName.get(rawName);
242
+ if (zodExpr !== undefined)
243
+ result.push({ rawName, zodExpr });
244
+ }
245
+ return result;
246
+ }
247
+ /**
248
+ * Collect header parameters from an operation.
249
+ */
250
+ function getHeaderParams(operation, spec) {
251
+ const parameters = operation.parameters;
252
+ if (parameters === undefined)
253
+ return [];
254
+ const result = [];
255
+ for (const p of parameters) {
256
+ const resolved = resolveParam(p, spec);
257
+ if (resolved === undefined || resolved.in !== 'header')
258
+ continue;
259
+ result.push({
260
+ rawName: resolved.name,
261
+ required: resolved.required === true,
262
+ });
263
+ }
264
+ return result;
265
+ }
266
+ /**
267
+ * Determine whether query params need a Zod validation block.
268
+ * Triggered when any param is required or has a non-string type (to catch NaN/invalid input).
269
+ */
270
+ function queryParamsNeedValidation(queryParams) {
271
+ return queryParams.some((q) => q.required || q.tsType !== 'string');
272
+ }
273
+ /**
274
+ * Emit Zod validation lines for query parameters into the handler line buffer.
275
+ * Uses the already-extracted params object (after Number() coercion).
276
+ * Uses short variable name _qv to keep the 422 return line under Prettier's print width.
277
+ * @param indent - outer handler indent (e.g. ' ')
278
+ */
279
+ function emitQueryValidation(lines, queryParams, indent) {
280
+ const inner = `${indent} `;
281
+ const fieldIndent = `${indent} `;
282
+ const fields = queryParams
283
+ .map((q) => {
284
+ const expr = paramZodExpr(q.tsType, q.required);
285
+ return `${fieldIndent}${q.name}: ${expr}`;
286
+ })
287
+ .join(',\n');
288
+ lines.push(`${inner}// Validate query parameters: returns 422 with Zod issues on failure`);
289
+ lines.push(`${inner}const _qv = z.object({`);
290
+ lines.push(fields);
291
+ lines.push(`${inner}}).safeParse(params)`);
292
+ }
293
+ /**
294
+ * Emit Zod validation lines for path parameters (format constraints) into the handler line buffer.
295
+ * Uses short variable name _pv to keep the 422 return line under Prettier's print width.
296
+ * @param indent - outer handler indent (e.g. ' ')
297
+ * @param framework - used to generate the correct param accessor syntax
298
+ */
299
+ function emitPathValidation(lines, validations, indent, framework) {
300
+ const inner = `${indent} `;
301
+ const fieldIndent = `${indent} `;
302
+ const schemaFields = validations
303
+ .map((v) => {
304
+ const key = /[^a-zA-Z0-9_$]/.test(v.rawName) ? JSON.stringify(v.rawName) : v.rawName;
305
+ return `${fieldIndent}${key}: ${v.zodExpr}`;
306
+ })
307
+ .join(',\n');
308
+ const rawFields = validations
309
+ .map((v) => {
310
+ const key = /[^a-zA-Z0-9_$]/.test(v.rawName) ? JSON.stringify(v.rawName) : v.rawName;
311
+ let access;
312
+ if (framework === 'hono') {
313
+ access = `c.req.param(${JSON.stringify(v.rawName)})`;
314
+ }
315
+ else if (framework === 'express') {
316
+ access = `req.params[${JSON.stringify(v.rawName)}]`;
317
+ }
318
+ else {
319
+ access = /[^a-zA-Z0-9_$]/.test(v.rawName)
320
+ ? `req.params[${JSON.stringify(v.rawName)}]`
321
+ : `req.params.${v.rawName}`;
322
+ }
323
+ return `${fieldIndent}${key}: ${access}`;
324
+ })
325
+ .join(',\n');
326
+ lines.push(`${inner}// Validate path parameters: returns 422 with Zod issues on failure`);
327
+ lines.push(`${inner}const _pv = z.object({`);
328
+ lines.push(schemaFields);
329
+ lines.push(`${inner}}).safeParse({`);
330
+ lines.push(rawFields);
331
+ lines.push(`${inner}})`);
332
+ }
333
+ /**
334
+ * Emit Zod validation lines for header parameters into the handler line buffer.
335
+ * Uses short variable name _hv to keep the 422 return line under Prettier's print width.
336
+ * @param indent - outer handler indent (e.g. ' ')
337
+ * @param framework - used to generate the correct header accessor syntax
338
+ */
339
+ function emitHeaderValidation(lines, headerParams, indent, framework) {
340
+ const inner = `${indent} `;
341
+ const fieldIndent = `${indent} `;
342
+ const schemaFields = headerParams
343
+ .map((h) => {
344
+ const key = JSON.stringify(h.rawName);
345
+ const expr = h.required ? 'z.string()' : 'z.string().optional()';
346
+ return `${fieldIndent}${key}: ${expr}`;
347
+ })
348
+ .join(',\n');
349
+ const rawFields = headerParams
350
+ .map((h) => {
351
+ const key = JSON.stringify(h.rawName);
352
+ let access;
353
+ if (framework === 'hono') {
354
+ access = `c.req.header(${key})`;
355
+ }
356
+ else if (framework === 'express') {
357
+ access = `req.headers[${key}] as string | undefined`;
358
+ }
359
+ else {
360
+ access = `req.headers[${key}]`;
361
+ }
362
+ return `${fieldIndent}${key}: ${access}`;
363
+ })
364
+ .join(',\n');
365
+ lines.push(`${inner}// Validate request headers: returns 422 with Zod issues on failure`);
366
+ lines.push(`${inner}const _hv = z.object({`);
367
+ lines.push(schemaFields);
368
+ lines.push(`${inner}}).safeParse({`);
369
+ lines.push(rawFields);
370
+ lines.push(`${inner}})`);
371
+ }
153
372
  function getBodyInfo(operation) {
154
373
  const requestBody = operation.requestBody;
155
374
  if (requestBody === undefined)
@@ -205,7 +424,9 @@ function collectOperations(spec) {
205
424
  continue;
206
425
  const methodName = deriveMethodName(operation.operationId, method, path);
207
426
  const pathParams = extractPathParamsFromPath(path);
427
+ const pathParamValidations = getPathParamValidations(operation, spec, pathParams);
208
428
  const queryParams = getQueryParams(operation, spec);
429
+ const headerParams = getHeaderParams(operation, spec);
209
430
  const bodyInfo = getBodyInfo(operation);
210
431
  const responseStatus = getResponseStatus(operation, method);
211
432
  operations.push({
@@ -214,7 +435,9 @@ function collectOperations(spec) {
214
435
  path,
215
436
  honoPath: toHonoPath(path),
216
437
  pathParams,
438
+ pathParamValidations,
217
439
  queryParams,
440
+ headerParams,
218
441
  bodyInfo,
219
442
  responseStatus,
220
443
  });
@@ -222,10 +445,53 @@ function collectOperations(spec) {
222
445
  }
223
446
  return operations;
224
447
  }
448
+ /** Collect sorted body type names from all operations. */
449
+ function collectSortedBodyTypes(operations) {
450
+ const bodyTypes = new Set();
451
+ for (const op of operations) {
452
+ if (op.bodyInfo?.typeName !== undefined)
453
+ bodyTypes.add(op.bodyInfo.typeName);
454
+ }
455
+ return Array.from(bodyTypes).sort();
456
+ }
457
+ /** Collect the subset of schemaNames actually used by the given operations. */
458
+ function collectUsedSchemaNames(operations, schemaNames) {
459
+ const used = new Set();
460
+ for (const op of operations) {
461
+ const typeName = op.bodyInfo?.typeName;
462
+ if (typeName === undefined)
463
+ continue;
464
+ const schemaName = `${typeName}Schema`;
465
+ if (schemaNames.has(schemaName))
466
+ used.add(schemaName);
467
+ }
468
+ return used;
469
+ }
470
+ /**
471
+ * Collect body type names, used schema names, and whether Zod is needed.
472
+ * Shared by all three generator functions to avoid duplication.
473
+ */
474
+ function collectGeneratorSetup(operations, options) {
475
+ const sortedBodyTypes = collectSortedBodyTypes(operations);
476
+ const usedSchemaNames = options?.schemaNames !== undefined
477
+ ? collectUsedSchemaNames(operations, options.schemaNames)
478
+ : new Set();
479
+ const needsZod = (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) ||
480
+ operationsNeedZodForParams(operations);
481
+ return { sortedBodyTypes, usedSchemaNames, needsZod };
482
+ }
483
+ // ── Hono route handler ────────────────────────────────────────────────────────
225
484
  // fallow-ignore-next-line complexity
226
485
  function buildRouteHandler(op, indent, schemaNames) {
227
486
  const lines = [];
228
487
  lines.push(`${indent}app.${op.httpMethod}(${JSON.stringify(op.honoPath)}, async (c) => {`);
488
+ // Path param format validation (e.g. uuid)
489
+ if (op.pathParamValidations.length > 0) {
490
+ emitPathValidation(lines, op.pathParamValidations, indent, 'hono');
491
+ lines.push(`${indent} if (!_pv.success) {`);
492
+ lines.push(`${indent} return c.json({ error: 'Invalid path parameters', issues: _pv.error.issues }, 422)`);
493
+ lines.push(`${indent} }`);
494
+ }
229
495
  // Query params extraction
230
496
  if (op.queryParams.length > 0) {
231
497
  const fields = op.queryParams
@@ -239,6 +505,20 @@ function buildRouteHandler(op, indent, schemaNames) {
239
505
  lines.push(`${indent} const params = {`);
240
506
  lines.push(fields);
241
507
  lines.push(`${indent} }`);
508
+ // Validate query params when there are required or typed (non-string) params
509
+ if (queryParamsNeedValidation(op.queryParams)) {
510
+ emitQueryValidation(lines, op.queryParams, indent);
511
+ lines.push(`${indent} if (!_qv.success) {`);
512
+ lines.push(`${indent} return c.json({ error: 'Invalid query parameters', issues: _qv.error.issues }, 422)`);
513
+ lines.push(`${indent} }`);
514
+ }
515
+ }
516
+ // Header param validation
517
+ if (op.headerParams.length > 0) {
518
+ emitHeaderValidation(lines, op.headerParams, indent, 'hono');
519
+ lines.push(`${indent} if (!_hv.success) {`);
520
+ lines.push(`${indent} return c.json({ error: 'Invalid request headers', issues: _hv.error.issues }, 422)`);
521
+ lines.push(`${indent} }`);
242
522
  }
243
523
  // Body extraction
244
524
  let bodyVarName = 'body';
@@ -283,11 +563,18 @@ function buildRouteHandler(op, indent, schemaNames) {
283
563
  lines.push(`${indent}})`);
284
564
  return lines.join('\n');
285
565
  }
286
- // ── Express router generator ───────────────────────────────────────────────────
566
+ // ── Express route handler ─────────────────────────────────────────────────────
287
567
  // fallow-ignore-next-line complexity
288
568
  function buildExpressRouteHandler(op, indent, schemaNames) {
289
569
  const lines = [];
290
570
  lines.push(`${indent}router.${op.httpMethod}(${JSON.stringify(op.honoPath)}, async (req: Request, res: Response) => {`);
571
+ // Path param format validation (e.g. uuid)
572
+ if (op.pathParamValidations.length > 0) {
573
+ emitPathValidation(lines, op.pathParamValidations, indent, 'express');
574
+ lines.push(`${indent} if (!_pv.success) {`);
575
+ lines.push(`${indent} return void res.status(422).json({ error: 'Invalid path parameters', issues: _pv.error.issues })`);
576
+ lines.push(`${indent} }`);
577
+ }
291
578
  // Query params extraction
292
579
  if (op.queryParams.length > 0) {
293
580
  const fields = op.queryParams
@@ -304,6 +591,20 @@ function buildExpressRouteHandler(op, indent, schemaNames) {
304
591
  lines.push(`${indent} const params = {`);
305
592
  lines.push(fields);
306
593
  lines.push(`${indent} }`);
594
+ // Validate query params when there are required or typed (non-string) params
595
+ if (queryParamsNeedValidation(op.queryParams)) {
596
+ emitQueryValidation(lines, op.queryParams, indent);
597
+ lines.push(`${indent} if (!_qv.success) {`);
598
+ lines.push(`${indent} return void res.status(422).json({ error: 'Invalid query parameters', issues: _qv.error.issues })`);
599
+ lines.push(`${indent} }`);
600
+ }
601
+ }
602
+ // Header param validation
603
+ if (op.headerParams.length > 0) {
604
+ emitHeaderValidation(lines, op.headerParams, indent, 'express');
605
+ lines.push(`${indent} if (!_hv.success) {`);
606
+ lines.push(`${indent} return void res.status(422).json({ error: 'Invalid request headers', issues: _hv.error.issues })`);
607
+ lines.push(`${indent} }`);
307
608
  }
308
609
  // Body extraction, with optional Zod validation
309
610
  let bodyVarName = 'body';
@@ -350,63 +651,7 @@ function buildExpressRouteHandler(op, indent, schemaNames) {
350
651
  lines.push(`${indent}})`);
351
652
  return lines.join('\n');
352
653
  }
353
- // fallow-ignore-next-line complexity
354
- export function generateExpressRouter(spec, options) {
355
- const serviceName = deriveServiceName(spec);
356
- const operations = collectOperations(spec);
357
- // Collect body type names for import from models.js
358
- const bodyTypes = new Set();
359
- for (const op of operations) {
360
- if (op.bodyInfo?.typeName !== undefined) {
361
- bodyTypes.add(op.bodyInfo.typeName);
362
- }
363
- }
364
- const sortedBodyTypes = Array.from(bodyTypes).sort();
365
- // Collect which schema names are actually needed (only for ops with a matching schema)
366
- const usedSchemaNames = new Set();
367
- if (options?.schemaNames !== undefined) {
368
- for (const op of operations) {
369
- const typeName = op.bodyInfo?.typeName;
370
- if (typeName !== undefined) {
371
- const schemaName = `${typeName}Schema`;
372
- if (options.schemaNames.has(schemaName)) {
373
- usedSchemaNames.add(schemaName);
374
- }
375
- }
376
- }
377
- }
378
- const lines = [];
379
- lines.push('// This file is auto-generated. Do not edit manually.');
380
- lines.push('// Express: apply express.json() middleware before mounting this router so req.body is populated.');
381
- lines.push('');
382
- lines.push("import { Router } from 'express'");
383
- lines.push("import type { Request, Response } from 'express'");
384
- if (sortedBodyTypes.length > 0) {
385
- lines.push(`import type { ${sortedBodyTypes.join(', ')} } from './models.js'`);
386
- }
387
- lines.push(`import type { ${serviceName} } from './service.js'`);
388
- if (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) {
389
- lines.push(`import { z } from 'zod'`);
390
- const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
391
- lines.push(`import { ${sortedUsedSchemas.join(', ')} } from '${options.schemaImportPath}'`);
392
- }
393
- lines.push('');
394
- lines.push(`export function createRouter(service: ${serviceName}): Router {`);
395
- lines.push(' const router = Router()');
396
- lines.push('');
397
- for (const op of operations) {
398
- lines.push(buildExpressRouteHandler(op, ' ', options?.schemaNames));
399
- lines.push('');
400
- }
401
- lines.push(' return router');
402
- lines.push('}');
403
- lines.push('');
404
- return {
405
- filename: 'router.ts',
406
- content: lines.join('\n'),
407
- };
408
- }
409
- // ── Fastify router generator ───────────────────────────────────────────────────
654
+ // ── Fastify route handler ─────────────────────────────────────────────────────
410
655
  // fallow-ignore-next-line complexity
411
656
  function buildFastifyRouteHandler(op, indent, schemaNames) {
412
657
  const lines = [];
@@ -436,12 +681,42 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
436
681
  }
437
682
  const generic = genericParts.length > 0 ? `<{ ${genericParts.join('; ')} }>` : '';
438
683
  lines.push(`${indent}app.${op.httpMethod}${generic}(${JSON.stringify(op.honoPath)}, async (req, reply) => {`);
684
+ // Path param format validation (e.g. uuid)
685
+ if (op.pathParamValidations.length > 0) {
686
+ emitPathValidation(lines, op.pathParamValidations, indent, 'fastify');
687
+ lines.push(`${indent} if (!_pv.success) {`);
688
+ lines.push(`${indent} return reply.status(422).send({`);
689
+ lines.push(`${indent} error: 'Invalid path parameters',`);
690
+ lines.push(`${indent} issues: _pv.error.issues,`);
691
+ lines.push(`${indent} })`);
692
+ lines.push(`${indent} }`);
693
+ }
439
694
  // Query params extraction
440
695
  if (op.queryParams.length > 0) {
441
696
  const fields = op.queryParams.map((q) => ` ${q.name}: req.query.${q.name}`).join(',\n');
442
697
  lines.push(`${indent} const params = {`);
443
698
  lines.push(fields);
444
699
  lines.push(`${indent} }`);
700
+ // Validate query params when there are required or typed (non-string) params
701
+ if (queryParamsNeedValidation(op.queryParams)) {
702
+ emitQueryValidation(lines, op.queryParams, indent);
703
+ lines.push(`${indent} if (!_qv.success) {`);
704
+ lines.push(`${indent} return reply.status(422).send({`);
705
+ lines.push(`${indent} error: 'Invalid query parameters',`);
706
+ lines.push(`${indent} issues: _qv.error.issues,`);
707
+ lines.push(`${indent} })`);
708
+ lines.push(`${indent} }`);
709
+ }
710
+ }
711
+ // Header param validation
712
+ if (op.headerParams.length > 0) {
713
+ emitHeaderValidation(lines, op.headerParams, indent, 'fastify');
714
+ lines.push(`${indent} if (!_hv.success) {`);
715
+ lines.push(`${indent} return reply.status(422).send({`);
716
+ lines.push(`${indent} error: 'Invalid request headers',`);
717
+ lines.push(`${indent} issues: _hv.error.issues,`);
718
+ lines.push(`${indent} })`);
719
+ lines.push(`${indent} }`);
445
720
  }
446
721
  // Body handling, with optional Zod validation
447
722
  let bodyVarName = 'req.body';
@@ -484,31 +759,67 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
484
759
  lines.push(`${indent}})`);
485
760
  return lines.join('\n');
486
761
  }
762
+ // ── Zod import helpers ────────────────────────────────────────────────────────
763
+ /**
764
+ * Returns true when any operation in the list generates param validation code
765
+ * that requires Zod (path format validation, required/typed query params, or header params).
766
+ */
767
+ function operationsNeedZodForParams(operations) {
768
+ for (const op of operations) {
769
+ if (op.pathParamValidations.length > 0)
770
+ return true;
771
+ if (queryParamsNeedValidation(op.queryParams))
772
+ return true;
773
+ if (op.headerParams.length > 0)
774
+ return true;
775
+ }
776
+ return false;
777
+ }
778
+ // ── Express router generator ──────────────────────────────────────────────────
487
779
  // fallow-ignore-next-line complexity
488
- export function generateFastifyRouter(spec, options) {
780
+ export function generateExpressRouter(spec, options) {
489
781
  const serviceName = deriveServiceName(spec);
490
782
  const operations = collectOperations(spec);
491
- // Collect body type names for import from models.js
492
- const bodyTypes = new Set();
493
- for (const op of operations) {
494
- if (op.bodyInfo?.typeName !== undefined) {
495
- bodyTypes.add(op.bodyInfo.typeName);
496
- }
783
+ const { sortedBodyTypes, usedSchemaNames, needsZod } = collectGeneratorSetup(operations, options);
784
+ const lines = [];
785
+ lines.push('// This file is auto-generated. Do not edit manually.');
786
+ lines.push('// Express: apply express.json() middleware before mounting this router so req.body is populated.');
787
+ lines.push('');
788
+ lines.push("import { Router } from 'express'");
789
+ lines.push("import type { Request, Response } from 'express'");
790
+ if (sortedBodyTypes.length > 0) {
791
+ lines.push(`import type { ${sortedBodyTypes.join(', ')} } from './models.js'`);
497
792
  }
498
- const sortedBodyTypes = Array.from(bodyTypes).sort();
499
- // Collect which schema names are actually needed (only for ops with a matching schema)
500
- const usedSchemaNames = new Set();
501
- if (options?.schemaNames !== undefined) {
502
- for (const op of operations) {
503
- const typeName = op.bodyInfo?.typeName;
504
- if (typeName !== undefined) {
505
- const schemaName = `${typeName}Schema`;
506
- if (options.schemaNames.has(schemaName)) {
507
- usedSchemaNames.add(schemaName);
508
- }
509
- }
510
- }
793
+ lines.push(`import type { ${serviceName} } from './service.js'`);
794
+ if (needsZod) {
795
+ lines.push(`import { z } from 'zod'`);
511
796
  }
797
+ if (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) {
798
+ const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
799
+ lines.push(`import { ${sortedUsedSchemas.join(', ')} } from '${options.schemaImportPath}'`);
800
+ }
801
+ lines.push('');
802
+ lines.push(`export function createRouter(service: ${serviceName}): Router {`);
803
+ lines.push(' const router = Router()');
804
+ lines.push('');
805
+ for (const op of operations) {
806
+ lines.push(buildExpressRouteHandler(op, ' ', options?.schemaNames));
807
+ lines.push('');
808
+ }
809
+ lines.push(' return router');
810
+ lines.push('}');
811
+ lines.push('');
812
+ return {
813
+ filename: 'router.ts',
814
+ content: lines.join('\n'),
815
+ };
816
+ }
817
+ // ── Fastify router generator ──────────────────────────────────────────────────
818
+ // fallow-ignore-next-line complexity
819
+ export function generateFastifyRouter(spec, options) {
820
+ const serviceName = deriveServiceName(spec);
821
+ const operations = collectOperations(spec);
822
+ const { sortedBodyTypes, usedSchemaNames, needsZod } = collectGeneratorSetup(operations, options);
512
823
  const lines = [];
513
824
  lines.push('// This file is auto-generated. Do not edit manually.');
514
825
  lines.push('');
@@ -517,8 +828,10 @@ export function generateFastifyRouter(spec, options) {
517
828
  lines.push(`import type { ${sortedBodyTypes.join(', ')} } from './models.js'`);
518
829
  }
519
830
  lines.push(`import type { ${serviceName} } from './service.js'`);
520
- if (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) {
831
+ if (needsZod) {
521
832
  lines.push(`import { z } from 'zod'`);
833
+ }
834
+ if (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) {
522
835
  const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
523
836
  lines.push(`import { ${sortedUsedSchemas.join(', ')} } from '${options.schemaImportPath}'`);
524
837
  }
@@ -540,27 +853,7 @@ export function generateFastifyRouter(spec, options) {
540
853
  export function generateRouter(spec, options) {
541
854
  const serviceName = deriveServiceName(spec);
542
855
  const operations = collectOperations(spec);
543
- // Collect body type names for import from models.js
544
- const bodyTypes = new Set();
545
- for (const op of operations) {
546
- if (op.bodyInfo?.typeName !== undefined) {
547
- bodyTypes.add(op.bodyInfo.typeName);
548
- }
549
- }
550
- const sortedBodyTypes = Array.from(bodyTypes).sort();
551
- // Collect which schema names are actually needed (only for ops with a matching schema)
552
- const usedSchemaNames = new Set();
553
- if (options?.schemaNames !== undefined) {
554
- for (const op of operations) {
555
- const typeName = op.bodyInfo?.typeName;
556
- if (typeName !== undefined) {
557
- const schemaName = `${typeName}Schema`;
558
- if (options.schemaNames.has(schemaName)) {
559
- usedSchemaNames.add(schemaName);
560
- }
561
- }
562
- }
563
- }
856
+ const { sortedBodyTypes, usedSchemaNames, needsZod } = collectGeneratorSetup(operations, options);
564
857
  const lines = [];
565
858
  lines.push('// This file is auto-generated. Do not edit manually.');
566
859
  lines.push('');
@@ -569,8 +862,10 @@ export function generateRouter(spec, options) {
569
862
  lines.push(`import type { ${sortedBodyTypes.join(', ')} } from './models.js'`);
570
863
  }
571
864
  lines.push(`import type { ${serviceName} } from './service.js'`);
572
- if (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) {
865
+ if (needsZod) {
573
866
  lines.push(`import { z } from 'zod'`);
867
+ }
868
+ if (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) {
574
869
  const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
575
870
  lines.push(`import { ${sortedUsedSchemas.join(', ')} } from '${options.schemaImportPath}'`);
576
871
  }