@vertz/openapi 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/openapi.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1 -1
- package/dist/shared/{chunk-h8tb765a.js → chunk-070f9ezy.js} +431 -56
- package/package.json +1 -1
package/bin/openapi.ts
CHANGED
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ interface ParsedResource {
|
|
|
17
17
|
interface ParsedOperation {
|
|
18
18
|
operationId: string;
|
|
19
19
|
methodName: string;
|
|
20
|
+
/** PascalCase prefix for generated type names (shorter than operationId for path-heavy IDs). */
|
|
21
|
+
typePrefix?: string;
|
|
20
22
|
method: HttpMethod;
|
|
21
23
|
path: string;
|
|
22
24
|
pathParams: ParsedParameter[];
|
|
@@ -26,6 +28,8 @@ interface ParsedOperation {
|
|
|
26
28
|
responseStatus: number;
|
|
27
29
|
tags: string[];
|
|
28
30
|
security?: OperationSecurity;
|
|
31
|
+
streamingFormat?: "sse" | "ndjson";
|
|
32
|
+
jsonResponse?: ParsedSchema;
|
|
29
33
|
}
|
|
30
34
|
interface OperationSecurity {
|
|
31
35
|
required: boolean;
|
|
@@ -181,7 +185,7 @@ declare function jsonSchemaToZod(schema: Record<string, unknown>, namedSchemas:
|
|
|
181
185
|
/**
|
|
182
186
|
* Generate resource SDK files for all resources + a barrel index.
|
|
183
187
|
*/
|
|
184
|
-
declare function generateResources(resources: ParsedResource[]): GeneratedFile[];
|
|
188
|
+
declare function generateResources(resources: ParsedResource[], schemas?: ParsedSchema[]): GeneratedFile[];
|
|
185
189
|
/**
|
|
186
190
|
* Generate Zod schema files for all resources + barrel index.
|
|
187
191
|
*/
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,15 @@ function groupOperations(operations, strategy, options) {
|
|
|
42
42
|
|
|
43
43
|
// src/generators/client-generator.ts
|
|
44
44
|
function camelCase(name) {
|
|
45
|
+
const leadingUpper = name.match(/^[A-Z]+/);
|
|
46
|
+
if (leadingUpper) {
|
|
47
|
+
const prefix = leadingUpper[0];
|
|
48
|
+
if (prefix.length >= name.length)
|
|
49
|
+
return name.toLowerCase();
|
|
50
|
+
if (prefix.length > 1) {
|
|
51
|
+
return prefix.slice(0, -1).toLowerCase() + name.slice(prefix.length - 1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
45
54
|
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
46
55
|
}
|
|
47
56
|
function generateAuthField(scheme) {
|
|
@@ -228,6 +237,33 @@ ${lines.join(`
|
|
|
228
237
|
}
|
|
229
238
|
`;
|
|
230
239
|
}
|
|
240
|
+
function collectCircularRefs(schema, refs = new Set) {
|
|
241
|
+
if (typeof schema.$circular === "string") {
|
|
242
|
+
refs.add(schema.$circular);
|
|
243
|
+
return refs;
|
|
244
|
+
}
|
|
245
|
+
if (Array.isArray(schema.anyOf)) {
|
|
246
|
+
for (const member of schema.anyOf) {
|
|
247
|
+
collectCircularRefs(member, refs);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (Array.isArray(schema.oneOf)) {
|
|
251
|
+
for (const member of schema.oneOf) {
|
|
252
|
+
collectCircularRefs(member, refs);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (schema.type === "array" && schema.items && typeof schema.items === "object") {
|
|
256
|
+
collectCircularRefs(schema.items, refs);
|
|
257
|
+
}
|
|
258
|
+
if (schema.properties && typeof schema.properties === "object") {
|
|
259
|
+
for (const propSchema of Object.values(schema.properties)) {
|
|
260
|
+
if (propSchema && typeof propSchema === "object") {
|
|
261
|
+
collectCircularRefs(propSchema, refs);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return refs;
|
|
266
|
+
}
|
|
231
267
|
function mapPrimitive(type) {
|
|
232
268
|
if (type === "string")
|
|
233
269
|
return "string";
|
|
@@ -241,14 +277,26 @@ var VALID_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
|
241
277
|
function isValidIdentifier(name) {
|
|
242
278
|
return VALID_IDENTIFIER.test(name);
|
|
243
279
|
}
|
|
280
|
+
function toPascalCase(name) {
|
|
281
|
+
const cleaned = name.replace(/[^A-Za-z0-9]+/g, " ").trim();
|
|
282
|
+
if (!cleaned)
|
|
283
|
+
return "_";
|
|
284
|
+
const segments = cleaned.split(/\s+/).filter(Boolean);
|
|
285
|
+
const result = segments.map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
286
|
+
return /^[0-9]/.test(result) ? `_${result}` : result;
|
|
287
|
+
}
|
|
288
|
+
function getTypePrefix(op) {
|
|
289
|
+
return op.typePrefix ?? toPascalCase(op.operationId);
|
|
290
|
+
}
|
|
244
291
|
|
|
245
292
|
// src/generators/resource-generator.ts
|
|
246
|
-
function generateResources(resources) {
|
|
293
|
+
function generateResources(resources, schemas = []) {
|
|
247
294
|
const files = [];
|
|
295
|
+
const componentNames = new Set(schemas.filter((s) => s.name).map((s) => s.name));
|
|
248
296
|
for (const resource of resources) {
|
|
249
297
|
files.push({
|
|
250
298
|
path: `resources/${resource.identifier}.ts`,
|
|
251
|
-
content: generateResourceFile(resource)
|
|
299
|
+
content: generateResourceFile(resource, componentNames)
|
|
252
300
|
});
|
|
253
301
|
}
|
|
254
302
|
const exports = resources.map((r) => `export { create${r.name}Resource } from './${r.identifier}';`).join(`
|
|
@@ -257,12 +305,16 @@ function generateResources(resources) {
|
|
|
257
305
|
` });
|
|
258
306
|
return files;
|
|
259
307
|
}
|
|
260
|
-
function generateResourceFile(resource) {
|
|
308
|
+
function generateResourceFile(resource, componentNames) {
|
|
261
309
|
const lines = [];
|
|
262
|
-
const
|
|
310
|
+
const { resourceImports, componentImports } = collectTypeImports(resource, componentNames);
|
|
263
311
|
lines.push("import type { FetchClient, FetchResponse } from '@vertz/fetch';");
|
|
264
|
-
if (
|
|
265
|
-
const sorted = [...
|
|
312
|
+
if (componentImports.size > 0) {
|
|
313
|
+
const sorted = [...componentImports].sort();
|
|
314
|
+
lines.push(`import type { ${sorted.join(", ")} } from '../types/components';`);
|
|
315
|
+
}
|
|
316
|
+
if (resourceImports.size > 0) {
|
|
317
|
+
const sorted = [...resourceImports].sort();
|
|
266
318
|
lines.push(`import type { ${sorted.join(", ")} } from '../types/${resource.identifier}';`);
|
|
267
319
|
}
|
|
268
320
|
lines.push("");
|
|
@@ -270,7 +322,23 @@ function generateResourceFile(resource) {
|
|
|
270
322
|
lines.push(" return {");
|
|
271
323
|
validateUniqueMethodNames(resource);
|
|
272
324
|
for (const op of resource.operations) {
|
|
273
|
-
|
|
325
|
+
if (op.streamingFormat && op.jsonResponse) {
|
|
326
|
+
const jsonOp = {
|
|
327
|
+
...op,
|
|
328
|
+
streamingFormat: undefined,
|
|
329
|
+
jsonResponse: undefined,
|
|
330
|
+
response: op.jsonResponse
|
|
331
|
+
};
|
|
332
|
+
lines.push(` ${generateMethod(jsonOp)},`);
|
|
333
|
+
const streamOp = {
|
|
334
|
+
...op,
|
|
335
|
+
methodName: op.methodName + "Stream",
|
|
336
|
+
jsonResponse: undefined
|
|
337
|
+
};
|
|
338
|
+
lines.push(` ${generateMethod(streamOp)},`);
|
|
339
|
+
} else {
|
|
340
|
+
lines.push(` ${generateMethod(op)},`);
|
|
341
|
+
}
|
|
274
342
|
}
|
|
275
343
|
lines.push(" };");
|
|
276
344
|
lines.push("}");
|
|
@@ -281,6 +349,12 @@ function generateResourceFile(resource) {
|
|
|
281
349
|
function generateMethod(op) {
|
|
282
350
|
const params = buildParams(op);
|
|
283
351
|
const returnType = buildReturnType(op);
|
|
352
|
+
if (op.streamingFormat) {
|
|
353
|
+
const call2 = buildStreamingCall(op);
|
|
354
|
+
return `/** @throws {FetchError} on non-2xx response */
|
|
355
|
+
${op.methodName}: (${params}): ${returnType} =>
|
|
356
|
+
${call2}`;
|
|
357
|
+
}
|
|
284
358
|
const call = buildCall(op);
|
|
285
359
|
return `${op.methodName}: (${params}): ${returnType} =>
|
|
286
360
|
${call}`;
|
|
@@ -296,14 +370,24 @@ function validateUniqueMethodNames(resource) {
|
|
|
296
370
|
}
|
|
297
371
|
}
|
|
298
372
|
const duplicates = [...seen.entries()].filter(([, ids]) => ids.length > 1);
|
|
299
|
-
if (duplicates.length
|
|
300
|
-
|
|
301
|
-
const details = duplicates.map(([name, ids]) => ` - "${name}" used by: ${ids.join(", ")}`).join(`
|
|
373
|
+
if (duplicates.length > 0) {
|
|
374
|
+
const details = duplicates.map(([name, ids]) => ` - "${name}" used by: ${ids.join(", ")}`).join(`
|
|
302
375
|
`);
|
|
303
|
-
|
|
376
|
+
const rawTags = [...new Set(resource.operations.flatMap((op) => op.tags))];
|
|
377
|
+
const tagHint = rawTags.length > 0 ? ` (tags: ${rawTags.map((t) => `"${t}"`).join(", ")})` : "";
|
|
378
|
+
throw new Error(`Duplicate method name${duplicates.length > 1 ? "s" : ""} ${duplicates.map(([n]) => `"${n}"`).join(", ")} in resource "${resource.name}"${tagHint}. ` + `Each operation within a resource must have a unique method name.
|
|
304
379
|
${details}
|
|
305
380
|
|
|
306
381
|
` + `Fix: use excludeTags to skip this tag, use a different groupBy strategy, ` + `or provide operationIds.overrides to rename conflicting operations.`);
|
|
382
|
+
}
|
|
383
|
+
for (const op of resource.operations) {
|
|
384
|
+
if (op.streamingFormat && op.jsonResponse) {
|
|
385
|
+
const streamName = op.methodName + "Stream";
|
|
386
|
+
if (seen.has(streamName)) {
|
|
387
|
+
throw new Error(`Method name collision: dual-content operation "${op.operationId}" generates ` + `"${streamName}" which conflicts with existing method "${streamName}" in resource "${resource.name}".`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
307
391
|
}
|
|
308
392
|
function buildParams(op) {
|
|
309
393
|
const parts = [];
|
|
@@ -318,9 +402,16 @@ function buildParams(op) {
|
|
|
318
402
|
const queryName = deriveQueryName(op);
|
|
319
403
|
parts.push(`query?: ${queryName}`);
|
|
320
404
|
}
|
|
405
|
+
if (op.streamingFormat) {
|
|
406
|
+
parts.push("options?: { signal?: AbortSignal }");
|
|
407
|
+
}
|
|
321
408
|
return parts.join(", ");
|
|
322
409
|
}
|
|
323
410
|
function buildReturnType(op) {
|
|
411
|
+
if (op.streamingFormat) {
|
|
412
|
+
const typeName = deriveStreamingTypeName(op);
|
|
413
|
+
return `AsyncGenerator<${typeName}>`;
|
|
414
|
+
}
|
|
324
415
|
if (op.responseStatus === 204)
|
|
325
416
|
return "Promise<FetchResponse<void>>";
|
|
326
417
|
if (op.response?.name) {
|
|
@@ -334,7 +425,7 @@ function buildReturnType(op) {
|
|
|
334
425
|
return "Promise<FetchResponse<unknown[]>>";
|
|
335
426
|
}
|
|
336
427
|
if (op.response) {
|
|
337
|
-
const name =
|
|
428
|
+
const name = getTypePrefix(op) + "Response";
|
|
338
429
|
return `Promise<FetchResponse<${name}>>`;
|
|
339
430
|
}
|
|
340
431
|
return "Promise<FetchResponse<void>>";
|
|
@@ -351,6 +442,28 @@ function buildCall(op) {
|
|
|
351
442
|
}
|
|
352
443
|
return `client.${method}(${args.join(", ")})`;
|
|
353
444
|
}
|
|
445
|
+
function buildStreamingCall(op) {
|
|
446
|
+
const typeName = deriveStreamingTypeName(op);
|
|
447
|
+
const path = buildPath(op);
|
|
448
|
+
const props = [
|
|
449
|
+
`method: '${op.method}'`,
|
|
450
|
+
`path: ${path}`,
|
|
451
|
+
`format: '${op.streamingFormat}'`
|
|
452
|
+
];
|
|
453
|
+
if (op.requestBody)
|
|
454
|
+
props.push("body");
|
|
455
|
+
if (op.queryParams.length > 0)
|
|
456
|
+
props.push("query");
|
|
457
|
+
props.push("signal: options?.signal");
|
|
458
|
+
return `client.requestStream<${typeName}>({ ${props.join(", ")} })`;
|
|
459
|
+
}
|
|
460
|
+
function deriveStreamingTypeName(op) {
|
|
461
|
+
if (op.response?.name)
|
|
462
|
+
return sanitizeTypeName(op.response.name);
|
|
463
|
+
if (op.response)
|
|
464
|
+
return toPascalCase(op.operationId) + "Event";
|
|
465
|
+
return "unknown";
|
|
466
|
+
}
|
|
354
467
|
function buildPath(op) {
|
|
355
468
|
if (op.pathParams.length === 0) {
|
|
356
469
|
return `'${op.path}'`;
|
|
@@ -361,35 +474,70 @@ function buildPath(op) {
|
|
|
361
474
|
}
|
|
362
475
|
return `\`${path}\``;
|
|
363
476
|
}
|
|
364
|
-
function collectTypeImports(resource) {
|
|
365
|
-
const
|
|
477
|
+
function collectTypeImports(resource, componentNames) {
|
|
478
|
+
const resourceImports = new Set;
|
|
479
|
+
const componentImports = new Set;
|
|
366
480
|
for (const op of resource.operations) {
|
|
481
|
+
if (op.response) {
|
|
482
|
+
for (const ref of collectCircularRefs(op.response.jsonSchema)) {
|
|
483
|
+
if (componentNames.has(ref))
|
|
484
|
+
componentImports.add(ref);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (op.requestBody) {
|
|
488
|
+
for (const ref of collectCircularRefs(op.requestBody.jsonSchema)) {
|
|
489
|
+
if (componentNames.has(ref))
|
|
490
|
+
componentImports.add(ref);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
367
493
|
if (op.responseStatus !== 204 && op.response) {
|
|
368
|
-
if (op.
|
|
369
|
-
|
|
494
|
+
if (op.streamingFormat) {
|
|
495
|
+
const streamTypeName = deriveStreamingTypeName(op);
|
|
496
|
+
if (streamTypeName !== "unknown") {
|
|
497
|
+
if (componentNames.has(streamTypeName)) {
|
|
498
|
+
componentImports.add(streamTypeName);
|
|
499
|
+
} else {
|
|
500
|
+
resourceImports.add(streamTypeName);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
const name = op.response.name ? sanitizeTypeName(op.response.name) : getTypePrefix(op) + "Response";
|
|
505
|
+
if (componentNames.has(name)) {
|
|
506
|
+
componentImports.add(name);
|
|
507
|
+
} else {
|
|
508
|
+
resourceImports.add(name);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (op.jsonResponse) {
|
|
513
|
+
const name = op.jsonResponse.name ? sanitizeTypeName(op.jsonResponse.name) : getTypePrefix(op) + "Response";
|
|
514
|
+
if (componentNames.has(name)) {
|
|
515
|
+
componentImports.add(name);
|
|
370
516
|
} else {
|
|
371
|
-
|
|
517
|
+
resourceImports.add(name);
|
|
372
518
|
}
|
|
373
519
|
}
|
|
374
520
|
if (op.requestBody) {
|
|
375
|
-
|
|
521
|
+
const name = deriveInputName(op);
|
|
522
|
+
if (componentNames.has(name)) {
|
|
523
|
+
componentImports.add(name);
|
|
524
|
+
} else {
|
|
525
|
+
resourceImports.add(name);
|
|
526
|
+
}
|
|
376
527
|
}
|
|
377
528
|
if (op.queryParams.length > 0) {
|
|
378
|
-
|
|
529
|
+
resourceImports.add(deriveQueryName(op));
|
|
379
530
|
}
|
|
380
531
|
}
|
|
381
|
-
return
|
|
532
|
+
return { resourceImports, componentImports };
|
|
382
533
|
}
|
|
383
534
|
function deriveInputName(op) {
|
|
384
535
|
if (op.requestBody?.name)
|
|
385
536
|
return sanitizeTypeName(op.requestBody.name);
|
|
386
|
-
return
|
|
537
|
+
return getTypePrefix(op) + "Input";
|
|
387
538
|
}
|
|
388
539
|
function deriveQueryName(op) {
|
|
389
|
-
return
|
|
390
|
-
}
|
|
391
|
-
function capitalize(s) {
|
|
392
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
540
|
+
return getTypePrefix(op) + "Query";
|
|
393
541
|
}
|
|
394
542
|
|
|
395
543
|
// src/generators/json-schema-to-zod.ts
|
|
@@ -534,13 +682,25 @@ function zodPrimitive(type, schema) {
|
|
|
534
682
|
function generateSchemas(resources, schemas) {
|
|
535
683
|
const files = [];
|
|
536
684
|
const namedSchemas = buildNamedSchemaMap(schemas);
|
|
685
|
+
if (schemas.length > 0) {
|
|
686
|
+
files.push({
|
|
687
|
+
path: "schemas/components.ts",
|
|
688
|
+
content: generateComponentSchemas(schemas, namedSchemas)
|
|
689
|
+
});
|
|
690
|
+
}
|
|
537
691
|
for (const resource of resources) {
|
|
538
692
|
const content = generateResourceSchemas(resource, namedSchemas);
|
|
539
693
|
files.push({ path: `schemas/${resource.identifier}.ts`, content });
|
|
540
694
|
}
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
695
|
+
const barrelLines = [];
|
|
696
|
+
if (schemas.length > 0) {
|
|
697
|
+
barrelLines.push("export * from './components';");
|
|
698
|
+
}
|
|
699
|
+
for (const r of resources) {
|
|
700
|
+
barrelLines.push(`export * from './${r.identifier}';`);
|
|
701
|
+
}
|
|
702
|
+
files.push({ path: "schemas/index.ts", content: barrelLines.join(`
|
|
703
|
+
`) + `
|
|
544
704
|
` });
|
|
545
705
|
return files;
|
|
546
706
|
}
|
|
@@ -553,30 +713,51 @@ function buildNamedSchemaMap(schemas) {
|
|
|
553
713
|
}
|
|
554
714
|
return map;
|
|
555
715
|
}
|
|
716
|
+
function generateComponentSchemas(schemas, namedSchemas) {
|
|
717
|
+
const lines = [];
|
|
718
|
+
lines.push("import { z } from 'zod';");
|
|
719
|
+
lines.push("");
|
|
720
|
+
for (const s of schemas) {
|
|
721
|
+
if (s.name) {
|
|
722
|
+
const varName = toSchemaVarName(s.name);
|
|
723
|
+
const zod = jsonSchemaToZod(s.jsonSchema, namedSchemas);
|
|
724
|
+
lines.push(`export const ${varName} = ${zod};`);
|
|
725
|
+
lines.push("");
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return lines.join(`
|
|
729
|
+
`);
|
|
730
|
+
}
|
|
556
731
|
function generateResourceSchemas(resource, namedSchemas) {
|
|
557
732
|
const lines = [];
|
|
558
733
|
lines.push("import { z } from 'zod';");
|
|
559
734
|
lines.push("");
|
|
560
735
|
const emitted = new Set;
|
|
736
|
+
const componentImports = new Set;
|
|
561
737
|
for (const op of resource.operations) {
|
|
738
|
+
collectOperationComponentRefs(op, componentImports, namedSchemas);
|
|
562
739
|
if (op.response) {
|
|
563
740
|
const varName = deriveResponseSchemaName(op);
|
|
564
741
|
if (!emitted.has(varName)) {
|
|
565
742
|
emitted.add(varName);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
743
|
+
if (!isComponentSchemaVar(varName, namedSchemas)) {
|
|
744
|
+
const schema = op.response.jsonSchema;
|
|
745
|
+
const effectiveSchema = schema.type === "array" && schema.items && typeof schema.items === "object" ? schema.items : schema;
|
|
746
|
+
const zod = jsonSchemaToZod(effectiveSchema, namedSchemas);
|
|
747
|
+
lines.push(`export const ${varName} = ${zod};`);
|
|
748
|
+
lines.push("");
|
|
749
|
+
}
|
|
571
750
|
}
|
|
572
751
|
}
|
|
573
752
|
if (op.requestBody) {
|
|
574
753
|
const varName = deriveInputSchemaName(op);
|
|
575
754
|
if (!emitted.has(varName)) {
|
|
576
755
|
emitted.add(varName);
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
756
|
+
if (!isComponentSchemaVar(varName, namedSchemas)) {
|
|
757
|
+
const zod = jsonSchemaToZod(op.requestBody.jsonSchema, namedSchemas);
|
|
758
|
+
lines.push(`export const ${varName} = ${zod};`);
|
|
759
|
+
lines.push("");
|
|
760
|
+
}
|
|
580
761
|
}
|
|
581
762
|
}
|
|
582
763
|
if (op.queryParams.length > 0) {
|
|
@@ -588,9 +769,37 @@ function generateResourceSchemas(resource, namedSchemas) {
|
|
|
588
769
|
}
|
|
589
770
|
}
|
|
590
771
|
}
|
|
772
|
+
const actualImports = [...componentImports].sort();
|
|
773
|
+
if (actualImports.length > 0) {
|
|
774
|
+
lines.splice(2, 0, `import { ${actualImports.join(", ")} } from './components';`, "");
|
|
775
|
+
}
|
|
591
776
|
return lines.join(`
|
|
592
777
|
`);
|
|
593
778
|
}
|
|
779
|
+
function collectOperationComponentRefs(op, imports, namedSchemas) {
|
|
780
|
+
const schemas = [];
|
|
781
|
+
if (op.response)
|
|
782
|
+
schemas.push(op.response.jsonSchema);
|
|
783
|
+
if (op.requestBody)
|
|
784
|
+
schemas.push(op.requestBody.jsonSchema);
|
|
785
|
+
for (const param of op.queryParams)
|
|
786
|
+
schemas.push(param.schema);
|
|
787
|
+
for (const schema of schemas) {
|
|
788
|
+
const refs = collectCircularRefs(schema);
|
|
789
|
+
for (const ref of refs) {
|
|
790
|
+
const varName = namedSchemas.get(ref);
|
|
791
|
+
if (varName)
|
|
792
|
+
imports.add(varName);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function isComponentSchemaVar(varName, namedSchemas) {
|
|
797
|
+
for (const componentVarName of namedSchemas.values()) {
|
|
798
|
+
if (componentVarName === varName)
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
594
803
|
function buildQueryZodSchema(op, namedSchemas) {
|
|
595
804
|
const entries = op.queryParams.map((param) => {
|
|
596
805
|
let zod = jsonSchemaToZod(param.schema, namedSchemas);
|
|
@@ -607,18 +816,19 @@ ${entries.join(`,
|
|
|
607
816
|
function deriveResponseSchemaName(op) {
|
|
608
817
|
if (op.response?.name)
|
|
609
818
|
return toSchemaVarName(op.response.name);
|
|
610
|
-
return toSchemaVarName(op
|
|
819
|
+
return toSchemaVarName(getTypePrefix(op) + "Response");
|
|
611
820
|
}
|
|
612
821
|
function deriveInputSchemaName(op) {
|
|
613
822
|
if (op.requestBody?.name)
|
|
614
823
|
return toSchemaVarName(op.requestBody.name);
|
|
615
|
-
return toSchemaVarName(op
|
|
824
|
+
return toSchemaVarName(getTypePrefix(op) + "Input");
|
|
616
825
|
}
|
|
617
826
|
function deriveQuerySchemaName(op) {
|
|
618
|
-
return toSchemaVarName(op
|
|
827
|
+
return toSchemaVarName(getTypePrefix(op) + "Query");
|
|
619
828
|
}
|
|
620
829
|
function toSchemaVarName(name) {
|
|
621
|
-
const
|
|
830
|
+
const sanitized = sanitizeTypeName(name);
|
|
831
|
+
const camel = sanitized.charAt(0).toLowerCase() + sanitized.slice(1);
|
|
622
832
|
return camel + "Schema";
|
|
623
833
|
}
|
|
624
834
|
|
|
@@ -626,13 +836,25 @@ function toSchemaVarName(name) {
|
|
|
626
836
|
function generateTypes(resources, schemas) {
|
|
627
837
|
const files = [];
|
|
628
838
|
const namedSchemas = buildNamedSchemaMap2(schemas);
|
|
839
|
+
if (schemas.length > 0) {
|
|
840
|
+
files.push({
|
|
841
|
+
path: "types/components.ts",
|
|
842
|
+
content: generateComponentTypes(schemas, namedSchemas)
|
|
843
|
+
});
|
|
844
|
+
}
|
|
629
845
|
for (const resource of resources) {
|
|
630
846
|
const content = generateResourceTypes(resource, namedSchemas);
|
|
631
847
|
files.push({ path: `types/${resource.identifier}.ts`, content: content || "" });
|
|
632
848
|
}
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
849
|
+
const barrelLines = [];
|
|
850
|
+
if (schemas.length > 0) {
|
|
851
|
+
barrelLines.push("export * from './components';");
|
|
852
|
+
}
|
|
853
|
+
for (const r of resources) {
|
|
854
|
+
barrelLines.push(`export * from './${r.identifier}';`);
|
|
855
|
+
}
|
|
856
|
+
files.push({ path: "types/index.ts", content: barrelLines.join(`
|
|
857
|
+
`) + `
|
|
636
858
|
` });
|
|
637
859
|
return files;
|
|
638
860
|
}
|
|
@@ -645,24 +867,44 @@ function buildNamedSchemaMap2(schemas) {
|
|
|
645
867
|
}
|
|
646
868
|
return map;
|
|
647
869
|
}
|
|
870
|
+
function generateComponentTypes(schemas, namedSchemas) {
|
|
871
|
+
const interfaces = [];
|
|
872
|
+
for (const s of schemas) {
|
|
873
|
+
if (s.name) {
|
|
874
|
+
interfaces.push(generateInterface(s.name, s.jsonSchema, namedSchemas));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return interfaces.join(`
|
|
878
|
+
`);
|
|
879
|
+
}
|
|
648
880
|
function generateResourceTypes(resource, namedSchemas) {
|
|
649
881
|
const interfaces = [];
|
|
650
882
|
const emitted = new Set;
|
|
883
|
+
const componentImports = new Set;
|
|
651
884
|
for (const op of resource.operations) {
|
|
885
|
+
collectOperationCircularRefs(op, componentImports, namedSchemas);
|
|
652
886
|
if (op.response) {
|
|
653
887
|
const name = deriveResponseName(op);
|
|
654
888
|
if (!emitted.has(name)) {
|
|
655
889
|
emitted.add(name);
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
890
|
+
if (namedSchemas.has(name)) {
|
|
891
|
+
componentImports.add(name);
|
|
892
|
+
} else {
|
|
893
|
+
const schema = op.response.jsonSchema;
|
|
894
|
+
const effectiveSchema = schema.type === "array" && schema.items && typeof schema.items === "object" ? schema.items : schema;
|
|
895
|
+
interfaces.push(generateInterface(name, effectiveSchema, namedSchemas));
|
|
896
|
+
}
|
|
659
897
|
}
|
|
660
898
|
}
|
|
661
899
|
if (op.requestBody) {
|
|
662
900
|
const name = deriveInputName2(op);
|
|
663
901
|
if (!emitted.has(name)) {
|
|
664
902
|
emitted.add(name);
|
|
665
|
-
|
|
903
|
+
if (namedSchemas.has(name)) {
|
|
904
|
+
componentImports.add(name);
|
|
905
|
+
} else {
|
|
906
|
+
interfaces.push(generateInterface(name, op.requestBody.jsonSchema, namedSchemas));
|
|
907
|
+
}
|
|
666
908
|
}
|
|
667
909
|
}
|
|
668
910
|
if (op.queryParams.length > 0) {
|
|
@@ -673,21 +915,52 @@ function generateResourceTypes(resource, namedSchemas) {
|
|
|
673
915
|
}
|
|
674
916
|
}
|
|
675
917
|
}
|
|
676
|
-
|
|
918
|
+
const lines = [];
|
|
919
|
+
const actualImports = [...componentImports].filter((name) => namedSchemas.has(name)).sort();
|
|
920
|
+
if (actualImports.length > 0) {
|
|
921
|
+
lines.push(`import type { ${actualImports.join(", ")} } from './components';`);
|
|
922
|
+
lines.push("");
|
|
923
|
+
}
|
|
924
|
+
lines.push(interfaces.join(`
|
|
925
|
+
`));
|
|
926
|
+
return lines.join(`
|
|
677
927
|
`);
|
|
678
928
|
}
|
|
929
|
+
function collectOperationCircularRefs(op, imports, namedSchemas) {
|
|
930
|
+
if (op.response) {
|
|
931
|
+
const refs = collectCircularRefs(op.response.jsonSchema);
|
|
932
|
+
for (const ref of refs) {
|
|
933
|
+
if (namedSchemas.has(ref))
|
|
934
|
+
imports.add(ref);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (op.requestBody) {
|
|
938
|
+
const refs = collectCircularRefs(op.requestBody.jsonSchema);
|
|
939
|
+
for (const ref of refs) {
|
|
940
|
+
if (namedSchemas.has(ref))
|
|
941
|
+
imports.add(ref);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
for (const param of op.queryParams) {
|
|
945
|
+
const refs = collectCircularRefs(param.schema);
|
|
946
|
+
for (const ref of refs) {
|
|
947
|
+
if (namedSchemas.has(ref))
|
|
948
|
+
imports.add(ref);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
679
952
|
function deriveResponseName(op) {
|
|
680
953
|
if (op.response?.name)
|
|
681
954
|
return sanitizeTypeName(op.response.name);
|
|
682
|
-
return
|
|
955
|
+
return getTypePrefix(op) + "Response";
|
|
683
956
|
}
|
|
684
957
|
function deriveInputName2(op) {
|
|
685
958
|
if (op.requestBody?.name)
|
|
686
959
|
return sanitizeTypeName(op.requestBody.name);
|
|
687
|
-
return
|
|
960
|
+
return getTypePrefix(op) + "Input";
|
|
688
961
|
}
|
|
689
962
|
function deriveQueryName2(op) {
|
|
690
|
-
return
|
|
963
|
+
return getTypePrefix(op) + "Query";
|
|
691
964
|
}
|
|
692
965
|
function generateQueryInterface(name, op, namedSchemas) {
|
|
693
966
|
const lines = op.queryParams.map((param) => {
|
|
@@ -696,15 +969,13 @@ function generateQueryInterface(name, op, namedSchemas) {
|
|
|
696
969
|
const safeKey = isValidIdentifier(param.name) ? param.name : `'${param.name.replace(/'/g, "\\'")}'`;
|
|
697
970
|
return ` ${safeKey}${optional}: ${tsType};`;
|
|
698
971
|
});
|
|
972
|
+
lines.push(" [key: string]: unknown;");
|
|
699
973
|
return `export interface ${sanitizeTypeName(name)} {
|
|
700
974
|
${lines.join(`
|
|
701
975
|
`)}
|
|
702
976
|
}
|
|
703
977
|
`;
|
|
704
978
|
}
|
|
705
|
-
function capitalize2(s) {
|
|
706
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
707
|
-
}
|
|
708
979
|
|
|
709
980
|
// src/generators/index.ts
|
|
710
981
|
function generateAll(spec, options) {
|
|
@@ -712,7 +983,7 @@ function generateAll(spec, options) {
|
|
|
712
983
|
const opts = { schemas: false, baseURL: "", ...options };
|
|
713
984
|
const files = [];
|
|
714
985
|
files.push(...generateTypes(resources, schemas));
|
|
715
|
-
files.push(...generateResources(resources));
|
|
986
|
+
files.push(...generateResources(resources, schemas));
|
|
716
987
|
files.push(generateClient(resources, {
|
|
717
988
|
baseURL: opts.baseURL,
|
|
718
989
|
securitySchemes: spec.securitySchemes
|
|
@@ -930,6 +1201,50 @@ function detectCrudMethod(method, path) {
|
|
|
930
1201
|
}
|
|
931
1202
|
return;
|
|
932
1203
|
}
|
|
1204
|
+
function deriveTypePrefix(operationId, path) {
|
|
1205
|
+
const withoutController = operationId.replace(/^[A-Za-z0-9]+Controller[_.-]+/, "");
|
|
1206
|
+
const words = splitWords2(withoutController).map((w) => w.toLowerCase());
|
|
1207
|
+
if (words.length > 1 && words.at(-1) && HTTP_METHOD_WORDS.has(words.at(-1))) {
|
|
1208
|
+
words.pop();
|
|
1209
|
+
}
|
|
1210
|
+
const pathWordList = [];
|
|
1211
|
+
for (const segment of getPathSegments(path)) {
|
|
1212
|
+
const name = isPathParam(segment) ? segment.slice(1, -1) : segment;
|
|
1213
|
+
for (const w of splitWords2(name)) {
|
|
1214
|
+
pathWordList.push(w.toLowerCase());
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
const cutIndex = findPathSuffixStart(words, pathWordList);
|
|
1218
|
+
const meaningful = cutIndex >= 2 ? words.slice(0, cutIndex) : words;
|
|
1219
|
+
return meaningful.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
1220
|
+
}
|
|
1221
|
+
function findPathSuffixStart(words, pathWords) {
|
|
1222
|
+
if (pathWords.length === 0)
|
|
1223
|
+
return words.length;
|
|
1224
|
+
for (let start = 1;start < words.length; start++) {
|
|
1225
|
+
if (isSuffixMatchingPath(words, start, pathWords)) {
|
|
1226
|
+
return start;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return words.length;
|
|
1230
|
+
}
|
|
1231
|
+
function isSuffixMatchingPath(words, start, pathWords) {
|
|
1232
|
+
let pathIdx = 0;
|
|
1233
|
+
for (let i = start;i < words.length; i++) {
|
|
1234
|
+
let found = false;
|
|
1235
|
+
while (pathIdx < pathWords.length) {
|
|
1236
|
+
if (words[i] === pathWords[pathIdx]) {
|
|
1237
|
+
pathIdx++;
|
|
1238
|
+
found = true;
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
pathIdx++;
|
|
1242
|
+
}
|
|
1243
|
+
if (!found)
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
return true;
|
|
1247
|
+
}
|
|
933
1248
|
function normalizeOperationId(operationId, method, path, config, context) {
|
|
934
1249
|
if (config?.overrides?.[operationId]) {
|
|
935
1250
|
return config.overrides[operationId];
|
|
@@ -1130,6 +1445,20 @@ function getJsonContentSchema(value) {
|
|
|
1130
1445
|
}
|
|
1131
1446
|
return mediaType.schema;
|
|
1132
1447
|
}
|
|
1448
|
+
function getStreamingContentSchema(value) {
|
|
1449
|
+
if (!isRecord2(value) || !isRecord2(value.content)) {
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
const sse = value.content["text/event-stream"];
|
|
1453
|
+
if (isRecord2(sse)) {
|
|
1454
|
+
return { schema: isRecord2(sse.schema) ? sse.schema : undefined, format: "sse" };
|
|
1455
|
+
}
|
|
1456
|
+
const ndjson = value.content["application/x-ndjson"];
|
|
1457
|
+
if (isRecord2(ndjson)) {
|
|
1458
|
+
return { schema: isRecord2(ndjson.schema) ? ndjson.schema : undefined, format: "ndjson" };
|
|
1459
|
+
}
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1133
1462
|
function extractRefName(schema) {
|
|
1134
1463
|
if (typeof schema.$ref === "string") {
|
|
1135
1464
|
const segments = schema.$ref.split("/");
|
|
@@ -1163,9 +1492,26 @@ function pickSuccessResponse(operation, spec) {
|
|
|
1163
1492
|
return { status: 200 };
|
|
1164
1493
|
}
|
|
1165
1494
|
const resolvedResponse = resolveOpenAPIObject(first.response, spec);
|
|
1495
|
+
const jsonSchema = getJsonContentSchema(resolvedResponse);
|
|
1496
|
+
const streaming = getStreamingContentSchema(resolvedResponse);
|
|
1497
|
+
if (streaming && jsonSchema) {
|
|
1498
|
+
return {
|
|
1499
|
+
status: first.status,
|
|
1500
|
+
schema: jsonSchema,
|
|
1501
|
+
streamingFormat: streaming.format,
|
|
1502
|
+
streamingSchema: streaming.schema
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
if (streaming) {
|
|
1506
|
+
return {
|
|
1507
|
+
status: first.status,
|
|
1508
|
+
schema: streaming.schema,
|
|
1509
|
+
streamingFormat: streaming.format
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1166
1512
|
return {
|
|
1167
1513
|
status: first.status,
|
|
1168
|
-
schema:
|
|
1514
|
+
schema: jsonSchema
|
|
1169
1515
|
};
|
|
1170
1516
|
}
|
|
1171
1517
|
function getCombinedParameters(pathItem, operation, spec) {
|
|
@@ -1300,6 +1646,7 @@ function parseOpenAPI(spec) {
|
|
|
1300
1646
|
const parsed = {
|
|
1301
1647
|
operationId,
|
|
1302
1648
|
methodName: normalizeOperationId(operationId, method.toUpperCase(), path),
|
|
1649
|
+
typePrefix: deriveTypePrefix(operationId, path),
|
|
1303
1650
|
method: method.toUpperCase(),
|
|
1304
1651
|
path,
|
|
1305
1652
|
pathParams,
|
|
@@ -1311,9 +1658,17 @@ function parseOpenAPI(spec) {
|
|
|
1311
1658
|
};
|
|
1312
1659
|
if (security)
|
|
1313
1660
|
parsed.security = security;
|
|
1661
|
+
if (successResponse.streamingFormat) {
|
|
1662
|
+
parsed.streamingFormat = successResponse.streamingFormat;
|
|
1663
|
+
if (successResponse.streamingSchema) {
|
|
1664
|
+
parsed.response = resolveSchemaForOutput(successResponse.streamingSchema, spec, version);
|
|
1665
|
+
parsed.jsonResponse = successResponse.schema ? resolveSchemaForOutput(successResponse.schema, spec, version) : undefined;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1314
1668
|
operations.push(parsed);
|
|
1315
1669
|
}
|
|
1316
1670
|
}
|
|
1671
|
+
deduplicateTypePrefixes(operations);
|
|
1317
1672
|
return {
|
|
1318
1673
|
operations,
|
|
1319
1674
|
schemas: collectComponentSchemas(spec, version),
|
|
@@ -1321,6 +1676,26 @@ function parseOpenAPI(spec) {
|
|
|
1321
1676
|
version
|
|
1322
1677
|
};
|
|
1323
1678
|
}
|
|
1679
|
+
function deduplicateTypePrefixes(operations) {
|
|
1680
|
+
const seen = new Map;
|
|
1681
|
+
for (const op of operations) {
|
|
1682
|
+
if (!op.typePrefix)
|
|
1683
|
+
continue;
|
|
1684
|
+
const existing = seen.get(op.typePrefix);
|
|
1685
|
+
if (existing) {
|
|
1686
|
+
existing.push(op);
|
|
1687
|
+
} else {
|
|
1688
|
+
seen.set(op.typePrefix, [op]);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
for (const [, ops] of seen) {
|
|
1692
|
+
if (ops.length > 1) {
|
|
1693
|
+
for (const op of ops) {
|
|
1694
|
+
op.typePrefix = undefined;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1324
1699
|
|
|
1325
1700
|
// src/writer/incremental.ts
|
|
1326
1701
|
import { createHash } from "node:crypto";
|