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
@@ -0,0 +1,493 @@
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 { buildDiscriminatorRegistry, renameAwareTypeName } from './discriminator-registry.js';
5
+ import { analyzePathsDetailed } from './path-analyzer.js';
6
+ import { SchemaMapper } from './schema-mapper.js';
7
+ /**
8
+ * Indent unit matching `INDENT_UNIT` in schema-mapper.ts (2 spaces, pinned by
9
+ * the mapper's golden tests). Kept local to avoid modifying the mapper module.
10
+ */
11
+ const INDENT_UNIT = ' ';
12
+ /**
13
+ * Shared warning channel for analyze() and the discriminator registry —
14
+ * the same sink the mapper defaults to.
15
+ */
16
+ function writeWarning(message) {
17
+ process.stderr.write(message);
18
+ }
19
+ /**
20
+ * Build a description-only type-level JSDoc comment for operation-derived
21
+ * types (body / response). Returns '' when the description is absent or
22
+ * whitespace-only so callers can skip emission entirely.
23
+ */
24
+ export function buildDescriptionJsDoc(description) {
25
+ if (description === undefined)
26
+ return '';
27
+ const sanitized = sanitizeJsDocText(description);
28
+ return sanitized === '' ? '' : `/** ${sanitized} */`;
29
+ }
30
+ /**
31
+ * Prefix a type definition with its JSDoc block (single- or multi-line),
32
+ * keeping the comment directly above the `export type` line.
33
+ */
34
+ function attachTypeJsDoc(jsDoc, definition) {
35
+ return jsDoc === '' ? definition : `${jsDoc}\n${definition}`;
36
+ }
37
+ /**
38
+ * Indent every line of a JSDoc block by one indent unit so property comments
39
+ * line up exactly with their property line (mapper indentation contract).
40
+ */
41
+ function indentJsDocBlock(jsDoc) {
42
+ return jsDoc.split('\n').map((line) => `${INDENT_UNIT}${line}`);
43
+ }
44
+ /**
45
+ * Build the JSDoc block for a query/header parameter property using the
46
+ * pinned parameter merge rule: parameter-level description / deprecated /
47
+ * example win over the schema's; default, title and examples come from the
48
+ * schema only (carried by the spread).
49
+ */
50
+ function buildParamPropertyJsDoc(param, schema) {
51
+ return buildTypeJsDoc({
52
+ ...schema,
53
+ description: param.description ?? schema.description,
54
+ deprecated: param.deprecated === true || schema.deprecated === true,
55
+ example: param.example ?? schema.example,
56
+ });
57
+ }
58
+ /**
59
+ * Render a `${prefix}Query` / `${prefix}Headers` object body in the mapper's
60
+ * multi-line format with per-property JSDoc from the parameter merge rule.
61
+ */
62
+ function buildParamTypeBody(params, mapper) {
63
+ const lines = [];
64
+ for (const param of params) {
65
+ const paramSchema = param.schema ?? { type: 'string' };
66
+ const result = mapper.mapSchema(paramSchema);
67
+ const optional = param.required ? '' : '?';
68
+ const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `"${param.name}"`;
69
+ const jsDoc = buildParamPropertyJsDoc(param, paramSchema);
70
+ if (jsDoc !== '') {
71
+ lines.push(...indentJsDocBlock(jsDoc));
72
+ }
73
+ lines.push(`${INDENT_UNIT}${key}${optional}: ${result.tsType};`);
74
+ }
75
+ return `{\n${lines.join('\n')}\n}`;
76
+ }
77
+ /**
78
+ * Sort ContractEntry list so that referenced types appear before referrers.
79
+ * Uses DFS-based topological sort; cycles are broken by skipping.
80
+ */
81
+ function topologicalSort(entries, allNames) {
82
+ if (entries.length <= 1)
83
+ return entries;
84
+ const nameToIndex = new Map();
85
+ entries.forEach((e, i) => nameToIndex.set(e.name, i));
86
+ const graph = new Map();
87
+ for (let i = 0; i < entries.length; i++) {
88
+ const deps = new Set();
89
+ const def = entries[i].definition;
90
+ for (const name of allNames) {
91
+ if (name === entries[i].name)
92
+ continue;
93
+ if (new RegExp(`\\b${name}\\b`).test(def)) {
94
+ const depIdx = nameToIndex.get(name);
95
+ if (depIdx !== undefined) {
96
+ deps.add(depIdx);
97
+ }
98
+ }
99
+ }
100
+ graph.set(i, deps);
101
+ }
102
+ const sorted = [];
103
+ const visited = new Set();
104
+ const inStack = new Set();
105
+ function visit(idx) {
106
+ if (visited.has(idx))
107
+ return;
108
+ if (inStack.has(idx))
109
+ return;
110
+ inStack.add(idx);
111
+ const deps = graph.get(idx);
112
+ if (deps) {
113
+ for (const dep of deps) {
114
+ visit(dep);
115
+ }
116
+ }
117
+ inStack.delete(idx);
118
+ visited.add(idx);
119
+ sorted.push(entries[idx]);
120
+ }
121
+ for (let i = 0; i < entries.length; i++) {
122
+ visit(i);
123
+ }
124
+ return sorted;
125
+ }
126
+ function securitySchemeToTsType(scheme) {
127
+ const parts = [`type: "${scheme.type}"`];
128
+ if (scheme.description) {
129
+ parts.push(`description: "${scheme.description}"`);
130
+ }
131
+ if (scheme.type === 'apiKey') {
132
+ if (scheme.name)
133
+ parts.push(`name: "${scheme.name}"`);
134
+ if (scheme.in)
135
+ parts.push(`in: "${scheme.in}"`);
136
+ }
137
+ if (scheme.type === 'http') {
138
+ if (scheme.scheme)
139
+ parts.push(`scheme: "${scheme.scheme}"`);
140
+ if (scheme.bearerFormat)
141
+ parts.push(`bearerFormat: "${scheme.bearerFormat}"`);
142
+ }
143
+ if (scheme.type === 'oauth2' && scheme.flows) {
144
+ const flowParts = [];
145
+ const flows = scheme.flows;
146
+ if (flows.implicit) {
147
+ flowParts.push(`implicit: ${oAuth2FlowToTs(flows.implicit, true)}`);
148
+ }
149
+ if (flows.password) {
150
+ flowParts.push(`password: ${oAuth2FlowToTs(flows.password, false)}`);
151
+ }
152
+ if (flows.clientCredentials) {
153
+ flowParts.push(`clientCredentials: ${oAuth2FlowToTs(flows.clientCredentials, false)}`);
154
+ }
155
+ if (flows.authorizationCode) {
156
+ flowParts.push(`authorizationCode: ${oAuth2FlowToTs(flows.authorizationCode, true)}`);
157
+ }
158
+ parts.push(`flows: { ${flowParts.join('; ')} }`);
159
+ }
160
+ if (scheme.type === 'openIdConnect' && scheme.openIdConnectUrl) {
161
+ parts.push(`openIdConnectUrl: "${scheme.openIdConnectUrl}"`);
162
+ }
163
+ return `{ ${parts.join('; ')} }`;
164
+ }
165
+ function oAuth2FlowToTs(flow, hasAuthUrl) {
166
+ const entries = [];
167
+ if (hasAuthUrl && flow.authorizationUrl) {
168
+ entries.push(`authorizationUrl: "${flow.authorizationUrl}"`);
169
+ }
170
+ if (flow.tokenUrl) {
171
+ entries.push(`tokenUrl: "${flow.tokenUrl}"`);
172
+ }
173
+ if (flow.refreshUrl) {
174
+ entries.push(`refreshUrl: "${flow.refreshUrl}"`);
175
+ }
176
+ const scopeEntries = Object.entries(flow.scopes)
177
+ .map(([k, v]) => `"${k}": "${v}"`)
178
+ .join('; ');
179
+ entries.push(`scopes: { ${scopeEntries} }`);
180
+ return `{ ${entries.join('; ')} }`;
181
+ }
182
+ /**
183
+ * Translate `components.securitySchemes` into finished `{Name}Auth` type
184
+ * declarations: sanitize + PascalCase the key, number collisions, render the
185
+ * object type text and the description JSDoc. Spec key order preserved.
186
+ */
187
+ function analyzeSecuritySchemes(securitySchemes) {
188
+ const entries = [];
189
+ if (!securitySchemes || Object.keys(securitySchemes).length === 0) {
190
+ return entries;
191
+ }
192
+ const usedSecurityTypeNames = new Set();
193
+ for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
194
+ const baseTypeName = `${sanitizeTypeName(toPascalCase(schemeName))}Auth`;
195
+ let typeName = baseTypeName;
196
+ let n = 2;
197
+ while (usedSecurityTypeNames.has(typeName)) {
198
+ typeName = `${baseTypeName}${n}`;
199
+ n += 1;
200
+ }
201
+ usedSecurityTypeNames.add(typeName);
202
+ entries.push({
203
+ name: typeName,
204
+ tsType: securitySchemeToTsType(scheme),
205
+ jsDoc: buildDescriptionJsDoc(scheme.description),
206
+ });
207
+ }
208
+ return entries;
209
+ }
210
+ /**
211
+ * Translate `doc.servers` into finished server variable interface
212
+ * declarations, one per server that declares variables. Names are index-aware
213
+ * over ALL servers (`ServerParams` for a single-server document,
214
+ * `Server{i}Params` otherwise), so variable-less servers still occupy an
215
+ * index. Property lines carry their JSDoc and indentation.
216
+ */
217
+ function analyzeServers(servers) {
218
+ const entries = [];
219
+ if (!servers)
220
+ return entries;
221
+ for (let serverIdx = 0; serverIdx < servers.length; serverIdx++) {
222
+ const server = servers[serverIdx];
223
+ if (!server.variables || Object.keys(server.variables).length === 0) {
224
+ continue;
225
+ }
226
+ const typeName = servers.length === 1 ? 'ServerParams' : `Server${serverIdx + 1}Params`;
227
+ const props = [];
228
+ for (const [varName, variable] of Object.entries(server.variables)) {
229
+ const sv = variable;
230
+ const jsDocParts = [];
231
+ if (sv.description) {
232
+ const description = sanitizeJsDocText(sv.description);
233
+ if (description !== '') {
234
+ jsDocParts.push(description);
235
+ }
236
+ }
237
+ if (sv.default !== undefined) {
238
+ jsDocParts.push(`@default ${sanitizeJsDocText(String(sv.default))}`);
239
+ }
240
+ let tsType;
241
+ if (sv.enum && sv.enum.length > 0) {
242
+ tsType = sv.enum.map((v) => `"${v}"`).join(' | ');
243
+ }
244
+ else {
245
+ tsType = 'string';
246
+ }
247
+ const jsDoc = jsDocParts.length > 0 ? ` /** ${jsDocParts.join(' ')} */` : null;
248
+ if (jsDoc) {
249
+ props.push(jsDoc);
250
+ }
251
+ props.push(` ${varName}: ${tsType};`);
252
+ }
253
+ entries.push({
254
+ name: typeName,
255
+ url: server.url,
256
+ propertyLines: props,
257
+ });
258
+ }
259
+ return entries;
260
+ }
261
+ /**
262
+ * Analyze an OpenAPI document into the finished model every renderer consumes:
263
+ * schema→TS-type translation (rename machinery, discriminator discovery, the
264
+ * schema mapper) runs here exactly once, producing render-ready type text.
265
+ *
266
+ * @param doc - The parsed and validated OpenAPI document
267
+ * @param opts - Optional resolver override and method naming strategy
268
+ * @returns The AnalyzedSpec model consumed by all generators
269
+ */
270
+ export function analyze(doc, opts = {}) {
271
+ const resolver = opts.resolver ?? new RefResolver(doc);
272
+ const strategy = opts.strategy ?? 'path-based';
273
+ const schemaNameList = doc.components?.schemas ? Object.keys(doc.components.schemas) : [];
274
+ const renameMap = buildSchemaRenameMap(schemaNameList, RESERVED_TYPE_NAMES);
275
+ for (const [original, renamed] of renameMap) {
276
+ // TODO: Replace with structured logging solution
277
+ // oxlint-disable-next-line no-console
278
+ console.warn(`Warning: Schema "${original}" collides with a built-in type or another schema and was renamed to "${renamed}".`);
279
+ }
280
+ const renamingTypeGenerator = (refString) => {
281
+ const segments = refString.split('/');
282
+ const rawSegment = segments[segments.length - 1] || 'unknown';
283
+ return renameAwareTypeName(renameMap, rawSegment);
284
+ };
285
+ const allSchemaNames = new Set();
286
+ if (doc.components?.schemas) {
287
+ for (const name of Object.keys(doc.components.schemas)) {
288
+ allSchemaNames.add(renameMap.get(name) ?? sanitizeTypeName(name));
289
+ }
290
+ }
291
+ // Family-aware discriminator registry (plan D5) — the single knowledge
292
+ // source for discriminator translation: the mapper's ref-translation seam
293
+ // consumes it, and the always-emitted `{Base}Variant` unions below are
294
+ // emitted from it.
295
+ const discriminatorRegistry = buildDiscriminatorRegistry(doc.components?.schemas, resolver, renameMap, allSchemaNames, writeWarning);
296
+ const mapper = new SchemaMapper(resolver, {
297
+ typeNameGenerator: renamingTypeGenerator,
298
+ emittedNames: allSchemaNames,
299
+ discriminatorRegistry,
300
+ effectiveVersion: opts?.effectiveVersion,
301
+ });
302
+ // Section 1: Schema types
303
+ const schemaEntries = [];
304
+ if (doc.components?.schemas) {
305
+ for (const [name, schema] of Object.entries(doc.components.schemas)) {
306
+ const renamedName = renameMap.get(name) ?? sanitizeTypeName(name);
307
+ const result = mapper.mapSchema(schema, renamedName);
308
+ const resolved = resolver.resolve(schema);
309
+ const definition = `export type ${renamedName} = ${result.tsType};`;
310
+ schemaEntries.push({
311
+ name: renamedName,
312
+ kind: 'type',
313
+ definition,
314
+ jsDoc: buildTypeJsDoc(resolved) || undefined,
315
+ });
316
+ }
317
+ }
318
+ const sortedSchemas = topologicalSort(schemaEntries, allSchemaNames);
319
+ const schemaTypes = sortedSchemas.map((entry) => ({
320
+ name: entry.name,
321
+ definition: entry.definition,
322
+ jsDoc: entry.jsDoc,
323
+ }));
324
+ // Variant unions are emitted from the registry (D6): ALWAYS (mapping
325
+ // values ∪ implicit oneOf/anyOf refs, deduped by rename-aware name) and
326
+ // under collision-safe names so a user schema named `{Base}Variant`
327
+ // cannot collide with the generated union.
328
+ for (const family of discriminatorRegistry.families.values()) {
329
+ if (family.variantUnionMembers.length === 0)
330
+ continue;
331
+ schemaTypes.push({
332
+ name: family.variantUnionName,
333
+ definition: `export type ${family.variantUnionName} = ${family.variantUnionMembers.join(' | ')};`,
334
+ });
335
+ allSchemaNames.add(family.variantUnionName);
336
+ }
337
+ const { operations: analyzedOperations, requestBodySchemas } = analyzePathsDetailed(doc, resolver, strategy);
338
+ const operations = [];
339
+ for (const op of analyzedOperations) {
340
+ operations.push({
341
+ ...op,
342
+ contractsLines: [],
343
+ });
344
+ }
345
+ const hasFileUpload = operations.some((op) => op.requestBody?.isMultipart);
346
+ // Sections 2-4: Operation-derived types. Presence decisions and type
347
+ // names come from the emission inventory (operationEmissions); the
348
+ // finished lines are assembled here so renderers never re-translate.
349
+ for (let opIdx = 0; opIdx < operations.length; opIdx++) {
350
+ const op = operations[opIdx];
351
+ // Raw request body schema, parallel to `operations` (analyzer-internal
352
+ // translation input; presence mirrors requestBody.hasSchema).
353
+ const rawBodySchema = requestBodySchemas[opIdx];
354
+ const emissions = operationEmissions(op);
355
+ const opLines = [];
356
+ // Section 2: Query parameter types
357
+ if (emissions.query) {
358
+ opLines.push(`export type ${emissions.query} = ${buildParamTypeBody(op.queryParams, mapper)};`);
359
+ }
360
+ // Section 2b: Header parameter types
361
+ if (emissions.headers) {
362
+ opLines.push(`export type ${emissions.headers} = ${buildParamTypeBody(op.headerParams, mapper)};`);
363
+ }
364
+ // Section 3: Request body types
365
+ const bodyJsDoc = op.requestBody ? buildDescriptionJsDoc(op.requestBody.description) : '';
366
+ if (op.requestBody?.isMultipart && rawBodySchema !== undefined) {
367
+ const schema = resolver.resolveSchema(rawBodySchema);
368
+ const requiredSet = new Set(schema.required ?? []);
369
+ const propLines = [];
370
+ const fileUploadProperties = [];
371
+ for (const [name, propSchema] of Object.entries(schema.properties ?? {})) {
372
+ const resolved = resolver.resolveSchema(propSchema);
373
+ const optional = requiredSet.has(name) ? '' : '?';
374
+ let tsType;
375
+ let kind;
376
+ if (resolved.format === 'binary') {
377
+ tsType = 'FileInput';
378
+ kind = 'file';
379
+ }
380
+ else if (resolved.type === 'array' && resolved.items?.format === 'binary') {
381
+ tsType = 'FileInput[]';
382
+ kind = 'file-array';
383
+ }
384
+ else {
385
+ tsType = 'string';
386
+ kind = 'field';
387
+ }
388
+ fileUploadProperties.push({ name, required: requiredSet.has(name), kind });
389
+ const jsDoc = buildTypeJsDoc(resolved);
390
+ if (jsDoc !== '') {
391
+ propLines.push(...indentJsDocBlock(jsDoc));
392
+ }
393
+ propLines.push(`${INDENT_UNIT}${name}${optional}: ${tsType};`);
394
+ }
395
+ op.fileUploadProperties = fileUploadProperties;
396
+ if (propLines.length > 0) {
397
+ opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${emissions.body} = {\n${propLines.join('\n')}\n};`));
398
+ }
399
+ else {
400
+ opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${emissions.body} = Record<string, never>;`));
401
+ }
402
+ }
403
+ else if (op.requestBody?.hasSchema && rawBodySchema !== undefined) {
404
+ if (op.requestBody.isBinary) {
405
+ opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${emissions.body} = Blob;`));
406
+ }
407
+ else {
408
+ const result = mapper.mapSchema(rawBodySchema, undefined, 'request');
409
+ opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${emissions.body} = ${result.tsType};`));
410
+ }
411
+ }
412
+ // Section 4: Response types
413
+ const successResponses = op.responses.filter((r) => r.isSuccess);
414
+ const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
415
+ const defaultResponse = op.responses.find((r) => !r.isSuccess && r.statusCode === 'default');
416
+ // Success type
417
+ if (emissions.response) {
418
+ const types = successResponses.map((r) => {
419
+ if (r.isBinary) {
420
+ r.finishedType = 'StreamResponse';
421
+ return 'StreamResponse';
422
+ }
423
+ if (r.schema) {
424
+ const tsType = mapper.mapSchema(r.schema, undefined, 'response').tsType;
425
+ r.finishedType = tsType;
426
+ return tsType;
427
+ }
428
+ r.finishedType = r.isVoid ? 'void' : 'unknown';
429
+ return r.finishedType;
430
+ });
431
+ const successType = types.length === 1 ? types[0] : types.join(' | ');
432
+ // Type-level JSDoc from the lowest-numbered 2xx response (the analyzer
433
+ // returns success responses in ascending status order).
434
+ const responseJsDoc = buildDescriptionJsDoc(successResponses[0].description);
435
+ opLines.push(attachTypeJsDoc(responseJsDoc, `export type ${emissions.response} = ${successType};`));
436
+ }
437
+ // Error types per status (inventory pairs 1:1 with errorResponses by
438
+ // construction: same filter, same order)
439
+ for (let i = 0; i < errorResponses.length; i++) {
440
+ const err = errorResponses[i];
441
+ const errorTypeName = emissions.statusErrors[i].name;
442
+ let errorTsType;
443
+ if (err.isBinary) {
444
+ errorTsType = 'StreamResponse';
445
+ }
446
+ else if (err.schema) {
447
+ errorTsType = mapper.mapSchema(err.schema, undefined, 'response').tsType;
448
+ }
449
+ else {
450
+ errorTsType = 'unknown';
451
+ }
452
+ err.finishedType = errorTsType;
453
+ opLines.push(`export type ${errorTypeName} = ${errorTsType};`);
454
+ }
455
+ // Default response error type
456
+ if (emissions.defaultError) {
457
+ let defaultTsType;
458
+ if (defaultResponse?.isBinary) {
459
+ defaultTsType = 'StreamResponse';
460
+ }
461
+ else if (defaultResponse?.schema) {
462
+ defaultTsType = mapper.mapSchema(defaultResponse.schema, undefined, 'response').tsType;
463
+ }
464
+ else {
465
+ defaultTsType = 'unknown';
466
+ }
467
+ if (defaultResponse) {
468
+ defaultResponse.finishedType = defaultTsType;
469
+ }
470
+ opLines.push(`export type ${emissions.defaultError} = ${defaultTsType};`);
471
+ }
472
+ // Error union
473
+ if (emissions.errorsUnion) {
474
+ const unionParts = emissions.statusErrors.map((e) => `ApiError<${e.status}, ${e.name}>`);
475
+ opLines.push(`export type ${emissions.errorsUnion} = ${unionParts.join(' | ')};`);
476
+ }
477
+ op.contractsLines = opLines;
478
+ }
479
+ // Branded types snapshot after every mapSchema call has run, mirroring the
480
+ // pre-refactor read position at the end of generateContracts.
481
+ const brandedTypes = Array.from(mapper.getBrandedTypes().values());
482
+ const securitySchemeTypes = analyzeSecuritySchemes(doc.components?.securitySchemes);
483
+ const serverTypes = analyzeServers(doc.servers);
484
+ return {
485
+ specVersion: doc.openapi,
486
+ operations,
487
+ schemaTypes,
488
+ brandedTypes,
489
+ hasFileUpload,
490
+ securitySchemeTypes,
491
+ serverTypes,
492
+ };
493
+ }
@@ -0,0 +1,108 @@
1
+ import type { RefResolver } from '../parser/ref-resolver.js';
2
+ import type { SchemaObject } from '../types/openapi.js';
3
+ /**
4
+ * Family-aware discriminator registry (plan D5) — the single knowledge source
5
+ * for discriminator families the mapper seam will translate through:
6
+ *
7
+ * - One record per discriminator family (a component schema declaring
8
+ * `discriminator`), keyed by `familyId` (the base's raw component name).
9
+ * - Variant entries keyed by the FULL `$ref` string (mapping values are exact
10
+ * ref strings) plus a derived rename-aware name index.
11
+ * - Literals are stored PRE-ESCAPED (D8: `'` and `\` only) so the mapper never
12
+ * interpolates raw values into string literals.
13
+ * - Implicit variants (oneOf/anyOf `$ref` members absent from `mapping`) carry
14
+ * the RAW last ref segment as their literal (D3 precedence: mapping key >
15
+ * own `const` > single-value `enum` > raw segment).
16
+ *
17
+ * Field budget (D5): audited — every remaining field has a live consumer in
18
+ * production (seam / Variant-union emission) or in the registry unit tests.
19
+ * Rename-aware keying is mandatory: the name index goes through the same
20
+ * `buildSchemaRenameMap` machinery `analyze()` uses (see the past-bug note at
21
+ * schema-mapper.ts:75–78 — raw-name keying silently drops literal injection
22
+ * for renamed subtypes).
23
+ */
24
+ /** Rename-aware type-name derivation — the exact expression `analyze()` wires
25
+ * into the mapper's `typeNameGenerator` (`renameMap.get(raw) ?? sanitized`).
26
+ * Single source of truth so registry name keys can never diverge from the
27
+ * names the mapper emits for the same `$ref`.
28
+ */
29
+ export declare function renameAwareTypeName(renameMap: Map<string, string>, rawName: string): string;
30
+ /**
31
+ * One discriminator variant target inside a family. Consumers: `propertyName`
32
+ * → literal append + `Omit<M, quoteKey(P)>` key + union wrapper;
33
+ * `literalValue` → the appended literal (pre-escaped — render as
34
+ * `'${literalValue}'`); `isNamed` → bare-name vs inline-expansion decision;
35
+ * `isUnion`/`isNullable` → spine `Omit` fallback decision (D2); `familyId` →
36
+ * same-family spine detection (D1/D2); `refStr` → identity match at the
37
+ * ref-translation seam; `typeName` → bare-name emission and the name index.
38
+ */
39
+ export interface DiscriminatorVariantEntry {
40
+ /** Discriminator property name declared by the family base (e.g. `'$type'`). */
41
+ propertyName: string;
42
+ /** Pre-escaped literal (D8) — the mapping key, own const/enum value, or raw ref segment. No surrounding quotes. */
43
+ literalValue: string;
44
+ /** The rename-aware derived name is an emitted component schema (`∈ allSchemaNames`). */
45
+ isNamed: boolean;
46
+ /** The target's own resolved schema declares `oneOf`/`anyOf` (spine `Omit` would drop variant props — D2 fallback). */
47
+ isUnion: boolean;
48
+ /** The target's own resolved schema is nullable (3.0 `nullable: true` or a 3.1 type array containing `'null'`). */
49
+ isNullable: boolean;
50
+ /** Owning family id — the discriminator base's raw component name. */
51
+ familyId: string;
52
+ /** The exact `$ref` string that produced this entry (mapping value or oneOf/anyOf member ref). */
53
+ refStr: string;
54
+ /** Rename-aware type name for the ref (same derivation as the mapper's `typeNameGenerator`). */
55
+ typeName: string;
56
+ }
57
+ /**
58
+ * One discriminator family: a component schema declaring `discriminator`
59
+ * (`familyId` = its raw component name). Consumers: `baseRefStr` → base-ref
60
+ * detection at the seam (D1: `$ref` to the base emits `variantUnionName`
61
+ * outside same-family spines); `variantUnionName` / `variantUnionMembers` →
62
+ * the always-emit `{Base}Variant` union (D6); `entriesByRef` → family
63
+ * construction substrate and per-family lookups.
64
+ */
65
+ export interface DiscriminatorFamily {
66
+ /** Raw component name of the discriminator base — the family id. */
67
+ familyId: string;
68
+ /** Rename-aware emitted type name of the base. */
69
+ baseTypeName: string;
70
+ /** Canonical ref of the base component (`#/components/schemas/<encoded key>`). */
71
+ baseRefStr: string;
72
+ /** Discriminator property name (`discriminator.propertyName`). */
73
+ propertyName: string;
74
+ /** Collision-safe `{Base}Variant` union name (D6 — via the reserved-name machinery). */
75
+ variantUnionName: string;
76
+ /** Union member type names (rename-aware, deduped; mapping order, then unmapped oneOf/anyOf refs). */
77
+ variantUnionMembers: string[];
78
+ /** Family entries keyed by the FULL `$ref` string (mapping value or oneOf/anyOf member ref). */
79
+ entriesByRef: Map<string, DiscriminatorVariantEntry>;
80
+ }
81
+ /**
82
+ * The discriminator registry built once per `analyze()` run. Consumers:
83
+ * `byRef`/`byName` for variant-target lookup at the ref-translation seam;
84
+ * `baseRefs` for D1 base-ref translation; `families` for same-family spine
85
+ * scoping (entry `familyId` → family) and the `{Base}Variant` union emission.
86
+ * Flat lookups are first-wins across families (spec order) — ambiguous
87
+ * cross-family targets stay reachable through their per-family maps.
88
+ */
89
+ export interface DiscriminatorRegistry {
90
+ /** Families keyed by familyId (raw base component name), in spec order. */
91
+ families: Map<string, DiscriminatorFamily>;
92
+ /** Flat lookup: full refStr → entry (first-wins across families). */
93
+ byRef: Map<string, DiscriminatorVariantEntry>;
94
+ /** Flat lookup: rename-aware type name → entry (first-wins across families). */
95
+ byName: Map<string, DiscriminatorVariantEntry>;
96
+ /** Canonical base refStr → family, for D1 base-ref translation. */
97
+ baseRefs: Map<string, DiscriminatorFamily>;
98
+ }
99
+ /**
100
+ * Build the family-aware discriminator registry from the component schemas.
101
+ *
102
+ * Inputs must be the SAME `renameMap` / `allSchemaNames` `analyze()` derives
103
+ * (the rename-aware keying discipline) — pass `buildSchemaRenameMap`'s output
104
+ * and the emitted-name set, never a parallel sanitization. Pure with respect
105
+ * to the document: no mutation of the schemas; warnings (D3 mapping-key
106
+ * conflicts) go to `warnSink`.
107
+ */
108
+ export declare function buildDiscriminatorRegistry(schemas: Record<string, SchemaObject> | undefined, resolver: RefResolver, renameMap: Map<string, string>, allSchemaNames: ReadonlySet<string>, warnSink?: (message: string) => void): DiscriminatorRegistry;