genoc 0.1.0

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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/analyzer/naming.d.ts +24 -0
  4. package/dist/analyzer/naming.js +122 -0
  5. package/dist/analyzer/path-analyzer.d.ts +53 -0
  6. package/dist/analyzer/path-analyzer.js +222 -0
  7. package/dist/analyzer/schema-mapper.d.ts +48 -0
  8. package/dist/analyzer/schema-mapper.js +435 -0
  9. package/dist/cli/app.d.ts +9 -0
  10. package/dist/cli/app.js +60 -0
  11. package/dist/cli/errors.d.ts +3 -0
  12. package/dist/cli/errors.js +6 -0
  13. package/dist/cli/impl.d.ts +3 -0
  14. package/dist/cli/impl.js +45 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +5 -0
  17. package/dist/generator/client-generator.d.ts +21 -0
  18. package/dist/generator/client-generator.js +287 -0
  19. package/dist/generator/contracts-generator.d.ts +16 -0
  20. package/dist/generator/contracts-generator.js +525 -0
  21. package/dist/generator/error-types.d.ts +24 -0
  22. package/dist/generator/error-types.js +94 -0
  23. package/dist/generator/method-generator.d.ts +9 -0
  24. package/dist/generator/method-generator.js +249 -0
  25. package/dist/index.d.ts +7 -0
  26. package/dist/index.js +8 -0
  27. package/dist/parser/ref-resolver.d.ts +24 -0
  28. package/dist/parser/ref-resolver.js +119 -0
  29. package/dist/parser/spec-reader.d.ts +4 -0
  30. package/dist/parser/spec-reader.js +116 -0
  31. package/dist/parser/validators.d.ts +7 -0
  32. package/dist/parser/validators.js +79 -0
  33. package/dist/parser/version/index.d.ts +18 -0
  34. package/dist/parser/version/index.js +16 -0
  35. package/dist/parser/version/normalized-spec.d.ts +199 -0
  36. package/dist/parser/version/normalized-spec.js +1 -0
  37. package/dist/parser/version/registry.d.ts +28 -0
  38. package/dist/parser/version/registry.js +44 -0
  39. package/dist/parser/version/v3.0/index.d.ts +3 -0
  40. package/dist/parser/version/v3.0/index.js +3 -0
  41. package/dist/parser/version/v3.0/normalizer.d.ts +15 -0
  42. package/dist/parser/version/v3.0/normalizer.js +389 -0
  43. package/dist/parser/version/v3.0/strategy.d.ts +27 -0
  44. package/dist/parser/version/v3.0/strategy.js +96 -0
  45. package/dist/parser/version/v3.0/validator.d.ts +13 -0
  46. package/dist/parser/version/v3.0/validator.js +117 -0
  47. package/dist/parser/version/v3.1/index.d.ts +1 -0
  48. package/dist/parser/version/v3.1/index.js +1 -0
  49. package/dist/parser/version/v3.1/strategy.d.ts +42 -0
  50. package/dist/parser/version/v3.1/strategy.js +513 -0
  51. package/dist/parser/version/v3.2/index.d.ts +4 -0
  52. package/dist/parser/version/v3.2/index.js +4 -0
  53. package/dist/parser/version/v3.2/strategy.d.ts +39 -0
  54. package/dist/parser/version/v3.2/strategy.js +57 -0
  55. package/dist/parser/version/version-detector.d.ts +4 -0
  56. package/dist/parser/version/version-detector.js +34 -0
  57. package/dist/parser/version/version-strategy.d.ts +31 -0
  58. package/dist/parser/version/version-strategy.js +1 -0
  59. package/dist/types/client.d.ts +25 -0
  60. package/dist/types/client.js +1 -0
  61. package/dist/types/contracts.d.ts +13 -0
  62. package/dist/types/contracts.js +1 -0
  63. package/dist/types/openapi.d.ts +173 -0
  64. package/dist/types/openapi.js +1 -0
  65. package/dist/utils/case.d.ts +5 -0
  66. package/dist/utils/case.js +51 -0
  67. package/dist/utils/generator-helpers.d.ts +23 -0
  68. package/dist/utils/generator-helpers.js +66 -0
  69. package/dist/utils/string.d.ts +34 -0
  70. package/dist/utils/string.js +182 -0
  71. package/dist/utils/url.d.ts +10 -0
  72. package/dist/utils/url.js +40 -0
  73. package/package.json +60 -0
@@ -0,0 +1,525 @@
1
+ import { analyzePaths } from '../analyzer/path-analyzer.js';
2
+ import { SchemaMapper } from '../analyzer/schema-mapper.js';
3
+ import { RefResolver } from '../parser/ref-resolver.js';
4
+ import { toPascalCase, getOperationTypePrefix, makeHeader } from '../utils/generator-helpers.js';
5
+ function isBinaryContentType(ct) {
6
+ if (ct === 'application/octet-stream')
7
+ return true;
8
+ if (ct.startsWith('image/'))
9
+ return true;
10
+ if (ct.startsWith('video/'))
11
+ return true;
12
+ if (ct.startsWith('audio/'))
13
+ return true;
14
+ return false;
15
+ }
16
+ /**
17
+ * If the schema is a $ref to a discriminated base type (or an array whose items
18
+ * are), replace the type name with the {Base}Variant union type.
19
+ */
20
+ function substituteDiscriminatedType(tsType, schema, discriminatorInfo) {
21
+ const refSchema = schema;
22
+ if (!refSchema || typeof refSchema !== 'object')
23
+ return tsType;
24
+ if (typeof refSchema.$ref === 'string') {
25
+ const schemaName = refSchema.$ref.split('/').pop();
26
+ if (schemaName && discriminatorInfo.has(schemaName)) {
27
+ return tsType.replace(new RegExp(`\\b${schemaName}\\b`, 'g'), `${schemaName}Variant`);
28
+ }
29
+ }
30
+ if (refSchema.items && typeof refSchema.items === 'object') {
31
+ const items = refSchema.items;
32
+ if (typeof items.$ref === 'string') {
33
+ const schemaName = items.$ref.split('/').pop();
34
+ if (schemaName && discriminatorInfo.has(schemaName)) {
35
+ return tsType.replace(new RegExp(`\\b${schemaName}\\b`, 'g'), `${schemaName}Variant`);
36
+ }
37
+ }
38
+ }
39
+ return tsType;
40
+ }
41
+ function buildJsDoc(description) {
42
+ if (!description)
43
+ return undefined;
44
+ return `/** ${description} */`;
45
+ }
46
+ function securitySchemeToTsType(scheme) {
47
+ const parts = [`type: "${scheme.type}"`];
48
+ if (scheme.description) {
49
+ parts.push(`description: "${scheme.description}"`);
50
+ }
51
+ if (scheme.type === 'apiKey') {
52
+ if (scheme.name)
53
+ parts.push(`name: "${scheme.name}"`);
54
+ if (scheme.in)
55
+ parts.push(`in: "${scheme.in}"`);
56
+ }
57
+ if (scheme.type === 'http') {
58
+ if (scheme.scheme)
59
+ parts.push(`scheme: "${scheme.scheme}"`);
60
+ if (scheme.bearerFormat)
61
+ parts.push(`bearerFormat: "${scheme.bearerFormat}"`);
62
+ }
63
+ if (scheme.type === 'oauth2' && scheme.flows) {
64
+ const flowParts = [];
65
+ const flows = scheme.flows;
66
+ if (flows.implicit) {
67
+ flowParts.push(`implicit: ${oAuth2FlowToTs(flows.implicit, true)}`);
68
+ }
69
+ if (flows.password) {
70
+ flowParts.push(`password: ${oAuth2FlowToTs(flows.password, false)}`);
71
+ }
72
+ if (flows.clientCredentials) {
73
+ flowParts.push(`clientCredentials: ${oAuth2FlowToTs(flows.clientCredentials, false)}`);
74
+ }
75
+ if (flows.authorizationCode) {
76
+ flowParts.push(`authorizationCode: ${oAuth2FlowToTs(flows.authorizationCode, true)}`);
77
+ }
78
+ parts.push(`flows: { ${flowParts.join('; ')} }`);
79
+ }
80
+ if (scheme.type === 'openIdConnect' && scheme.openIdConnectUrl) {
81
+ parts.push(`openIdConnectUrl: "${scheme.openIdConnectUrl}"`);
82
+ }
83
+ return `{ ${parts.join('; ')} }`;
84
+ }
85
+ function oAuth2FlowToTs(flow, hasAuthUrl) {
86
+ const entries = [];
87
+ if (hasAuthUrl && flow.authorizationUrl) {
88
+ entries.push(`authorizationUrl: "${flow.authorizationUrl}"`);
89
+ }
90
+ if (flow.tokenUrl) {
91
+ entries.push(`tokenUrl: "${flow.tokenUrl}"`);
92
+ }
93
+ if (flow.refreshUrl) {
94
+ entries.push(`refreshUrl: "${flow.refreshUrl}"`);
95
+ }
96
+ const scopeEntries = Object.entries(flow.scopes)
97
+ .map(([k, v]) => `"${k}": "${v}"`)
98
+ .join('; ');
99
+ entries.push(`scopes: { ${scopeEntries} }`);
100
+ return `{ ${entries.join('; ')} }`;
101
+ }
102
+ /**
103
+ * Sort ContractEntry list so that referenced types appear before referrers.
104
+ * Uses DFS-based topological sort; cycles are broken by skipping.
105
+ */
106
+ function topologicalSort(entries, allNames) {
107
+ if (entries.length <= 1)
108
+ return entries;
109
+ const nameToIndex = new Map();
110
+ entries.forEach((e, i) => nameToIndex.set(e.name, i));
111
+ const graph = new Map();
112
+ for (let i = 0; i < entries.length; i++) {
113
+ const deps = new Set();
114
+ const def = entries[i].definition;
115
+ for (const name of allNames) {
116
+ if (name === entries[i].name)
117
+ continue;
118
+ if (new RegExp(`\\b${name}\\b`).test(def)) {
119
+ const depIdx = nameToIndex.get(name);
120
+ if (depIdx !== undefined) {
121
+ deps.add(depIdx);
122
+ }
123
+ }
124
+ }
125
+ graph.set(i, deps);
126
+ }
127
+ const sorted = [];
128
+ const visited = new Set();
129
+ const inStack = new Set();
130
+ function visit(idx) {
131
+ if (visited.has(idx))
132
+ return;
133
+ if (inStack.has(idx))
134
+ return;
135
+ inStack.add(idx);
136
+ const deps = graph.get(idx);
137
+ if (deps) {
138
+ for (const dep of deps) {
139
+ visit(dep);
140
+ }
141
+ }
142
+ inStack.delete(idx);
143
+ visited.add(idx);
144
+ sorted.push(entries[idx]);
145
+ }
146
+ for (let i = 0; i < entries.length; i++) {
147
+ visit(i);
148
+ }
149
+ return sorted;
150
+ }
151
+ /**
152
+ * Generate the complete `*.contracts.ts` file content as a string.
153
+ *
154
+ * Sections produced:
155
+ * 1. Header comment
156
+ * 2. Schema types from `components/schemas`
157
+ * 3. Query parameter types per operation
158
+ * 4. Header parameter types per operation
159
+ * 5. Request body types per operation
160
+ * 6. Response / error types per operation
161
+ * 7. ApiError class
162
+ * 7b. UnspecifiedApiError class
163
+ */
164
+ export function generateContracts(doc, resolver) {
165
+ const discriminatorInfo = new Map();
166
+ if (doc.components?.schemas) {
167
+ for (const [name, schema] of Object.entries(doc.components.schemas)) {
168
+ const resolved = resolver.resolve(schema);
169
+ if (resolved.discriminator) {
170
+ const mapping = new Map();
171
+ if (resolved.discriminator.mapping) {
172
+ for (const [key, ref] of Object.entries(resolved.discriminator.mapping)) {
173
+ const targetName = ref.split('/').pop() || key;
174
+ mapping.set(key, targetName);
175
+ }
176
+ }
177
+ discriminatorInfo.set(name, {
178
+ propertyName: resolved.discriminator.propertyName,
179
+ mapping,
180
+ });
181
+ }
182
+ }
183
+ }
184
+ const discriminatorTargets = new Map();
185
+ for (const [, info] of discriminatorInfo) {
186
+ for (const [mappingKey, schemaName] of info.mapping) {
187
+ discriminatorTargets.set(schemaName, {
188
+ propertyName: info.propertyName,
189
+ literalValue: mappingKey,
190
+ });
191
+ }
192
+ }
193
+ // reservedNames: prevent branded types from colliding with user-defined schema names
194
+ const allSchemaNames = new Set();
195
+ if (doc.components?.schemas) {
196
+ for (const name of Object.keys(doc.components.schemas)) {
197
+ allSchemaNames.add(name);
198
+ }
199
+ }
200
+ const mapper = new SchemaMapper(resolver, undefined, discriminatorTargets, allSchemaNames);
201
+ const lines = [];
202
+ lines.push(makeHeader(doc.openapi));
203
+ // Section 1: Schema types
204
+ const schemaEntries = [];
205
+ if (doc.components?.schemas) {
206
+ for (const [name, schema] of Object.entries(doc.components.schemas)) {
207
+ const result = mapper.mapSchema(schema, name);
208
+ const resolved = resolver.resolve(schema);
209
+ const definition = `export type ${name} = ${result.tsType};`;
210
+ schemaEntries.push({
211
+ name,
212
+ kind: 'type',
213
+ definition,
214
+ jsDoc: buildJsDoc(resolved.description),
215
+ });
216
+ }
217
+ }
218
+ const sortedSchemas = topologicalSort(schemaEntries, allSchemaNames);
219
+ for (const entry of sortedSchemas) {
220
+ lines.push('');
221
+ if (entry.jsDoc) {
222
+ lines.push(entry.jsDoc);
223
+ }
224
+ lines.push(entry.definition);
225
+ }
226
+ for (const [baseName, info] of discriminatorInfo) {
227
+ const subtypeNames = Array.from(info.mapping.values());
228
+ if (subtypeNames.length === 0)
229
+ continue;
230
+ const unionType = subtypeNames.join(' | ');
231
+ lines.push('');
232
+ lines.push(`export type ${baseName}Variant = ${unionType};`);
233
+ allSchemaNames.add(`${baseName}Variant`);
234
+ }
235
+ // Section 1b: Security scheme types
236
+ const securitySchemes = doc.components?.securitySchemes;
237
+ if (securitySchemes && Object.keys(securitySchemes).length > 0) {
238
+ const securityTypeNames = [];
239
+ for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
240
+ const typeName = `${toPascalCase(schemeName)}Auth`;
241
+ const tsType = securitySchemeToTsType(scheme);
242
+ if (scheme.description) {
243
+ lines.push('');
244
+ lines.push(buildJsDoc(scheme.description));
245
+ }
246
+ lines.push('');
247
+ lines.push(`export type ${typeName} = ${tsType};`);
248
+ securityTypeNames.push(typeName);
249
+ }
250
+ if (securityTypeNames.length > 1) {
251
+ lines.push('');
252
+ lines.push(`export type SecuritySchemes = ${securityTypeNames.join(' | ')};`);
253
+ }
254
+ }
255
+ // Section 1c: Server variable types
256
+ if (doc.servers) {
257
+ for (let serverIdx = 0; serverIdx < doc.servers.length; serverIdx++) {
258
+ const server = doc.servers[serverIdx];
259
+ if (!server.variables || Object.keys(server.variables).length === 0) {
260
+ continue;
261
+ }
262
+ const typeName = doc.servers.length === 1 ? 'ServerParams' : `Server${serverIdx + 1}Params`;
263
+ const props = [];
264
+ for (const [varName, variable] of Object.entries(server.variables)) {
265
+ const sv = variable;
266
+ const jsDocParts = [];
267
+ if (sv.description) {
268
+ jsDocParts.push(sv.description);
269
+ }
270
+ if (sv.default !== undefined) {
271
+ jsDocParts.push(`@default ${sv.default}`);
272
+ }
273
+ let tsType;
274
+ if (sv.enum && sv.enum.length > 0) {
275
+ tsType = sv.enum.map((v) => `"${v}"`).join(' | ');
276
+ }
277
+ else {
278
+ tsType = 'string';
279
+ }
280
+ const jsDoc = jsDocParts.length > 0 ? ` /** ${jsDocParts.join(' ')} */` : null;
281
+ if (jsDoc) {
282
+ props.push(jsDoc);
283
+ }
284
+ props.push(` ${varName}: ${tsType};`);
285
+ }
286
+ lines.push('');
287
+ if (server.url) {
288
+ lines.push(`/** Server: ${server.url} */`);
289
+ }
290
+ lines.push(`export interface ${typeName} {`);
291
+ for (const prop of props) {
292
+ lines.push(prop);
293
+ }
294
+ lines.push('}');
295
+ }
296
+ }
297
+ const operations = analyzePaths(doc, resolver);
298
+ const hasFileUpload = operations.some((op) => op.requestBody?.isMultipart);
299
+ if (hasFileUpload) {
300
+ lines.push('');
301
+ lines.push('export interface FileInput {');
302
+ lines.push(' data: Blob;');
303
+ lines.push(' filename: string;');
304
+ lines.push('}');
305
+ }
306
+ // Sections 2-4: Operation-derived types
307
+ for (const op of operations) {
308
+ const prefix = getOperationTypePrefix(op);
309
+ const opLines = [];
310
+ // Section 2: Query parameter types
311
+ if (op.queryParams.length > 0) {
312
+ const props = op.queryParams.map((param) => {
313
+ const paramSchema = param.schema ?? { type: 'string' };
314
+ const result = mapper.mapSchema(paramSchema);
315
+ const optional = param.required ? '' : '?';
316
+ const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `"${param.name}"`;
317
+ return `${key}${optional}: ${result.tsType}`;
318
+ });
319
+ opLines.push(`export type ${prefix}Query = { ${props.join('; ')}; };`);
320
+ }
321
+ // Section 2b: Header parameter types
322
+ if (op.headerParams.length > 0) {
323
+ const props = op.headerParams.map((param) => {
324
+ const paramSchema = param.schema ?? { type: 'string' };
325
+ const result = mapper.mapSchema(paramSchema);
326
+ const optional = param.required ? '' : '?';
327
+ const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `"${param.name}"`;
328
+ return `${key}${optional}: ${result.tsType}`;
329
+ });
330
+ opLines.push(`export type ${prefix}Headers = { ${props.join('; ')}; };`);
331
+ }
332
+ // Section 3: Request body types
333
+ if (op.requestBody?.isMultipart && op.requestBody.schema) {
334
+ const schema = resolver.resolveSchema(op.requestBody.schema);
335
+ const requiredSet = new Set(schema.required ?? []);
336
+ const props = Object.entries(schema.properties ?? {}).map(([name, propSchema]) => {
337
+ const resolved = resolver.resolveSchema(propSchema);
338
+ const optional = requiredSet.has(name) ? '' : '?';
339
+ let tsType;
340
+ if (resolved.format === 'binary') {
341
+ tsType = 'FileInput';
342
+ }
343
+ else if (resolved.type === 'array' && resolved.items?.format === 'binary') {
344
+ tsType = 'FileInput[]';
345
+ }
346
+ else {
347
+ tsType = 'string';
348
+ }
349
+ return `${name}${optional}: ${tsType}`;
350
+ });
351
+ if (props.length > 0) {
352
+ opLines.push(`export type ${prefix}Body = { ${props.join('; ')}; };`);
353
+ }
354
+ else {
355
+ opLines.push(`export type ${prefix}Body = Record<string, never>;`);
356
+ }
357
+ }
358
+ else if (op.requestBody?.schema) {
359
+ const hasBinaryContentType = op.requestBody.contentTypes.some(isBinaryContentType);
360
+ if (hasBinaryContentType) {
361
+ opLines.push(`export type ${prefix}Body = Blob;`);
362
+ }
363
+ else {
364
+ const result = mapper.mapSchema(op.requestBody.schema, undefined, 'request');
365
+ opLines.push(`export type ${prefix}Body = ${result.tsType};`);
366
+ }
367
+ }
368
+ // Section 4: Response types
369
+ const successResponses = op.responses.filter((r) => r.isSuccess);
370
+ const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
371
+ const defaultResponse = op.responses.find((r) => !r.isSuccess && r.statusCode === 'default');
372
+ // Success type
373
+ if (successResponses.length > 0) {
374
+ const types = successResponses.map((r) => {
375
+ if (r.isBinary)
376
+ return 'StreamResponse';
377
+ if (r.schema) {
378
+ const result = mapper.mapSchema(r.schema, undefined, 'response').tsType;
379
+ return substituteDiscriminatedType(result, r.schema, discriminatorInfo);
380
+ }
381
+ return r.tsType;
382
+ });
383
+ const successType = types.length === 1 ? types[0] : types.join(' | ');
384
+ opLines.push(`export type ${prefix}Response = ${successType};`);
385
+ }
386
+ // Error types per status
387
+ const errorTypes = [];
388
+ for (const err of errorResponses) {
389
+ const errorTypeName = `${prefix}Error${err.statusCode}`;
390
+ let errorTsType;
391
+ if (err.isBinary) {
392
+ errorTsType = 'StreamResponse';
393
+ }
394
+ else if (err.schema) {
395
+ errorTsType = mapper.mapSchema(err.schema, undefined, 'response').tsType;
396
+ }
397
+ else {
398
+ errorTsType = err.tsType;
399
+ }
400
+ opLines.push(`export type ${errorTypeName} = ${errorTsType};`);
401
+ errorTypes.push({ status: err.statusCode, typeName: errorTypeName });
402
+ }
403
+ // Default response error type
404
+ if (defaultResponse) {
405
+ const defaultTypeName = `${prefix}DefaultError`;
406
+ let defaultTsType;
407
+ if (defaultResponse.isBinary) {
408
+ defaultTsType = 'StreamResponse';
409
+ }
410
+ else if (defaultResponse.schema) {
411
+ defaultTsType = mapper.mapSchema(defaultResponse.schema, undefined, 'response').tsType;
412
+ }
413
+ else {
414
+ defaultTsType = 'unknown';
415
+ }
416
+ opLines.push(`export type ${defaultTypeName} = ${defaultTsType};`);
417
+ }
418
+ // Error union
419
+ if (errorTypes.length > 0) {
420
+ const unionParts = errorTypes.map((e) => `ApiError<${e.status}, ${e.typeName}>`);
421
+ opLines.push(`export type ${prefix}Errors = ${unionParts.join(' | ')};`);
422
+ }
423
+ if (opLines.length > 0) {
424
+ for (const line of opLines) {
425
+ lines.push('');
426
+ lines.push(line);
427
+ }
428
+ }
429
+ }
430
+ // Emit branded type definitions after header, before everything else
431
+ const brandedTypes = mapper.getBrandedTypes();
432
+ if (brandedTypes.size > 0) {
433
+ const brandLines = [];
434
+ for (const brand of brandedTypes.values()) {
435
+ brandLines.push(`export type ${brand.name} = ${brand.baseType} & { readonly __format?: '${brand.format}' };`);
436
+ }
437
+ lines.splice(1, 0, '', ...brandLines);
438
+ }
439
+ // Always emit StreamResponse class (used by Requester type)
440
+ lines.push('');
441
+ lines.push('export class StreamResponse {');
442
+ lines.push(' constructor(');
443
+ lines.push(' public readonly data: ReadableStream<Uint8Array>,');
444
+ lines.push(' public readonly filename?: string,');
445
+ lines.push(' public readonly headers: Headers = new Headers(),');
446
+ lines.push(' ) {}');
447
+ lines.push('}');
448
+ lines.push('');
449
+ lines.push(`export function streamResponse(
450
+ data: ReadableStream<Uint8Array>,
451
+ filename?: string,
452
+ headers?: Headers,
453
+ ): StreamResponse {
454
+ return new StreamResponse(data, filename, headers ?? new Headers());
455
+ }`);
456
+ // ErrorResponse class
457
+ lines.push('');
458
+ lines.push(`export class ErrorResponse {
459
+ constructor(
460
+ public readonly status: number,
461
+ public readonly data: unknown,
462
+ public readonly headers: Headers,
463
+ public readonly message?: string,
464
+ ) {}
465
+ }`);
466
+ // errorResponse() helper
467
+ lines.push('');
468
+ lines.push(`export function errorResponse(
469
+ status: number,
470
+ data: unknown,
471
+ headers?: Headers,
472
+ message?: string,
473
+ ): ErrorResponse {
474
+ return new ErrorResponse(status, data, headers ?? new Headers(), message);
475
+ }`);
476
+ // RequesterFailError class
477
+ lines.push('');
478
+ lines.push(`export class RequesterFailError extends Error {
479
+ constructor(
480
+ public readonly cause: unknown,
481
+ ) {
482
+ super(\`Request failed: \${cause instanceof Error ? cause.message : String(cause)}\`);
483
+ this.name = "RequesterFailError";
484
+ }
485
+ }`);
486
+ // Section 5: ApiError class
487
+ lines.push('');
488
+ lines.push(`export class ApiError<TStatus extends number, TData> extends Error {
489
+ constructor(
490
+ public readonly status: TStatus,
491
+ public readonly data: TData,
492
+ message: string,
493
+ ) {
494
+ super(message);
495
+ this.name = "ApiError";
496
+ }
497
+ }`);
498
+ // Section 5b: UnspecifiedApiError class
499
+ lines.push('');
500
+ lines.push(`export class UnspecifiedApiError extends ApiError<number, unknown> {
501
+ constructor(
502
+ status: number,
503
+ data: unknown,
504
+ message: string,
505
+ ) {
506
+ super(status, data, message);
507
+ this.name = "UnspecifiedApiError";
508
+ }
509
+ }`);
510
+ const needsDefaultApiError = operations.some((op) => op.responses.some((r) => !r.isSuccess && r.statusCode === 'default'));
511
+ if (needsDefaultApiError) {
512
+ lines.push('');
513
+ lines.push(`export class DefaultApiError<TData> extends Error {
514
+ constructor(
515
+ public readonly status: number,
516
+ public readonly data: TData,
517
+ message: string,
518
+ ) {
519
+ super(message);
520
+ this.name = "DefaultApiError";
521
+ }
522
+ }`);
523
+ }
524
+ return lines.join('\n');
525
+ }
@@ -0,0 +1,24 @@
1
+ import type { AnalyzedOperation } from '../analyzer/path-analyzer.js';
2
+ export declare class ApiError<TStatus extends number, TData> extends Error {
3
+ readonly status: TStatus;
4
+ readonly data: TData;
5
+ constructor(status: TStatus, data: TData, message: string);
6
+ }
7
+ export declare class DefaultApiError<TData> extends Error {
8
+ readonly status: number;
9
+ readonly data: TData;
10
+ constructor(status: number, data: TData, message: string);
11
+ }
12
+ /**
13
+ * Generate error types for a set of analyzed operations.
14
+ *
15
+ * Produces:
16
+ * 1. `ApiError<TStatus, TData>` class
17
+ * 2. `UnspecifiedApiError` class (extends ApiError, for status codes not in spec)
18
+ * 3. `isError` type guard function
19
+ * 4. Per-operation error union types (e.g. `GetApiV1ProductsErrors`)
20
+ * 5. Per-operation per-status error type aliases (e.g. `GetApiV1ProductsError400`)
21
+ * 6. `DefaultErrorBody` type for `default` responses
22
+ * 7. Catch-all `UnspecifiedApiError` in error unions for unexpected status codes
23
+ */
24
+ export declare function generateErrorTypes(operations: AnalyzedOperation[]): string;
@@ -0,0 +1,94 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ data;
4
+ constructor(status, data, message) {
5
+ super(message);
6
+ this.status = status;
7
+ this.data = data;
8
+ this.name = 'ApiError';
9
+ }
10
+ }
11
+ export class DefaultApiError extends Error {
12
+ status;
13
+ data;
14
+ constructor(status, data, message) {
15
+ super(message);
16
+ this.status = status;
17
+ this.data = data;
18
+ this.name = 'DefaultApiError';
19
+ }
20
+ }
21
+ /**
22
+ * Generate error types for a set of analyzed operations.
23
+ *
24
+ * Produces:
25
+ * 1. `ApiError<TStatus, TData>` class
26
+ * 2. `UnspecifiedApiError` class (extends ApiError, for status codes not in spec)
27
+ * 3. `isError` type guard function
28
+ * 4. Per-operation error union types (e.g. `GetApiV1ProductsErrors`)
29
+ * 5. Per-operation per-status error type aliases (e.g. `GetApiV1ProductsError400`)
30
+ * 6. `DefaultErrorBody` type for `default` responses
31
+ * 7. Catch-all `UnspecifiedApiError` in error unions for unexpected status codes
32
+ */
33
+ export function generateErrorTypes(operations) {
34
+ const lines = [];
35
+ lines.push(`export class ApiError<TStatus extends number, TData> extends Error {
36
+ constructor(
37
+ public readonly status: TStatus,
38
+ public readonly data: TData,
39
+ message: string,
40
+ ) {
41
+ super(message);
42
+ this.name = "ApiError";
43
+ }
44
+ }`);
45
+ lines.push('');
46
+ lines.push(`export class UnspecifiedApiError extends ApiError<number, unknown> {
47
+ constructor(
48
+ status: number,
49
+ data: unknown,
50
+ message: string,
51
+ ) {
52
+ super(status, data, message);
53
+ this.name = "UnspecifiedApiError";
54
+ }
55
+ }`);
56
+ lines.push('');
57
+ lines.push(`export function isError<T extends { status: number }, S extends number>(
58
+ response: T,
59
+ status: S,
60
+ ): response is Extract<T, { status: S }> {
61
+ return response.status === status;
62
+ }`);
63
+ let needsDefaultErrorBody = false;
64
+ for (const op of operations) {
65
+ const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
66
+ const defaultResponse = op.responses.find((r) => !r.isSuccess && r.statusCode === 'default');
67
+ if (errorResponses.length === 0 && !defaultResponse) {
68
+ continue;
69
+ }
70
+ const methodName = op.methodName;
71
+ const errorTypeName = (status) => `${methodName}Error${status}`;
72
+ lines.push('');
73
+ for (const err of errorResponses) {
74
+ const tsType = err.tsType || 'unknown';
75
+ lines.push(`export type ${errorTypeName(err.statusCode)} = ${tsType};`);
76
+ }
77
+ const unionParts = [];
78
+ for (const err of errorResponses) {
79
+ const status = Number(err.statusCode);
80
+ unionParts.push(`ApiError<${status}, ${errorTypeName(err.statusCode)}>`);
81
+ }
82
+ if (defaultResponse) {
83
+ needsDefaultErrorBody = true;
84
+ unionParts.push(`ApiError<number, DefaultErrorBody>`);
85
+ }
86
+ unionParts.push(`UnspecifiedApiError`);
87
+ lines.push(`export type ${methodName}Errors = ${unionParts.join('\n | ')};`);
88
+ }
89
+ if (needsDefaultErrorBody) {
90
+ lines.push('');
91
+ lines.push('export type DefaultErrorBody = unknown;');
92
+ }
93
+ return lines.join('\n');
94
+ }
@@ -0,0 +1,9 @@
1
+ import type { AnalyzedOperation } from '../analyzer/path-analyzer.js';
2
+ import type { GeneratedMethod } from '../types/client.js';
3
+ /**
4
+ * Generate a client method from an analyzed OpenAPI operation.
5
+ *
6
+ * @param op - The analyzed operation
7
+ * @returns Generated method with name, JSDoc, signature, and implementation
8
+ */
9
+ export declare function generateMethod(op: AnalyzedOperation): GeneratedMethod;