@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { runCLI } from '../src/cli';
2
+ import { runCLI } from '../dist/cli.js';
3
3
 
4
4
  const result = await runCLI(process.argv.slice(2));
5
5
 
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  loadSpec,
5
5
  parseOpenAPI,
6
6
  resolveConfig
7
- } from "./shared/chunk-h8tb765a.js";
7
+ } from "./shared/chunk-070f9ezy.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-h8tb765a.js";
24
+ } from "./shared/chunk-070f9ezy.js";
25
25
  export {
26
26
  writeIncremental,
27
27
  sanitizeTypeName,
@@ -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 typeImports = collectTypeImports(resource);
310
+ const { resourceImports, componentImports } = collectTypeImports(resource, componentNames);
263
311
  lines.push("import type { FetchClient, FetchResponse } from '@vertz/fetch';");
264
- if (typeImports.size > 0) {
265
- 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();
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
- 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
+ }
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 === 0)
300
- return;
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
- 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.
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 = capitalize(op.operationId) + "Response";
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 types = new Set;
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.response.name) {
369
- 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);
370
516
  } else {
371
- types.add(capitalize(op.operationId) + "Response");
517
+ resourceImports.add(name);
372
518
  }
373
519
  }
374
520
  if (op.requestBody) {
375
- 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
+ }
376
527
  }
377
528
  if (op.queryParams.length > 0) {
378
- types.add(deriveQueryName(op));
529
+ resourceImports.add(deriveQueryName(op));
379
530
  }
380
531
  }
381
- return types;
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 capitalize(op.operationId) + "Input";
537
+ return getTypePrefix(op) + "Input";
387
538
  }
388
539
  function deriveQueryName(op) {
389
- return capitalize(op.operationId) + "Query";
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 exports = resources.map((r) => `export * from './${r.identifier}';`).join(`
542
- `);
543
- 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
+ `) + `
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
- const schema = op.response.jsonSchema;
567
- const effectiveSchema = schema.type === "array" && schema.items && typeof schema.items === "object" ? schema.items : schema;
568
- const zod = jsonSchemaToZod(effectiveSchema, namedSchemas);
569
- lines.push(`export const ${varName} = ${zod};`);
570
- lines.push("");
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
- const zod = jsonSchemaToZod(op.requestBody.jsonSchema, namedSchemas);
578
- lines.push(`export const ${varName} = ${zod};`);
579
- 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
+ }
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.operationId + "Response");
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.operationId + "Input");
824
+ return toSchemaVarName(getTypePrefix(op) + "Input");
616
825
  }
617
826
  function deriveQuerySchemaName(op) {
618
- return toSchemaVarName(op.operationId + "Query");
827
+ return toSchemaVarName(getTypePrefix(op) + "Query");
619
828
  }
620
829
  function toSchemaVarName(name) {
621
- const camel = name.charAt(0).toLowerCase() + name.slice(1);
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 exports = resources.map((r) => `export * from './${r.identifier}';`).join(`
634
- `);
635
- 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
+ `) + `
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
- const schema = op.response.jsonSchema;
657
- const effectiveSchema = schema.type === "array" && schema.items && typeof schema.items === "object" ? schema.items : schema;
658
- interfaces.push(generateInterface(name, effectiveSchema, namedSchemas));
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
- 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
+ }
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
- 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(`
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 capitalize2(op.operationId) + "Response";
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 capitalize2(op.operationId) + "Input";
960
+ return getTypePrefix(op) + "Input";
688
961
  }
689
962
  function deriveQueryName2(op) {
690
- return capitalize2(op.operationId) + "Query";
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: getJsonContentSchema(resolvedResponse)
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/openapi",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "OpenAPI 3.x parser and TypeScript SDK generator for Vertz",
5
5
  "license": "MIT",
6
6
  "repository": {