genoc 0.1.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/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/analyzer/naming.d.ts +24 -0
- package/dist/analyzer/naming.js +122 -0
- package/dist/analyzer/path-analyzer.d.ts +53 -0
- package/dist/analyzer/path-analyzer.js +222 -0
- package/dist/analyzer/schema-mapper.d.ts +48 -0
- package/dist/analyzer/schema-mapper.js +435 -0
- package/dist/cli/app.d.ts +9 -0
- package/dist/cli/app.js +60 -0
- package/dist/cli/errors.d.ts +3 -0
- package/dist/cli/errors.js +6 -0
- package/dist/cli/impl.d.ts +3 -0
- package/dist/cli/impl.js +45 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +5 -0
- package/dist/generator/client-generator.d.ts +21 -0
- package/dist/generator/client-generator.js +287 -0
- package/dist/generator/contracts-generator.d.ts +16 -0
- package/dist/generator/contracts-generator.js +525 -0
- package/dist/generator/error-types.d.ts +24 -0
- package/dist/generator/error-types.js +94 -0
- package/dist/generator/method-generator.d.ts +9 -0
- package/dist/generator/method-generator.js +249 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/parser/ref-resolver.d.ts +24 -0
- package/dist/parser/ref-resolver.js +119 -0
- package/dist/parser/spec-reader.d.ts +4 -0
- package/dist/parser/spec-reader.js +116 -0
- package/dist/parser/validators.d.ts +7 -0
- package/dist/parser/validators.js +79 -0
- package/dist/parser/version/index.d.ts +18 -0
- package/dist/parser/version/index.js +16 -0
- package/dist/parser/version/normalized-spec.d.ts +199 -0
- package/dist/parser/version/normalized-spec.js +1 -0
- package/dist/parser/version/registry.d.ts +28 -0
- package/dist/parser/version/registry.js +44 -0
- package/dist/parser/version/v3.0/index.d.ts +3 -0
- package/dist/parser/version/v3.0/index.js +3 -0
- package/dist/parser/version/v3.0/normalizer.d.ts +15 -0
- package/dist/parser/version/v3.0/normalizer.js +389 -0
- package/dist/parser/version/v3.0/strategy.d.ts +27 -0
- package/dist/parser/version/v3.0/strategy.js +96 -0
- package/dist/parser/version/v3.0/validator.d.ts +13 -0
- package/dist/parser/version/v3.0/validator.js +117 -0
- package/dist/parser/version/v3.1/index.d.ts +1 -0
- package/dist/parser/version/v3.1/index.js +1 -0
- package/dist/parser/version/v3.1/strategy.d.ts +42 -0
- package/dist/parser/version/v3.1/strategy.js +513 -0
- package/dist/parser/version/v3.2/index.d.ts +4 -0
- package/dist/parser/version/v3.2/index.js +4 -0
- package/dist/parser/version/v3.2/strategy.d.ts +39 -0
- package/dist/parser/version/v3.2/strategy.js +57 -0
- package/dist/parser/version/version-detector.d.ts +4 -0
- package/dist/parser/version/version-detector.js +34 -0
- package/dist/parser/version/version-strategy.d.ts +31 -0
- package/dist/parser/version/version-strategy.js +1 -0
- package/dist/types/client.d.ts +25 -0
- package/dist/types/client.js +1 -0
- package/dist/types/contracts.d.ts +13 -0
- package/dist/types/contracts.js +1 -0
- package/dist/types/openapi.d.ts +173 -0
- package/dist/types/openapi.js +1 -0
- package/dist/utils/case.d.ts +5 -0
- package/dist/utils/case.js +51 -0
- package/dist/utils/generator-helpers.d.ts +23 -0
- package/dist/utils/generator-helpers.js +66 -0
- package/dist/utils/string.d.ts +34 -0
- package/dist/utils/string.js +182 -0
- package/dist/utils/url.d.ts +10 -0
- package/dist/utils/url.js +40 -0
- package/package.json +60 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { analyzePaths } from '../analyzer/path-analyzer.js';
|
|
2
|
+
import { SchemaMapper } from '../analyzer/schema-mapper.js';
|
|
3
|
+
import { RefResolver } from '../parser/ref-resolver.js';
|
|
4
|
+
import { 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) {
|
|
21
|
+
const refSchema = schema;
|
|
22
|
+
if (!refSchema || typeof refSchema !== 'object')
|
|
23
|
+
return tsType;
|
|
24
|
+
if (typeof refSchema.$ref === 'string') {
|
|
25
|
+
const schemaName = refSchema.$ref.split('/').pop();
|
|
26
|
+
if (schemaName && discriminatorInfo.has(schemaName)) {
|
|
27
|
+
return tsType.replace(new RegExp(`\\b${schemaName}\\b`, 'g'), `${schemaName}Variant`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (refSchema.items && typeof refSchema.items === 'object') {
|
|
31
|
+
const items = refSchema.items;
|
|
32
|
+
if (typeof items.$ref === 'string') {
|
|
33
|
+
const schemaName = items.$ref.split('/').pop();
|
|
34
|
+
if (schemaName && discriminatorInfo.has(schemaName)) {
|
|
35
|
+
return tsType.replace(new RegExp(`\\b${schemaName}\\b`, 'g'), `${schemaName}Variant`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return tsType;
|
|
40
|
+
}
|
|
41
|
+
function buildJsDoc(description) {
|
|
42
|
+
if (!description)
|
|
43
|
+
return undefined;
|
|
44
|
+
return `/** ${description} */`;
|
|
45
|
+
}
|
|
46
|
+
function securitySchemeToTsType(scheme) {
|
|
47
|
+
const parts = [`type: "${scheme.type}"`];
|
|
48
|
+
if (scheme.description) {
|
|
49
|
+
parts.push(`description: "${scheme.description}"`);
|
|
50
|
+
}
|
|
51
|
+
if (scheme.type === 'apiKey') {
|
|
52
|
+
if (scheme.name)
|
|
53
|
+
parts.push(`name: "${scheme.name}"`);
|
|
54
|
+
if (scheme.in)
|
|
55
|
+
parts.push(`in: "${scheme.in}"`);
|
|
56
|
+
}
|
|
57
|
+
if (scheme.type === 'http') {
|
|
58
|
+
if (scheme.scheme)
|
|
59
|
+
parts.push(`scheme: "${scheme.scheme}"`);
|
|
60
|
+
if (scheme.bearerFormat)
|
|
61
|
+
parts.push(`bearerFormat: "${scheme.bearerFormat}"`);
|
|
62
|
+
}
|
|
63
|
+
if (scheme.type === 'oauth2' && scheme.flows) {
|
|
64
|
+
const flowParts = [];
|
|
65
|
+
const flows = scheme.flows;
|
|
66
|
+
if (flows.implicit) {
|
|
67
|
+
flowParts.push(`implicit: ${oAuth2FlowToTs(flows.implicit, true)}`);
|
|
68
|
+
}
|
|
69
|
+
if (flows.password) {
|
|
70
|
+
flowParts.push(`password: ${oAuth2FlowToTs(flows.password, false)}`);
|
|
71
|
+
}
|
|
72
|
+
if (flows.clientCredentials) {
|
|
73
|
+
flowParts.push(`clientCredentials: ${oAuth2FlowToTs(flows.clientCredentials, false)}`);
|
|
74
|
+
}
|
|
75
|
+
if (flows.authorizationCode) {
|
|
76
|
+
flowParts.push(`authorizationCode: ${oAuth2FlowToTs(flows.authorizationCode, true)}`);
|
|
77
|
+
}
|
|
78
|
+
parts.push(`flows: { ${flowParts.join('; ')} }`);
|
|
79
|
+
}
|
|
80
|
+
if (scheme.type === 'openIdConnect' && scheme.openIdConnectUrl) {
|
|
81
|
+
parts.push(`openIdConnectUrl: "${scheme.openIdConnectUrl}"`);
|
|
82
|
+
}
|
|
83
|
+
return `{ ${parts.join('; ')} }`;
|
|
84
|
+
}
|
|
85
|
+
function oAuth2FlowToTs(flow, hasAuthUrl) {
|
|
86
|
+
const entries = [];
|
|
87
|
+
if (hasAuthUrl && flow.authorizationUrl) {
|
|
88
|
+
entries.push(`authorizationUrl: "${flow.authorizationUrl}"`);
|
|
89
|
+
}
|
|
90
|
+
if (flow.tokenUrl) {
|
|
91
|
+
entries.push(`tokenUrl: "${flow.tokenUrl}"`);
|
|
92
|
+
}
|
|
93
|
+
if (flow.refreshUrl) {
|
|
94
|
+
entries.push(`refreshUrl: "${flow.refreshUrl}"`);
|
|
95
|
+
}
|
|
96
|
+
const scopeEntries = Object.entries(flow.scopes)
|
|
97
|
+
.map(([k, v]) => `"${k}": "${v}"`)
|
|
98
|
+
.join('; ');
|
|
99
|
+
entries.push(`scopes: { ${scopeEntries} }`);
|
|
100
|
+
return `{ ${entries.join('; ')} }`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Sort ContractEntry list so that referenced types appear before referrers.
|
|
104
|
+
* Uses DFS-based topological sort; cycles are broken by skipping.
|
|
105
|
+
*/
|
|
106
|
+
function topologicalSort(entries, allNames) {
|
|
107
|
+
if (entries.length <= 1)
|
|
108
|
+
return entries;
|
|
109
|
+
const nameToIndex = new Map();
|
|
110
|
+
entries.forEach((e, i) => nameToIndex.set(e.name, i));
|
|
111
|
+
const graph = new Map();
|
|
112
|
+
for (let i = 0; i < entries.length; i++) {
|
|
113
|
+
const deps = new Set();
|
|
114
|
+
const def = entries[i].definition;
|
|
115
|
+
for (const name of allNames) {
|
|
116
|
+
if (name === entries[i].name)
|
|
117
|
+
continue;
|
|
118
|
+
if (new RegExp(`\\b${name}\\b`).test(def)) {
|
|
119
|
+
const depIdx = nameToIndex.get(name);
|
|
120
|
+
if (depIdx !== undefined) {
|
|
121
|
+
deps.add(depIdx);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
graph.set(i, deps);
|
|
126
|
+
}
|
|
127
|
+
const sorted = [];
|
|
128
|
+
const visited = new Set();
|
|
129
|
+
const inStack = new Set();
|
|
130
|
+
function visit(idx) {
|
|
131
|
+
if (visited.has(idx))
|
|
132
|
+
return;
|
|
133
|
+
if (inStack.has(idx))
|
|
134
|
+
return;
|
|
135
|
+
inStack.add(idx);
|
|
136
|
+
const deps = graph.get(idx);
|
|
137
|
+
if (deps) {
|
|
138
|
+
for (const dep of deps) {
|
|
139
|
+
visit(dep);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
inStack.delete(idx);
|
|
143
|
+
visited.add(idx);
|
|
144
|
+
sorted.push(entries[idx]);
|
|
145
|
+
}
|
|
146
|
+
for (let i = 0; i < entries.length; i++) {
|
|
147
|
+
visit(i);
|
|
148
|
+
}
|
|
149
|
+
return sorted;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Generate the complete `*.contracts.ts` file content as a string.
|
|
153
|
+
*
|
|
154
|
+
* Sections produced:
|
|
155
|
+
* 1. Header comment
|
|
156
|
+
* 2. Schema types from `components/schemas`
|
|
157
|
+
* 3. Query parameter types per operation
|
|
158
|
+
* 4. Header parameter types per operation
|
|
159
|
+
* 5. Request body types per operation
|
|
160
|
+
* 6. Response / error types per operation
|
|
161
|
+
* 7. ApiError class
|
|
162
|
+
* 7b. UnspecifiedApiError class
|
|
163
|
+
*/
|
|
164
|
+
export function generateContracts(doc, resolver) {
|
|
165
|
+
const discriminatorInfo = new Map();
|
|
166
|
+
if (doc.components?.schemas) {
|
|
167
|
+
for (const [name, schema] of Object.entries(doc.components.schemas)) {
|
|
168
|
+
const resolved = resolver.resolve(schema);
|
|
169
|
+
if (resolved.discriminator) {
|
|
170
|
+
const mapping = new Map();
|
|
171
|
+
if (resolved.discriminator.mapping) {
|
|
172
|
+
for (const [key, ref] of Object.entries(resolved.discriminator.mapping)) {
|
|
173
|
+
const targetName = ref.split('/').pop() || key;
|
|
174
|
+
mapping.set(key, targetName);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
discriminatorInfo.set(name, {
|
|
178
|
+
propertyName: resolved.discriminator.propertyName,
|
|
179
|
+
mapping,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const discriminatorTargets = new Map();
|
|
185
|
+
for (const [, info] of discriminatorInfo) {
|
|
186
|
+
for (const [mappingKey, schemaName] of info.mapping) {
|
|
187
|
+
discriminatorTargets.set(schemaName, {
|
|
188
|
+
propertyName: info.propertyName,
|
|
189
|
+
literalValue: mappingKey,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// reservedNames: prevent branded types from colliding with user-defined schema names
|
|
194
|
+
const allSchemaNames = new Set();
|
|
195
|
+
if (doc.components?.schemas) {
|
|
196
|
+
for (const name of Object.keys(doc.components.schemas)) {
|
|
197
|
+
allSchemaNames.add(name);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const mapper = new SchemaMapper(resolver, undefined, discriminatorTargets, allSchemaNames);
|
|
201
|
+
const lines = [];
|
|
202
|
+
lines.push(makeHeader(doc.openapi));
|
|
203
|
+
// Section 1: Schema types
|
|
204
|
+
const schemaEntries = [];
|
|
205
|
+
if (doc.components?.schemas) {
|
|
206
|
+
for (const [name, schema] of Object.entries(doc.components.schemas)) {
|
|
207
|
+
const result = mapper.mapSchema(schema, name);
|
|
208
|
+
const resolved = resolver.resolve(schema);
|
|
209
|
+
const definition = `export type ${name} = ${result.tsType};`;
|
|
210
|
+
schemaEntries.push({
|
|
211
|
+
name,
|
|
212
|
+
kind: 'type',
|
|
213
|
+
definition,
|
|
214
|
+
jsDoc: buildJsDoc(resolved.description),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const sortedSchemas = topologicalSort(schemaEntries, allSchemaNames);
|
|
219
|
+
for (const entry of sortedSchemas) {
|
|
220
|
+
lines.push('');
|
|
221
|
+
if (entry.jsDoc) {
|
|
222
|
+
lines.push(entry.jsDoc);
|
|
223
|
+
}
|
|
224
|
+
lines.push(entry.definition);
|
|
225
|
+
}
|
|
226
|
+
for (const [baseName, info] of discriminatorInfo) {
|
|
227
|
+
const subtypeNames = Array.from(info.mapping.values());
|
|
228
|
+
if (subtypeNames.length === 0)
|
|
229
|
+
continue;
|
|
230
|
+
const unionType = subtypeNames.join(' | ');
|
|
231
|
+
lines.push('');
|
|
232
|
+
lines.push(`export type ${baseName}Variant = ${unionType};`);
|
|
233
|
+
allSchemaNames.add(`${baseName}Variant`);
|
|
234
|
+
}
|
|
235
|
+
// Section 1b: Security scheme types
|
|
236
|
+
const securitySchemes = doc.components?.securitySchemes;
|
|
237
|
+
if (securitySchemes && Object.keys(securitySchemes).length > 0) {
|
|
238
|
+
const securityTypeNames = [];
|
|
239
|
+
for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
|
|
240
|
+
const typeName = `${toPascalCase(schemeName)}Auth`;
|
|
241
|
+
const tsType = securitySchemeToTsType(scheme);
|
|
242
|
+
if (scheme.description) {
|
|
243
|
+
lines.push('');
|
|
244
|
+
lines.push(buildJsDoc(scheme.description));
|
|
245
|
+
}
|
|
246
|
+
lines.push('');
|
|
247
|
+
lines.push(`export type ${typeName} = ${tsType};`);
|
|
248
|
+
securityTypeNames.push(typeName);
|
|
249
|
+
}
|
|
250
|
+
if (securityTypeNames.length > 1) {
|
|
251
|
+
lines.push('');
|
|
252
|
+
lines.push(`export type SecuritySchemes = ${securityTypeNames.join(' | ')};`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Section 1c: Server variable types
|
|
256
|
+
if (doc.servers) {
|
|
257
|
+
for (let serverIdx = 0; serverIdx < doc.servers.length; serverIdx++) {
|
|
258
|
+
const server = doc.servers[serverIdx];
|
|
259
|
+
if (!server.variables || Object.keys(server.variables).length === 0) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const typeName = doc.servers.length === 1 ? 'ServerParams' : `Server${serverIdx + 1}Params`;
|
|
263
|
+
const props = [];
|
|
264
|
+
for (const [varName, variable] of Object.entries(server.variables)) {
|
|
265
|
+
const sv = variable;
|
|
266
|
+
const jsDocParts = [];
|
|
267
|
+
if (sv.description) {
|
|
268
|
+
jsDocParts.push(sv.description);
|
|
269
|
+
}
|
|
270
|
+
if (sv.default !== undefined) {
|
|
271
|
+
jsDocParts.push(`@default ${sv.default}`);
|
|
272
|
+
}
|
|
273
|
+
let tsType;
|
|
274
|
+
if (sv.enum && sv.enum.length > 0) {
|
|
275
|
+
tsType = sv.enum.map((v) => `"${v}"`).join(' | ');
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
tsType = 'string';
|
|
279
|
+
}
|
|
280
|
+
const jsDoc = jsDocParts.length > 0 ? ` /** ${jsDocParts.join(' ')} */` : null;
|
|
281
|
+
if (jsDoc) {
|
|
282
|
+
props.push(jsDoc);
|
|
283
|
+
}
|
|
284
|
+
props.push(` ${varName}: ${tsType};`);
|
|
285
|
+
}
|
|
286
|
+
lines.push('');
|
|
287
|
+
if (server.url) {
|
|
288
|
+
lines.push(`/** Server: ${server.url} */`);
|
|
289
|
+
}
|
|
290
|
+
lines.push(`export interface ${typeName} {`);
|
|
291
|
+
for (const prop of props) {
|
|
292
|
+
lines.push(prop);
|
|
293
|
+
}
|
|
294
|
+
lines.push('}');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const operations = analyzePaths(doc, resolver);
|
|
298
|
+
const hasFileUpload = operations.some((op) => op.requestBody?.isMultipart);
|
|
299
|
+
if (hasFileUpload) {
|
|
300
|
+
lines.push('');
|
|
301
|
+
lines.push('export interface FileInput {');
|
|
302
|
+
lines.push(' data: Blob;');
|
|
303
|
+
lines.push(' filename: string;');
|
|
304
|
+
lines.push('}');
|
|
305
|
+
}
|
|
306
|
+
// Sections 2-4: Operation-derived types
|
|
307
|
+
for (const op of operations) {
|
|
308
|
+
const prefix = getOperationTypePrefix(op);
|
|
309
|
+
const opLines = [];
|
|
310
|
+
// Section 2: Query parameter types
|
|
311
|
+
if (op.queryParams.length > 0) {
|
|
312
|
+
const props = op.queryParams.map((param) => {
|
|
313
|
+
const paramSchema = param.schema ?? { type: 'string' };
|
|
314
|
+
const result = mapper.mapSchema(paramSchema);
|
|
315
|
+
const optional = param.required ? '' : '?';
|
|
316
|
+
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `"${param.name}"`;
|
|
317
|
+
return `${key}${optional}: ${result.tsType}`;
|
|
318
|
+
});
|
|
319
|
+
opLines.push(`export type ${prefix}Query = { ${props.join('; ')}; };`);
|
|
320
|
+
}
|
|
321
|
+
// Section 2b: Header parameter types
|
|
322
|
+
if (op.headerParams.length > 0) {
|
|
323
|
+
const props = op.headerParams.map((param) => {
|
|
324
|
+
const paramSchema = param.schema ?? { type: 'string' };
|
|
325
|
+
const result = mapper.mapSchema(paramSchema);
|
|
326
|
+
const optional = param.required ? '' : '?';
|
|
327
|
+
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `"${param.name}"`;
|
|
328
|
+
return `${key}${optional}: ${result.tsType}`;
|
|
329
|
+
});
|
|
330
|
+
opLines.push(`export type ${prefix}Headers = { ${props.join('; ')}; };`);
|
|
331
|
+
}
|
|
332
|
+
// Section 3: Request body types
|
|
333
|
+
if (op.requestBody?.isMultipart && op.requestBody.schema) {
|
|
334
|
+
const schema = resolver.resolveSchema(op.requestBody.schema);
|
|
335
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
336
|
+
const props = Object.entries(schema.properties ?? {}).map(([name, propSchema]) => {
|
|
337
|
+
const resolved = resolver.resolveSchema(propSchema);
|
|
338
|
+
const optional = requiredSet.has(name) ? '' : '?';
|
|
339
|
+
let tsType;
|
|
340
|
+
if (resolved.format === 'binary') {
|
|
341
|
+
tsType = 'FileInput';
|
|
342
|
+
}
|
|
343
|
+
else if (resolved.type === 'array' && resolved.items?.format === 'binary') {
|
|
344
|
+
tsType = 'FileInput[]';
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
tsType = 'string';
|
|
348
|
+
}
|
|
349
|
+
return `${name}${optional}: ${tsType}`;
|
|
350
|
+
});
|
|
351
|
+
if (props.length > 0) {
|
|
352
|
+
opLines.push(`export type ${prefix}Body = { ${props.join('; ')}; };`);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
opLines.push(`export type ${prefix}Body = Record<string, never>;`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else if (op.requestBody?.schema) {
|
|
359
|
+
const hasBinaryContentType = op.requestBody.contentTypes.some(isBinaryContentType);
|
|
360
|
+
if (hasBinaryContentType) {
|
|
361
|
+
opLines.push(`export type ${prefix}Body = Blob;`);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
const result = mapper.mapSchema(op.requestBody.schema, undefined, 'request');
|
|
365
|
+
opLines.push(`export type ${prefix}Body = ${result.tsType};`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Section 4: Response types
|
|
369
|
+
const successResponses = op.responses.filter((r) => r.isSuccess);
|
|
370
|
+
const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
|
|
371
|
+
const defaultResponse = op.responses.find((r) => !r.isSuccess && r.statusCode === 'default');
|
|
372
|
+
// Success type
|
|
373
|
+
if (successResponses.length > 0) {
|
|
374
|
+
const types = successResponses.map((r) => {
|
|
375
|
+
if (r.isBinary)
|
|
376
|
+
return 'StreamResponse';
|
|
377
|
+
if (r.schema) {
|
|
378
|
+
const result = mapper.mapSchema(r.schema, undefined, 'response').tsType;
|
|
379
|
+
return substituteDiscriminatedType(result, r.schema, discriminatorInfo);
|
|
380
|
+
}
|
|
381
|
+
return r.tsType;
|
|
382
|
+
});
|
|
383
|
+
const successType = types.length === 1 ? types[0] : types.join(' | ');
|
|
384
|
+
opLines.push(`export type ${prefix}Response = ${successType};`);
|
|
385
|
+
}
|
|
386
|
+
// Error types per status
|
|
387
|
+
const errorTypes = [];
|
|
388
|
+
for (const err of errorResponses) {
|
|
389
|
+
const errorTypeName = `${prefix}Error${err.statusCode}`;
|
|
390
|
+
let errorTsType;
|
|
391
|
+
if (err.isBinary) {
|
|
392
|
+
errorTsType = 'StreamResponse';
|
|
393
|
+
}
|
|
394
|
+
else if (err.schema) {
|
|
395
|
+
errorTsType = mapper.mapSchema(err.schema, undefined, 'response').tsType;
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
errorTsType = err.tsType;
|
|
399
|
+
}
|
|
400
|
+
opLines.push(`export type ${errorTypeName} = ${errorTsType};`);
|
|
401
|
+
errorTypes.push({ status: err.statusCode, typeName: errorTypeName });
|
|
402
|
+
}
|
|
403
|
+
// Default response error type
|
|
404
|
+
if (defaultResponse) {
|
|
405
|
+
const defaultTypeName = `${prefix}DefaultError`;
|
|
406
|
+
let defaultTsType;
|
|
407
|
+
if (defaultResponse.isBinary) {
|
|
408
|
+
defaultTsType = 'StreamResponse';
|
|
409
|
+
}
|
|
410
|
+
else if (defaultResponse.schema) {
|
|
411
|
+
defaultTsType = mapper.mapSchema(defaultResponse.schema, undefined, 'response').tsType;
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
defaultTsType = 'unknown';
|
|
415
|
+
}
|
|
416
|
+
opLines.push(`export type ${defaultTypeName} = ${defaultTsType};`);
|
|
417
|
+
}
|
|
418
|
+
// Error union
|
|
419
|
+
if (errorTypes.length > 0) {
|
|
420
|
+
const unionParts = errorTypes.map((e) => `ApiError<${e.status}, ${e.typeName}>`);
|
|
421
|
+
opLines.push(`export type ${prefix}Errors = ${unionParts.join(' | ')};`);
|
|
422
|
+
}
|
|
423
|
+
if (opLines.length > 0) {
|
|
424
|
+
for (const line of opLines) {
|
|
425
|
+
lines.push('');
|
|
426
|
+
lines.push(line);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Emit branded type definitions after header, before everything else
|
|
431
|
+
const brandedTypes = mapper.getBrandedTypes();
|
|
432
|
+
if (brandedTypes.size > 0) {
|
|
433
|
+
const brandLines = [];
|
|
434
|
+
for (const brand of brandedTypes.values()) {
|
|
435
|
+
brandLines.push(`export type ${brand.name} = ${brand.baseType} & { readonly __format?: '${brand.format}' };`);
|
|
436
|
+
}
|
|
437
|
+
lines.splice(1, 0, '', ...brandLines);
|
|
438
|
+
}
|
|
439
|
+
// Always emit StreamResponse class (used by Requester type)
|
|
440
|
+
lines.push('');
|
|
441
|
+
lines.push('export class StreamResponse {');
|
|
442
|
+
lines.push(' constructor(');
|
|
443
|
+
lines.push(' public readonly data: ReadableStream<Uint8Array>,');
|
|
444
|
+
lines.push(' public readonly filename?: string,');
|
|
445
|
+
lines.push(' public readonly headers: Headers = new Headers(),');
|
|
446
|
+
lines.push(' ) {}');
|
|
447
|
+
lines.push('}');
|
|
448
|
+
lines.push('');
|
|
449
|
+
lines.push(`export function streamResponse(
|
|
450
|
+
data: ReadableStream<Uint8Array>,
|
|
451
|
+
filename?: string,
|
|
452
|
+
headers?: Headers,
|
|
453
|
+
): StreamResponse {
|
|
454
|
+
return new StreamResponse(data, filename, headers ?? new Headers());
|
|
455
|
+
}`);
|
|
456
|
+
// ErrorResponse class
|
|
457
|
+
lines.push('');
|
|
458
|
+
lines.push(`export class ErrorResponse {
|
|
459
|
+
constructor(
|
|
460
|
+
public readonly status: number,
|
|
461
|
+
public readonly data: unknown,
|
|
462
|
+
public readonly headers: Headers,
|
|
463
|
+
public readonly message?: string,
|
|
464
|
+
) {}
|
|
465
|
+
}`);
|
|
466
|
+
// errorResponse() helper
|
|
467
|
+
lines.push('');
|
|
468
|
+
lines.push(`export function errorResponse(
|
|
469
|
+
status: number,
|
|
470
|
+
data: unknown,
|
|
471
|
+
headers?: Headers,
|
|
472
|
+
message?: string,
|
|
473
|
+
): ErrorResponse {
|
|
474
|
+
return new ErrorResponse(status, data, headers ?? new Headers(), message);
|
|
475
|
+
}`);
|
|
476
|
+
// RequesterFailError class
|
|
477
|
+
lines.push('');
|
|
478
|
+
lines.push(`export class RequesterFailError extends Error {
|
|
479
|
+
constructor(
|
|
480
|
+
public readonly cause: unknown,
|
|
481
|
+
) {
|
|
482
|
+
super(\`Request failed: \${cause instanceof Error ? cause.message : String(cause)}\`);
|
|
483
|
+
this.name = "RequesterFailError";
|
|
484
|
+
}
|
|
485
|
+
}`);
|
|
486
|
+
// Section 5: ApiError class
|
|
487
|
+
lines.push('');
|
|
488
|
+
lines.push(`export class ApiError<TStatus extends number, TData> extends Error {
|
|
489
|
+
constructor(
|
|
490
|
+
public readonly status: TStatus,
|
|
491
|
+
public readonly data: TData,
|
|
492
|
+
message: string,
|
|
493
|
+
) {
|
|
494
|
+
super(message);
|
|
495
|
+
this.name = "ApiError";
|
|
496
|
+
}
|
|
497
|
+
}`);
|
|
498
|
+
// Section 5b: UnspecifiedApiError class
|
|
499
|
+
lines.push('');
|
|
500
|
+
lines.push(`export class UnspecifiedApiError extends ApiError<number, unknown> {
|
|
501
|
+
constructor(
|
|
502
|
+
status: number,
|
|
503
|
+
data: unknown,
|
|
504
|
+
message: string,
|
|
505
|
+
) {
|
|
506
|
+
super(status, data, message);
|
|
507
|
+
this.name = "UnspecifiedApiError";
|
|
508
|
+
}
|
|
509
|
+
}`);
|
|
510
|
+
const needsDefaultApiError = operations.some((op) => op.responses.some((r) => !r.isSuccess && r.statusCode === 'default'));
|
|
511
|
+
if (needsDefaultApiError) {
|
|
512
|
+
lines.push('');
|
|
513
|
+
lines.push(`export class DefaultApiError<TData> extends Error {
|
|
514
|
+
constructor(
|
|
515
|
+
public readonly status: number,
|
|
516
|
+
public readonly data: TData,
|
|
517
|
+
message: string,
|
|
518
|
+
) {
|
|
519
|
+
super(message);
|
|
520
|
+
this.name = "DefaultApiError";
|
|
521
|
+
}
|
|
522
|
+
}`);
|
|
523
|
+
}
|
|
524
|
+
return lines.join('\n');
|
|
525
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AnalyzedOperation } from '../analyzer/path-analyzer.js';
|
|
2
|
+
export declare class ApiError<TStatus extends number, TData> extends Error {
|
|
3
|
+
readonly status: TStatus;
|
|
4
|
+
readonly data: TData;
|
|
5
|
+
constructor(status: TStatus, data: TData, message: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class DefaultApiError<TData> extends Error {
|
|
8
|
+
readonly status: number;
|
|
9
|
+
readonly data: TData;
|
|
10
|
+
constructor(status: number, data: TData, message: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Generate error types for a set of analyzed operations.
|
|
14
|
+
*
|
|
15
|
+
* Produces:
|
|
16
|
+
* 1. `ApiError<TStatus, TData>` class
|
|
17
|
+
* 2. `UnspecifiedApiError` class (extends ApiError, for status codes not in spec)
|
|
18
|
+
* 3. `isError` type guard function
|
|
19
|
+
* 4. Per-operation error union types (e.g. `GetApiV1ProductsErrors`)
|
|
20
|
+
* 5. Per-operation per-status error type aliases (e.g. `GetApiV1ProductsError400`)
|
|
21
|
+
* 6. `DefaultErrorBody` type for `default` responses
|
|
22
|
+
* 7. Catch-all `UnspecifiedApiError` in error unions for unexpected status codes
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateErrorTypes(operations: AnalyzedOperation[]): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
data;
|
|
4
|
+
constructor(status, data, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.data = data;
|
|
8
|
+
this.name = 'ApiError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class DefaultApiError extends Error {
|
|
12
|
+
status;
|
|
13
|
+
data;
|
|
14
|
+
constructor(status, data, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.data = data;
|
|
18
|
+
this.name = 'DefaultApiError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generate error types for a set of analyzed operations.
|
|
23
|
+
*
|
|
24
|
+
* Produces:
|
|
25
|
+
* 1. `ApiError<TStatus, TData>` class
|
|
26
|
+
* 2. `UnspecifiedApiError` class (extends ApiError, for status codes not in spec)
|
|
27
|
+
* 3. `isError` type guard function
|
|
28
|
+
* 4. Per-operation error union types (e.g. `GetApiV1ProductsErrors`)
|
|
29
|
+
* 5. Per-operation per-status error type aliases (e.g. `GetApiV1ProductsError400`)
|
|
30
|
+
* 6. `DefaultErrorBody` type for `default` responses
|
|
31
|
+
* 7. Catch-all `UnspecifiedApiError` in error unions for unexpected status codes
|
|
32
|
+
*/
|
|
33
|
+
export function generateErrorTypes(operations) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
lines.push(`export class ApiError<TStatus extends number, TData> extends Error {
|
|
36
|
+
constructor(
|
|
37
|
+
public readonly status: TStatus,
|
|
38
|
+
public readonly data: TData,
|
|
39
|
+
message: string,
|
|
40
|
+
) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "ApiError";
|
|
43
|
+
}
|
|
44
|
+
}`);
|
|
45
|
+
lines.push('');
|
|
46
|
+
lines.push(`export class UnspecifiedApiError extends ApiError<number, unknown> {
|
|
47
|
+
constructor(
|
|
48
|
+
status: number,
|
|
49
|
+
data: unknown,
|
|
50
|
+
message: string,
|
|
51
|
+
) {
|
|
52
|
+
super(status, data, message);
|
|
53
|
+
this.name = "UnspecifiedApiError";
|
|
54
|
+
}
|
|
55
|
+
}`);
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push(`export function isError<T extends { status: number }, S extends number>(
|
|
58
|
+
response: T,
|
|
59
|
+
status: S,
|
|
60
|
+
): response is Extract<T, { status: S }> {
|
|
61
|
+
return response.status === status;
|
|
62
|
+
}`);
|
|
63
|
+
let needsDefaultErrorBody = false;
|
|
64
|
+
for (const op of operations) {
|
|
65
|
+
const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
|
|
66
|
+
const defaultResponse = op.responses.find((r) => !r.isSuccess && r.statusCode === 'default');
|
|
67
|
+
if (errorResponses.length === 0 && !defaultResponse) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const methodName = op.methodName;
|
|
71
|
+
const errorTypeName = (status) => `${methodName}Error${status}`;
|
|
72
|
+
lines.push('');
|
|
73
|
+
for (const err of errorResponses) {
|
|
74
|
+
const tsType = err.tsType || 'unknown';
|
|
75
|
+
lines.push(`export type ${errorTypeName(err.statusCode)} = ${tsType};`);
|
|
76
|
+
}
|
|
77
|
+
const unionParts = [];
|
|
78
|
+
for (const err of errorResponses) {
|
|
79
|
+
const status = Number(err.statusCode);
|
|
80
|
+
unionParts.push(`ApiError<${status}, ${errorTypeName(err.statusCode)}>`);
|
|
81
|
+
}
|
|
82
|
+
if (defaultResponse) {
|
|
83
|
+
needsDefaultErrorBody = true;
|
|
84
|
+
unionParts.push(`ApiError<number, DefaultErrorBody>`);
|
|
85
|
+
}
|
|
86
|
+
unionParts.push(`UnspecifiedApiError`);
|
|
87
|
+
lines.push(`export type ${methodName}Errors = ${unionParts.join('\n | ')};`);
|
|
88
|
+
}
|
|
89
|
+
if (needsDefaultErrorBody) {
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('export type DefaultErrorBody = unknown;');
|
|
92
|
+
}
|
|
93
|
+
return lines.join('\n');
|
|
94
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnalyzedOperation } from '../analyzer/path-analyzer.js';
|
|
2
|
+
import type { GeneratedMethod } from '../types/client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generate a client method from an analyzed OpenAPI operation.
|
|
5
|
+
*
|
|
6
|
+
* @param op - The analyzed operation
|
|
7
|
+
* @returns Generated method with name, JSDoc, signature, and implementation
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateMethod(op: AnalyzedOperation): GeneratedMethod;
|