@vertz/openapi 0.1.0 → 0.1.1

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