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.
@@ -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
- function buildJsDoc(description) {
44
- if (!description)
45
- return undefined;
46
- return `/** ${description} */`;
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. ApiError class
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: buildJsDoc(resolved.description),
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
- if (scheme.description) {
338
+ const schemeJsDoc = buildDescriptionJsDoc(scheme.description);
339
+ if (schemeJsDoc !== '') {
261
340
  lines.push('');
262
- lines.push(buildJsDoc(scheme.description));
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
- jsDocParts.push(sv.description);
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
- const props = op.queryParams.map((param) => {
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
- const props = op.headerParams.map((param) => {
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 props = Object.entries(schema.properties ?? {}).map(([name, propSchema]) => {
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
- return `${name}${optional}: ${tsType}`;
368
- });
369
- if (props.length > 0) {
370
- opLines.push(`export type ${prefix}Body = { ${props.join('; ')}; };`);
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
- opLines.push(`export type ${prefix}Response = ${successType};`);
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 type { GeneratorConfig, ApiClient, ApiError, DefaultApiError };
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
+ }
@@ -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>;
@@ -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;