genoc 0.3.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.
- package/README.md +30 -186
- package/dist/analyzer/analyze.d.ts +18 -0
- package/dist/analyzer/analyze.js +534 -0
- package/dist/analyzer/path-analyzer.d.ts +43 -6
- package/dist/analyzer/path-analyzer.js +57 -84
- package/dist/analyzer/schema-mapper.d.ts +3 -2
- package/dist/analyzer/schema-mapper.js +11 -7
- package/dist/analyzer/types.d.ts +119 -0
- package/dist/cli/app.d.ts +4 -2
- package/dist/cli/app.js +23 -4
- package/dist/cli/config-loader.d.ts +20 -0
- package/dist/cli/config-loader.js +186 -0
- package/dist/cli/config-schema.d.ts +105 -0
- package/dist/cli/config-schema.js +183 -0
- package/dist/cli/impl.d.ts +9 -1
- package/dist/cli/impl.js +186 -36
- package/dist/generator/client-generator.d.ts +13 -14
- package/dist/generator/client-generator.js +45 -77
- package/dist/generator/contracts-generator.d.ts +11 -9
- package/dist/generator/contracts-generator.js +45 -491
- package/dist/generator/method-generator.d.ts +1 -1
- package/dist/generator/method-generator.js +10 -150
- package/dist/parser/ref-resolver.d.ts +1 -1
- package/dist/parser/ref-resolver.js +1 -2
- package/dist/parser/version/index.d.ts +4 -16
- package/dist/parser/version/index.js +3 -14
- package/dist/parser/version/v3.0/validator.d.ts +1 -1
- package/dist/parser/version/v3.1/validator.d.ts +17 -0
- package/dist/parser/version/v3.1/validator.js +126 -0
- package/dist/parser/version/validate.d.ts +9 -0
- package/dist/parser/version/validate.js +12 -0
- package/dist/parser/version/version-detector.d.ts +21 -0
- package/dist/parser/version/version-detector.js +19 -0
- package/dist/pipeline.d.ts +13 -0
- package/dist/pipeline.js +24 -0
- package/dist/types/client.d.ts +0 -12
- package/dist/utils/generator-helpers.d.ts +1 -24
- package/dist/utils/generator-helpers.js +11 -78
- package/dist/utils/operation-naming.d.ts +84 -0
- package/dist/utils/operation-naming.js +178 -0
- package/package.json +4 -8
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -11
- package/dist/parser/validators.d.ts +0 -7
- package/dist/parser/validators.js +0 -79
- package/dist/parser/version/normalized-spec.d.ts +0 -199
- package/dist/parser/version/registry.d.ts +0 -28
- package/dist/parser/version/registry.js +0 -44
- package/dist/parser/version/v3.0/index.d.ts +0 -3
- package/dist/parser/version/v3.0/index.js +0 -3
- package/dist/parser/version/v3.0/normalizer.d.ts +0 -15
- package/dist/parser/version/v3.0/normalizer.js +0 -389
- package/dist/parser/version/v3.0/strategy.d.ts +0 -27
- package/dist/parser/version/v3.0/strategy.js +0 -96
- package/dist/parser/version/v3.1/index.d.ts +0 -1
- package/dist/parser/version/v3.1/index.js +0 -1
- package/dist/parser/version/v3.1/strategy.d.ts +0 -42
- package/dist/parser/version/v3.1/strategy.js +0 -513
- package/dist/parser/version/v3.2/index.d.ts +0 -4
- package/dist/parser/version/v3.2/index.js +0 -4
- package/dist/parser/version/v3.2/strategy.d.ts +0 -39
- package/dist/parser/version/v3.2/strategy.js +0 -57
- package/dist/parser/version/version-strategy.d.ts +0 -31
- package/dist/parser/version/version-strategy.js +0 -1
- /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
|
+
}
|
|
@@ -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,18 +14,35 @@ export interface AnalyzedParameter {
|
|
|
14
14
|
export interface AnalyzedRequestBody {
|
|
15
15
|
required: boolean;
|
|
16
16
|
contentTypes: string[];
|
|
17
|
-
|
|
18
|
-
|
|
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;
|
|
@@ -46,8 +63,28 @@ export interface AnalyzedOperation {
|
|
|
46
63
|
responses: AnalyzedResponse[];
|
|
47
64
|
}
|
|
48
65
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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.
|
|
51
88
|
*
|
|
52
89
|
* @param doc - The parsed and validated OpenAPI document
|
|
53
90
|
* @param resolver - A RefResolver for resolving $ref pointers
|