@vertz/openapi 0.1.3 → 0.1.5
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/dist/cli.js +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1 -1
- package/dist/shared/{chunk-19t7k664.js → chunk-pd1709kk.js} +411 -49
- package/package.json +1 -1
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
|
@@ -237,6 +237,33 @@ ${lines.join(`
|
|
|
237
237
|
}
|
|
238
238
|
`;
|
|
239
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
|
+
}
|
|
240
267
|
function mapPrimitive(type) {
|
|
241
268
|
if (type === "string")
|
|
242
269
|
return "string";
|
|
@@ -258,14 +285,18 @@ function toPascalCase(name) {
|
|
|
258
285
|
const result = segments.map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
259
286
|
return /^[0-9]/.test(result) ? `_${result}` : result;
|
|
260
287
|
}
|
|
288
|
+
function getTypePrefix(op) {
|
|
289
|
+
return op.typePrefix ?? toPascalCase(op.operationId);
|
|
290
|
+
}
|
|
261
291
|
|
|
262
292
|
// src/generators/resource-generator.ts
|
|
263
|
-
function generateResources(resources) {
|
|
293
|
+
function generateResources(resources, schemas = []) {
|
|
264
294
|
const files = [];
|
|
295
|
+
const componentNames = new Set(schemas.filter((s) => s.name).map((s) => s.name));
|
|
265
296
|
for (const resource of resources) {
|
|
266
297
|
files.push({
|
|
267
298
|
path: `resources/${resource.identifier}.ts`,
|
|
268
|
-
content: generateResourceFile(resource)
|
|
299
|
+
content: generateResourceFile(resource, componentNames)
|
|
269
300
|
});
|
|
270
301
|
}
|
|
271
302
|
const exports = resources.map((r) => `export { create${r.name}Resource } from './${r.identifier}';`).join(`
|
|
@@ -274,12 +305,16 @@ function generateResources(resources) {
|
|
|
274
305
|
` });
|
|
275
306
|
return files;
|
|
276
307
|
}
|
|
277
|
-
function generateResourceFile(resource) {
|
|
308
|
+
function generateResourceFile(resource, componentNames) {
|
|
278
309
|
const lines = [];
|
|
279
|
-
const
|
|
310
|
+
const { resourceImports, componentImports } = collectTypeImports(resource, componentNames);
|
|
280
311
|
lines.push("import type { FetchClient, FetchResponse } from '@vertz/fetch';");
|
|
281
|
-
if (
|
|
282
|
-
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();
|
|
283
318
|
lines.push(`import type { ${sorted.join(", ")} } from '../types/${resource.identifier}';`);
|
|
284
319
|
}
|
|
285
320
|
lines.push("");
|
|
@@ -287,7 +322,23 @@ function generateResourceFile(resource) {
|
|
|
287
322
|
lines.push(" return {");
|
|
288
323
|
validateUniqueMethodNames(resource);
|
|
289
324
|
for (const op of resource.operations) {
|
|
290
|
-
|
|
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
|
+
}
|
|
291
342
|
}
|
|
292
343
|
lines.push(" };");
|
|
293
344
|
lines.push("}");
|
|
@@ -298,6 +349,12 @@ function generateResourceFile(resource) {
|
|
|
298
349
|
function generateMethod(op) {
|
|
299
350
|
const params = buildParams(op);
|
|
300
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
|
+
}
|
|
301
358
|
const call = buildCall(op);
|
|
302
359
|
return `${op.methodName}: (${params}): ${returnType} =>
|
|
303
360
|
${call}`;
|
|
@@ -313,14 +370,24 @@ function validateUniqueMethodNames(resource) {
|
|
|
313
370
|
}
|
|
314
371
|
}
|
|
315
372
|
const duplicates = [...seen.entries()].filter(([, ids]) => ids.length > 1);
|
|
316
|
-
if (duplicates.length
|
|
317
|
-
|
|
318
|
-
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(`
|
|
319
375
|
`);
|
|
320
|
-
|
|
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.
|
|
321
379
|
${details}
|
|
322
380
|
|
|
323
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
|
+
}
|
|
324
391
|
}
|
|
325
392
|
function buildParams(op) {
|
|
326
393
|
const parts = [];
|
|
@@ -335,9 +402,16 @@ function buildParams(op) {
|
|
|
335
402
|
const queryName = deriveQueryName(op);
|
|
336
403
|
parts.push(`query?: ${queryName}`);
|
|
337
404
|
}
|
|
405
|
+
if (op.streamingFormat) {
|
|
406
|
+
parts.push("options?: { signal?: AbortSignal }");
|
|
407
|
+
}
|
|
338
408
|
return parts.join(", ");
|
|
339
409
|
}
|
|
340
410
|
function buildReturnType(op) {
|
|
411
|
+
if (op.streamingFormat) {
|
|
412
|
+
const typeName = deriveStreamingTypeName(op);
|
|
413
|
+
return `AsyncGenerator<${typeName}>`;
|
|
414
|
+
}
|
|
341
415
|
if (op.responseStatus === 204)
|
|
342
416
|
return "Promise<FetchResponse<void>>";
|
|
343
417
|
if (op.response?.name) {
|
|
@@ -351,7 +425,7 @@ function buildReturnType(op) {
|
|
|
351
425
|
return "Promise<FetchResponse<unknown[]>>";
|
|
352
426
|
}
|
|
353
427
|
if (op.response) {
|
|
354
|
-
const name =
|
|
428
|
+
const name = getTypePrefix(op) + "Response";
|
|
355
429
|
return `Promise<FetchResponse<${name}>>`;
|
|
356
430
|
}
|
|
357
431
|
return "Promise<FetchResponse<void>>";
|
|
@@ -368,6 +442,28 @@ function buildCall(op) {
|
|
|
368
442
|
}
|
|
369
443
|
return `client.${method}(${args.join(", ")})`;
|
|
370
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
|
+
}
|
|
371
467
|
function buildPath(op) {
|
|
372
468
|
if (op.pathParams.length === 0) {
|
|
373
469
|
return `'${op.path}'`;
|
|
@@ -378,32 +474,70 @@ function buildPath(op) {
|
|
|
378
474
|
}
|
|
379
475
|
return `\`${path}\``;
|
|
380
476
|
}
|
|
381
|
-
function collectTypeImports(resource) {
|
|
382
|
-
const
|
|
477
|
+
function collectTypeImports(resource, componentNames) {
|
|
478
|
+
const resourceImports = new Set;
|
|
479
|
+
const componentImports = new Set;
|
|
383
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
|
+
}
|
|
384
493
|
if (op.responseStatus !== 204 && op.response) {
|
|
385
|
-
if (op.
|
|
386
|
-
|
|
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);
|
|
387
516
|
} else {
|
|
388
|
-
|
|
517
|
+
resourceImports.add(name);
|
|
389
518
|
}
|
|
390
519
|
}
|
|
391
520
|
if (op.requestBody) {
|
|
392
|
-
|
|
521
|
+
const name = deriveInputName(op);
|
|
522
|
+
if (componentNames.has(name)) {
|
|
523
|
+
componentImports.add(name);
|
|
524
|
+
} else {
|
|
525
|
+
resourceImports.add(name);
|
|
526
|
+
}
|
|
393
527
|
}
|
|
394
528
|
if (op.queryParams.length > 0) {
|
|
395
|
-
|
|
529
|
+
resourceImports.add(deriveQueryName(op));
|
|
396
530
|
}
|
|
397
531
|
}
|
|
398
|
-
return
|
|
532
|
+
return { resourceImports, componentImports };
|
|
399
533
|
}
|
|
400
534
|
function deriveInputName(op) {
|
|
401
535
|
if (op.requestBody?.name)
|
|
402
536
|
return sanitizeTypeName(op.requestBody.name);
|
|
403
|
-
return
|
|
537
|
+
return getTypePrefix(op) + "Input";
|
|
404
538
|
}
|
|
405
539
|
function deriveQueryName(op) {
|
|
406
|
-
return
|
|
540
|
+
return getTypePrefix(op) + "Query";
|
|
407
541
|
}
|
|
408
542
|
|
|
409
543
|
// src/generators/json-schema-to-zod.ts
|
|
@@ -548,13 +682,25 @@ function zodPrimitive(type, schema) {
|
|
|
548
682
|
function generateSchemas(resources, schemas) {
|
|
549
683
|
const files = [];
|
|
550
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
|
+
}
|
|
551
691
|
for (const resource of resources) {
|
|
552
692
|
const content = generateResourceSchemas(resource, namedSchemas);
|
|
553
693
|
files.push({ path: `schemas/${resource.identifier}.ts`, content });
|
|
554
694
|
}
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
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
|
+
`) + `
|
|
558
704
|
` });
|
|
559
705
|
return files;
|
|
560
706
|
}
|
|
@@ -567,30 +713,51 @@ function buildNamedSchemaMap(schemas) {
|
|
|
567
713
|
}
|
|
568
714
|
return map;
|
|
569
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
|
+
}
|
|
570
731
|
function generateResourceSchemas(resource, namedSchemas) {
|
|
571
732
|
const lines = [];
|
|
572
733
|
lines.push("import { z } from 'zod';");
|
|
573
734
|
lines.push("");
|
|
574
735
|
const emitted = new Set;
|
|
736
|
+
const componentImports = new Set;
|
|
575
737
|
for (const op of resource.operations) {
|
|
738
|
+
collectOperationComponentRefs(op, componentImports, namedSchemas);
|
|
576
739
|
if (op.response) {
|
|
577
740
|
const varName = deriveResponseSchemaName(op);
|
|
578
741
|
if (!emitted.has(varName)) {
|
|
579
742
|
emitted.add(varName);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
+
}
|
|
585
750
|
}
|
|
586
751
|
}
|
|
587
752
|
if (op.requestBody) {
|
|
588
753
|
const varName = deriveInputSchemaName(op);
|
|
589
754
|
if (!emitted.has(varName)) {
|
|
590
755
|
emitted.add(varName);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
756
|
+
if (!isComponentSchemaVar(varName, namedSchemas)) {
|
|
757
|
+
const zod = jsonSchemaToZod(op.requestBody.jsonSchema, namedSchemas);
|
|
758
|
+
lines.push(`export const ${varName} = ${zod};`);
|
|
759
|
+
lines.push("");
|
|
760
|
+
}
|
|
594
761
|
}
|
|
595
762
|
}
|
|
596
763
|
if (op.queryParams.length > 0) {
|
|
@@ -602,9 +769,37 @@ function generateResourceSchemas(resource, namedSchemas) {
|
|
|
602
769
|
}
|
|
603
770
|
}
|
|
604
771
|
}
|
|
772
|
+
const actualImports = [...componentImports].sort();
|
|
773
|
+
if (actualImports.length > 0) {
|
|
774
|
+
lines.splice(2, 0, `import { ${actualImports.join(", ")} } from './components';`, "");
|
|
775
|
+
}
|
|
605
776
|
return lines.join(`
|
|
606
777
|
`);
|
|
607
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
|
+
}
|
|
608
803
|
function buildQueryZodSchema(op, namedSchemas) {
|
|
609
804
|
const entries = op.queryParams.map((param) => {
|
|
610
805
|
let zod = jsonSchemaToZod(param.schema, namedSchemas);
|
|
@@ -621,15 +816,15 @@ ${entries.join(`,
|
|
|
621
816
|
function deriveResponseSchemaName(op) {
|
|
622
817
|
if (op.response?.name)
|
|
623
818
|
return toSchemaVarName(op.response.name);
|
|
624
|
-
return toSchemaVarName(
|
|
819
|
+
return toSchemaVarName(getTypePrefix(op) + "Response");
|
|
625
820
|
}
|
|
626
821
|
function deriveInputSchemaName(op) {
|
|
627
822
|
if (op.requestBody?.name)
|
|
628
823
|
return toSchemaVarName(op.requestBody.name);
|
|
629
|
-
return toSchemaVarName(
|
|
824
|
+
return toSchemaVarName(getTypePrefix(op) + "Input");
|
|
630
825
|
}
|
|
631
826
|
function deriveQuerySchemaName(op) {
|
|
632
|
-
return toSchemaVarName(
|
|
827
|
+
return toSchemaVarName(getTypePrefix(op) + "Query");
|
|
633
828
|
}
|
|
634
829
|
function toSchemaVarName(name) {
|
|
635
830
|
const sanitized = sanitizeTypeName(name);
|
|
@@ -641,13 +836,25 @@ function toSchemaVarName(name) {
|
|
|
641
836
|
function generateTypes(resources, schemas) {
|
|
642
837
|
const files = [];
|
|
643
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
|
+
}
|
|
644
845
|
for (const resource of resources) {
|
|
645
846
|
const content = generateResourceTypes(resource, namedSchemas);
|
|
646
847
|
files.push({ path: `types/${resource.identifier}.ts`, content: content || "" });
|
|
647
848
|
}
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
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
|
+
`) + `
|
|
651
858
|
` });
|
|
652
859
|
return files;
|
|
653
860
|
}
|
|
@@ -660,24 +867,44 @@ function buildNamedSchemaMap2(schemas) {
|
|
|
660
867
|
}
|
|
661
868
|
return map;
|
|
662
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
|
+
}
|
|
663
880
|
function generateResourceTypes(resource, namedSchemas) {
|
|
664
881
|
const interfaces = [];
|
|
665
882
|
const emitted = new Set;
|
|
883
|
+
const componentImports = new Set;
|
|
666
884
|
for (const op of resource.operations) {
|
|
885
|
+
collectOperationCircularRefs(op, componentImports, namedSchemas);
|
|
667
886
|
if (op.response) {
|
|
668
887
|
const name = deriveResponseName(op);
|
|
669
888
|
if (!emitted.has(name)) {
|
|
670
889
|
emitted.add(name);
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
+
}
|
|
674
897
|
}
|
|
675
898
|
}
|
|
676
899
|
if (op.requestBody) {
|
|
677
900
|
const name = deriveInputName2(op);
|
|
678
901
|
if (!emitted.has(name)) {
|
|
679
902
|
emitted.add(name);
|
|
680
|
-
|
|
903
|
+
if (namedSchemas.has(name)) {
|
|
904
|
+
componentImports.add(name);
|
|
905
|
+
} else {
|
|
906
|
+
interfaces.push(generateInterface(name, op.requestBody.jsonSchema, namedSchemas));
|
|
907
|
+
}
|
|
681
908
|
}
|
|
682
909
|
}
|
|
683
910
|
if (op.queryParams.length > 0) {
|
|
@@ -688,21 +915,52 @@ function generateResourceTypes(resource, namedSchemas) {
|
|
|
688
915
|
}
|
|
689
916
|
}
|
|
690
917
|
}
|
|
691
|
-
|
|
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(`
|
|
692
927
|
`);
|
|
693
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
|
+
}
|
|
694
952
|
function deriveResponseName(op) {
|
|
695
953
|
if (op.response?.name)
|
|
696
954
|
return sanitizeTypeName(op.response.name);
|
|
697
|
-
return
|
|
955
|
+
return getTypePrefix(op) + "Response";
|
|
698
956
|
}
|
|
699
957
|
function deriveInputName2(op) {
|
|
700
958
|
if (op.requestBody?.name)
|
|
701
959
|
return sanitizeTypeName(op.requestBody.name);
|
|
702
|
-
return
|
|
960
|
+
return getTypePrefix(op) + "Input";
|
|
703
961
|
}
|
|
704
962
|
function deriveQueryName2(op) {
|
|
705
|
-
return
|
|
963
|
+
return getTypePrefix(op) + "Query";
|
|
706
964
|
}
|
|
707
965
|
function generateQueryInterface(name, op, namedSchemas) {
|
|
708
966
|
const lines = op.queryParams.map((param) => {
|
|
@@ -724,7 +982,7 @@ function generateAll(spec, options) {
|
|
|
724
982
|
const opts = { schemas: false, baseURL: "", ...options };
|
|
725
983
|
const files = [];
|
|
726
984
|
files.push(...generateTypes(resources, schemas));
|
|
727
|
-
files.push(...generateResources(resources));
|
|
985
|
+
files.push(...generateResources(resources, schemas));
|
|
728
986
|
files.push(generateClient(resources, {
|
|
729
987
|
baseURL: opts.baseURL,
|
|
730
988
|
securitySchemes: spec.securitySchemes
|
|
@@ -942,6 +1200,50 @@ function detectCrudMethod(method, path) {
|
|
|
942
1200
|
}
|
|
943
1201
|
return;
|
|
944
1202
|
}
|
|
1203
|
+
function deriveTypePrefix(operationId, path) {
|
|
1204
|
+
const withoutController = operationId.replace(/^[A-Za-z0-9]+Controller[_.-]+/, "");
|
|
1205
|
+
const words = splitWords2(withoutController).map((w) => w.toLowerCase());
|
|
1206
|
+
if (words.length > 1 && words.at(-1) && HTTP_METHOD_WORDS.has(words.at(-1))) {
|
|
1207
|
+
words.pop();
|
|
1208
|
+
}
|
|
1209
|
+
const pathWordList = [];
|
|
1210
|
+
for (const segment of getPathSegments(path)) {
|
|
1211
|
+
const name = isPathParam(segment) ? segment.slice(1, -1) : segment;
|
|
1212
|
+
for (const w of splitWords2(name)) {
|
|
1213
|
+
pathWordList.push(w.toLowerCase());
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const cutIndex = findPathSuffixStart(words, pathWordList);
|
|
1217
|
+
const meaningful = cutIndex >= 2 ? words.slice(0, cutIndex) : words;
|
|
1218
|
+
return meaningful.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
1219
|
+
}
|
|
1220
|
+
function findPathSuffixStart(words, pathWords) {
|
|
1221
|
+
if (pathWords.length === 0)
|
|
1222
|
+
return words.length;
|
|
1223
|
+
for (let start = 1;start < words.length; start++) {
|
|
1224
|
+
if (isSuffixMatchingPath(words, start, pathWords)) {
|
|
1225
|
+
return start;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return words.length;
|
|
1229
|
+
}
|
|
1230
|
+
function isSuffixMatchingPath(words, start, pathWords) {
|
|
1231
|
+
let pathIdx = 0;
|
|
1232
|
+
for (let i = start;i < words.length; i++) {
|
|
1233
|
+
let found = false;
|
|
1234
|
+
while (pathIdx < pathWords.length) {
|
|
1235
|
+
if (words[i] === pathWords[pathIdx]) {
|
|
1236
|
+
pathIdx++;
|
|
1237
|
+
found = true;
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
pathIdx++;
|
|
1241
|
+
}
|
|
1242
|
+
if (!found)
|
|
1243
|
+
return false;
|
|
1244
|
+
}
|
|
1245
|
+
return true;
|
|
1246
|
+
}
|
|
945
1247
|
function normalizeOperationId(operationId, method, path, config, context) {
|
|
946
1248
|
if (config?.overrides?.[operationId]) {
|
|
947
1249
|
return config.overrides[operationId];
|
|
@@ -1142,6 +1444,20 @@ function getJsonContentSchema(value) {
|
|
|
1142
1444
|
}
|
|
1143
1445
|
return mediaType.schema;
|
|
1144
1446
|
}
|
|
1447
|
+
function getStreamingContentSchema(value) {
|
|
1448
|
+
if (!isRecord2(value) || !isRecord2(value.content)) {
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
const sse = value.content["text/event-stream"];
|
|
1452
|
+
if (isRecord2(sse)) {
|
|
1453
|
+
return { schema: isRecord2(sse.schema) ? sse.schema : undefined, format: "sse" };
|
|
1454
|
+
}
|
|
1455
|
+
const ndjson = value.content["application/x-ndjson"];
|
|
1456
|
+
if (isRecord2(ndjson)) {
|
|
1457
|
+
return { schema: isRecord2(ndjson.schema) ? ndjson.schema : undefined, format: "ndjson" };
|
|
1458
|
+
}
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1145
1461
|
function extractRefName(schema) {
|
|
1146
1462
|
if (typeof schema.$ref === "string") {
|
|
1147
1463
|
const segments = schema.$ref.split("/");
|
|
@@ -1175,9 +1491,26 @@ function pickSuccessResponse(operation, spec) {
|
|
|
1175
1491
|
return { status: 200 };
|
|
1176
1492
|
}
|
|
1177
1493
|
const resolvedResponse = resolveOpenAPIObject(first.response, spec);
|
|
1494
|
+
const jsonSchema = getJsonContentSchema(resolvedResponse);
|
|
1495
|
+
const streaming = getStreamingContentSchema(resolvedResponse);
|
|
1496
|
+
if (streaming && jsonSchema) {
|
|
1497
|
+
return {
|
|
1498
|
+
status: first.status,
|
|
1499
|
+
schema: jsonSchema,
|
|
1500
|
+
streamingFormat: streaming.format,
|
|
1501
|
+
streamingSchema: streaming.schema
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
if (streaming) {
|
|
1505
|
+
return {
|
|
1506
|
+
status: first.status,
|
|
1507
|
+
schema: streaming.schema,
|
|
1508
|
+
streamingFormat: streaming.format
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1178
1511
|
return {
|
|
1179
1512
|
status: first.status,
|
|
1180
|
-
schema:
|
|
1513
|
+
schema: jsonSchema
|
|
1181
1514
|
};
|
|
1182
1515
|
}
|
|
1183
1516
|
function getCombinedParameters(pathItem, operation, spec) {
|
|
@@ -1312,6 +1645,7 @@ function parseOpenAPI(spec) {
|
|
|
1312
1645
|
const parsed = {
|
|
1313
1646
|
operationId,
|
|
1314
1647
|
methodName: normalizeOperationId(operationId, method.toUpperCase(), path),
|
|
1648
|
+
typePrefix: deriveTypePrefix(operationId, path),
|
|
1315
1649
|
method: method.toUpperCase(),
|
|
1316
1650
|
path,
|
|
1317
1651
|
pathParams,
|
|
@@ -1323,9 +1657,17 @@ function parseOpenAPI(spec) {
|
|
|
1323
1657
|
};
|
|
1324
1658
|
if (security)
|
|
1325
1659
|
parsed.security = security;
|
|
1660
|
+
if (successResponse.streamingFormat) {
|
|
1661
|
+
parsed.streamingFormat = successResponse.streamingFormat;
|
|
1662
|
+
if (successResponse.streamingSchema) {
|
|
1663
|
+
parsed.response = resolveSchemaForOutput(successResponse.streamingSchema, spec, version);
|
|
1664
|
+
parsed.jsonResponse = successResponse.schema ? resolveSchemaForOutput(successResponse.schema, spec, version) : undefined;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1326
1667
|
operations.push(parsed);
|
|
1327
1668
|
}
|
|
1328
1669
|
}
|
|
1670
|
+
deduplicateTypePrefixes(operations);
|
|
1329
1671
|
return {
|
|
1330
1672
|
operations,
|
|
1331
1673
|
schemas: collectComponentSchemas(spec, version),
|
|
@@ -1333,6 +1675,26 @@ function parseOpenAPI(spec) {
|
|
|
1333
1675
|
version
|
|
1334
1676
|
};
|
|
1335
1677
|
}
|
|
1678
|
+
function deduplicateTypePrefixes(operations) {
|
|
1679
|
+
const seen = new Map;
|
|
1680
|
+
for (const op of operations) {
|
|
1681
|
+
if (!op.typePrefix)
|
|
1682
|
+
continue;
|
|
1683
|
+
const existing = seen.get(op.typePrefix);
|
|
1684
|
+
if (existing) {
|
|
1685
|
+
existing.push(op);
|
|
1686
|
+
} else {
|
|
1687
|
+
seen.set(op.typePrefix, [op]);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
for (const [, ops] of seen) {
|
|
1691
|
+
if (ops.length > 1) {
|
|
1692
|
+
for (const op of ops) {
|
|
1693
|
+
op.typePrefix = undefined;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1336
1698
|
|
|
1337
1699
|
// src/writer/incremental.ts
|
|
1338
1700
|
import { createHash } from "node:crypto";
|