genoc 0.2.0 → 0.4.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 (72) hide show
  1. package/README.md +30 -163
  2. package/dist/analyzer/analyze.d.ts +18 -0
  3. package/dist/analyzer/analyze.js +534 -0
  4. package/dist/analyzer/naming.js +3 -2
  5. package/dist/analyzer/path-analyzer.d.ts +45 -6
  6. package/dist/analyzer/path-analyzer.js +82 -81
  7. package/dist/analyzer/schema-mapper.d.ts +3 -2
  8. package/dist/analyzer/schema-mapper.js +16 -8
  9. package/dist/analyzer/types.d.ts +119 -0
  10. package/dist/cli/app.d.ts +5 -2
  11. package/dist/cli/app.js +30 -4
  12. package/dist/cli/config-loader.d.ts +20 -0
  13. package/dist/cli/config-loader.js +186 -0
  14. package/dist/cli/config-schema.d.ts +105 -0
  15. package/dist/cli/config-schema.js +183 -0
  16. package/dist/cli/impl.d.ts +9 -1
  17. package/dist/cli/impl.js +190 -31
  18. package/dist/generator/client-generator.d.ts +13 -14
  19. package/dist/generator/client-generator.js +45 -77
  20. package/dist/generator/contracts-generator.d.ts +11 -9
  21. package/dist/generator/contracts-generator.js +45 -482
  22. package/dist/generator/method-generator.d.ts +1 -1
  23. package/dist/generator/method-generator.js +10 -150
  24. package/dist/parser/ref-resolver.d.ts +1 -1
  25. package/dist/parser/ref-resolver.js +1 -2
  26. package/dist/parser/spec-reader.d.ts +3 -2
  27. package/dist/parser/spec-reader.js +5 -4
  28. package/dist/parser/version/index.d.ts +4 -16
  29. package/dist/parser/version/index.js +3 -14
  30. package/dist/parser/version/v3.0/validator.d.ts +1 -1
  31. package/dist/parser/version/v3.1/validator.d.ts +17 -0
  32. package/dist/parser/version/v3.1/validator.js +126 -0
  33. package/dist/parser/version/validate.d.ts +9 -0
  34. package/dist/parser/version/validate.js +12 -0
  35. package/dist/parser/version/version-detector.d.ts +21 -0
  36. package/dist/parser/version/version-detector.js +19 -0
  37. package/dist/pipeline.d.ts +13 -0
  38. package/dist/pipeline.js +24 -0
  39. package/dist/types/client.d.ts +1 -12
  40. package/dist/utils/generator-helpers.d.ts +23 -25
  41. package/dist/utils/generator-helpers.js +144 -76
  42. package/dist/utils/operation-naming.d.ts +84 -0
  43. package/dist/utils/operation-naming.js +178 -0
  44. package/dist/utils/proxy.d.ts +6 -0
  45. package/dist/utils/proxy.js +81 -0
  46. package/package.json +5 -8
  47. package/dist/generator/error-types.d.ts +0 -13
  48. package/dist/generator/error-types.js +0 -67
  49. package/dist/index.d.ts +0 -8
  50. package/dist/index.js +0 -8
  51. package/dist/parser/validators.d.ts +0 -7
  52. package/dist/parser/validators.js +0 -79
  53. package/dist/parser/version/normalized-spec.d.ts +0 -199
  54. package/dist/parser/version/registry.d.ts +0 -28
  55. package/dist/parser/version/registry.js +0 -44
  56. package/dist/parser/version/v3.0/index.d.ts +0 -3
  57. package/dist/parser/version/v3.0/index.js +0 -3
  58. package/dist/parser/version/v3.0/normalizer.d.ts +0 -15
  59. package/dist/parser/version/v3.0/normalizer.js +0 -389
  60. package/dist/parser/version/v3.0/strategy.d.ts +0 -27
  61. package/dist/parser/version/v3.0/strategy.js +0 -96
  62. package/dist/parser/version/v3.1/index.d.ts +0 -1
  63. package/dist/parser/version/v3.1/index.js +0 -1
  64. package/dist/parser/version/v3.1/strategy.d.ts +0 -42
  65. package/dist/parser/version/v3.1/strategy.js +0 -513
  66. package/dist/parser/version/v3.2/index.d.ts +0 -4
  67. package/dist/parser/version/v3.2/index.js +0 -4
  68. package/dist/parser/version/v3.2/strategy.d.ts +0 -39
  69. package/dist/parser/version/v3.2/strategy.js +0 -57
  70. package/dist/parser/version/version-strategy.d.ts +0 -31
  71. package/dist/parser/version/version-strategy.js +0 -1
  72. /package/dist/{parser/version/normalized-spec.js → analyzer/types.js} +0 -0
@@ -1,213 +1,5 @@
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 { RESERVED_TYPE_NAMES, buildSchemaRenameMap, DEFAULT_RUNTIME_IMPORT_PATH, buildTypeJsDoc, sanitizeJsDocText, sanitizeTypeName, 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, renameMap) {
21
- const refSchema = schema;
22
- if (!refSchema || typeof refSchema !== 'object')
23
- return tsType;
24
- if (typeof refSchema.$ref === 'string') {
25
- const schemaName = sanitizeTypeName(refSchema.$ref.split('/').pop());
26
- if (schemaName && discriminatorInfo.has(schemaName)) {
27
- const renamed = renameMap.get(schemaName) ?? schemaName;
28
- return tsType.replace(new RegExp(`\\b${renamed}\\b`, 'g'), `${renamed}Variant`);
29
- }
30
- }
31
- if (refSchema.items && typeof refSchema.items === 'object') {
32
- const items = refSchema.items;
33
- if (typeof items.$ref === 'string') {
34
- const schemaName = sanitizeTypeName(items.$ref.split('/').pop());
35
- if (schemaName && discriminatorInfo.has(schemaName)) {
36
- const renamed = renameMap.get(schemaName) ?? schemaName;
37
- return tsType.replace(new RegExp(`\\b${renamed}\\b`, 'g'), `${renamed}Variant`);
38
- }
39
- }
40
- }
41
- return tsType;
42
- }
43
- /**
44
- * Indent unit matching `INDENT_UNIT` in schema-mapper.ts (2 spaces, pinned by
45
- * the mapper's golden tests). Kept local to avoid modifying the mapper module.
46
- */
47
- const INDENT_UNIT = ' ';
48
- /**
49
- * Build a description-only type-level JSDoc comment for operation-derived
50
- * types (body / response). Returns '' when the description is absent or
51
- * whitespace-only so callers can skip emission entirely.
52
- */
53
- function buildDescriptionJsDoc(description) {
54
- if (description === undefined)
55
- return '';
56
- const sanitized = sanitizeJsDocText(description);
57
- return sanitized === '' ? '' : `/** ${sanitized} */`;
58
- }
59
- /**
60
- * Prefix a type definition with its JSDoc block (single- or multi-line),
61
- * keeping the comment directly above the `export type` line.
62
- */
63
- function attachTypeJsDoc(jsDoc, definition) {
64
- return jsDoc === '' ? definition : `${jsDoc}\n${definition}`;
65
- }
66
- /**
67
- * Indent every line of a JSDoc block by one indent unit so property comments
68
- * line up exactly with their property line (mapper indentation contract).
69
- */
70
- function indentJsDocBlock(jsDoc) {
71
- return jsDoc.split('\n').map((line) => `${INDENT_UNIT}${line}`);
72
- }
73
- /**
74
- * Build the JSDoc block for a query/header parameter property using the
75
- * pinned parameter merge rule: parameter-level description / deprecated /
76
- * example win over the schema's; default, title and examples come from the
77
- * schema only (carried by the spread).
78
- */
79
- function buildParamPropertyJsDoc(param, schema) {
80
- return buildTypeJsDoc({
81
- ...schema,
82
- description: param.description ?? schema.description,
83
- deprecated: param.deprecated === true || schema.deprecated === true,
84
- example: param.example ?? schema.example,
85
- });
86
- }
87
- /**
88
- * Render a `${prefix}Query` / `${prefix}Headers` object body in the mapper's
89
- * multi-line format with per-property JSDoc from the parameter merge rule.
90
- */
91
- function buildParamTypeBody(params, mapper) {
92
- const lines = [];
93
- for (const param of params) {
94
- const paramSchema = param.schema ?? { type: 'string' };
95
- const result = mapper.mapSchema(paramSchema);
96
- const optional = param.required ? '' : '?';
97
- const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `"${param.name}"`;
98
- const jsDoc = buildParamPropertyJsDoc(param, paramSchema);
99
- if (jsDoc !== '') {
100
- lines.push(...indentJsDocBlock(jsDoc));
101
- }
102
- lines.push(`${INDENT_UNIT}${key}${optional}: ${result.tsType};`);
103
- }
104
- return `{\n${lines.join('\n')}\n}`;
105
- }
106
- function securitySchemeToTsType(scheme) {
107
- const parts = [`type: "${scheme.type}"`];
108
- if (scheme.description) {
109
- parts.push(`description: "${scheme.description}"`);
110
- }
111
- if (scheme.type === 'apiKey') {
112
- if (scheme.name)
113
- parts.push(`name: "${scheme.name}"`);
114
- if (scheme.in)
115
- parts.push(`in: "${scheme.in}"`);
116
- }
117
- if (scheme.type === 'http') {
118
- if (scheme.scheme)
119
- parts.push(`scheme: "${scheme.scheme}"`);
120
- if (scheme.bearerFormat)
121
- parts.push(`bearerFormat: "${scheme.bearerFormat}"`);
122
- }
123
- if (scheme.type === 'oauth2' && scheme.flows) {
124
- const flowParts = [];
125
- const flows = scheme.flows;
126
- if (flows.implicit) {
127
- flowParts.push(`implicit: ${oAuth2FlowToTs(flows.implicit, true)}`);
128
- }
129
- if (flows.password) {
130
- flowParts.push(`password: ${oAuth2FlowToTs(flows.password, false)}`);
131
- }
132
- if (flows.clientCredentials) {
133
- flowParts.push(`clientCredentials: ${oAuth2FlowToTs(flows.clientCredentials, false)}`);
134
- }
135
- if (flows.authorizationCode) {
136
- flowParts.push(`authorizationCode: ${oAuth2FlowToTs(flows.authorizationCode, true)}`);
137
- }
138
- parts.push(`flows: { ${flowParts.join('; ')} }`);
139
- }
140
- if (scheme.type === 'openIdConnect' && scheme.openIdConnectUrl) {
141
- parts.push(`openIdConnectUrl: "${scheme.openIdConnectUrl}"`);
142
- }
143
- return `{ ${parts.join('; ')} }`;
144
- }
145
- function oAuth2FlowToTs(flow, hasAuthUrl) {
146
- const entries = [];
147
- if (hasAuthUrl && flow.authorizationUrl) {
148
- entries.push(`authorizationUrl: "${flow.authorizationUrl}"`);
149
- }
150
- if (flow.tokenUrl) {
151
- entries.push(`tokenUrl: "${flow.tokenUrl}"`);
152
- }
153
- if (flow.refreshUrl) {
154
- entries.push(`refreshUrl: "${flow.refreshUrl}"`);
155
- }
156
- const scopeEntries = Object.entries(flow.scopes)
157
- .map(([k, v]) => `"${k}": "${v}"`)
158
- .join('; ');
159
- entries.push(`scopes: { ${scopeEntries} }`);
160
- return `{ ${entries.join('; ')} }`;
161
- }
162
- /**
163
- * Sort ContractEntry list so that referenced types appear before referrers.
164
- * Uses DFS-based topological sort; cycles are broken by skipping.
165
- */
166
- function topologicalSort(entries, allNames) {
167
- if (entries.length <= 1)
168
- return entries;
169
- const nameToIndex = new Map();
170
- entries.forEach((e, i) => nameToIndex.set(e.name, i));
171
- const graph = new Map();
172
- for (let i = 0; i < entries.length; i++) {
173
- const deps = new Set();
174
- const def = entries[i].definition;
175
- for (const name of allNames) {
176
- if (name === entries[i].name)
177
- continue;
178
- if (new RegExp(`\\b${name}\\b`).test(def)) {
179
- const depIdx = nameToIndex.get(name);
180
- if (depIdx !== undefined) {
181
- deps.add(depIdx);
182
- }
183
- }
184
- }
185
- graph.set(i, deps);
186
- }
187
- const sorted = [];
188
- const visited = new Set();
189
- const inStack = new Set();
190
- function visit(idx) {
191
- if (visited.has(idx))
192
- return;
193
- if (inStack.has(idx))
194
- return;
195
- inStack.add(idx);
196
- const deps = graph.get(idx);
197
- if (deps) {
198
- for (const dep of deps) {
199
- visit(dep);
200
- }
201
- }
202
- inStack.delete(idx);
203
- visited.add(idx);
204
- sorted.push(entries[idx]);
205
- }
206
- for (let i = 0; i < entries.length; i++) {
207
- visit(i);
208
- }
209
- return sorted;
210
- }
1
+ import { DEFAULT_RUNTIME_IMPORT_PATH, makeHeader } from '../utils/generator-helpers.js';
2
+ import { RUNTIME_CLASS_NAMES } from '../utils/operation-naming.js';
211
3
  /**
212
4
  * Build the import + re-export block for the shared runtime classes.
213
5
  * Generated code re-uses the genoc runtime as the single source of truth
@@ -218,315 +10,86 @@ function buildRuntimeReexport(runtimeImportPath) {
218
10
  '',
219
11
  `import { ApiError, StreamResponse } from '${runtimeImportPath}';`,
220
12
  'export {',
221
- ' ApiError,',
222
- ' UnspecifiedApiError,',
223
- ' DefaultApiError,',
224
- ' RequesterFailError,',
225
- ' StreamResponse,',
226
- ' ErrorResponse,',
13
+ ...RUNTIME_CLASS_NAMES.map((name) => ` ${name},`),
227
14
  `} from '${runtimeImportPath}';`,
228
15
  ];
229
16
  }
230
17
  /**
231
- * Generate the complete `*.contracts.ts` file content as a string.
18
+ * Render the complete `*.contracts.ts` file content from the analyzed model.
19
+ * Pure string assembly — reads ONLY the AnalyzedSpec, never the raw spec or
20
+ * a resolver.
232
21
  *
233
22
  * Sections produced:
234
23
  * 1. Header comment
235
- * 2. Schema types from `components/schemas`
236
- * 3. Query parameter types per operation
237
- * 4. Header parameter types per operation
238
- * 5. Request body types per operation
239
- * 6. Response / error types per operation
24
+ * 2. Schema types from `components/schemas` (finished model entries)
25
+ * 3. Security scheme types (finished model entries; `SecuritySchemes` union
26
+ * when more than one)
27
+ * 4. Server variable types (finished model entries)
28
+ * 5. `FileInput` when any operation uploads files
29
+ * 6. Operation-derived types (finished emission lines from the model)
240
30
  * 7. Runtime re-export block (shared classes from the genoc runtime package)
241
31
  */
242
- export function generateContracts(doc, resolver, runtimeImportPath = DEFAULT_RUNTIME_IMPORT_PATH) {
243
- const schemaNameList = doc.components?.schemas ? Object.keys(doc.components.schemas) : [];
244
- const renameMap = buildSchemaRenameMap(schemaNameList, RESERVED_TYPE_NAMES);
245
- for (const [original, renamed] of renameMap) {
246
- // TODO: Replace with structured logging solution
247
- // oxlint-disable-next-line no-console
248
- console.warn(`Warning: Schema "${original}" collides with a built-in type and was renamed to "${renamed}".`);
249
- }
250
- const renamingTypeGenerator = (refString) => {
251
- const segments = refString.split('/');
252
- const rawSegment = segments[segments.length - 1] || 'unknown';
253
- const lastSegment = sanitizeTypeName(rawSegment);
254
- return renameMap.get(lastSegment) ?? lastSegment;
255
- };
256
- const discriminatorInfo = new Map();
257
- if (doc.components?.schemas) {
258
- for (const [name, schema] of Object.entries(doc.components.schemas)) {
259
- const resolved = resolver.resolve(schema);
260
- if (resolved.discriminator) {
261
- const mapping = new Map();
262
- if (resolved.discriminator.mapping) {
263
- for (const [key, ref] of Object.entries(resolved.discriminator.mapping)) {
264
- const targetName = sanitizeTypeName(ref.split('/').pop() || key);
265
- const renamedTarget = renameMap.get(targetName) ?? targetName;
266
- mapping.set(key, renamedTarget);
267
- }
268
- }
269
- discriminatorInfo.set(sanitizeTypeName(name), {
270
- propertyName: resolved.discriminator.propertyName,
271
- mapping,
272
- });
273
- }
274
- }
275
- }
276
- const discriminatorTargets = new Map();
277
- for (const [, info] of discriminatorInfo) {
278
- for (const [mappingKey, schemaName] of info.mapping) {
279
- discriminatorTargets.set(schemaName, {
280
- propertyName: info.propertyName,
281
- literalValue: mappingKey,
282
- });
283
- }
284
- }
285
- const allSchemaNames = new Set();
286
- if (doc.components?.schemas) {
287
- for (const name of Object.keys(doc.components.schemas)) {
288
- allSchemaNames.add(sanitizeTypeName(name));
289
- }
290
- }
291
- const mapper = new SchemaMapper(resolver, renamingTypeGenerator, discriminatorTargets, allSchemaNames);
32
+ export function renderContracts(analyzed, runtimeImportPath = DEFAULT_RUNTIME_IMPORT_PATH) {
292
33
  const lines = [];
293
- lines.push(makeHeader(doc.openapi));
34
+ lines.push(makeHeader(analyzed.specVersion));
294
35
  const runtimeReexport = buildRuntimeReexport(runtimeImportPath);
295
36
  lines.push(...runtimeReexport);
296
- // Section 1: Schema types
297
- const schemaEntries = [];
298
- if (doc.components?.schemas) {
299
- for (const [name, schema] of Object.entries(doc.components.schemas)) {
300
- const sanitizedName = sanitizeTypeName(name);
301
- const renamedName = renameMap.get(sanitizedName) ?? sanitizedName;
302
- const result = mapper.mapSchema(schema, sanitizedName);
303
- const resolved = resolver.resolve(schema);
304
- const definition = `export type ${renamedName} = ${result.tsType};`;
305
- schemaEntries.push({
306
- name: renamedName,
307
- kind: 'type',
308
- definition,
309
- jsDoc: buildTypeJsDoc(resolved) || undefined,
310
- });
311
- }
312
- }
313
- const sortedSchemas = topologicalSort(schemaEntries, allSchemaNames);
314
- for (const entry of sortedSchemas) {
37
+ // Section 1: Schema types (finished entries; emission order — topologically
38
+ // sorted schemas, then discriminator variant unions — was fixed by analyze()).
39
+ for (const entry of analyzed.schemaTypes) {
315
40
  lines.push('');
316
41
  if (entry.jsDoc) {
317
42
  lines.push(entry.jsDoc);
318
43
  }
319
44
  lines.push(entry.definition);
320
45
  }
321
- for (const [baseName, info] of discriminatorInfo) {
322
- const subtypeNames = Array.from(info.mapping.values());
323
- if (subtypeNames.length === 0)
324
- continue;
325
- const unionType = subtypeNames.join(' | ');
326
- const renamedBase = renameMap.get(sanitizeTypeName(baseName)) ?? sanitizeTypeName(baseName);
327
- lines.push('');
328
- lines.push(`export type ${renamedBase}Variant = ${unionType};`);
329
- allSchemaNames.add(`${renamedBase}Variant`);
330
- }
331
- // Section 1b: Security scheme types
332
- const securitySchemes = doc.components?.securitySchemes;
333
- if (securitySchemes && Object.keys(securitySchemes).length > 0) {
334
- const securityTypeNames = [];
335
- for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
336
- const typeName = `${toPascalCase(schemeName)}Auth`;
337
- const tsType = securitySchemeToTsType(scheme);
338
- const schemeJsDoc = buildDescriptionJsDoc(scheme.description);
339
- if (schemeJsDoc !== '') {
46
+ // Section 1b: Security scheme types (finished entries in spec key order)
47
+ if (analyzed.securitySchemeTypes.length > 0) {
48
+ for (const entry of analyzed.securitySchemeTypes) {
49
+ if (entry.jsDoc !== '') {
340
50
  lines.push('');
341
- lines.push(schemeJsDoc);
51
+ lines.push(entry.jsDoc);
342
52
  }
343
53
  lines.push('');
344
- lines.push(`export type ${typeName} = ${tsType};`);
345
- securityTypeNames.push(typeName);
54
+ lines.push(`export type ${entry.name} = ${entry.tsType};`);
346
55
  }
347
- if (securityTypeNames.length > 1) {
56
+ if (analyzed.securitySchemeTypes.length > 1) {
57
+ const unionMembers = analyzed.securitySchemeTypes.map((entry) => entry.name).join(' | ');
348
58
  lines.push('');
349
- lines.push(`export type SecuritySchemes = ${securityTypeNames.join(' | ')};`);
59
+ lines.push(`export type SecuritySchemes = ${unionMembers};`);
350
60
  }
351
61
  }
352
- // Section 1c: Server variable types
353
- if (doc.servers) {
354
- for (let serverIdx = 0; serverIdx < doc.servers.length; serverIdx++) {
355
- const server = doc.servers[serverIdx];
356
- if (!server.variables || Object.keys(server.variables).length === 0) {
357
- continue;
358
- }
359
- const typeName = doc.servers.length === 1 ? 'ServerParams' : `Server${serverIdx + 1}Params`;
360
- const props = [];
361
- for (const [varName, variable] of Object.entries(server.variables)) {
362
- const sv = variable;
363
- const jsDocParts = [];
364
- if (sv.description) {
365
- const description = sanitizeJsDocText(sv.description);
366
- if (description !== '') {
367
- jsDocParts.push(description);
368
- }
369
- }
370
- if (sv.default !== undefined) {
371
- jsDocParts.push(`@default ${sanitizeJsDocText(String(sv.default))}`);
372
- }
373
- let tsType;
374
- if (sv.enum && sv.enum.length > 0) {
375
- tsType = sv.enum.map((v) => `"${v}"`).join(' | ');
376
- }
377
- else {
378
- tsType = 'string';
379
- }
380
- const jsDoc = jsDocParts.length > 0 ? ` /** ${jsDocParts.join(' ')} */` : null;
381
- if (jsDoc) {
382
- props.push(jsDoc);
383
- }
384
- props.push(` ${varName}: ${tsType};`);
385
- }
386
- lines.push('');
387
- if (server.url) {
388
- lines.push(`/** Server: ${server.url} */`);
389
- }
390
- lines.push(`export interface ${typeName} {`);
391
- for (const prop of props) {
392
- lines.push(prop);
393
- }
394
- lines.push('}');
62
+ // Section 1c: Server variable types (finished entries in spec order)
63
+ for (const entry of analyzed.serverTypes) {
64
+ lines.push('');
65
+ if (entry.url) {
66
+ lines.push(`/** Server: ${entry.url} */`);
67
+ }
68
+ lines.push(`export interface ${entry.name} {`);
69
+ for (const prop of entry.propertyLines) {
70
+ lines.push(prop);
395
71
  }
72
+ lines.push('}');
396
73
  }
397
- const operations = analyzePaths(doc, resolver);
398
- const hasFileUpload = operations.some((op) => op.requestBody?.isMultipart);
399
- if (hasFileUpload) {
74
+ if (analyzed.hasFileUpload) {
400
75
  lines.push('');
401
76
  lines.push('export interface FileInput {');
402
77
  lines.push(' data: Blob;');
403
78
  lines.push(' filename: string;');
404
79
  lines.push('}');
405
80
  }
406
- // Sections 2-4: Operation-derived types
407
- for (const op of operations) {
408
- const prefix = getOperationTypePrefix(op);
409
- const opLines = [];
410
- // Section 2: Query parameter types
411
- if (op.queryParams.length > 0) {
412
- opLines.push(`export type ${prefix}Query = ${buildParamTypeBody(op.queryParams, mapper)};`);
413
- }
414
- // Section 2b: Header parameter types
415
- if (op.headerParams.length > 0) {
416
- opLines.push(`export type ${prefix}Headers = ${buildParamTypeBody(op.headerParams, mapper)};`);
417
- }
418
- // Section 3: Request body types
419
- const bodyJsDoc = op.requestBody ? buildDescriptionJsDoc(op.requestBody.description) : '';
420
- if (op.requestBody?.isMultipart && op.requestBody.schema) {
421
- const schema = resolver.resolveSchema(op.requestBody.schema);
422
- const requiredSet = new Set(schema.required ?? []);
423
- const propLines = [];
424
- for (const [name, propSchema] of Object.entries(schema.properties ?? {})) {
425
- const resolved = resolver.resolveSchema(propSchema);
426
- const optional = requiredSet.has(name) ? '' : '?';
427
- let tsType;
428
- if (resolved.format === 'binary') {
429
- tsType = 'FileInput';
430
- }
431
- else if (resolved.type === 'array' && resolved.items?.format === 'binary') {
432
- tsType = 'FileInput[]';
433
- }
434
- else {
435
- tsType = 'string';
436
- }
437
- const jsDoc = buildTypeJsDoc(resolved);
438
- if (jsDoc !== '') {
439
- propLines.push(...indentJsDocBlock(jsDoc));
440
- }
441
- propLines.push(`${INDENT_UNIT}${name}${optional}: ${tsType};`);
442
- }
443
- if (propLines.length > 0) {
444
- opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${prefix}Body = {\n${propLines.join('\n')}\n};`));
445
- }
446
- else {
447
- opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${prefix}Body = Record<string, never>;`));
448
- }
449
- }
450
- else if (op.requestBody?.schema) {
451
- const hasBinaryContentType = op.requestBody.contentTypes.some(isBinaryContentType);
452
- if (hasBinaryContentType) {
453
- opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${prefix}Body = Blob;`));
454
- }
455
- else {
456
- const result = mapper.mapSchema(op.requestBody.schema, undefined, 'request');
457
- opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${prefix}Body = ${result.tsType};`));
458
- }
459
- }
460
- // Section 4: Response types
461
- const successResponses = op.responses.filter((r) => r.isSuccess);
462
- const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
463
- const defaultResponse = op.responses.find((r) => !r.isSuccess && r.statusCode === 'default');
464
- // Success type
465
- if (successResponses.length > 0) {
466
- const types = successResponses.map((r) => {
467
- if (r.isBinary)
468
- return 'StreamResponse';
469
- if (r.schema) {
470
- const result = mapper.mapSchema(r.schema, undefined, 'response').tsType;
471
- return substituteDiscriminatedType(result, r.schema, discriminatorInfo, renameMap);
472
- }
473
- return r.tsType;
474
- });
475
- const successType = types.length === 1 ? types[0] : types.join(' | ');
476
- // Type-level JSDoc from the lowest-numbered 2xx response (the analyzer
477
- // returns success responses in ascending status order).
478
- const responseJsDoc = buildDescriptionJsDoc(successResponses[0].description);
479
- opLines.push(attachTypeJsDoc(responseJsDoc, `export type ${prefix}Response = ${successType};`));
480
- }
481
- // Error types per status
482
- const errorTypes = [];
483
- for (const err of errorResponses) {
484
- const errorTypeName = `${prefix}Error${err.statusCode}`;
485
- let errorTsType;
486
- if (err.isBinary) {
487
- errorTsType = 'StreamResponse';
488
- }
489
- else if (err.schema) {
490
- errorTsType = mapper.mapSchema(err.schema, undefined, 'response').tsType;
491
- }
492
- else {
493
- errorTsType = err.tsType;
494
- }
495
- opLines.push(`export type ${errorTypeName} = ${errorTsType};`);
496
- errorTypes.push({ status: err.statusCode, typeName: errorTypeName });
497
- }
498
- // Default response error type
499
- if (defaultResponse) {
500
- const defaultTypeName = `${prefix}DefaultError`;
501
- let defaultTsType;
502
- if (defaultResponse.isBinary) {
503
- defaultTsType = 'StreamResponse';
504
- }
505
- else if (defaultResponse.schema) {
506
- defaultTsType = mapper.mapSchema(defaultResponse.schema, undefined, 'response').tsType;
507
- }
508
- else {
509
- defaultTsType = 'unknown';
510
- }
511
- opLines.push(`export type ${defaultTypeName} = ${defaultTsType};`);
512
- }
513
- // Error union
514
- if (errorTypes.length > 0) {
515
- const unionParts = errorTypes.map((e) => `ApiError<${e.status}, ${e.typeName}>`);
516
- opLines.push(`export type ${prefix}Errors = ${unionParts.join(' | ')};`);
517
- }
518
- if (opLines.length > 0) {
519
- for (const line of opLines) {
520
- lines.push('');
521
- lines.push(line);
522
- }
81
+ // Sections 2-4: Operation-derived types (finished emission lines assembled
82
+ // by analyze(); spilled unchanged with a blank line before each).
83
+ for (const op of analyzed.operations) {
84
+ for (const line of op.contractsLines) {
85
+ lines.push('');
86
+ lines.push(line);
523
87
  }
524
88
  }
525
89
  // Emit branded type definitions after header, before everything else
526
- const brandedTypes = mapper.getBrandedTypes();
527
- if (brandedTypes.size > 0) {
90
+ if (analyzed.brandedTypes.length > 0) {
528
91
  const brandLines = [];
529
- for (const brand of brandedTypes.values()) {
92
+ for (const brand of analyzed.brandedTypes) {
530
93
  brandLines.push(`export type ${brand.name} = ${brand.baseType} & { readonly __format?: '${brand.format}' };`);
531
94
  }
532
95
  lines.splice(1 + runtimeReexport.length, 0, '', ...brandLines);
@@ -4,6 +4,6 @@ import type { GeneratedMethod } from '../types/client.js';
4
4
  * Generate a client method from an analyzed OpenAPI operation.
5
5
  *
6
6
  * @param op - The analyzed operation
7
- * @returns Generated method with name, JSDoc, signature, and implementation
7
+ * @returns Generated method with name, JSDoc, and signature
8
8
  */
9
9
  export declare function generateMethod(op: AnalyzedOperation): GeneratedMethod;