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