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