@vertz/openapi 0.1.0 → 0.1.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.
@@ -0,0 +1,1481 @@
1
+ // src/adapter/identifier.ts
2
+ function splitWords(name) {
3
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^A-Za-z0-9]+/g, " ").split(/\s+/).filter(Boolean);
4
+ }
5
+ function sanitizeIdentifier(name) {
6
+ const words = splitWords(name);
7
+ if (words.length === 0) {
8
+ return "_";
9
+ }
10
+ const [first, ...rest] = words.map((word) => word.toLowerCase());
11
+ const camelCase = first + rest.map((word) => word[0]?.toUpperCase() + word.slice(1)).join("");
12
+ return /^[0-9]/.test(camelCase) ? `_${camelCase}` : camelCase;
13
+ }
14
+
15
+ // src/adapter/resource-grouper.ts
16
+ function getPathGroupKey(path) {
17
+ const meaningfulSegments = path.split("/").filter(Boolean).filter((segment) => segment !== "api").filter((segment) => !/^v\d+$/i.test(segment)).filter((segment) => !(segment.startsWith("{") && segment.endsWith("}")));
18
+ return meaningfulSegments[meaningfulSegments.length - 1] ?? "_ungrouped";
19
+ }
20
+ function toResourceName(identifier) {
21
+ return identifier === "_ungrouped" ? "Ungrouped" : identifier.charAt(0).toUpperCase() + identifier.slice(1);
22
+ }
23
+ function groupOperations(operations, strategy, options) {
24
+ const excludeSet = options?.excludeTags ? new Set(options.excludeTags) : undefined;
25
+ const resources = new Map;
26
+ for (const operation of operations) {
27
+ if (excludeSet && operation.tags.some((t) => excludeSet.has(t))) {
28
+ continue;
29
+ }
30
+ const groupKey = strategy === "tag" ? operation.tags[0] ?? "_ungrouped" : strategy === "none" ? operation.operationId : getPathGroupKey(operation.path);
31
+ const identifier = groupKey === "_ungrouped" ? "_ungrouped" : sanitizeIdentifier(groupKey);
32
+ const existing = resources.get(identifier) ?? [];
33
+ existing.push(operation);
34
+ resources.set(identifier, existing);
35
+ }
36
+ return [...resources.entries()].map(([identifier, resourceOperations]) => ({
37
+ name: toResourceName(identifier),
38
+ identifier,
39
+ operations: resourceOperations
40
+ }));
41
+ }
42
+
43
+ // src/generators/client-generator.ts
44
+ function camelCase(name) {
45
+ return name.charAt(0).toLowerCase() + name.slice(1);
46
+ }
47
+ function generateAuthField(scheme) {
48
+ switch (scheme.type) {
49
+ case "bearer":
50
+ return ` ${camelCase(scheme.name)}?: string | (() => string | Promise<string>);`;
51
+ case "basic":
52
+ return ` ${camelCase(scheme.name)}?: { username: string; password: string };`;
53
+ case "apiKey":
54
+ return ` ${camelCase(scheme.name)}?: string | (() => string | Promise<string>);`;
55
+ case "oauth2":
56
+ return ` ${camelCase(scheme.name)}?: string | (() => string | Promise<string>);`;
57
+ }
58
+ }
59
+ function generateAuthStrategy(scheme) {
60
+ const fieldName = camelCase(scheme.name);
61
+ const lines = [];
62
+ switch (scheme.type) {
63
+ case "bearer":
64
+ lines.push(` ...(options.auth?.${fieldName} ? [{`);
65
+ lines.push(` type: 'bearer' as const,`);
66
+ lines.push(` token: options.auth?.${fieldName},`);
67
+ lines.push(" }] : []),");
68
+ break;
69
+ case "basic":
70
+ lines.push(` ...(options.auth?.${fieldName} ? [{`);
71
+ lines.push(` type: 'basic' as const,`);
72
+ lines.push(` ...options.auth?.${fieldName},`);
73
+ lines.push(" }] : []),");
74
+ break;
75
+ case "apiKey":
76
+ lines.push(` ...(options.auth?.${fieldName} ? [{`);
77
+ lines.push(` type: 'apiKey' as const,`);
78
+ lines.push(` key: options.auth?.${fieldName},`);
79
+ lines.push(` location: '${scheme.in}' as const,`);
80
+ lines.push(` name: '${scheme.paramName}',`);
81
+ lines.push(" }] : []),");
82
+ break;
83
+ case "oauth2":
84
+ lines.push(` ...(options.auth?.${fieldName} ? [{`);
85
+ lines.push(` type: 'bearer' as const,`);
86
+ lines.push(` token: options.auth?.${fieldName},`);
87
+ lines.push(" }] : []),");
88
+ break;
89
+ }
90
+ return lines;
91
+ }
92
+ function generateClient(resources, config) {
93
+ const defaultBaseURL = config.baseURL ? `'${config.baseURL.replace(/'/g, "\\'")}'` : "''";
94
+ const schemes = config.securitySchemes ?? [];
95
+ const hasAuth = schemes.length > 0;
96
+ const lines = [];
97
+ lines.push("// Generated by @vertz/openapi — do not edit");
98
+ lines.push("");
99
+ lines.push("import { FetchClient } from '@vertz/fetch';");
100
+ lines.push("import type { FetchClientConfig } from '@vertz/fetch';");
101
+ for (const r of resources) {
102
+ lines.push(`import { create${r.name}Resource } from './resources/${r.identifier}';`);
103
+ }
104
+ lines.push("");
105
+ if (hasAuth) {
106
+ lines.push("export interface ClientAuth {");
107
+ for (const scheme of schemes) {
108
+ lines.push(generateAuthField(scheme));
109
+ }
110
+ lines.push("}");
111
+ lines.push("");
112
+ }
113
+ if (hasAuth) {
114
+ lines.push("export type ClientOptions = FetchClientConfig & { auth?: ClientAuth };");
115
+ } else {
116
+ lines.push("export type ClientOptions = FetchClientConfig;");
117
+ }
118
+ lines.push("");
119
+ lines.push("export function createClient(options: ClientOptions = {}) {");
120
+ lines.push(` const client = new FetchClient({`);
121
+ lines.push(` baseURL: ${defaultBaseURL},`);
122
+ if (hasAuth) {
123
+ lines.push(" authStrategies: [");
124
+ for (const scheme of schemes) {
125
+ lines.push(...generateAuthStrategy(scheme));
126
+ }
127
+ lines.push(" ],");
128
+ }
129
+ lines.push(" ...options,");
130
+ lines.push(" });");
131
+ lines.push("");
132
+ lines.push(" return {");
133
+ for (const r of resources) {
134
+ lines.push(` ${r.identifier}: create${r.name}Resource(client),`);
135
+ }
136
+ lines.push(" };");
137
+ lines.push("}");
138
+ lines.push("");
139
+ lines.push("export type Client = ReturnType<typeof createClient>;");
140
+ lines.push("");
141
+ return { path: "client.ts", content: lines.join(`
142
+ `) };
143
+ }
144
+
145
+ // src/generators/json-schema-to-ts.ts
146
+ function jsonSchemaToTS(schema, namedSchemas) {
147
+ if (typeof schema.$circular === "string") {
148
+ return schema.$circular;
149
+ }
150
+ if (Array.isArray(schema.enum)) {
151
+ return schema.enum.map((v) => typeof v === "number" ? String(v) : `'${String(v).replace(/'/g, "\\'")}'`).join(" | ");
152
+ }
153
+ if (Array.isArray(schema.anyOf)) {
154
+ const members = schema.anyOf.map((s) => jsonSchemaToTS(s, namedSchemas));
155
+ return [...new Set(members)].join(" | ");
156
+ }
157
+ if (Array.isArray(schema.oneOf)) {
158
+ const members = schema.oneOf.map((s) => jsonSchemaToTS(s, namedSchemas));
159
+ return [...new Set(members)].join(" | ");
160
+ }
161
+ const type = schema.type;
162
+ if (type === "null")
163
+ return "null";
164
+ if (Array.isArray(type)) {
165
+ const nonNull = type.filter((t) => t !== "null");
166
+ const baseType = nonNull.length === 1 ? mapPrimitive(nonNull[0]) : "unknown";
167
+ return type.includes("null") ? `${baseType} | null` : baseType;
168
+ }
169
+ if (type === "string")
170
+ return "string";
171
+ if (type === "number" || type === "integer")
172
+ return "number";
173
+ if (type === "boolean")
174
+ return "boolean";
175
+ if (type === "array") {
176
+ const items = schema.items;
177
+ if (!items)
178
+ return "unknown[]";
179
+ const itemType = jsonSchemaToTS(items, namedSchemas);
180
+ const needsParens = itemType.includes("|") && !itemType.includes("{");
181
+ return needsParens ? `(${itemType})[]` : `${itemType}[]`;
182
+ }
183
+ if (type === "object") {
184
+ if (schema.additionalProperties === true && !schema.properties) {
185
+ return "Record<string, unknown>";
186
+ }
187
+ const properties = schema.properties;
188
+ if (!properties)
189
+ return "Record<string, unknown>";
190
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
191
+ const entries = Object.entries(properties).map(([key, propSchema]) => {
192
+ const tsType = jsonSchemaToTS(propSchema, namedSchemas);
193
+ const optional = required.has(key) ? "" : "?";
194
+ const safeKey = isValidIdentifier(key) ? key : `'${key.replace(/'/g, "\\'")}'`;
195
+ return `${safeKey}${optional}: ${tsType}`;
196
+ });
197
+ return `{ ${entries.join("; ")} }`;
198
+ }
199
+ return "unknown";
200
+ }
201
+ function sanitizeTypeName(name) {
202
+ if (isValidIdentifier(name))
203
+ return name;
204
+ const cleaned = name.replace(/[^A-Za-z0-9_$]+/g, " ").trim();
205
+ if (!cleaned)
206
+ return "_";
207
+ const segments = cleaned.split(/\s+/).filter(Boolean);
208
+ const result = segments.map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
209
+ return /^[0-9]/.test(result) ? `_${result}` : result;
210
+ }
211
+ function generateInterface(name, schema, namedSchemas) {
212
+ const safeName = sanitizeTypeName(name);
213
+ const properties = schema.properties;
214
+ if (!properties) {
215
+ return `export interface ${safeName} {}
216
+ `;
217
+ }
218
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
219
+ const lines = Object.entries(properties).map(([key, propSchema]) => {
220
+ const tsType = jsonSchemaToTS(propSchema, namedSchemas);
221
+ const optional = required.has(key) ? "" : "?";
222
+ const safeKey = isValidIdentifier(key) ? key : `'${key.replace(/'/g, "\\'")}'`;
223
+ return ` ${safeKey}${optional}: ${tsType};`;
224
+ });
225
+ return `export interface ${safeName} {
226
+ ${lines.join(`
227
+ `)}
228
+ }
229
+ `;
230
+ }
231
+ function mapPrimitive(type) {
232
+ if (type === "string")
233
+ return "string";
234
+ if (type === "number" || type === "integer")
235
+ return "number";
236
+ if (type === "boolean")
237
+ return "boolean";
238
+ return "unknown";
239
+ }
240
+ var VALID_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
241
+ function isValidIdentifier(name) {
242
+ return VALID_IDENTIFIER.test(name);
243
+ }
244
+
245
+ // src/generators/resource-generator.ts
246
+ function generateResources(resources) {
247
+ const files = [];
248
+ for (const resource of resources) {
249
+ files.push({
250
+ path: `resources/${resource.identifier}.ts`,
251
+ content: generateResourceFile(resource)
252
+ });
253
+ }
254
+ const exports = resources.map((r) => `export { create${r.name}Resource } from './${r.identifier}';`).join(`
255
+ `);
256
+ files.push({ path: "resources/index.ts", content: exports + `
257
+ ` });
258
+ return files;
259
+ }
260
+ function generateResourceFile(resource) {
261
+ const lines = [];
262
+ const typeImports = collectTypeImports(resource);
263
+ lines.push("import type { FetchClient, FetchResponse } from '@vertz/fetch';");
264
+ if (typeImports.size > 0) {
265
+ const sorted = [...typeImports].sort();
266
+ lines.push(`import type { ${sorted.join(", ")} } from '../types/${resource.identifier}';`);
267
+ }
268
+ lines.push("");
269
+ lines.push(`export function create${resource.name}Resource(client: FetchClient) {`);
270
+ lines.push(" return {");
271
+ validateUniqueMethodNames(resource);
272
+ for (const op of resource.operations) {
273
+ lines.push(` ${generateMethod(op)},`);
274
+ }
275
+ lines.push(" };");
276
+ lines.push("}");
277
+ lines.push("");
278
+ return lines.join(`
279
+ `);
280
+ }
281
+ function generateMethod(op) {
282
+ const params = buildParams(op);
283
+ const returnType = buildReturnType(op);
284
+ const call = buildCall(op);
285
+ return `${op.methodName}: (${params}): ${returnType} =>
286
+ ${call}`;
287
+ }
288
+ function validateUniqueMethodNames(resource) {
289
+ const seen = new Map;
290
+ for (const op of resource.operations) {
291
+ const existing = seen.get(op.methodName);
292
+ if (existing) {
293
+ existing.push(op.operationId);
294
+ } else {
295
+ seen.set(op.methodName, [op.operationId]);
296
+ }
297
+ }
298
+ const duplicates = [...seen.entries()].filter(([, ids]) => ids.length > 1);
299
+ if (duplicates.length === 0)
300
+ return;
301
+ const details = duplicates.map(([name, ids]) => ` - "${name}" used by: ${ids.join(", ")}`).join(`
302
+ `);
303
+ throw new Error(`Duplicate method name${duplicates.length > 1 ? "s" : ""} ${duplicates.map(([n]) => `"${n}"`).join(", ")} in resource "${resource.name}". ` + `Each operation within a resource must have a unique method name.
304
+ ${details}
305
+
306
+ ` + `Fix: use excludeTags to skip this tag, use a different groupBy strategy, ` + `or provide operationIds.overrides to rename conflicting operations.`);
307
+ }
308
+ function buildParams(op) {
309
+ const parts = [];
310
+ for (const p of op.pathParams) {
311
+ parts.push(`${p.name}: string`);
312
+ }
313
+ if (op.requestBody) {
314
+ const inputName = deriveInputName(op);
315
+ parts.push(`body: ${inputName}`);
316
+ }
317
+ if (op.queryParams.length > 0) {
318
+ const queryName = deriveQueryName(op);
319
+ parts.push(`query?: ${queryName}`);
320
+ }
321
+ return parts.join(", ");
322
+ }
323
+ function buildReturnType(op) {
324
+ if (op.responseStatus === 204)
325
+ return "Promise<FetchResponse<void>>";
326
+ if (op.response?.name) {
327
+ const safeName = sanitizeTypeName(op.response.name);
328
+ if (op.response.jsonSchema.type === "array") {
329
+ return `Promise<FetchResponse<${safeName}[]>>`;
330
+ }
331
+ return `Promise<FetchResponse<${safeName}>>`;
332
+ }
333
+ if (op.response?.jsonSchema.type === "array") {
334
+ return "Promise<FetchResponse<unknown[]>>";
335
+ }
336
+ if (op.response) {
337
+ const name = capitalize(op.operationId) + "Response";
338
+ return `Promise<FetchResponse<${name}>>`;
339
+ }
340
+ return "Promise<FetchResponse<void>>";
341
+ }
342
+ function buildCall(op) {
343
+ const method = op.method.toLowerCase();
344
+ const path = buildPath(op);
345
+ const args = [path];
346
+ if (op.requestBody) {
347
+ args.push("body");
348
+ }
349
+ if (op.queryParams.length > 0) {
350
+ args.push("{ query }");
351
+ }
352
+ return `client.${method}(${args.join(", ")})`;
353
+ }
354
+ function buildPath(op) {
355
+ if (op.pathParams.length === 0) {
356
+ return `'${op.path}'`;
357
+ }
358
+ let path = op.path;
359
+ for (const p of op.pathParams) {
360
+ path = path.replace(`{${p.name}}`, `\${encodeURIComponent(${p.name})}`);
361
+ }
362
+ return `\`${path}\``;
363
+ }
364
+ function collectTypeImports(resource) {
365
+ const types = new Set;
366
+ for (const op of resource.operations) {
367
+ if (op.responseStatus !== 204 && op.response) {
368
+ if (op.response.name) {
369
+ types.add(sanitizeTypeName(op.response.name));
370
+ } else {
371
+ types.add(capitalize(op.operationId) + "Response");
372
+ }
373
+ }
374
+ if (op.requestBody) {
375
+ types.add(deriveInputName(op));
376
+ }
377
+ if (op.queryParams.length > 0) {
378
+ types.add(deriveQueryName(op));
379
+ }
380
+ }
381
+ return types;
382
+ }
383
+ function deriveInputName(op) {
384
+ if (op.requestBody?.name)
385
+ return sanitizeTypeName(op.requestBody.name);
386
+ return capitalize(op.operationId) + "Input";
387
+ }
388
+ function deriveQueryName(op) {
389
+ return capitalize(op.operationId) + "Query";
390
+ }
391
+ function capitalize(s) {
392
+ return s.charAt(0).toUpperCase() + s.slice(1);
393
+ }
394
+
395
+ // src/generators/json-schema-to-zod.ts
396
+ function jsonSchemaToZod(schema, namedSchemas) {
397
+ if (typeof schema.$circular === "string") {
398
+ const ref = namedSchemas.get(schema.$circular) ?? schema.$circular;
399
+ return `z.lazy(() => ${ref})`;
400
+ }
401
+ if (Array.isArray(schema.enum)) {
402
+ const values = schema.enum;
403
+ const allStrings = values.every((v) => typeof v === "string");
404
+ if (allStrings) {
405
+ const items = values.map((v) => `'${String(v).replace(/'/g, "\\'")}'`).join(", ");
406
+ return `z.enum([${items}])`;
407
+ }
408
+ const literals = values.map((v) => typeof v === "string" ? `z.literal('${String(v).replace(/'/g, "\\'")}')` : `z.literal(${String(v)})`).join(", ");
409
+ return `z.union([${literals}])`;
410
+ }
411
+ if (Array.isArray(schema.anyOf)) {
412
+ const members = schema.anyOf;
413
+ const nullMember = members.find((m) => m.type === "null");
414
+ const nonNull = members.filter((m) => m.type !== "null");
415
+ const isNullable = nullMember !== undefined;
416
+ if (nonNull.length === 1) {
417
+ const base = jsonSchemaToZod(nonNull[0], namedSchemas);
418
+ return isNullable ? `${base}.nullable()` : base;
419
+ }
420
+ const union = `z.union([${nonNull.map((m) => jsonSchemaToZod(m, namedSchemas)).join(", ")}])`;
421
+ return isNullable ? `${union}.nullable()` : union;
422
+ }
423
+ if (Array.isArray(schema.oneOf)) {
424
+ const members = schema.oneOf.map((m) => jsonSchemaToZod(m, namedSchemas));
425
+ return `z.union([${members.join(", ")}])`;
426
+ }
427
+ const type = schema.type;
428
+ if (type === "null")
429
+ return "z.null()";
430
+ if (Array.isArray(type)) {
431
+ const nonNull = type.filter((t) => t !== "null");
432
+ const isNullable = type.includes("null");
433
+ const base = nonNull.length === 1 ? zodPrimitive(nonNull[0], schema) : "z.unknown()";
434
+ return isNullable ? `${base}.nullable()` : base;
435
+ }
436
+ if (type === "string")
437
+ return zodString(schema);
438
+ if (type === "number")
439
+ return zodNumber(schema);
440
+ if (type === "integer")
441
+ return zodInteger(schema);
442
+ if (type === "boolean")
443
+ return zodBoolean(schema);
444
+ if (type === "array") {
445
+ const items = schema.items;
446
+ const itemZod = items ? jsonSchemaToZod(items, namedSchemas) : "z.unknown()";
447
+ return `z.array(${itemZod})`;
448
+ }
449
+ if (type === "object") {
450
+ return zodObject(schema, namedSchemas);
451
+ }
452
+ return "z.unknown()";
453
+ }
454
+ function zodString(schema) {
455
+ let result = "z.string()";
456
+ const format = schema.format;
457
+ if (format === "email")
458
+ result += ".email()";
459
+ else if (format === "uuid")
460
+ result += ".uuid()";
461
+ else if (format === "date-time")
462
+ result += ".datetime()";
463
+ else if (format === "uri")
464
+ result += ".url()";
465
+ if (typeof schema.minLength === "number")
466
+ result += `.min(${schema.minLength})`;
467
+ if (typeof schema.maxLength === "number")
468
+ result += `.max(${schema.maxLength})`;
469
+ if (typeof schema.pattern === "string") {
470
+ const escaped = schema.pattern.replace(/\//g, "\\/");
471
+ result += `.regex(/${escaped}/)`;
472
+ }
473
+ if (schema.default !== undefined) {
474
+ result += `.default('${String(schema.default).replace(/'/g, "\\'")}')`;
475
+ }
476
+ return result;
477
+ }
478
+ function zodNumber(schema) {
479
+ let result = "z.number()";
480
+ if (typeof schema.minimum === "number")
481
+ result += `.min(${schema.minimum})`;
482
+ if (typeof schema.maximum === "number")
483
+ result += `.max(${schema.maximum})`;
484
+ if (schema.default !== undefined)
485
+ result += `.default(${schema.default})`;
486
+ return result;
487
+ }
488
+ function zodInteger(schema) {
489
+ let result = "z.number().int()";
490
+ if (typeof schema.minimum === "number")
491
+ result += `.min(${schema.minimum})`;
492
+ if (typeof schema.maximum === "number")
493
+ result += `.max(${schema.maximum})`;
494
+ if (schema.default !== undefined)
495
+ result += `.default(${schema.default})`;
496
+ return result;
497
+ }
498
+ function zodBoolean(schema) {
499
+ let result = "z.boolean()";
500
+ if (schema.default !== undefined)
501
+ result += `.default(${schema.default})`;
502
+ return result;
503
+ }
504
+ function zodObject(schema, namedSchemas) {
505
+ const properties = schema.properties;
506
+ if (!properties)
507
+ return "z.object({})";
508
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
509
+ const entries = Object.entries(properties).map(([key, propSchema]) => {
510
+ let zod = jsonSchemaToZod(propSchema, namedSchemas);
511
+ if (!required.has(key))
512
+ zod += ".optional()";
513
+ const safeKey = isValidIdentifier(key) ? key : `'${key.replace(/'/g, "\\'")}'`;
514
+ return ` ${safeKey}: ${zod}`;
515
+ });
516
+ return `z.object({
517
+ ${entries.join(`,
518
+ `)},
519
+ })`;
520
+ }
521
+ function zodPrimitive(type, schema) {
522
+ if (type === "string")
523
+ return zodString(schema);
524
+ if (type === "number")
525
+ return zodNumber(schema);
526
+ if (type === "integer")
527
+ return zodInteger(schema);
528
+ if (type === "boolean")
529
+ return zodBoolean(schema);
530
+ return "z.unknown()";
531
+ }
532
+
533
+ // src/generators/schema-generator.ts
534
+ function generateSchemas(resources, schemas) {
535
+ const files = [];
536
+ const namedSchemas = buildNamedSchemaMap(schemas);
537
+ for (const resource of resources) {
538
+ const content = generateResourceSchemas(resource, namedSchemas);
539
+ files.push({ path: `schemas/${resource.identifier}.ts`, content });
540
+ }
541
+ const exports = resources.map((r) => `export * from './${r.identifier}';`).join(`
542
+ `);
543
+ files.push({ path: "schemas/index.ts", content: exports + `
544
+ ` });
545
+ return files;
546
+ }
547
+ function buildNamedSchemaMap(schemas) {
548
+ const map = new Map;
549
+ for (const s of schemas) {
550
+ if (s.name) {
551
+ map.set(s.name, toSchemaVarName(s.name));
552
+ }
553
+ }
554
+ return map;
555
+ }
556
+ function generateResourceSchemas(resource, namedSchemas) {
557
+ const lines = [];
558
+ lines.push("import { z } from 'zod';");
559
+ lines.push("");
560
+ const emitted = new Set;
561
+ for (const op of resource.operations) {
562
+ if (op.response) {
563
+ const varName = deriveResponseSchemaName(op);
564
+ if (!emitted.has(varName)) {
565
+ emitted.add(varName);
566
+ const schema = op.response.jsonSchema;
567
+ const effectiveSchema = schema.type === "array" && schema.items && typeof schema.items === "object" ? schema.items : schema;
568
+ const zod = jsonSchemaToZod(effectiveSchema, namedSchemas);
569
+ lines.push(`export const ${varName} = ${zod};`);
570
+ lines.push("");
571
+ }
572
+ }
573
+ if (op.requestBody) {
574
+ const varName = deriveInputSchemaName(op);
575
+ if (!emitted.has(varName)) {
576
+ emitted.add(varName);
577
+ const zod = jsonSchemaToZod(op.requestBody.jsonSchema, namedSchemas);
578
+ lines.push(`export const ${varName} = ${zod};`);
579
+ lines.push("");
580
+ }
581
+ }
582
+ if (op.queryParams.length > 0) {
583
+ const varName = deriveQuerySchemaName(op);
584
+ if (!emitted.has(varName)) {
585
+ emitted.add(varName);
586
+ lines.push(`export const ${varName} = ${buildQueryZodSchema(op, namedSchemas)};`);
587
+ lines.push("");
588
+ }
589
+ }
590
+ }
591
+ return lines.join(`
592
+ `);
593
+ }
594
+ function buildQueryZodSchema(op, namedSchemas) {
595
+ const entries = op.queryParams.map((param) => {
596
+ let zod = jsonSchemaToZod(param.schema, namedSchemas);
597
+ if (!param.required)
598
+ zod += ".optional()";
599
+ const safeKey = isValidIdentifier(param.name) ? param.name : `'${param.name.replace(/'/g, "\\'")}'`;
600
+ return ` ${safeKey}: ${zod}`;
601
+ });
602
+ return `z.object({
603
+ ${entries.join(`,
604
+ `)},
605
+ })`;
606
+ }
607
+ function deriveResponseSchemaName(op) {
608
+ if (op.response?.name)
609
+ return toSchemaVarName(op.response.name);
610
+ return toSchemaVarName(op.operationId + "Response");
611
+ }
612
+ function deriveInputSchemaName(op) {
613
+ if (op.requestBody?.name)
614
+ return toSchemaVarName(op.requestBody.name);
615
+ return toSchemaVarName(op.operationId + "Input");
616
+ }
617
+ function deriveQuerySchemaName(op) {
618
+ return toSchemaVarName(op.operationId + "Query");
619
+ }
620
+ function toSchemaVarName(name) {
621
+ const camel = name.charAt(0).toLowerCase() + name.slice(1);
622
+ return camel + "Schema";
623
+ }
624
+
625
+ // src/generators/types-generator.ts
626
+ function generateTypes(resources, schemas) {
627
+ const files = [];
628
+ const namedSchemas = buildNamedSchemaMap2(schemas);
629
+ for (const resource of resources) {
630
+ const content = generateResourceTypes(resource, namedSchemas);
631
+ files.push({ path: `types/${resource.identifier}.ts`, content: content || "" });
632
+ }
633
+ const exports = resources.map((r) => `export * from './${r.identifier}';`).join(`
634
+ `);
635
+ files.push({ path: "types/index.ts", content: exports + `
636
+ ` });
637
+ return files;
638
+ }
639
+ function buildNamedSchemaMap2(schemas) {
640
+ const map = new Map;
641
+ for (const s of schemas) {
642
+ if (s.name) {
643
+ map.set(s.name, s.name);
644
+ }
645
+ }
646
+ return map;
647
+ }
648
+ function generateResourceTypes(resource, namedSchemas) {
649
+ const interfaces = [];
650
+ const emitted = new Set;
651
+ for (const op of resource.operations) {
652
+ if (op.response) {
653
+ const name = deriveResponseName(op);
654
+ if (!emitted.has(name)) {
655
+ emitted.add(name);
656
+ const schema = op.response.jsonSchema;
657
+ const effectiveSchema = schema.type === "array" && schema.items && typeof schema.items === "object" ? schema.items : schema;
658
+ interfaces.push(generateInterface(name, effectiveSchema, namedSchemas));
659
+ }
660
+ }
661
+ if (op.requestBody) {
662
+ const name = deriveInputName2(op);
663
+ if (!emitted.has(name)) {
664
+ emitted.add(name);
665
+ interfaces.push(generateInterface(name, op.requestBody.jsonSchema, namedSchemas));
666
+ }
667
+ }
668
+ if (op.queryParams.length > 0) {
669
+ const name = deriveQueryName2(op);
670
+ if (!emitted.has(name)) {
671
+ emitted.add(name);
672
+ interfaces.push(generateQueryInterface(name, op, namedSchemas));
673
+ }
674
+ }
675
+ }
676
+ return interfaces.join(`
677
+ `);
678
+ }
679
+ function deriveResponseName(op) {
680
+ if (op.response?.name)
681
+ return sanitizeTypeName(op.response.name);
682
+ return capitalize2(op.operationId) + "Response";
683
+ }
684
+ function deriveInputName2(op) {
685
+ if (op.requestBody?.name)
686
+ return sanitizeTypeName(op.requestBody.name);
687
+ return capitalize2(op.operationId) + "Input";
688
+ }
689
+ function deriveQueryName2(op) {
690
+ return capitalize2(op.operationId) + "Query";
691
+ }
692
+ function generateQueryInterface(name, op, namedSchemas) {
693
+ const lines = op.queryParams.map((param) => {
694
+ const tsType = jsonSchemaToTS(param.schema, namedSchemas);
695
+ const optional = param.required ? "" : "?";
696
+ const safeKey = isValidIdentifier(param.name) ? param.name : `'${param.name.replace(/'/g, "\\'")}'`;
697
+ return ` ${safeKey}${optional}: ${tsType};`;
698
+ });
699
+ return `export interface ${sanitizeTypeName(name)} {
700
+ ${lines.join(`
701
+ `)}
702
+ }
703
+ `;
704
+ }
705
+ function capitalize2(s) {
706
+ return s.charAt(0).toUpperCase() + s.slice(1);
707
+ }
708
+
709
+ // src/generators/index.ts
710
+ function generateAll(spec, options) {
711
+ const { resources, schemas } = spec;
712
+ const opts = { schemas: false, baseURL: "", ...options };
713
+ const files = [];
714
+ files.push(...generateTypes(resources, schemas));
715
+ files.push(...generateResources(resources));
716
+ files.push(generateClient(resources, {
717
+ baseURL: opts.baseURL,
718
+ securitySchemes: spec.securitySchemes
719
+ }));
720
+ if (opts.schemas) {
721
+ files.push(...generateSchemas(resources, schemas));
722
+ }
723
+ files.push(generateReadme(spec, opts));
724
+ return files;
725
+ }
726
+ function generateReadme(spec, options) {
727
+ const lines = [];
728
+ lines.push(`# ${spec.info.title} SDK`);
729
+ lines.push("");
730
+ lines.push(`> Auto-generated from OpenAPI ${spec.version} spec (v${spec.info.version})`);
731
+ lines.push("");
732
+ lines.push("## Prerequisites");
733
+ lines.push("");
734
+ lines.push("This SDK requires `@vertz/fetch` as a peer dependency:");
735
+ lines.push("");
736
+ lines.push("```bash");
737
+ lines.push("bun add @vertz/fetch");
738
+ lines.push("```");
739
+ lines.push("");
740
+ lines.push("## Usage");
741
+ lines.push("");
742
+ lines.push("```typescript");
743
+ lines.push("import { createClient } from './client';");
744
+ lines.push("import { isOk } from '@vertz/fetch';");
745
+ lines.push("");
746
+ lines.push(`const api = createClient(${options.baseURL ? `{ baseURL: '${options.baseURL}' }` : ""});`);
747
+ lines.push("```");
748
+ lines.push("");
749
+ lines.push("## Resources");
750
+ lines.push("");
751
+ for (const r of spec.resources) {
752
+ lines.push(`### ${r.name}`);
753
+ lines.push("");
754
+ for (const op of r.operations) {
755
+ lines.push(`- \`api.${r.identifier}.${op.methodName}()\` — ${op.method} ${op.path}`);
756
+ }
757
+ lines.push("");
758
+ }
759
+ lines.push("## Committing");
760
+ lines.push("");
761
+ lines.push("We recommend committing generated code to source control.");
762
+ lines.push("This ensures your CI builds work without running the generator.");
763
+ lines.push("");
764
+ lines.push("## Regenerating");
765
+ lines.push("");
766
+ lines.push("```bash");
767
+ lines.push("npx @vertz/openapi generate");
768
+ lines.push("```");
769
+ lines.push("");
770
+ return { path: "README.md", content: lines.join(`
771
+ `) };
772
+ }
773
+
774
+ // src/loader.ts
775
+ import { existsSync, readFileSync } from "node:fs";
776
+ import { extname } from "node:path";
777
+ import { parse as parseYAML } from "yaml";
778
+
779
+ class LoaderError extends Error {
780
+ name = "LoaderError";
781
+ }
782
+ async function loadSpec(source) {
783
+ if (source.startsWith("http://") || source.startsWith("https://")) {
784
+ return loadFromURL(source);
785
+ }
786
+ return loadFromFile(source);
787
+ }
788
+ function loadFromFile(filePath) {
789
+ if (!existsSync(filePath)) {
790
+ throw new LoaderError(`Spec file not found: ${filePath}`);
791
+ }
792
+ const content = readFileSync(filePath, "utf-8");
793
+ const ext = extname(filePath).toLowerCase();
794
+ if (ext === ".yaml" || ext === ".yml") {
795
+ return parseAsYAML(content, filePath);
796
+ }
797
+ if (ext === ".json") {
798
+ return parseAsJSON(content, filePath);
799
+ }
800
+ const trimmed = content.trimStart();
801
+ if (trimmed.startsWith("{")) {
802
+ return parseAsJSON(content, filePath);
803
+ }
804
+ return parseAsYAML(content, filePath);
805
+ }
806
+ async function loadFromURL(url) {
807
+ let response;
808
+ try {
809
+ response = await globalThis.fetch(url);
810
+ } catch (err) {
811
+ throw new LoaderError(`Failed to fetch spec from ${url}: ${err instanceof Error ? err.message : String(err)}`);
812
+ }
813
+ if (!response.ok) {
814
+ throw new LoaderError(`Failed to fetch spec from ${url}: HTTP ${response.status}`);
815
+ }
816
+ const content = await response.text();
817
+ const urlPath = new URL(url).pathname;
818
+ const ext = extname(urlPath).toLowerCase();
819
+ if (ext === ".yaml" || ext === ".yml") {
820
+ return parseAsYAML(content, url);
821
+ }
822
+ if (ext === ".json") {
823
+ return parseAsJSON(content, url);
824
+ }
825
+ const trimmed = content.trimStart();
826
+ if (trimmed.startsWith("{")) {
827
+ return parseAsJSON(content, url);
828
+ }
829
+ return parseAsYAML(content, url);
830
+ }
831
+ function parseAsJSON(content, source) {
832
+ try {
833
+ const parsed = JSON.parse(content);
834
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
835
+ throw new LoaderError(`Failed to parse ${source}: expected an object`);
836
+ }
837
+ return parsed;
838
+ } catch (err) {
839
+ if (err instanceof LoaderError)
840
+ throw err;
841
+ throw new LoaderError(`Failed to parse JSON from ${source}: ${err instanceof Error ? err.message : String(err)}`);
842
+ }
843
+ }
844
+ function parseAsYAML(content, source) {
845
+ try {
846
+ const parsed = parseYAML(content);
847
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
848
+ throw new LoaderError(`Failed to parse ${source}: expected an object`);
849
+ }
850
+ return parsed;
851
+ } catch (err) {
852
+ if (err instanceof LoaderError)
853
+ throw err;
854
+ throw new LoaderError(`Failed to parse YAML from ${source}: ${err instanceof Error ? err.message : String(err)}`);
855
+ }
856
+ }
857
+
858
+ // src/parser/operation-id-normalizer.ts
859
+ var HTTP_METHOD_WORDS = new Set(["get", "post", "put", "delete", "patch"]);
860
+ function isPathParam(segment) {
861
+ return segment.startsWith("{") && segment.endsWith("}");
862
+ }
863
+ function getPathSegments(path) {
864
+ return path.split("/").filter(Boolean);
865
+ }
866
+ function splitWords2(input) {
867
+ return input.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^A-Za-z0-9]+/g, " ").split(/\s+/).filter(Boolean);
868
+ }
869
+ function toCamelCase(words) {
870
+ if (words.length === 0) {
871
+ return "";
872
+ }
873
+ const [first = "", ...rest] = words.map((word) => word.toLowerCase());
874
+ return first + rest.map((word) => word[0]?.toUpperCase() + word.slice(1)).join("");
875
+ }
876
+ function singularize(word) {
877
+ if (word.endsWith("ies") && word.length > 3) {
878
+ return `${word.slice(0, -3)}y`;
879
+ }
880
+ if (word.endsWith("s") && word.length > 1) {
881
+ return word.slice(0, -1);
882
+ }
883
+ return word;
884
+ }
885
+ function getPrimaryResourceWords(path) {
886
+ const segments = getPathSegments(path).filter((segment) => !isPathParam(segment));
887
+ const firstSegment = segments[0];
888
+ if (!firstSegment) {
889
+ return [];
890
+ }
891
+ return splitWords2(firstSegment).map((word) => word.toLowerCase());
892
+ }
893
+ function autoCleanOperationId(operationId, path) {
894
+ const withoutControllerPrefix = operationId.replace(/^[A-Za-z0-9]+Controller[_.-]+/, "");
895
+ const words = splitWords2(withoutControllerPrefix).map((word) => word.toLowerCase());
896
+ const lastWord = words.at(-1);
897
+ if (words.length > 1 && lastWord && HTTP_METHOD_WORDS.has(lastWord)) {
898
+ words.pop();
899
+ }
900
+ const resourcePhrase = getPrimaryResourceWords(path);
901
+ const resourceWords = new Set([...resourcePhrase, ...resourcePhrase.map(singularize)]);
902
+ if (resourcePhrase.length === 1) {
903
+ while (words.length > 1 && words[0] && resourceWords.has(words[0])) {
904
+ words.shift();
905
+ }
906
+ while (words.length > 1 && words.at(-1) && resourceWords.has(words.at(-1))) {
907
+ words.pop();
908
+ }
909
+ }
910
+ return toCamelCase(words);
911
+ }
912
+ function detectCrudMethod(method, path) {
913
+ const segments = getPathSegments(path);
914
+ if (segments.length === 1 && method === "GET") {
915
+ return "list";
916
+ }
917
+ if (segments.length === 2 && segments[1] && isPathParam(segments[1])) {
918
+ if (method === "GET") {
919
+ return "get";
920
+ }
921
+ if (method === "PUT" || method === "PATCH") {
922
+ return "update";
923
+ }
924
+ if (method === "DELETE") {
925
+ return "delete";
926
+ }
927
+ }
928
+ if (segments.length === 1 && method === "POST") {
929
+ return "create";
930
+ }
931
+ return;
932
+ }
933
+ function normalizeOperationId(operationId, method, path, config, context) {
934
+ if (config?.overrides?.[operationId]) {
935
+ return config.overrides[operationId];
936
+ }
937
+ const cleaned = autoCleanOperationId(operationId, path);
938
+ if (config?.transform) {
939
+ const ctx = context ?? {
940
+ operationId,
941
+ method,
942
+ path,
943
+ tags: [],
944
+ hasBody: false
945
+ };
946
+ return config.transform(cleaned, ctx);
947
+ }
948
+ return detectCrudMethod(method, path) ?? cleaned;
949
+ }
950
+
951
+ // src/parser/ref-resolver.ts
952
+ class OpenAPIParserError extends Error {
953
+ name = "OpenAPIParserError";
954
+ }
955
+ function isRecord(value) {
956
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
957
+ }
958
+ function getRefSegments(ref) {
959
+ if (!ref.startsWith("#/")) {
960
+ throw new OpenAPIParserError(`External $ref values are not supported: ${ref}`);
961
+ }
962
+ return ref.slice(2).split("/");
963
+ }
964
+ function getRefName(ref) {
965
+ const segments = getRefSegments(ref);
966
+ return segments[segments.length - 1] ?? ref;
967
+ }
968
+ function getRawRefTarget(ref, document) {
969
+ let current = document;
970
+ for (const segment of getRefSegments(ref)) {
971
+ if (!isRecord(current) || !(segment in current)) {
972
+ throw new OpenAPIParserError(`Could not resolve $ref: ${ref}`);
973
+ }
974
+ current = current[segment];
975
+ }
976
+ if (!isRecord(current)) {
977
+ throw new OpenAPIParserError(`Resolved $ref is not an object schema: ${ref}`);
978
+ }
979
+ return current;
980
+ }
981
+ function resolveNestedValue(value, document, options, resolving) {
982
+ if (Array.isArray(value)) {
983
+ return value.map((entry) => isRecord(entry) ? resolveSchema(entry, document, options, resolving) : entry);
984
+ }
985
+ if (isRecord(value)) {
986
+ return resolveSchema(value, document, options, resolving);
987
+ }
988
+ return value;
989
+ }
990
+ function mergeSchemas(left, right) {
991
+ const merged = { ...left };
992
+ for (const [key, value] of Object.entries(right)) {
993
+ if (key === "properties" && isRecord(merged.properties) && isRecord(value)) {
994
+ merged.properties = { ...merged.properties, ...value };
995
+ continue;
996
+ }
997
+ if (key === "required" && Array.isArray(merged.required) && Array.isArray(value)) {
998
+ merged.required = [...new Set([...merged.required, ...value])];
999
+ continue;
1000
+ }
1001
+ if (isRecord(merged[key]) && isRecord(value)) {
1002
+ merged[key] = mergeSchemas(merged[key], value);
1003
+ continue;
1004
+ }
1005
+ merged[key] = value;
1006
+ }
1007
+ return merged;
1008
+ }
1009
+ function resolveRef(ref, document, options) {
1010
+ const target = getRawRefTarget(ref, document);
1011
+ if (typeof target.$ref === "string") {
1012
+ return resolveRef(target.$ref, document, options);
1013
+ }
1014
+ return resolveSchema(target, document, options);
1015
+ }
1016
+ function resolveSchema(schema, document, options, resolving = new Set) {
1017
+ let workingSchema = { ...schema };
1018
+ if (typeof workingSchema.$ref === "string") {
1019
+ const ref = workingSchema.$ref;
1020
+ if (resolving.has(ref)) {
1021
+ return { $circular: getRefName(ref) };
1022
+ }
1023
+ const nextResolving = new Set(resolving);
1024
+ nextResolving.add(ref);
1025
+ const resolvedTarget = resolveSchema(getRawRefTarget(ref, document), document, options, nextResolving);
1026
+ const siblingEntries = Object.entries(workingSchema).filter(([key]) => key !== "$ref");
1027
+ if (options.specVersion === "3.0" || siblingEntries.length === 0) {
1028
+ workingSchema = resolvedTarget;
1029
+ } else {
1030
+ const siblings = Object.fromEntries(siblingEntries);
1031
+ workingSchema = mergeSchemas(resolvedTarget, resolveSchema(siblings, document, options, nextResolving));
1032
+ }
1033
+ }
1034
+ if (Array.isArray(workingSchema.allOf)) {
1035
+ const flattened = workingSchema.allOf.reduce((accumulator, member) => {
1036
+ if (!isRecord(member)) {
1037
+ return accumulator;
1038
+ }
1039
+ return mergeSchemas(accumulator, resolveSchema(member, document, options, new Set(resolving)));
1040
+ }, {});
1041
+ const { allOf: _allOf, ...rest } = workingSchema;
1042
+ return mergeSchemas(flattened, resolveSchema(rest, document, options, resolving));
1043
+ }
1044
+ const resolved = {};
1045
+ for (const [key, value] of Object.entries(workingSchema)) {
1046
+ if (key === "properties" && isRecord(value)) {
1047
+ resolved.properties = Object.fromEntries(Object.entries(value).map(([propertyName, propertySchema]) => [
1048
+ propertyName,
1049
+ isRecord(propertySchema) ? resolveSchema(propertySchema, document, options, new Set(resolving)) : propertySchema
1050
+ ]));
1051
+ continue;
1052
+ }
1053
+ resolved[key] = resolveNestedValue(value, document, options, new Set(resolving));
1054
+ }
1055
+ return resolved;
1056
+ }
1057
+
1058
+ // src/parser/openapi-parser.ts
1059
+ class OpenAPIParserError2 extends Error {
1060
+ name = "OpenAPIParserError";
1061
+ }
1062
+ function isRecord2(value) {
1063
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1064
+ }
1065
+ function getVersion(spec) {
1066
+ const version = spec.openapi;
1067
+ if (typeof version !== "string") {
1068
+ throw new OpenAPIParserError2("OpenAPI spec is missing required field: openapi");
1069
+ }
1070
+ if (version.startsWith("3.0")) {
1071
+ return "3.0";
1072
+ }
1073
+ if (version.startsWith("3.1")) {
1074
+ return "3.1";
1075
+ }
1076
+ throw new OpenAPIParserError2(`Unsupported OpenAPI version: ${version}`);
1077
+ }
1078
+ function getRefTarget(ref, spec) {
1079
+ if (!ref.startsWith("#/")) {
1080
+ throw new OpenAPIParserError2(`External $ref values are not supported: ${ref}`);
1081
+ }
1082
+ let current = spec;
1083
+ for (const segment of ref.slice(2).split("/")) {
1084
+ if (!isRecord2(current) || !(segment in current)) {
1085
+ throw new OpenAPIParserError2(`Could not resolve $ref: ${ref}`);
1086
+ }
1087
+ current = current[segment];
1088
+ }
1089
+ if (!isRecord2(current)) {
1090
+ throw new OpenAPIParserError2(`Resolved $ref is not an object: ${ref}`);
1091
+ }
1092
+ return current;
1093
+ }
1094
+ function resolveOpenAPIObject(value, spec) {
1095
+ if (!isRecord2(value)) {
1096
+ return;
1097
+ }
1098
+ if (typeof value.$ref === "string") {
1099
+ return resolveOpenAPIObject(getRefTarget(value.$ref, spec), spec);
1100
+ }
1101
+ return value;
1102
+ }
1103
+ function normalizeNullableSchema(schema, version) {
1104
+ if (Array.isArray(schema)) {
1105
+ return schema.map((entry) => normalizeNullableSchema(entry, version));
1106
+ }
1107
+ if (!isRecord2(schema)) {
1108
+ return schema;
1109
+ }
1110
+ const normalized = Object.fromEntries(Object.entries(schema).map(([key, value]) => [key, normalizeNullableSchema(value, version)]));
1111
+ if (version !== "3.0" || normalized.nullable !== true) {
1112
+ return normalized;
1113
+ }
1114
+ const type = normalized.type;
1115
+ if (typeof type === "string") {
1116
+ normalized.type = [type, "null"];
1117
+ } else if (Array.isArray(type) && !type.includes("null")) {
1118
+ normalized.type = [...type, "null"];
1119
+ }
1120
+ delete normalized.nullable;
1121
+ return normalized;
1122
+ }
1123
+ function getJsonContentSchema(value) {
1124
+ if (!isRecord2(value) || !isRecord2(value.content)) {
1125
+ return;
1126
+ }
1127
+ const mediaType = value.content["application/json"];
1128
+ if (!isRecord2(mediaType) || !isRecord2(mediaType.schema)) {
1129
+ return;
1130
+ }
1131
+ return mediaType.schema;
1132
+ }
1133
+ function extractRefName(schema) {
1134
+ if (typeof schema.$ref === "string") {
1135
+ const segments = schema.$ref.split("/");
1136
+ return segments[segments.length - 1];
1137
+ }
1138
+ if (schema.type === "array" && isRecord2(schema.items) && typeof schema.items.$ref === "string") {
1139
+ const segments = schema.items.$ref.split("/");
1140
+ return segments[segments.length - 1];
1141
+ }
1142
+ return;
1143
+ }
1144
+ function resolveSchemaForOutput(schema, spec, version) {
1145
+ const name = extractRefName(schema);
1146
+ const result = {
1147
+ jsonSchema: normalizeNullableSchema(resolveSchema(schema, spec, { specVersion: version }), version)
1148
+ };
1149
+ if (name)
1150
+ result.name = name;
1151
+ return result;
1152
+ }
1153
+ function getOperationResponses(operation) {
1154
+ if (!isRecord2(operation.responses)) {
1155
+ return {};
1156
+ }
1157
+ return operation.responses;
1158
+ }
1159
+ function pickSuccessResponse(operation, spec) {
1160
+ const entries = Object.entries(getOperationResponses(operation)).map(([status, response]) => ({ status: Number(status), response })).filter(({ status }) => Number.isInteger(status) && status >= 200 && status < 300).sort((left, right) => left.status - right.status);
1161
+ const first = entries[0];
1162
+ if (!first) {
1163
+ return { status: 200 };
1164
+ }
1165
+ const resolvedResponse = resolveOpenAPIObject(first.response, spec);
1166
+ return {
1167
+ status: first.status,
1168
+ schema: getJsonContentSchema(resolvedResponse)
1169
+ };
1170
+ }
1171
+ function getCombinedParameters(pathItem, operation, spec) {
1172
+ const collect = (value) => Array.isArray(value) ? value.map((entry) => resolveOpenAPIObject(entry, spec)).filter((entry) => Boolean(entry)) : [];
1173
+ return [...collect(pathItem.parameters), ...collect(operation.parameters)];
1174
+ }
1175
+ function getPathParameterNames(path) {
1176
+ return [...path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]).filter((name) => Boolean(name));
1177
+ }
1178
+ function toParsedParameter(parameter, name, spec, version, requiredFallback) {
1179
+ const resolvedSchema = isRecord2(parameter?.schema) ? normalizeNullableSchema(resolveSchema(parameter.schema, spec, { specVersion: version }), version) : {};
1180
+ return {
1181
+ name,
1182
+ required: typeof parameter?.required === "boolean" ? parameter.required : requiredFallback,
1183
+ schema: resolvedSchema
1184
+ };
1185
+ }
1186
+ function extractParameters(path, pathItem, operation, spec, version) {
1187
+ const combined = getCombinedParameters(pathItem, operation, spec);
1188
+ const pathParameters = new Map;
1189
+ const queryParameters = [];
1190
+ for (const parameter of combined) {
1191
+ if (parameter.in === "path" && typeof parameter.name === "string") {
1192
+ pathParameters.set(parameter.name, parameter);
1193
+ }
1194
+ if (parameter.in === "query" && typeof parameter.name === "string") {
1195
+ queryParameters.push(toParsedParameter(parameter, parameter.name, spec, version, false));
1196
+ }
1197
+ }
1198
+ const pathNames = [...new Set([...getPathParameterNames(path), ...pathParameters.keys()])];
1199
+ return {
1200
+ pathParams: pathNames.map((name) => toParsedParameter(pathParameters.get(name), name, spec, version, true)),
1201
+ queryParams: queryParameters
1202
+ };
1203
+ }
1204
+ function extractRequestBody(operation, spec, version) {
1205
+ const requestBody = resolveOpenAPIObject(operation.requestBody, spec);
1206
+ const schema = getJsonContentSchema(requestBody);
1207
+ return schema ? resolveSchemaForOutput(schema, spec, version) : undefined;
1208
+ }
1209
+ function collectComponentSchemas(spec, version) {
1210
+ const schemas = isRecord2(spec.components) && isRecord2(spec.components.schemas) ? spec.components.schemas : undefined;
1211
+ if (!schemas) {
1212
+ return [];
1213
+ }
1214
+ return Object.entries(schemas).filter((entry) => isRecord2(entry[1])).map(([name, schema]) => ({
1215
+ ...resolveSchemaForOutput(schema, spec, version),
1216
+ name
1217
+ }));
1218
+ }
1219
+ function extractSecuritySchemes(spec) {
1220
+ const components = isRecord2(spec.components) ? spec.components : undefined;
1221
+ const schemes = components && isRecord2(components.securitySchemes) ? components.securitySchemes : undefined;
1222
+ if (!schemes)
1223
+ return [];
1224
+ const result = [];
1225
+ for (const [name, scheme] of Object.entries(schemes)) {
1226
+ if (!isRecord2(scheme))
1227
+ continue;
1228
+ const description = typeof scheme.description === "string" ? scheme.description : undefined;
1229
+ if (scheme.type === "http") {
1230
+ if (scheme.scheme === "bearer") {
1231
+ result.push({ type: "bearer", name, description });
1232
+ } else if (scheme.scheme === "basic") {
1233
+ result.push({ type: "basic", name, description });
1234
+ }
1235
+ } else if (scheme.type === "apiKey") {
1236
+ const location = scheme.in;
1237
+ const paramName = typeof scheme.name === "string" ? scheme.name : name;
1238
+ result.push({ type: "apiKey", name, in: location, paramName, description });
1239
+ } else if (scheme.type === "oauth2" && isRecord2(scheme.flows)) {
1240
+ const flows = {};
1241
+ const rawFlows = scheme.flows;
1242
+ if (isRecord2(rawFlows.authorizationCode)) {
1243
+ const ac = rawFlows.authorizationCode;
1244
+ flows.authorizationCode = {
1245
+ authorizationUrl: String(ac.authorizationUrl ?? ""),
1246
+ tokenUrl: String(ac.tokenUrl ?? ""),
1247
+ scopes: isRecord2(ac.scopes) ? ac.scopes : {}
1248
+ };
1249
+ }
1250
+ if (isRecord2(rawFlows.clientCredentials)) {
1251
+ const cc = rawFlows.clientCredentials;
1252
+ flows.clientCredentials = {
1253
+ tokenUrl: String(cc.tokenUrl ?? ""),
1254
+ scopes: isRecord2(cc.scopes) ? cc.scopes : {}
1255
+ };
1256
+ }
1257
+ result.push({ type: "oauth2", name, flows, description });
1258
+ }
1259
+ }
1260
+ return result;
1261
+ }
1262
+ function extractOperationSecurity(operation, globalSecurity) {
1263
+ const security = Array.isArray(operation.security) ? operation.security : globalSecurity;
1264
+ if (security.length === 0 && !Array.isArray(operation.security))
1265
+ return;
1266
+ const schemes = [];
1267
+ for (const requirement of security) {
1268
+ if (isRecord2(requirement)) {
1269
+ schemes.push(...Object.keys(requirement));
1270
+ }
1271
+ }
1272
+ return {
1273
+ required: schemes.length > 0,
1274
+ schemes
1275
+ };
1276
+ }
1277
+ function parseOpenAPI(spec) {
1278
+ const version = getVersion(spec);
1279
+ if (!isRecord2(spec.info)) {
1280
+ throw new OpenAPIParserError2("OpenAPI spec is missing required field: info");
1281
+ }
1282
+ if (!isRecord2(spec.paths)) {
1283
+ throw new OpenAPIParserError2("OpenAPI spec is missing required field: paths");
1284
+ }
1285
+ const globalSecurity = Array.isArray(spec.security) ? spec.security : [];
1286
+ const operations = [];
1287
+ for (const [path, pathItem] of Object.entries(spec.paths)) {
1288
+ if (!isRecord2(pathItem)) {
1289
+ continue;
1290
+ }
1291
+ for (const method of ["get", "post", "put", "delete", "patch"]) {
1292
+ const operation = pathItem[method];
1293
+ if (!isRecord2(operation)) {
1294
+ continue;
1295
+ }
1296
+ const operationId = typeof operation.operationId === "string" ? operation.operationId : `${method}_${path}`;
1297
+ const { pathParams, queryParams } = extractParameters(path, pathItem, operation, spec, version);
1298
+ const successResponse = pickSuccessResponse(operation, spec);
1299
+ const security = extractOperationSecurity(operation, globalSecurity);
1300
+ const parsed = {
1301
+ operationId,
1302
+ methodName: normalizeOperationId(operationId, method.toUpperCase(), path),
1303
+ method: method.toUpperCase(),
1304
+ path,
1305
+ pathParams,
1306
+ queryParams,
1307
+ requestBody: extractRequestBody(operation, spec, version),
1308
+ response: successResponse.schema ? resolveSchemaForOutput(successResponse.schema, spec, version) : undefined,
1309
+ responseStatus: successResponse.status,
1310
+ tags: Array.isArray(operation.tags) ? operation.tags.filter((tag) => typeof tag === "string") : []
1311
+ };
1312
+ if (security)
1313
+ parsed.security = security;
1314
+ operations.push(parsed);
1315
+ }
1316
+ }
1317
+ return {
1318
+ operations,
1319
+ schemas: collectComponentSchemas(spec, version),
1320
+ securitySchemes: extractSecuritySchemes(spec),
1321
+ version
1322
+ };
1323
+ }
1324
+
1325
+ // src/writer/incremental.ts
1326
+ import { createHash } from "node:crypto";
1327
+ import {
1328
+ existsSync as existsSync2,
1329
+ mkdirSync,
1330
+ readFileSync as readFileSync2,
1331
+ readdirSync,
1332
+ rmSync,
1333
+ statSync,
1334
+ writeFileSync
1335
+ } from "node:fs";
1336
+ import { dirname, join, relative } from "node:path";
1337
+ async function writeIncremental(files, outputDir, options) {
1338
+ const clean = options?.clean ?? false;
1339
+ const dryRun = options?.dryRun ?? false;
1340
+ const result = { written: 0, skipped: 0, removed: 0, filesWritten: [] };
1341
+ const generatedPaths = new Set(files.map((f) => f.path));
1342
+ for (const file of files) {
1343
+ const fullPath = join(outputDir, file.path);
1344
+ const newHash = sha256(file.content);
1345
+ if (existsSync2(fullPath)) {
1346
+ const existingContent = readFileSync2(fullPath, "utf-8");
1347
+ const existingHash = sha256(existingContent);
1348
+ if (newHash === existingHash) {
1349
+ result.skipped++;
1350
+ continue;
1351
+ }
1352
+ }
1353
+ result.written++;
1354
+ result.filesWritten.push(file.path);
1355
+ if (!dryRun) {
1356
+ mkdirSync(dirname(fullPath), { recursive: true });
1357
+ writeFileSync(fullPath, file.content);
1358
+ }
1359
+ }
1360
+ if (clean && existsSync2(outputDir)) {
1361
+ const existingFiles = collectFiles(outputDir, outputDir);
1362
+ for (const existing of existingFiles) {
1363
+ if (!generatedPaths.has(existing)) {
1364
+ result.removed++;
1365
+ if (!dryRun) {
1366
+ rmSync(join(outputDir, existing));
1367
+ }
1368
+ }
1369
+ }
1370
+ if (!dryRun) {
1371
+ removeEmptyDirs(outputDir);
1372
+ }
1373
+ }
1374
+ return result;
1375
+ }
1376
+ function sha256(content) {
1377
+ return createHash("sha256").update(content).digest("hex");
1378
+ }
1379
+ function removeEmptyDirs(dir) {
1380
+ if (!existsSync2(dir))
1381
+ return;
1382
+ for (const entry of readdirSync(dir)) {
1383
+ const fullPath = join(dir, entry);
1384
+ if (statSync(fullPath).isDirectory()) {
1385
+ removeEmptyDirs(fullPath);
1386
+ if (readdirSync(fullPath).length === 0) {
1387
+ rmSync(fullPath, { recursive: true });
1388
+ }
1389
+ }
1390
+ }
1391
+ }
1392
+ function collectFiles(dir, baseDir) {
1393
+ const files = [];
1394
+ if (!existsSync2(dir))
1395
+ return files;
1396
+ for (const entry of readdirSync(dir)) {
1397
+ const fullPath = join(dir, entry);
1398
+ if (statSync(fullPath).isDirectory()) {
1399
+ files.push(...collectFiles(fullPath, baseDir));
1400
+ } else {
1401
+ files.push(relative(baseDir, fullPath));
1402
+ }
1403
+ }
1404
+ return files;
1405
+ }
1406
+
1407
+ // src/generate.ts
1408
+ async function generateFromOpenAPI(config) {
1409
+ const raw = await loadSpec(config.source);
1410
+ const parsed = parseOpenAPI(raw);
1411
+ if (config.operationIds) {
1412
+ for (const op of parsed.operations) {
1413
+ op.methodName = normalizeOperationId(op.operationId, op.method, op.path, config.operationIds, {
1414
+ operationId: op.operationId,
1415
+ method: op.method,
1416
+ path: op.path,
1417
+ tags: op.tags,
1418
+ hasBody: op.requestBody !== undefined
1419
+ });
1420
+ }
1421
+ }
1422
+ const resources = groupOperations(parsed.operations, config.groupBy, {
1423
+ excludeTags: config.excludeTags
1424
+ });
1425
+ const info = raw.info;
1426
+ const spec = {
1427
+ version: parsed.version,
1428
+ info: {
1429
+ title: typeof info?.title === "string" ? info.title : "Unknown",
1430
+ version: typeof info?.version === "string" ? info.version : "0.0.0"
1431
+ },
1432
+ resources,
1433
+ schemas: parsed.schemas,
1434
+ securitySchemes: parsed.securitySchemes
1435
+ };
1436
+ const files = generateAll(spec, {
1437
+ schemas: config.schemas,
1438
+ baseURL: config.baseURL
1439
+ });
1440
+ return writeIncremental(files, config.output, {
1441
+ clean: true,
1442
+ dryRun: config.dryRun
1443
+ });
1444
+ }
1445
+
1446
+ // src/config.ts
1447
+ import { existsSync as existsSync3 } from "node:fs";
1448
+ import { join as join2 } from "node:path";
1449
+ var DEFAULTS = {
1450
+ output: "./src/generated",
1451
+ baseURL: "",
1452
+ groupBy: "tag",
1453
+ schemas: false
1454
+ };
1455
+ function resolveConfig(cliFlags, configFile) {
1456
+ const source = cliFlags.from ?? cliFlags.source ?? configFile?.source;
1457
+ if (!source) {
1458
+ throw new Error('Missing required "source" — provide --from <path> or set source in openapi.config.ts');
1459
+ }
1460
+ return {
1461
+ source,
1462
+ output: cliFlags.output ?? configFile?.output ?? DEFAULTS.output,
1463
+ baseURL: cliFlags.baseURL ?? configFile?.baseURL ?? DEFAULTS.baseURL,
1464
+ groupBy: cliFlags.groupBy ?? configFile?.groupBy ?? DEFAULTS.groupBy,
1465
+ schemas: cliFlags.schemas ?? configFile?.schemas ?? DEFAULTS.schemas,
1466
+ excludeTags: cliFlags.excludeTags ?? configFile?.excludeTags,
1467
+ operationIds: cliFlags.operationIds ?? configFile?.operationIds
1468
+ };
1469
+ }
1470
+ async function loadConfigFile(cwd) {
1471
+ const configPath = join2(cwd, "openapi.config.ts");
1472
+ if (!existsSync3(configPath))
1473
+ return;
1474
+ const mod = await import(configPath);
1475
+ return mod.default ?? mod;
1476
+ }
1477
+ function defineConfig(config) {
1478
+ return config;
1479
+ }
1480
+
1481
+ export { sanitizeIdentifier, groupOperations, generateClient, jsonSchemaToTS, sanitizeTypeName, generateInterface, isValidIdentifier, generateResources, jsonSchemaToZod, generateSchemas, generateTypes, generateAll, loadSpec, normalizeOperationId, resolveRef, resolveSchema, parseOpenAPI, writeIncremental, generateFromOpenAPI, resolveConfig, loadConfigFile, defineConfig };