@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 CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  loadSpec,
5
5
  parseOpenAPI,
6
6
  resolveConfig
7
- } from "./shared/chunk-19t7k664.js";
7
+ } from "./shared/chunk-pd1709kk.js";
8
8
 
9
9
  // src/cli.ts
10
10
  function parseArgs(args) {
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
@@ -21,7 +21,7 @@ import {
21
21
  sanitizeIdentifier,
22
22
  sanitizeTypeName,
23
23
  writeIncremental
24
- } from "./shared/chunk-19t7k664.js";
24
+ } from "./shared/chunk-pd1709kk.js";
25
25
  export {
26
26
  writeIncremental,
27
27
  sanitizeTypeName,
@@ -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 typeImports = collectTypeImports(resource);
310
+ const { resourceImports, componentImports } = collectTypeImports(resource, componentNames);
280
311
  lines.push("import type { FetchClient, FetchResponse } from '@vertz/fetch';");
281
- if (typeImports.size > 0) {
282
- const sorted = [...typeImports].sort();
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
- lines.push(` ${generateMethod(op)},`);
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 === 0)
317
- return;
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
- 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.
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 = toPascalCase(op.operationId) + "Response";
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 types = new Set;
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.response.name) {
386
- types.add(sanitizeTypeName(op.response.name));
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
- types.add(toPascalCase(op.operationId) + "Response");
517
+ resourceImports.add(name);
389
518
  }
390
519
  }
391
520
  if (op.requestBody) {
392
- types.add(deriveInputName(op));
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
- types.add(deriveQueryName(op));
529
+ resourceImports.add(deriveQueryName(op));
396
530
  }
397
531
  }
398
- return types;
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 toPascalCase(op.operationId) + "Input";
537
+ return getTypePrefix(op) + "Input";
404
538
  }
405
539
  function deriveQueryName(op) {
406
- return toPascalCase(op.operationId) + "Query";
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 exports = resources.map((r) => `export * from './${r.identifier}';`).join(`
556
- `);
557
- files.push({ path: "schemas/index.ts", content: exports + `
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
- const schema = op.response.jsonSchema;
581
- const effectiveSchema = schema.type === "array" && schema.items && typeof schema.items === "object" ? schema.items : schema;
582
- const zod = jsonSchemaToZod(effectiveSchema, namedSchemas);
583
- lines.push(`export const ${varName} = ${zod};`);
584
- lines.push("");
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
- const zod = jsonSchemaToZod(op.requestBody.jsonSchema, namedSchemas);
592
- lines.push(`export const ${varName} = ${zod};`);
593
- lines.push("");
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(toPascalCase(op.operationId) + "Response");
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(toPascalCase(op.operationId) + "Input");
824
+ return toSchemaVarName(getTypePrefix(op) + "Input");
630
825
  }
631
826
  function deriveQuerySchemaName(op) {
632
- return toSchemaVarName(toPascalCase(op.operationId) + "Query");
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 exports = resources.map((r) => `export * from './${r.identifier}';`).join(`
649
- `);
650
- files.push({ path: "types/index.ts", content: exports + `
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
- const schema = op.response.jsonSchema;
672
- const effectiveSchema = schema.type === "array" && schema.items && typeof schema.items === "object" ? schema.items : schema;
673
- interfaces.push(generateInterface(name, effectiveSchema, namedSchemas));
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
- interfaces.push(generateInterface(name, op.requestBody.jsonSchema, namedSchemas));
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
- return interfaces.join(`
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 toPascalCase(op.operationId) + "Response";
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 toPascalCase(op.operationId) + "Input";
960
+ return getTypePrefix(op) + "Input";
703
961
  }
704
962
  function deriveQueryName2(op) {
705
- return toPascalCase(op.operationId) + "Query";
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: getJsonContentSchema(resolvedResponse)
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/openapi",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "OpenAPI 3.x parser and TypeScript SDK generator for Vertz",
5
5
  "license": "MIT",
6
6
  "repository": {