genoc 0.3.0 → 0.4.1

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