genoc 0.1.3 → 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 +2 -0
- package/dist/analyzer/schema-mapper.d.ts +8 -0
- package/dist/analyzer/schema-mapper.js +64 -29
- 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 +116 -124
- package/dist/generator/error-types.d.ts +0 -10
- package/dist/generator/error-types.js +0 -20
- 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 +34 -0
- package/dist/utils/generator-helpers.js +77 -3
- 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 { RESERVED_TYPE_NAMES, buildSchemaRenameMap, sanitizeTypeName, 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;
|
|
@@ -40,10 +40,68 @@ function substituteDiscriminatedType(tsType, schema, discriminatorInfo, renameMa
|
|
|
40
40
|
}
|
|
41
41
|
return tsType;
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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}`;
|
|
47
105
|
}
|
|
48
106
|
function securitySchemeToTsType(scheme) {
|
|
49
107
|
const parts = [`type: "${scheme.type}"`];
|
|
@@ -150,6 +208,25 @@ function topologicalSort(entries, allNames) {
|
|
|
150
208
|
}
|
|
151
209
|
return sorted;
|
|
152
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
|
+
}
|
|
153
230
|
/**
|
|
154
231
|
* Generate the complete `*.contracts.ts` file content as a string.
|
|
155
232
|
*
|
|
@@ -160,10 +237,9 @@ function topologicalSort(entries, allNames) {
|
|
|
160
237
|
* 4. Header parameter types per operation
|
|
161
238
|
* 5. Request body types per operation
|
|
162
239
|
* 6. Response / error types per operation
|
|
163
|
-
* 7.
|
|
164
|
-
* 7b. UnspecifiedApiError class
|
|
240
|
+
* 7. Runtime re-export block (shared classes from the genoc runtime package)
|
|
165
241
|
*/
|
|
166
|
-
export function generateContracts(doc, resolver) {
|
|
242
|
+
export function generateContracts(doc, resolver, runtimeImportPath = DEFAULT_RUNTIME_IMPORT_PATH) {
|
|
167
243
|
const schemaNameList = doc.components?.schemas ? Object.keys(doc.components.schemas) : [];
|
|
168
244
|
const renameMap = buildSchemaRenameMap(schemaNameList, RESERVED_TYPE_NAMES);
|
|
169
245
|
for (const [original, renamed] of renameMap) {
|
|
@@ -215,6 +291,8 @@ export function generateContracts(doc, resolver) {
|
|
|
215
291
|
const mapper = new SchemaMapper(resolver, renamingTypeGenerator, discriminatorTargets, allSchemaNames);
|
|
216
292
|
const lines = [];
|
|
217
293
|
lines.push(makeHeader(doc.openapi));
|
|
294
|
+
const runtimeReexport = buildRuntimeReexport(runtimeImportPath);
|
|
295
|
+
lines.push(...runtimeReexport);
|
|
218
296
|
// Section 1: Schema types
|
|
219
297
|
const schemaEntries = [];
|
|
220
298
|
if (doc.components?.schemas) {
|
|
@@ -228,7 +306,7 @@ export function generateContracts(doc, resolver) {
|
|
|
228
306
|
name: renamedName,
|
|
229
307
|
kind: 'type',
|
|
230
308
|
definition,
|
|
231
|
-
jsDoc:
|
|
309
|
+
jsDoc: buildTypeJsDoc(resolved) || undefined,
|
|
232
310
|
});
|
|
233
311
|
}
|
|
234
312
|
}
|
|
@@ -257,9 +335,10 @@ export function generateContracts(doc, resolver) {
|
|
|
257
335
|
for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
|
|
258
336
|
const typeName = `${toPascalCase(schemeName)}Auth`;
|
|
259
337
|
const tsType = securitySchemeToTsType(scheme);
|
|
260
|
-
|
|
338
|
+
const schemeJsDoc = buildDescriptionJsDoc(scheme.description);
|
|
339
|
+
if (schemeJsDoc !== '') {
|
|
261
340
|
lines.push('');
|
|
262
|
-
lines.push(
|
|
341
|
+
lines.push(schemeJsDoc);
|
|
263
342
|
}
|
|
264
343
|
lines.push('');
|
|
265
344
|
lines.push(`export type ${typeName} = ${tsType};`);
|
|
@@ -283,10 +362,13 @@ export function generateContracts(doc, resolver) {
|
|
|
283
362
|
const sv = variable;
|
|
284
363
|
const jsDocParts = [];
|
|
285
364
|
if (sv.description) {
|
|
286
|
-
|
|
365
|
+
const description = sanitizeJsDocText(sv.description);
|
|
366
|
+
if (description !== '') {
|
|
367
|
+
jsDocParts.push(description);
|
|
368
|
+
}
|
|
287
369
|
}
|
|
288
370
|
if (sv.default !== undefined) {
|
|
289
|
-
jsDocParts.push(`@default ${sv.default}`);
|
|
371
|
+
jsDocParts.push(`@default ${sanitizeJsDocText(String(sv.default))}`);
|
|
290
372
|
}
|
|
291
373
|
let tsType;
|
|
292
374
|
if (sv.enum && sv.enum.length > 0) {
|
|
@@ -327,31 +409,19 @@ export function generateContracts(doc, resolver) {
|
|
|
327
409
|
const opLines = [];
|
|
328
410
|
// Section 2: Query parameter types
|
|
329
411
|
if (op.queryParams.length > 0) {
|
|
330
|
-
|
|
331
|
-
const paramSchema = param.schema ?? { type: 'string' };
|
|
332
|
-
const result = mapper.mapSchema(paramSchema);
|
|
333
|
-
const optional = param.required ? '' : '?';
|
|
334
|
-
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `"${param.name}"`;
|
|
335
|
-
return `${key}${optional}: ${result.tsType}`;
|
|
336
|
-
});
|
|
337
|
-
opLines.push(`export type ${prefix}Query = { ${props.join('; ')}; };`);
|
|
412
|
+
opLines.push(`export type ${prefix}Query = ${buildParamTypeBody(op.queryParams, mapper)};`);
|
|
338
413
|
}
|
|
339
414
|
// Section 2b: Header parameter types
|
|
340
415
|
if (op.headerParams.length > 0) {
|
|
341
|
-
|
|
342
|
-
const paramSchema = param.schema ?? { type: 'string' };
|
|
343
|
-
const result = mapper.mapSchema(paramSchema);
|
|
344
|
-
const optional = param.required ? '' : '?';
|
|
345
|
-
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `"${param.name}"`;
|
|
346
|
-
return `${key}${optional}: ${result.tsType}`;
|
|
347
|
-
});
|
|
348
|
-
opLines.push(`export type ${prefix}Headers = { ${props.join('; ')}; };`);
|
|
416
|
+
opLines.push(`export type ${prefix}Headers = ${buildParamTypeBody(op.headerParams, mapper)};`);
|
|
349
417
|
}
|
|
350
418
|
// Section 3: Request body types
|
|
419
|
+
const bodyJsDoc = op.requestBody ? buildDescriptionJsDoc(op.requestBody.description) : '';
|
|
351
420
|
if (op.requestBody?.isMultipart && op.requestBody.schema) {
|
|
352
421
|
const schema = resolver.resolveSchema(op.requestBody.schema);
|
|
353
422
|
const requiredSet = new Set(schema.required ?? []);
|
|
354
|
-
const
|
|
423
|
+
const propLines = [];
|
|
424
|
+
for (const [name, propSchema] of Object.entries(schema.properties ?? {})) {
|
|
355
425
|
const resolved = resolver.resolveSchema(propSchema);
|
|
356
426
|
const optional = requiredSet.has(name) ? '' : '?';
|
|
357
427
|
let tsType;
|
|
@@ -364,23 +434,27 @@ export function generateContracts(doc, resolver) {
|
|
|
364
434
|
else {
|
|
365
435
|
tsType = 'string';
|
|
366
436
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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};`));
|
|
371
445
|
}
|
|
372
446
|
else {
|
|
373
|
-
opLines.push(`export type ${prefix}Body = Record<string, never>;`);
|
|
447
|
+
opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${prefix}Body = Record<string, never>;`));
|
|
374
448
|
}
|
|
375
449
|
}
|
|
376
450
|
else if (op.requestBody?.schema) {
|
|
377
451
|
const hasBinaryContentType = op.requestBody.contentTypes.some(isBinaryContentType);
|
|
378
452
|
if (hasBinaryContentType) {
|
|
379
|
-
opLines.push(`export type ${prefix}Body = Blob;`);
|
|
453
|
+
opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${prefix}Body = Blob;`));
|
|
380
454
|
}
|
|
381
455
|
else {
|
|
382
456
|
const result = mapper.mapSchema(op.requestBody.schema, undefined, 'request');
|
|
383
|
-
opLines.push(`export type ${prefix}Body = ${result.tsType};`);
|
|
457
|
+
opLines.push(attachTypeJsDoc(bodyJsDoc, `export type ${prefix}Body = ${result.tsType};`));
|
|
384
458
|
}
|
|
385
459
|
}
|
|
386
460
|
// Section 4: Response types
|
|
@@ -399,7 +473,10 @@ export function generateContracts(doc, resolver) {
|
|
|
399
473
|
return r.tsType;
|
|
400
474
|
});
|
|
401
475
|
const successType = types.length === 1 ? types[0] : types.join(' | ');
|
|
402
|
-
|
|
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};`));
|
|
403
480
|
}
|
|
404
481
|
// Error types per status
|
|
405
482
|
const errorTypes = [];
|
|
@@ -452,92 +529,7 @@ export function generateContracts(doc, resolver) {
|
|
|
452
529
|
for (const brand of brandedTypes.values()) {
|
|
453
530
|
brandLines.push(`export type ${brand.name} = ${brand.baseType} & { readonly __format?: '${brand.format}' };`);
|
|
454
531
|
}
|
|
455
|
-
lines.splice(1, 0, '', ...brandLines);
|
|
456
|
-
}
|
|
457
|
-
// Always emit StreamResponse class (used by Requester type)
|
|
458
|
-
lines.push('');
|
|
459
|
-
lines.push('export class StreamResponse {');
|
|
460
|
-
lines.push(' constructor(');
|
|
461
|
-
lines.push(' public readonly data: ReadableStream<Uint8Array>,');
|
|
462
|
-
lines.push(' public readonly filename?: string,');
|
|
463
|
-
lines.push(' public readonly headers: Record<string, string> = {},');
|
|
464
|
-
lines.push(' ) {}');
|
|
465
|
-
lines.push('}');
|
|
466
|
-
lines.push('');
|
|
467
|
-
lines.push(`export function streamResponse(
|
|
468
|
-
data: ReadableStream<Uint8Array>,
|
|
469
|
-
filename?: string,
|
|
470
|
-
headers?: Record<string, string>,
|
|
471
|
-
): StreamResponse {
|
|
472
|
-
return new StreamResponse(data, filename, headers ?? {});
|
|
473
|
-
}`);
|
|
474
|
-
// ErrorResponse class
|
|
475
|
-
lines.push('');
|
|
476
|
-
lines.push(`export class ErrorResponse {
|
|
477
|
-
constructor(
|
|
478
|
-
public readonly status: number,
|
|
479
|
-
public readonly data: unknown,
|
|
480
|
-
public readonly headers: Record<string, string>,
|
|
481
|
-
public readonly message?: string,
|
|
482
|
-
) {}
|
|
483
|
-
}`);
|
|
484
|
-
// errorResponse() helper
|
|
485
|
-
lines.push('');
|
|
486
|
-
lines.push(`export function errorResponse(
|
|
487
|
-
status: number,
|
|
488
|
-
data: unknown,
|
|
489
|
-
headers?: Record<string, string>,
|
|
490
|
-
message?: string,
|
|
491
|
-
): ErrorResponse {
|
|
492
|
-
return new ErrorResponse(status, data, headers ?? {}, message);
|
|
493
|
-
}`);
|
|
494
|
-
// RequesterFailError class
|
|
495
|
-
lines.push('');
|
|
496
|
-
lines.push(`export class RequesterFailError extends Error {
|
|
497
|
-
constructor(
|
|
498
|
-
public readonly cause: unknown,
|
|
499
|
-
) {
|
|
500
|
-
super(\`Request failed: \${cause instanceof Error ? cause.message : String(cause)}\`);
|
|
501
|
-
this.name = "RequesterFailError";
|
|
502
|
-
}
|
|
503
|
-
}`);
|
|
504
|
-
// Section 5: ApiError class
|
|
505
|
-
lines.push('');
|
|
506
|
-
lines.push(`export class ApiError<TStatus extends number, TData> extends Error {
|
|
507
|
-
constructor(
|
|
508
|
-
public readonly status: TStatus,
|
|
509
|
-
public readonly data: TData,
|
|
510
|
-
message: string,
|
|
511
|
-
) {
|
|
512
|
-
super(message);
|
|
513
|
-
this.name = "ApiError";
|
|
514
|
-
}
|
|
515
|
-
}`);
|
|
516
|
-
// Section 5b: UnspecifiedApiError class
|
|
517
|
-
lines.push('');
|
|
518
|
-
lines.push(`export class UnspecifiedApiError extends ApiError<number, unknown> {
|
|
519
|
-
constructor(
|
|
520
|
-
status: number,
|
|
521
|
-
data: unknown,
|
|
522
|
-
message: string,
|
|
523
|
-
) {
|
|
524
|
-
super(status, data, message);
|
|
525
|
-
this.name = "UnspecifiedApiError";
|
|
526
|
-
}
|
|
527
|
-
}`);
|
|
528
|
-
const needsDefaultApiError = operations.some((op) => op.responses.some((r) => !r.isSuccess && r.statusCode === 'default'));
|
|
529
|
-
if (needsDefaultApiError) {
|
|
530
|
-
lines.push('');
|
|
531
|
-
lines.push(`export class DefaultApiError<TData> extends Error {
|
|
532
|
-
constructor(
|
|
533
|
-
public readonly status: number,
|
|
534
|
-
public readonly data: TData,
|
|
535
|
-
message: string,
|
|
536
|
-
) {
|
|
537
|
-
super(message);
|
|
538
|
-
this.name = "DefaultApiError";
|
|
539
|
-
}
|
|
540
|
-
}`);
|
|
532
|
+
lines.splice(1 + runtimeReexport.length, 0, '', ...brandLines);
|
|
541
533
|
}
|
|
542
534
|
return lines.join('\n');
|
|
543
535
|
}
|
|
@@ -1,14 +1,4 @@
|
|
|
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
|
*
|
|
@@ -1,23 +1,3 @@
|
|
|
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
|
*
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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 class ApiError extends Error {
|
|
10
|
+
status;
|
|
11
|
+
data;
|
|
12
|
+
constructor(status, data, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.data = data;
|
|
16
|
+
this.name = 'ApiError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Error thrown for a status code not declared in the spec. */
|
|
20
|
+
export class UnspecifiedApiError extends ApiError {
|
|
21
|
+
constructor(status, data, message) {
|
|
22
|
+
super(status, data, message);
|
|
23
|
+
this.name = 'UnspecifiedApiError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Error thrown for operations that declare a `default` response. */
|
|
27
|
+
export class DefaultApiError extends Error {
|
|
28
|
+
status;
|
|
29
|
+
data;
|
|
30
|
+
constructor(status, data, message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.status = status;
|
|
33
|
+
this.data = data;
|
|
34
|
+
this.name = 'DefaultApiError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Wraps unexpected failures inside a `Requester` implementation. */
|
|
38
|
+
export class RequesterFailError extends Error {
|
|
39
|
+
cause;
|
|
40
|
+
constructor(cause) {
|
|
41
|
+
super(`Request failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
42
|
+
this.cause = cause;
|
|
43
|
+
this.name = 'RequesterFailError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public runtime contract shared by generated clients and user-provided
|
|
3
|
+
* `Requester` implementations.
|
|
4
|
+
*
|
|
5
|
+
* Import from `genoc/runtime` — the main `genoc` entry point is the
|
|
6
|
+
* generator API (dev-time only); this subpath is the tiny runtime surface.
|
|
7
|
+
*/
|
|
8
|
+
export { ApiError, UnspecifiedApiError, DefaultApiError, RequesterFailError } from './errors.js';
|
|
9
|
+
export { StreamResponse, ErrorResponse } from './responses.js';
|
|
10
|
+
export { decorateWithErrors, isDefinedError } from './defined-errors.js';
|
|
11
|
+
export type { Requester } from './requester.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public runtime contract shared by generated clients and user-provided
|
|
3
|
+
* `Requester` implementations.
|
|
4
|
+
*
|
|
5
|
+
* Import from `genoc/runtime` — the main `genoc` entry point is the
|
|
6
|
+
* generator API (dev-time only); this subpath is the tiny runtime surface.
|
|
7
|
+
*/
|
|
8
|
+
export { ApiError, UnspecifiedApiError, DefaultApiError, RequesterFailError } from './errors.js';
|
|
9
|
+
export { StreamResponse, ErrorResponse } from './responses.js';
|
|
10
|
+
export { decorateWithErrors, isDefinedError } from './defined-errors.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ErrorResponse, StreamResponse } from './responses.js';
|
|
2
|
+
/**
|
|
3
|
+
* Performs an HTTP request and returns the response.
|
|
4
|
+
*
|
|
5
|
+
* When `expectStream` is true, the implementation should return
|
|
6
|
+
* a `StreamResponse` containing the stream data, filename (from
|
|
7
|
+
* Content-Disposition header), and response headers.
|
|
8
|
+
*/
|
|
9
|
+
export type Requester = <TResponse>(method: string, path: string, options: {
|
|
10
|
+
query?: Record<string, unknown>;
|
|
11
|
+
body?: unknown;
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
expectStream?: true;
|
|
14
|
+
}) => Promise<TResponse | StreamResponse | ErrorResponse>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared response containers exchanged between generated clients and
|
|
3
|
+
* `Requester` implementations.
|
|
4
|
+
*/
|
|
5
|
+
/** Container for binary/stream responses returned by a `Requester`. */
|
|
6
|
+
export declare class StreamResponse {
|
|
7
|
+
readonly data: ReadableStream<Uint8Array>;
|
|
8
|
+
readonly filename?: string;
|
|
9
|
+
readonly headers: Record<string, string>;
|
|
10
|
+
constructor(data: ReadableStream<Uint8Array>, filename?: string, headers?: Record<string, string>);
|
|
11
|
+
}
|
|
12
|
+
/** Container for non-2xx responses returned by a `Requester`. */
|
|
13
|
+
export declare class ErrorResponse {
|
|
14
|
+
readonly status: number;
|
|
15
|
+
readonly data: unknown;
|
|
16
|
+
readonly headers: Record<string, string>;
|
|
17
|
+
readonly message?: string;
|
|
18
|
+
constructor(status: number, data: unknown, headers?: Record<string, string>, message?: string);
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared response containers exchanged between generated clients and
|
|
3
|
+
* `Requester` implementations.
|
|
4
|
+
*/
|
|
5
|
+
/** Container for binary/stream responses returned by a `Requester`. */
|
|
6
|
+
export class StreamResponse {
|
|
7
|
+
data;
|
|
8
|
+
filename;
|
|
9
|
+
headers;
|
|
10
|
+
constructor(data, filename, headers = {}) {
|
|
11
|
+
this.data = data;
|
|
12
|
+
this.filename = filename;
|
|
13
|
+
this.headers = headers;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Container for non-2xx responses returned by a `Requester`. */
|
|
17
|
+
export class ErrorResponse {
|
|
18
|
+
status;
|
|
19
|
+
data;
|
|
20
|
+
headers;
|
|
21
|
+
message;
|
|
22
|
+
constructor(status, data, headers = {}, message) {
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.data = data;
|
|
25
|
+
this.headers = headers;
|
|
26
|
+
this.message = message;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type GeneratorConfig = {
|
|
|
6
6
|
requesterModuleName?: string;
|
|
7
7
|
specVersion?: string;
|
|
8
8
|
strictVersion?: boolean;
|
|
9
|
+
runtimeImportPath?: string;
|
|
9
10
|
};
|
|
10
11
|
export type RequesterFunction = <TResponse>(method: string, path: string, options: {
|
|
11
12
|
query?: Record<string, unknown>;
|
package/dist/types/openapi.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ export type SchemaObject = {
|
|
|
16
16
|
const?: unknown;
|
|
17
17
|
default?: unknown;
|
|
18
18
|
description?: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
deprecated?: boolean;
|
|
21
|
+
example?: unknown;
|
|
22
|
+
examples?: unknown[];
|
|
19
23
|
nullable?: boolean;
|
|
20
24
|
readOnly?: boolean;
|
|
21
25
|
writeOnly?: boolean;
|