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
@@ -0,0 +1,534 @@
1
+ import { RefResolver } from '../parser/ref-resolver.js';
2
+ import { buildSchemaRenameMap, buildTypeJsDoc, sanitizeJsDocText, sanitizeTypeName, toPascalCase, } from '../utils/generator-helpers.js';
3
+ import { operationEmissions, RESERVED_TYPE_NAMES } from '../utils/operation-naming.js';
4
+ import { analyzePathsDetailed } from './path-analyzer.js';
5
+ import { SchemaMapper } from './schema-mapper.js';
6
+ /**
7
+ * If the schema is a $ref to a discriminated base type (or an array whose items
8
+ * are), replace the type name with the {Base}Variant union type.
9
+ */
10
+ function substituteDiscriminatedType(tsType, schema, discriminatorInfo, renameMap) {
11
+ const refSchema = schema;
12
+ if (!refSchema || typeof refSchema !== 'object')
13
+ return tsType;
14
+ if (typeof refSchema.$ref === 'string') {
15
+ const rawName = refSchema.$ref.split('/').pop();
16
+ const schemaName = sanitizeTypeName(rawName);
17
+ if (schemaName && discriminatorInfo.has(schemaName)) {
18
+ const renamed = renameMap.get(rawName) ?? schemaName;
19
+ return tsType.replace(new RegExp(`\\b${renamed}\\b`, 'g'), `${renamed}Variant`);
20
+ }
21
+ }
22
+ if (refSchema.items && typeof refSchema.items === 'object') {
23
+ const items = refSchema.items;
24
+ if (typeof items.$ref === 'string') {
25
+ const rawName = items.$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
+ }
33
+ return tsType;
34
+ }
35
+ /**
36
+ * Indent unit matching `INDENT_UNIT` in schema-mapper.ts (2 spaces, pinned by
37
+ * the mapper's golden tests). Kept local to avoid modifying the mapper module.
38
+ */
39
+ const INDENT_UNIT = ' ';
40
+ /**
41
+ * Build a description-only type-level JSDoc comment for operation-derived
42
+ * types (body / response). Returns '' when the description is absent or
43
+ * whitespace-only so callers can skip emission entirely.
44
+ */
45
+ export function buildDescriptionJsDoc(description) {
46
+ if (description === undefined)
47
+ return '';
48
+ const sanitized = sanitizeJsDocText(description);
49
+ return sanitized === '' ? '' : `/** ${sanitized} */`;
50
+ }
51
+ /**
52
+ * Prefix a type definition with its JSDoc block (single- or multi-line),
53
+ * keeping the comment directly above the `export type` line.
54
+ */
55
+ function attachTypeJsDoc(jsDoc, definition) {
56
+ return jsDoc === '' ? definition : `${jsDoc}\n${definition}`;
57
+ }
58
+ /**
59
+ * Indent every line of a JSDoc block by one indent unit so property comments
60
+ * line up exactly with their property line (mapper indentation contract).
61
+ */
62
+ function indentJsDocBlock(jsDoc) {
63
+ return jsDoc.split('\n').map((line) => `${INDENT_UNIT}${line}`);
64
+ }
65
+ /**
66
+ * Build the JSDoc block for a query/header parameter property using the
67
+ * pinned parameter merge rule: parameter-level description / deprecated /
68
+ * example win over the schema's; default, title and examples come from the
69
+ * schema only (carried by the spread).
70
+ */
71
+ function buildParamPropertyJsDoc(param, schema) {
72
+ return buildTypeJsDoc({
73
+ ...schema,
74
+ description: param.description ?? schema.description,
75
+ deprecated: param.deprecated === true || schema.deprecated === true,
76
+ example: param.example ?? schema.example,
77
+ });
78
+ }
79
+ /**
80
+ * Render a `${prefix}Query` / `${prefix}Headers` object body in the mapper's
81
+ * multi-line format with per-property JSDoc from the parameter merge rule.
82
+ */
83
+ function buildParamTypeBody(params, mapper) {
84
+ const lines = [];
85
+ for (const param of params) {
86
+ const paramSchema = param.schema ?? { type: 'string' };
87
+ const result = mapper.mapSchema(paramSchema);
88
+ const optional = param.required ? '' : '?';
89
+ const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `"${param.name}"`;
90
+ const jsDoc = buildParamPropertyJsDoc(param, paramSchema);
91
+ if (jsDoc !== '') {
92
+ lines.push(...indentJsDocBlock(jsDoc));
93
+ }
94
+ lines.push(`${INDENT_UNIT}${key}${optional}: ${result.tsType};`);
95
+ }
96
+ return `{\n${lines.join('\n')}\n}`;
97
+ }
98
+ /**
99
+ * Sort ContractEntry list so that referenced types appear before referrers.
100
+ * Uses DFS-based topological sort; cycles are broken by skipping.
101
+ */
102
+ function topologicalSort(entries, allNames) {
103
+ if (entries.length <= 1)
104
+ return entries;
105
+ const nameToIndex = new Map();
106
+ entries.forEach((e, i) => nameToIndex.set(e.name, i));
107
+ const graph = new Map();
108
+ for (let i = 0; i < entries.length; i++) {
109
+ const deps = new Set();
110
+ const def = entries[i].definition;
111
+ for (const name of allNames) {
112
+ if (name === entries[i].name)
113
+ continue;
114
+ if (new RegExp(`\\b${name}\\b`).test(def)) {
115
+ const depIdx = nameToIndex.get(name);
116
+ if (depIdx !== undefined) {
117
+ deps.add(depIdx);
118
+ }
119
+ }
120
+ }
121
+ graph.set(i, deps);
122
+ }
123
+ const sorted = [];
124
+ const visited = new Set();
125
+ const inStack = new Set();
126
+ function visit(idx) {
127
+ if (visited.has(idx))
128
+ return;
129
+ if (inStack.has(idx))
130
+ return;
131
+ inStack.add(idx);
132
+ const deps = graph.get(idx);
133
+ if (deps) {
134
+ for (const dep of deps) {
135
+ visit(dep);
136
+ }
137
+ }
138
+ inStack.delete(idx);
139
+ visited.add(idx);
140
+ sorted.push(entries[idx]);
141
+ }
142
+ for (let i = 0; i < entries.length; i++) {
143
+ visit(i);
144
+ }
145
+ return sorted;
146
+ }
147
+ function securitySchemeToTsType(scheme) {
148
+ const parts = [`type: "${scheme.type}"`];
149
+ if (scheme.description) {
150
+ parts.push(`description: "${scheme.description}"`);
151
+ }
152
+ if (scheme.type === 'apiKey') {
153
+ if (scheme.name)
154
+ parts.push(`name: "${scheme.name}"`);
155
+ if (scheme.in)
156
+ parts.push(`in: "${scheme.in}"`);
157
+ }
158
+ if (scheme.type === 'http') {
159
+ if (scheme.scheme)
160
+ parts.push(`scheme: "${scheme.scheme}"`);
161
+ if (scheme.bearerFormat)
162
+ parts.push(`bearerFormat: "${scheme.bearerFormat}"`);
163
+ }
164
+ if (scheme.type === 'oauth2' && scheme.flows) {
165
+ const flowParts = [];
166
+ const flows = scheme.flows;
167
+ if (flows.implicit) {
168
+ flowParts.push(`implicit: ${oAuth2FlowToTs(flows.implicit, true)}`);
169
+ }
170
+ if (flows.password) {
171
+ flowParts.push(`password: ${oAuth2FlowToTs(flows.password, false)}`);
172
+ }
173
+ if (flows.clientCredentials) {
174
+ flowParts.push(`clientCredentials: ${oAuth2FlowToTs(flows.clientCredentials, false)}`);
175
+ }
176
+ if (flows.authorizationCode) {
177
+ flowParts.push(`authorizationCode: ${oAuth2FlowToTs(flows.authorizationCode, true)}`);
178
+ }
179
+ parts.push(`flows: { ${flowParts.join('; ')} }`);
180
+ }
181
+ if (scheme.type === 'openIdConnect' && scheme.openIdConnectUrl) {
182
+ parts.push(`openIdConnectUrl: "${scheme.openIdConnectUrl}"`);
183
+ }
184
+ return `{ ${parts.join('; ')} }`;
185
+ }
186
+ function oAuth2FlowToTs(flow, hasAuthUrl) {
187
+ const entries = [];
188
+ if (hasAuthUrl && flow.authorizationUrl) {
189
+ entries.push(`authorizationUrl: "${flow.authorizationUrl}"`);
190
+ }
191
+ if (flow.tokenUrl) {
192
+ entries.push(`tokenUrl: "${flow.tokenUrl}"`);
193
+ }
194
+ if (flow.refreshUrl) {
195
+ entries.push(`refreshUrl: "${flow.refreshUrl}"`);
196
+ }
197
+ const scopeEntries = Object.entries(flow.scopes)
198
+ .map(([k, v]) => `"${k}": "${v}"`)
199
+ .join('; ');
200
+ entries.push(`scopes: { ${scopeEntries} }`);
201
+ return `{ ${entries.join('; ')} }`;
202
+ }
203
+ /**
204
+ * Translate `components.securitySchemes` into finished `{Name}Auth` type
205
+ * declarations: sanitize + PascalCase the key, number collisions, render the
206
+ * object type text and the description JSDoc. Spec key order preserved.
207
+ */
208
+ function analyzeSecuritySchemes(securitySchemes) {
209
+ const entries = [];
210
+ if (!securitySchemes || Object.keys(securitySchemes).length === 0) {
211
+ return entries;
212
+ }
213
+ const usedSecurityTypeNames = new Set();
214
+ for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
215
+ const baseTypeName = `${sanitizeTypeName(toPascalCase(schemeName))}Auth`;
216
+ let typeName = baseTypeName;
217
+ let n = 2;
218
+ while (usedSecurityTypeNames.has(typeName)) {
219
+ typeName = `${baseTypeName}${n}`;
220
+ n += 1;
221
+ }
222
+ usedSecurityTypeNames.add(typeName);
223
+ entries.push({
224
+ name: typeName,
225
+ tsType: securitySchemeToTsType(scheme),
226
+ jsDoc: buildDescriptionJsDoc(scheme.description),
227
+ });
228
+ }
229
+ return entries;
230
+ }
231
+ /**
232
+ * Translate `doc.servers` into finished server variable interface
233
+ * declarations, one per server that declares variables. Names are index-aware
234
+ * over ALL servers (`ServerParams` for a single-server document,
235
+ * `Server{i}Params` otherwise), so variable-less servers still occupy an
236
+ * index. Property lines carry their JSDoc and indentation.
237
+ */
238
+ function analyzeServers(servers) {
239
+ const entries = [];
240
+ if (!servers)
241
+ return entries;
242
+ for (let serverIdx = 0; serverIdx < servers.length; serverIdx++) {
243
+ const server = servers[serverIdx];
244
+ if (!server.variables || Object.keys(server.variables).length === 0) {
245
+ continue;
246
+ }
247
+ const typeName = servers.length === 1 ? 'ServerParams' : `Server${serverIdx + 1}Params`;
248
+ const props = [];
249
+ for (const [varName, variable] of Object.entries(server.variables)) {
250
+ const sv = variable;
251
+ const jsDocParts = [];
252
+ if (sv.description) {
253
+ const description = sanitizeJsDocText(sv.description);
254
+ if (description !== '') {
255
+ jsDocParts.push(description);
256
+ }
257
+ }
258
+ if (sv.default !== undefined) {
259
+ jsDocParts.push(`@default ${sanitizeJsDocText(String(sv.default))}`);
260
+ }
261
+ let tsType;
262
+ if (sv.enum && sv.enum.length > 0) {
263
+ tsType = sv.enum.map((v) => `"${v}"`).join(' | ');
264
+ }
265
+ else {
266
+ tsType = 'string';
267
+ }
268
+ const jsDoc = jsDocParts.length > 0 ? ` /** ${jsDocParts.join(' ')} */` : null;
269
+ if (jsDoc) {
270
+ props.push(jsDoc);
271
+ }
272
+ props.push(` ${varName}: ${tsType};`);
273
+ }
274
+ entries.push({
275
+ name: typeName,
276
+ url: server.url,
277
+ propertyLines: props,
278
+ });
279
+ }
280
+ return entries;
281
+ }
282
+ /**
283
+ * Analyze an OpenAPI document into the finished model every renderer consumes:
284
+ * schema→TS-type translation (rename machinery, discriminator discovery, the
285
+ * schema mapper) runs here exactly once, producing render-ready type text.
286
+ *
287
+ * @param doc - The parsed and validated OpenAPI document
288
+ * @param opts - Optional resolver override and method naming strategy
289
+ * @returns The AnalyzedSpec model consumed by all generators
290
+ */
291
+ export function analyze(doc, opts = {}) {
292
+ const resolver = opts.resolver ?? new RefResolver(doc);
293
+ const strategy = opts.strategy ?? 'path-based';
294
+ const schemaNameList = doc.components?.schemas ? Object.keys(doc.components.schemas) : [];
295
+ const renameMap = buildSchemaRenameMap(schemaNameList, RESERVED_TYPE_NAMES);
296
+ for (const [original, renamed] of renameMap) {
297
+ // TODO: Replace with structured logging solution
298
+ // oxlint-disable-next-line no-console
299
+ console.warn(`Warning: Schema "${original}" collides with a built-in type or another schema and was renamed to "${renamed}".`);
300
+ }
301
+ const renamingTypeGenerator = (refString) => {
302
+ const segments = refString.split('/');
303
+ const rawSegment = segments[segments.length - 1] || 'unknown';
304
+ return renameMap.get(rawSegment) ?? sanitizeTypeName(rawSegment);
305
+ };
306
+ const discriminatorInfo = new Map();
307
+ if (doc.components?.schemas) {
308
+ for (const [name, schema] of Object.entries(doc.components.schemas)) {
309
+ const resolved = resolver.resolve(schema);
310
+ if (resolved.discriminator) {
311
+ const mapping = new Map();
312
+ if (resolved.discriminator.mapping) {
313
+ for (const [key, ref] of Object.entries(resolved.discriminator.mapping)) {
314
+ const rawTarget = ref.split('/').pop() || key;
315
+ const renamedTarget = renameMap.get(rawTarget) ?? sanitizeTypeName(rawTarget);
316
+ mapping.set(key, renamedTarget);
317
+ }
318
+ }
319
+ discriminatorInfo.set(sanitizeTypeName(name), {
320
+ rawName: name,
321
+ propertyName: resolved.discriminator.propertyName,
322
+ mapping,
323
+ });
324
+ }
325
+ }
326
+ }
327
+ const discriminatorTargets = new Map();
328
+ for (const [, info] of discriminatorInfo) {
329
+ for (const [mappingKey, schemaName] of info.mapping) {
330
+ discriminatorTargets.set(schemaName, {
331
+ propertyName: info.propertyName,
332
+ literalValue: mappingKey,
333
+ });
334
+ }
335
+ }
336
+ const allSchemaNames = new Set();
337
+ if (doc.components?.schemas) {
338
+ for (const name of Object.keys(doc.components.schemas)) {
339
+ allSchemaNames.add(renameMap.get(name) ?? sanitizeTypeName(name));
340
+ }
341
+ }
342
+ const mapper = new SchemaMapper(resolver, renamingTypeGenerator, discriminatorTargets, allSchemaNames);
343
+ // Section 1: Schema types
344
+ const schemaEntries = [];
345
+ if (doc.components?.schemas) {
346
+ for (const [name, schema] of Object.entries(doc.components.schemas)) {
347
+ const renamedName = renameMap.get(name) ?? sanitizeTypeName(name);
348
+ const result = mapper.mapSchema(schema, renamedName);
349
+ const resolved = resolver.resolve(schema);
350
+ const definition = `export type ${renamedName} = ${result.tsType};`;
351
+ schemaEntries.push({
352
+ name: renamedName,
353
+ kind: 'type',
354
+ definition,
355
+ jsDoc: buildTypeJsDoc(resolved) || undefined,
356
+ });
357
+ }
358
+ }
359
+ const sortedSchemas = topologicalSort(schemaEntries, allSchemaNames);
360
+ const schemaTypes = sortedSchemas.map((entry) => ({
361
+ name: entry.name,
362
+ definition: entry.definition,
363
+ jsDoc: entry.jsDoc,
364
+ }));
365
+ for (const [, info] of discriminatorInfo) {
366
+ const subtypeNames = Array.from(info.mapping.values());
367
+ if (subtypeNames.length === 0)
368
+ continue;
369
+ const unionType = subtypeNames.join(' | ');
370
+ const renamedBase = renameMap.get(info.rawName) ?? sanitizeTypeName(info.rawName);
371
+ schemaTypes.push({
372
+ name: `${renamedBase}Variant`,
373
+ definition: `export type ${renamedBase}Variant = ${unionType};`,
374
+ });
375
+ allSchemaNames.add(`${renamedBase}Variant`);
376
+ }
377
+ const { operations: analyzedOperations, requestBodySchemas } = analyzePathsDetailed(doc, resolver, strategy);
378
+ const operations = [];
379
+ for (const op of analyzedOperations) {
380
+ operations.push({
381
+ ...op,
382
+ contractsLines: [],
383
+ });
384
+ }
385
+ const hasFileUpload = operations.some((op) => op.requestBody?.isMultipart);
386
+ // Sections 2-4: Operation-derived types. Presence decisions and type
387
+ // names come from the emission inventory (operationEmissions); the
388
+ // finished lines are assembled here so renderers never re-translate.
389
+ for (let opIdx = 0; opIdx < operations.length; opIdx++) {
390
+ const op = operations[opIdx];
391
+ // Raw request body schema, parallel to `operations` (analyzer-internal
392
+ // translation input; presence mirrors requestBody.hasSchema).
393
+ const rawBodySchema = requestBodySchemas[opIdx];
394
+ const emissions = operationEmissions(op);
395
+ const opLines = [];
396
+ // Section 2: Query parameter types
397
+ if (emissions.query) {
398
+ opLines.push(`export type ${emissions.query} = ${buildParamTypeBody(op.queryParams, mapper)};`);
399
+ }
400
+ // Section 2b: Header parameter types
401
+ if (emissions.headers) {
402
+ opLines.push(`export type ${emissions.headers} = ${buildParamTypeBody(op.headerParams, mapper)};`);
403
+ }
404
+ // Section 3: Request body types
405
+ const bodyJsDoc = op.requestBody ? buildDescriptionJsDoc(op.requestBody.description) : '';
406
+ if (op.requestBody?.isMultipart && rawBodySchema !== undefined) {
407
+ const schema = resolver.resolveSchema(rawBodySchema);
408
+ const requiredSet = new Set(schema.required ?? []);
409
+ const propLines = [];
410
+ const fileUploadProperties = [];
411
+ for (const [name, propSchema] of Object.entries(schema.properties ?? {})) {
412
+ const resolved = resolver.resolveSchema(propSchema);
413
+ const optional = requiredSet.has(name) ? '' : '?';
414
+ let tsType;
415
+ let kind;
416
+ if (resolved.format === 'binary') {
417
+ tsType = 'FileInput';
418
+ kind = 'file';
419
+ }
420
+ else if (resolved.type === 'array' && resolved.items?.format === 'binary') {
421
+ tsType = 'FileInput[]';
422
+ kind = 'file-array';
423
+ }
424
+ else {
425
+ tsType = 'string';
426
+ kind = 'field';
427
+ }
428
+ fileUploadProperties.push({ name, required: requiredSet.has(name), kind });
429
+ const jsDoc = buildTypeJsDoc(resolved);
430
+ if (jsDoc !== '') {
431
+ propLines.push(...indentJsDocBlock(jsDoc));
432
+ }
433
+ propLines.push(`${INDENT_UNIT}${name}${optional}: ${tsType};`);
434
+ }
435
+ op.fileUploadProperties = fileUploadProperties;
436
+ if (propLines.length > 0) {
437
+ opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${emissions.body} = {\n${propLines.join('\n')}\n};`));
438
+ }
439
+ else {
440
+ opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${emissions.body} = Record<string, never>;`));
441
+ }
442
+ }
443
+ else if (op.requestBody?.hasSchema && rawBodySchema !== undefined) {
444
+ if (op.requestBody.isBinary) {
445
+ opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${emissions.body} = Blob;`));
446
+ }
447
+ else {
448
+ const result = mapper.mapSchema(rawBodySchema, undefined, 'request');
449
+ opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${emissions.body} = ${result.tsType};`));
450
+ }
451
+ }
452
+ // Section 4: Response types
453
+ const successResponses = op.responses.filter((r) => r.isSuccess);
454
+ const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
455
+ const defaultResponse = op.responses.find((r) => !r.isSuccess && r.statusCode === 'default');
456
+ // Success type
457
+ if (emissions.response) {
458
+ const types = successResponses.map((r) => {
459
+ if (r.isBinary) {
460
+ r.finishedType = 'StreamResponse';
461
+ return 'StreamResponse';
462
+ }
463
+ if (r.schema) {
464
+ const result = mapper.mapSchema(r.schema, undefined, 'response').tsType;
465
+ const substituted = substituteDiscriminatedType(result, r.schema, discriminatorInfo, renameMap);
466
+ r.finishedType = substituted;
467
+ return substituted;
468
+ }
469
+ r.finishedType = r.isVoid ? 'void' : 'unknown';
470
+ return r.finishedType;
471
+ });
472
+ const successType = types.length === 1 ? types[0] : types.join(' | ');
473
+ // Type-level JSDoc from the lowest-numbered 2xx response (the analyzer
474
+ // returns success responses in ascending status order).
475
+ const responseJsDoc = buildDescriptionJsDoc(successResponses[0].description);
476
+ opLines.push(attachTypeJsDoc(responseJsDoc, `export type ${emissions.response} = ${successType};`));
477
+ }
478
+ // Error types per status (inventory pairs 1:1 with errorResponses by
479
+ // construction: same filter, same order)
480
+ for (let i = 0; i < errorResponses.length; i++) {
481
+ const err = errorResponses[i];
482
+ const errorTypeName = emissions.statusErrors[i].name;
483
+ let errorTsType;
484
+ if (err.isBinary) {
485
+ errorTsType = 'StreamResponse';
486
+ }
487
+ else if (err.schema) {
488
+ errorTsType = mapper.mapSchema(err.schema, undefined, 'response').tsType;
489
+ }
490
+ else {
491
+ errorTsType = 'unknown';
492
+ }
493
+ err.finishedType = errorTsType;
494
+ opLines.push(`export type ${errorTypeName} = ${errorTsType};`);
495
+ }
496
+ // Default response error type
497
+ if (emissions.defaultError) {
498
+ let defaultTsType;
499
+ if (defaultResponse?.isBinary) {
500
+ defaultTsType = 'StreamResponse';
501
+ }
502
+ else if (defaultResponse?.schema) {
503
+ defaultTsType = mapper.mapSchema(defaultResponse.schema, undefined, 'response').tsType;
504
+ }
505
+ else {
506
+ defaultTsType = 'unknown';
507
+ }
508
+ if (defaultResponse) {
509
+ defaultResponse.finishedType = defaultTsType;
510
+ }
511
+ opLines.push(`export type ${emissions.defaultError} = ${defaultTsType};`);
512
+ }
513
+ // Error union
514
+ if (emissions.errorsUnion) {
515
+ const unionParts = emissions.statusErrors.map((e) => `ApiError<${e.status}, ${e.name}>`);
516
+ opLines.push(`export type ${emissions.errorsUnion} = ${unionParts.join(' | ')};`);
517
+ }
518
+ op.contractsLines = opLines;
519
+ }
520
+ // Branded types snapshot after every mapSchema call has run, mirroring the
521
+ // pre-refactor read position at the end of generateContracts.
522
+ const brandedTypes = Array.from(mapper.getBrandedTypes().values());
523
+ const securitySchemeTypes = analyzeSecuritySchemes(doc.components?.securitySchemes);
524
+ const serverTypes = analyzeServers(doc.servers);
525
+ return {
526
+ specVersion: doc.openapi,
527
+ operations,
528
+ schemaTypes,
529
+ brandedTypes,
530
+ hasFileUpload,
531
+ securitySchemeTypes,
532
+ serverTypes,
533
+ };
534
+ }
@@ -1,4 +1,5 @@
1
1
  import { toPascalCaseSegment } from '../utils/case.js';
2
+ import { sanitizeTypeName } from '../utils/generator-helpers.js';
2
3
  import { sanitizeIdentifier } from '../utils/string.js';
3
4
  import { isPathParam, extractParamName } from '../utils/url.js';
4
5
  /**
@@ -51,9 +52,9 @@ export function generateMethodName(method, path) {
51
52
  }
52
53
  const transformedSegments = segments.map((segment, index) => {
53
54
  if (isParam[index]) {
54
- return `By${toPascalCaseSegment(segment)}`;
55
+ return `By${sanitizeTypeName(toPascalCaseSegment(segment))}`;
55
56
  }
56
- return toPascalCaseSegment(segment);
57
+ return sanitizeTypeName(toPascalCaseSegment(segment));
57
58
  });
58
59
  return lowerMethod + transformedSegments.join('');
59
60
  }
@@ -5,8 +5,8 @@ export interface AnalyzedParameter {
5
5
  name: string;
6
6
  in: 'path' | 'query' | 'header' | 'cookie';
7
7
  required: boolean;
8
+ /** Resolved param schema — analyzer-internal translation input for `analyze()`; renderers must not read it. */
8
9
  schema: SchemaObject | undefined;
9
- tsType: string;
10
10
  description?: string;
11
11
  deprecated?: boolean;
12
12
  example?: unknown;
@@ -14,24 +14,43 @@ export interface AnalyzedParameter {
14
14
  export interface AnalyzedRequestBody {
15
15
  required: boolean;
16
16
  contentTypes: string[];
17
- schema: SchemaObject | ReferenceObject | undefined;
18
- tsType: string;
17
+ /**
18
+ * Whether the first content entry carries a schema — drives the
19
+ * `{Prefix}Body` emission-presence decision (both the emission inventory in
20
+ * operation-naming and the Section-3 emission in analyze() read this fact,
21
+ * so they can never desync). The raw schema itself travels beside the
22
+ * operations array in `AnalyzedPathsResult`.
23
+ */
24
+ hasSchema: boolean;
19
25
  isMultipart: boolean;
26
+ isBinary: boolean;
20
27
  description?: string;
21
28
  }
22
29
  export interface AnalyzedResponse {
23
30
  statusCode: string;
24
31
  description?: string;
32
+ /** Raw (possibly `$ref`) first-content schema — analyzer-internal translation input for `analyze()`; renderers must not read it. */
25
33
  schema: SchemaObject | ReferenceObject | undefined;
26
- tsType: string;
27
34
  isSuccess: boolean;
28
35
  isBinary: boolean;
36
+ /**
37
+ * True exactly for schema-less, non-binary 2xx responses with no content
38
+ * (or empty content) — the "empty body → void" classification.
39
+ */
40
+ isVoid: boolean;
41
+ /**
42
+ * Finished (real-mapper) TS type text — StreamResponse substitution and
43
+ * discriminator variant rewrites applied. Set by `analyze()`.
44
+ */
45
+ finishedType?: string;
29
46
  }
30
47
  export interface AnalyzedOperation {
31
48
  method: string;
32
49
  path: string;
33
50
  operationId: string | undefined;
34
51
  methodName: string;
52
+ /** Deduped by analyzePaths; computed from method + path when absent. */
53
+ typePrefix?: string;
35
54
  summary: string | undefined;
36
55
  description: string | undefined;
37
56
  deprecated: boolean;
@@ -44,8 +63,28 @@ export interface AnalyzedOperation {
44
63
  responses: AnalyzedResponse[];
45
64
  }
46
65
  /**
47
- * Analyze all paths and operations from an OpenAPI document into structured data
48
- * for code generation.
66
+ * Analyzer-internal analysis result: the analyzed operations plus the raw
67
+ * request body schemas in a PARALLEL ARRAY (same length and order as
68
+ * `operations`). The raw schemas are translation input for `analyze()` and
69
+ * deliberately do not travel on the `AnalyzedRequestBody` seam.
70
+ */
71
+ export interface AnalyzedPathsResult {
72
+ operations: AnalyzedOperation[];
73
+ requestBodySchemas: (SchemaObject | ReferenceObject | undefined)[];
74
+ }
75
+ /**
76
+ * Analyze all paths and operations from an OpenAPI document into structured
77
+ * data for code generation, carrying the raw request body schemas alongside.
78
+ *
79
+ * @param doc - The parsed and validated OpenAPI document
80
+ * @param resolver - A RefResolver for resolving $ref pointers
81
+ * @param strategy - Method naming strategy (defaults to 'path-based')
82
+ * @returns Operations with parallel raw request body schemas
83
+ */
84
+ export declare function analyzePathsDetailed(doc: OpenAPIDocument, resolver: RefResolver, strategy?: MethodNameStrategy): AnalyzedPathsResult;
85
+ /**
86
+ * Analyze all paths and operations from an OpenAPI document into structured
87
+ * data for code generation.
49
88
  *
50
89
  * @param doc - The parsed and validated OpenAPI document
51
90
  * @param resolver - A RefResolver for resolving $ref pointers