genoc 0.1.2 → 0.2.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 +56 -26
- package/dist/analyzer/path-analyzer.d.ts +2 -0
- package/dist/analyzer/path-analyzer.js +4 -1
- package/dist/analyzer/schema-mapper.d.ts +8 -0
- package/dist/analyzer/schema-mapper.js +67 -30
- package/dist/cli/app.d.ts +1 -0
- package/dist/cli/app.js +7 -0
- package/dist/cli/impl.js +1 -0
- package/dist/generator/client-generator.js +9 -44
- package/dist/generator/contracts-generator.d.ts +2 -3
- package/dist/generator/contracts-generator.js +151 -141
- package/dist/generator/error-types.d.ts +4 -15
- package/dist/generator/error-types.js +4 -31
- package/dist/generator/method-generator.js +11 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/runtime/defined-errors.d.ts +17 -0
- package/dist/runtime/defined-errors.js +27 -0
- package/dist/runtime/errors.d.ts +28 -0
- package/dist/runtime/errors.js +45 -0
- package/dist/runtime/index.d.ts +11 -0
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/requester.d.ts +14 -0
- package/dist/runtime/requester.js +1 -0
- package/dist/runtime/responses.d.ts +19 -0
- package/dist/runtime/responses.js +28 -0
- package/dist/types/client.d.ts +1 -0
- package/dist/types/openapi.d.ts +4 -0
- package/dist/utils/generator-helpers.d.ts +52 -0
- package/dist/utils/generator-helpers.js +126 -0
- package/package.json +6 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { analyzePaths } from '../analyzer/path-analyzer.js';
|
|
2
2
|
import { SchemaMapper } from '../analyzer/schema-mapper.js';
|
|
3
3
|
import { RefResolver } from '../parser/ref-resolver.js';
|
|
4
|
-
import { toPascalCase, getOperationTypePrefix, makeHeader } from '../utils/generator-helpers.js';
|
|
4
|
+
import { RESERVED_TYPE_NAMES, buildSchemaRenameMap, DEFAULT_RUNTIME_IMPORT_PATH, buildTypeJsDoc, sanitizeJsDocText, sanitizeTypeName, toPascalCase, getOperationTypePrefix, makeHeader, } from '../utils/generator-helpers.js';
|
|
5
5
|
function isBinaryContentType(ct) {
|
|
6
6
|
if (ct === 'application/octet-stream')
|
|
7
7
|
return true;
|
|
@@ -17,31 +17,91 @@ function isBinaryContentType(ct) {
|
|
|
17
17
|
* If the schema is a $ref to a discriminated base type (or an array whose items
|
|
18
18
|
* are), replace the type name with the {Base}Variant union type.
|
|
19
19
|
*/
|
|
20
|
-
function substituteDiscriminatedType(tsType, schema, discriminatorInfo) {
|
|
20
|
+
function substituteDiscriminatedType(tsType, schema, discriminatorInfo, renameMap) {
|
|
21
21
|
const refSchema = schema;
|
|
22
22
|
if (!refSchema || typeof refSchema !== 'object')
|
|
23
23
|
return tsType;
|
|
24
24
|
if (typeof refSchema.$ref === 'string') {
|
|
25
|
-
const schemaName = refSchema.$ref.split('/').pop();
|
|
25
|
+
const schemaName = sanitizeTypeName(refSchema.$ref.split('/').pop());
|
|
26
26
|
if (schemaName && discriminatorInfo.has(schemaName)) {
|
|
27
|
-
|
|
27
|
+
const renamed = renameMap.get(schemaName) ?? schemaName;
|
|
28
|
+
return tsType.replace(new RegExp(`\\b${renamed}\\b`, 'g'), `${renamed}Variant`);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
if (refSchema.items && typeof refSchema.items === 'object') {
|
|
31
32
|
const items = refSchema.items;
|
|
32
33
|
if (typeof items.$ref === 'string') {
|
|
33
|
-
const schemaName = items.$ref.split('/').pop();
|
|
34
|
+
const schemaName = sanitizeTypeName(items.$ref.split('/').pop());
|
|
34
35
|
if (schemaName && discriminatorInfo.has(schemaName)) {
|
|
35
|
-
|
|
36
|
+
const renamed = renameMap.get(schemaName) ?? schemaName;
|
|
37
|
+
return tsType.replace(new RegExp(`\\b${renamed}\\b`, 'g'), `${renamed}Variant`);
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
return tsType;
|
|
40
42
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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}`;
|
|
45
105
|
}
|
|
46
106
|
function securitySchemeToTsType(scheme) {
|
|
47
107
|
const parts = [`type: "${scheme.type}"`];
|
|
@@ -148,6 +208,25 @@ function topologicalSort(entries, allNames) {
|
|
|
148
208
|
}
|
|
149
209
|
return sorted;
|
|
150
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Build the import + re-export block for the shared runtime classes.
|
|
213
|
+
* Generated code re-uses the genoc runtime as the single source of truth
|
|
214
|
+
* for class identity, so `instanceof` checks work across module boundaries.
|
|
215
|
+
*/
|
|
216
|
+
function buildRuntimeReexport(runtimeImportPath) {
|
|
217
|
+
return [
|
|
218
|
+
'',
|
|
219
|
+
`import { ApiError, StreamResponse } from '${runtimeImportPath}';`,
|
|
220
|
+
'export {',
|
|
221
|
+
' ApiError,',
|
|
222
|
+
' UnspecifiedApiError,',
|
|
223
|
+
' DefaultApiError,',
|
|
224
|
+
' RequesterFailError,',
|
|
225
|
+
' StreamResponse,',
|
|
226
|
+
' ErrorResponse,',
|
|
227
|
+
`} from '${runtimeImportPath}';`,
|
|
228
|
+
];
|
|
229
|
+
}
|
|
151
230
|
/**
|
|
152
231
|
* Generate the complete `*.contracts.ts` file content as a string.
|
|
153
232
|
*
|
|
@@ -158,10 +237,22 @@ function topologicalSort(entries, allNames) {
|
|
|
158
237
|
* 4. Header parameter types per operation
|
|
159
238
|
* 5. Request body types per operation
|
|
160
239
|
* 6. Response / error types per operation
|
|
161
|
-
* 7.
|
|
162
|
-
* 7b. UnspecifiedApiError class
|
|
240
|
+
* 7. Runtime re-export block (shared classes from the genoc runtime package)
|
|
163
241
|
*/
|
|
164
|
-
export function generateContracts(doc, resolver) {
|
|
242
|
+
export function generateContracts(doc, resolver, runtimeImportPath = DEFAULT_RUNTIME_IMPORT_PATH) {
|
|
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
|
+
};
|
|
165
256
|
const discriminatorInfo = new Map();
|
|
166
257
|
if (doc.components?.schemas) {
|
|
167
258
|
for (const [name, schema] of Object.entries(doc.components.schemas)) {
|
|
@@ -170,11 +261,12 @@ export function generateContracts(doc, resolver) {
|
|
|
170
261
|
const mapping = new Map();
|
|
171
262
|
if (resolved.discriminator.mapping) {
|
|
172
263
|
for (const [key, ref] of Object.entries(resolved.discriminator.mapping)) {
|
|
173
|
-
const targetName = ref.split('/').pop() || key;
|
|
174
|
-
|
|
264
|
+
const targetName = sanitizeTypeName(ref.split('/').pop() || key);
|
|
265
|
+
const renamedTarget = renameMap.get(targetName) ?? targetName;
|
|
266
|
+
mapping.set(key, renamedTarget);
|
|
175
267
|
}
|
|
176
268
|
}
|
|
177
|
-
discriminatorInfo.set(name, {
|
|
269
|
+
discriminatorInfo.set(sanitizeTypeName(name), {
|
|
178
270
|
propertyName: resolved.discriminator.propertyName,
|
|
179
271
|
mapping,
|
|
180
272
|
});
|
|
@@ -190,28 +282,31 @@ export function generateContracts(doc, resolver) {
|
|
|
190
282
|
});
|
|
191
283
|
}
|
|
192
284
|
}
|
|
193
|
-
// reservedNames: prevent branded types from colliding with user-defined schema names
|
|
194
285
|
const allSchemaNames = new Set();
|
|
195
286
|
if (doc.components?.schemas) {
|
|
196
287
|
for (const name of Object.keys(doc.components.schemas)) {
|
|
197
|
-
allSchemaNames.add(name);
|
|
288
|
+
allSchemaNames.add(sanitizeTypeName(name));
|
|
198
289
|
}
|
|
199
290
|
}
|
|
200
|
-
const mapper = new SchemaMapper(resolver,
|
|
291
|
+
const mapper = new SchemaMapper(resolver, renamingTypeGenerator, discriminatorTargets, allSchemaNames);
|
|
201
292
|
const lines = [];
|
|
202
293
|
lines.push(makeHeader(doc.openapi));
|
|
294
|
+
const runtimeReexport = buildRuntimeReexport(runtimeImportPath);
|
|
295
|
+
lines.push(...runtimeReexport);
|
|
203
296
|
// Section 1: Schema types
|
|
204
297
|
const schemaEntries = [];
|
|
205
298
|
if (doc.components?.schemas) {
|
|
206
299
|
for (const [name, schema] of Object.entries(doc.components.schemas)) {
|
|
207
|
-
const
|
|
300
|
+
const sanitizedName = sanitizeTypeName(name);
|
|
301
|
+
const renamedName = renameMap.get(sanitizedName) ?? sanitizedName;
|
|
302
|
+
const result = mapper.mapSchema(schema, sanitizedName);
|
|
208
303
|
const resolved = resolver.resolve(schema);
|
|
209
|
-
const definition = `export type ${
|
|
304
|
+
const definition = `export type ${renamedName} = ${result.tsType};`;
|
|
210
305
|
schemaEntries.push({
|
|
211
|
-
name,
|
|
306
|
+
name: renamedName,
|
|
212
307
|
kind: 'type',
|
|
213
308
|
definition,
|
|
214
|
-
jsDoc:
|
|
309
|
+
jsDoc: buildTypeJsDoc(resolved) || undefined,
|
|
215
310
|
});
|
|
216
311
|
}
|
|
217
312
|
}
|
|
@@ -228,9 +323,10 @@ export function generateContracts(doc, resolver) {
|
|
|
228
323
|
if (subtypeNames.length === 0)
|
|
229
324
|
continue;
|
|
230
325
|
const unionType = subtypeNames.join(' | ');
|
|
326
|
+
const renamedBase = renameMap.get(sanitizeTypeName(baseName)) ?? sanitizeTypeName(baseName);
|
|
231
327
|
lines.push('');
|
|
232
|
-
lines.push(`export type ${
|
|
233
|
-
allSchemaNames.add(`${
|
|
328
|
+
lines.push(`export type ${renamedBase}Variant = ${unionType};`);
|
|
329
|
+
allSchemaNames.add(`${renamedBase}Variant`);
|
|
234
330
|
}
|
|
235
331
|
// Section 1b: Security scheme types
|
|
236
332
|
const securitySchemes = doc.components?.securitySchemes;
|
|
@@ -239,9 +335,10 @@ export function generateContracts(doc, resolver) {
|
|
|
239
335
|
for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
|
|
240
336
|
const typeName = `${toPascalCase(schemeName)}Auth`;
|
|
241
337
|
const tsType = securitySchemeToTsType(scheme);
|
|
242
|
-
|
|
338
|
+
const schemeJsDoc = buildDescriptionJsDoc(scheme.description);
|
|
339
|
+
if (schemeJsDoc !== '') {
|
|
243
340
|
lines.push('');
|
|
244
|
-
lines.push(
|
|
341
|
+
lines.push(schemeJsDoc);
|
|
245
342
|
}
|
|
246
343
|
lines.push('');
|
|
247
344
|
lines.push(`export type ${typeName} = ${tsType};`);
|
|
@@ -265,10 +362,13 @@ export function generateContracts(doc, resolver) {
|
|
|
265
362
|
const sv = variable;
|
|
266
363
|
const jsDocParts = [];
|
|
267
364
|
if (sv.description) {
|
|
268
|
-
|
|
365
|
+
const description = sanitizeJsDocText(sv.description);
|
|
366
|
+
if (description !== '') {
|
|
367
|
+
jsDocParts.push(description);
|
|
368
|
+
}
|
|
269
369
|
}
|
|
270
370
|
if (sv.default !== undefined) {
|
|
271
|
-
jsDocParts.push(`@default ${sv.default}`);
|
|
371
|
+
jsDocParts.push(`@default ${sanitizeJsDocText(String(sv.default))}`);
|
|
272
372
|
}
|
|
273
373
|
let tsType;
|
|
274
374
|
if (sv.enum && sv.enum.length > 0) {
|
|
@@ -309,31 +409,19 @@ export function generateContracts(doc, resolver) {
|
|
|
309
409
|
const opLines = [];
|
|
310
410
|
// Section 2: Query parameter types
|
|
311
411
|
if (op.queryParams.length > 0) {
|
|
312
|
-
|
|
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('; ')}; };`);
|
|
412
|
+
opLines.push(`export type ${prefix}Query = ${buildParamTypeBody(op.queryParams, mapper)};`);
|
|
320
413
|
}
|
|
321
414
|
// Section 2b: Header parameter types
|
|
322
415
|
if (op.headerParams.length > 0) {
|
|
323
|
-
|
|
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('; ')}; };`);
|
|
416
|
+
opLines.push(`export type ${prefix}Headers = ${buildParamTypeBody(op.headerParams, mapper)};`);
|
|
331
417
|
}
|
|
332
418
|
// Section 3: Request body types
|
|
419
|
+
const bodyJsDoc = op.requestBody ? buildDescriptionJsDoc(op.requestBody.description) : '';
|
|
333
420
|
if (op.requestBody?.isMultipart && op.requestBody.schema) {
|
|
334
421
|
const schema = resolver.resolveSchema(op.requestBody.schema);
|
|
335
422
|
const requiredSet = new Set(schema.required ?? []);
|
|
336
|
-
const
|
|
423
|
+
const propLines = [];
|
|
424
|
+
for (const [name, propSchema] of Object.entries(schema.properties ?? {})) {
|
|
337
425
|
const resolved = resolver.resolveSchema(propSchema);
|
|
338
426
|
const optional = requiredSet.has(name) ? '' : '?';
|
|
339
427
|
let tsType;
|
|
@@ -346,23 +434,27 @@ export function generateContracts(doc, resolver) {
|
|
|
346
434
|
else {
|
|
347
435
|
tsType = 'string';
|
|
348
436
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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};`));
|
|
353
445
|
}
|
|
354
446
|
else {
|
|
355
|
-
opLines.push(`export type ${prefix}Body = Record<string, never>;`);
|
|
447
|
+
opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${prefix}Body = Record<string, never>;`));
|
|
356
448
|
}
|
|
357
449
|
}
|
|
358
450
|
else if (op.requestBody?.schema) {
|
|
359
451
|
const hasBinaryContentType = op.requestBody.contentTypes.some(isBinaryContentType);
|
|
360
452
|
if (hasBinaryContentType) {
|
|
361
|
-
opLines.push(`export type ${prefix}Body = Blob;`);
|
|
453
|
+
opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${prefix}Body = Blob;`));
|
|
362
454
|
}
|
|
363
455
|
else {
|
|
364
456
|
const result = mapper.mapSchema(op.requestBody.schema, undefined, 'request');
|
|
365
|
-
opLines.push(`export type ${prefix}Body = ${result.tsType};`);
|
|
457
|
+
opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${prefix}Body = ${result.tsType};`));
|
|
366
458
|
}
|
|
367
459
|
}
|
|
368
460
|
// Section 4: Response types
|
|
@@ -376,12 +468,15 @@ export function generateContracts(doc, resolver) {
|
|
|
376
468
|
return 'StreamResponse';
|
|
377
469
|
if (r.schema) {
|
|
378
470
|
const result = mapper.mapSchema(r.schema, undefined, 'response').tsType;
|
|
379
|
-
return substituteDiscriminatedType(result, r.schema, discriminatorInfo);
|
|
471
|
+
return substituteDiscriminatedType(result, r.schema, discriminatorInfo, renameMap);
|
|
380
472
|
}
|
|
381
473
|
return r.tsType;
|
|
382
474
|
});
|
|
383
475
|
const successType = types.length === 1 ? types[0] : types.join(' | ');
|
|
384
|
-
|
|
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};`));
|
|
385
480
|
}
|
|
386
481
|
// Error types per status
|
|
387
482
|
const errorTypes = [];
|
|
@@ -434,92 +529,7 @@ export function generateContracts(doc, resolver) {
|
|
|
434
529
|
for (const brand of brandedTypes.values()) {
|
|
435
530
|
brandLines.push(`export type ${brand.name} = ${brand.baseType} & { readonly __format?: '${brand.format}' };`);
|
|
436
531
|
}
|
|
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: Record<string, string> = {},');
|
|
446
|
-
lines.push(' ) {}');
|
|
447
|
-
lines.push('}');
|
|
448
|
-
lines.push('');
|
|
449
|
-
lines.push(`export function streamResponse(
|
|
450
|
-
data: ReadableStream<Uint8Array>,
|
|
451
|
-
filename?: string,
|
|
452
|
-
headers?: Record<string, string>,
|
|
453
|
-
): StreamResponse {
|
|
454
|
-
return new StreamResponse(data, filename, 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: Record<string, string>,
|
|
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?: Record<string, string>,
|
|
472
|
-
message?: string,
|
|
473
|
-
): ErrorResponse {
|
|
474
|
-
return new ErrorResponse(status, data, 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
|
-
}`);
|
|
532
|
+
lines.splice(1 + runtimeReexport.length, 0, '', ...brandLines);
|
|
523
533
|
}
|
|
524
534
|
return lines.join('\n');
|
|
525
535
|
}
|
|
@@ -1,24 +1,13 @@
|
|
|
1
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
2
|
/**
|
|
13
3
|
* Generate error types for a set of analyzed operations.
|
|
14
4
|
*
|
|
15
5
|
* Produces:
|
|
16
6
|
* 1. `ApiError<TStatus, TData>` class
|
|
17
7
|
* 2. `UnspecifiedApiError` class (extends ApiError, for status codes not in spec)
|
|
18
|
-
* 3.
|
|
19
|
-
* 4. Per-operation error
|
|
20
|
-
* 5.
|
|
21
|
-
* 6. `
|
|
22
|
-
* 7. Catch-all `UnspecifiedApiError` in error unions for unexpected status codes
|
|
8
|
+
* 3. Per-operation error union types (e.g. `GetApiV1ProductsErrors`)
|
|
9
|
+
* 4. Per-operation per-status error type aliases (e.g. `GetApiV1ProductsError400`)
|
|
10
|
+
* 5. `DefaultErrorBody` type for `default` responses
|
|
11
|
+
* 6. Catch-all `UnspecifiedApiError` in error unions for unexpected status codes
|
|
23
12
|
*/
|
|
24
13
|
export declare function generateErrorTypes(operations: AnalyzedOperation[]): string;
|
|
@@ -1,34 +1,13 @@
|
|
|
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
1
|
/**
|
|
22
2
|
* Generate error types for a set of analyzed operations.
|
|
23
3
|
*
|
|
24
4
|
* Produces:
|
|
25
5
|
* 1. `ApiError<TStatus, TData>` class
|
|
26
6
|
* 2. `UnspecifiedApiError` class (extends ApiError, for status codes not in spec)
|
|
27
|
-
* 3.
|
|
28
|
-
* 4. Per-operation error
|
|
29
|
-
* 5.
|
|
30
|
-
* 6. `
|
|
31
|
-
* 7. Catch-all `UnspecifiedApiError` in error unions for unexpected status codes
|
|
7
|
+
* 3. Per-operation error union types (e.g. `GetApiV1ProductsErrors`)
|
|
8
|
+
* 4. Per-operation per-status error type aliases (e.g. `GetApiV1ProductsError400`)
|
|
9
|
+
* 5. `DefaultErrorBody` type for `default` responses
|
|
10
|
+
* 6. Catch-all `UnspecifiedApiError` in error unions for unexpected status codes
|
|
32
11
|
*/
|
|
33
12
|
export function generateErrorTypes(operations) {
|
|
34
13
|
const lines = [];
|
|
@@ -54,12 +33,6 @@ export function generateErrorTypes(operations) {
|
|
|
54
33
|
}
|
|
55
34
|
}`);
|
|
56
35
|
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
36
|
let needsDefaultErrorBody = false;
|
|
64
37
|
for (const op of operations) {
|
|
65
38
|
const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getOperationTypePrefix, getSuccessType } from '../utils/generator-helpers.js';
|
|
1
|
+
import { getOperationTypePrefix, getSuccessType, sanitizeJsDocText, } from '../utils/generator-helpers.js';
|
|
2
2
|
function buildParameters(op) {
|
|
3
3
|
const params = [];
|
|
4
4
|
for (const param of op.pathParams) {
|
|
@@ -42,6 +42,16 @@ function buildJsDoc(op) {
|
|
|
42
42
|
}
|
|
43
43
|
lines.push(` * ${op.description}`);
|
|
44
44
|
}
|
|
45
|
+
// Responses iterate ascending-numeric with 'default' last (Object.entries
|
|
46
|
+
// integer-key semantics in path-analyzer), so the first success is the
|
|
47
|
+
// lowest-numbered 2xx. No fallback to later 2xx when it is undescribed.
|
|
48
|
+
const firstSuccess = op.responses.find((response) => response.isSuccess);
|
|
49
|
+
if (firstSuccess?.description !== undefined && firstSuccess.description.trim() !== '') {
|
|
50
|
+
if (lines.length > 0) {
|
|
51
|
+
lines.push(' *');
|
|
52
|
+
}
|
|
53
|
+
lines.push(` * ${sanitizeJsDocText(firstSuccess.description)}`);
|
|
54
|
+
}
|
|
45
55
|
const allParams = [...op.pathParams, ...op.queryParams, ...op.headerParams, ...op.cookieParams];
|
|
46
56
|
const paramsWithDescriptions = allParams.filter((param) => param.description);
|
|
47
57
|
if (paramsWithDescriptions.length > 0) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type ApiClient } from './generator/client-generator.js';
|
|
2
|
-
import { ApiError, DefaultApiError } from './generator/error-types.js';
|
|
3
2
|
import type { GeneratorConfig } from './types/client.js';
|
|
4
3
|
export declare function generateClient(config: GeneratorConfig): Promise<void>;
|
|
5
|
-
export
|
|
4
|
+
export { ApiError, UnspecifiedApiError, DefaultApiError, RequesterFailError, StreamResponse, ErrorResponse, decorateWithErrors, isDefinedError, } from './runtime/index.js';
|
|
5
|
+
export type { Requester } from './runtime/index.js';
|
|
6
|
+
export type { GeneratorConfig, ApiClient };
|
|
6
7
|
export type { GenerationOptions } from './generator/client-generator.js';
|
|
7
8
|
export { load as loadSpec } from './parser/spec-reader.js';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { generateFullOutput } from './generator/client-generator.js';
|
|
2
|
-
import { ApiError, DefaultApiError } from './generator/error-types.js';
|
|
3
2
|
import { load } from './parser/spec-reader.js';
|
|
4
3
|
export async function generateClient(config) {
|
|
5
4
|
const doc = await load(config.input);
|
|
6
5
|
await generateFullOutput(doc, config);
|
|
7
6
|
}
|
|
7
|
+
export { ApiError, UnspecifiedApiError, DefaultApiError, RequesterFailError, StreamResponse, ErrorResponse, decorateWithErrors, isDefinedError, } from './runtime/index.js';
|
|
8
8
|
export { load as loadSpec } from './parser/spec-reader.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ApiError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Attaches the `__definedErrors` metadata property to a client method.
|
|
4
|
+
* The property carries the method's error codes and is used by
|
|
5
|
+
* `isDefinedError` for type narrowing.
|
|
6
|
+
*/
|
|
7
|
+
export declare function decorateWithErrors<T, E>(item: T, runtimeErrors: unknown): T & {
|
|
8
|
+
__definedErrors: E;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Type guard that narrows a caught error to a method's defined error union.
|
|
12
|
+
* Returns false for `UnspecifiedApiError` (status not declared in the spec)
|
|
13
|
+
* and for anything that is not an `ApiError`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isDefinedError<E extends ApiError<number, unknown>>(err: unknown, _fn: {
|
|
16
|
+
__definedErrors: E;
|
|
17
|
+
}): err is E;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ApiError, UnspecifiedApiError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Attaches the `__definedErrors` metadata property to a client method.
|
|
4
|
+
* The property carries the method's error codes and is used by
|
|
5
|
+
* `isDefinedError` for type narrowing.
|
|
6
|
+
*/
|
|
7
|
+
export function decorateWithErrors(item, runtimeErrors) {
|
|
8
|
+
Object.defineProperty(item, '__definedErrors', {
|
|
9
|
+
value: runtimeErrors,
|
|
10
|
+
enumerable: false,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: false,
|
|
13
|
+
});
|
|
14
|
+
return item;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Type guard that narrows a caught error to a method's defined error union.
|
|
18
|
+
* Returns false for `UnspecifiedApiError` (status not declared in the spec)
|
|
19
|
+
* and for anything that is not an `ApiError`.
|
|
20
|
+
*/
|
|
21
|
+
export function isDefinedError(err, _fn) {
|
|
22
|
+
if (err instanceof UnspecifiedApiError)
|
|
23
|
+
return false;
|
|
24
|
+
if (!(err instanceof ApiError))
|
|
25
|
+
return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error classes for generated clients.
|
|
3
|
+
*
|
|
4
|
+
* This module is the single source of truth for error class identity:
|
|
5
|
+
* both generated client code and user-provided `Requester` implementations
|
|
6
|
+
* import these classes, so `instanceof` checks work across module boundaries.
|
|
7
|
+
*/
|
|
8
|
+
/** Error thrown for a specific status code declared in the spec. */
|
|
9
|
+
export declare class ApiError<TStatus extends number, TData> extends Error {
|
|
10
|
+
readonly status: TStatus;
|
|
11
|
+
readonly data: TData;
|
|
12
|
+
constructor(status: TStatus, data: TData, message: string);
|
|
13
|
+
}
|
|
14
|
+
/** Error thrown for a status code not declared in the spec. */
|
|
15
|
+
export declare class UnspecifiedApiError extends ApiError<number, unknown> {
|
|
16
|
+
constructor(status: number, data: unknown, message: string);
|
|
17
|
+
}
|
|
18
|
+
/** Error thrown for operations that declare a `default` response. */
|
|
19
|
+
export declare class DefaultApiError<TData> extends Error {
|
|
20
|
+
readonly status: number;
|
|
21
|
+
readonly data: TData;
|
|
22
|
+
constructor(status: number, data: TData, message: string);
|
|
23
|
+
}
|
|
24
|
+
/** Wraps unexpected failures inside a `Requester` implementation. */
|
|
25
|
+
export declare class RequesterFailError extends Error {
|
|
26
|
+
readonly cause: unknown;
|
|
27
|
+
constructor(cause: unknown);
|
|
28
|
+
}
|