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.
@@ -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;
@@ -1,4 +1,25 @@
1
1
  import type { AnalyzedOperation } from '../analyzer/path-analyzer.js';
2
+ import type { SchemaObject } from '../types/openapi.js';
3
+ /**
4
+ * Reserved type names used by the generated output's built-in classes,
5
+ * functions, and types. User-defined schema names that collide with these
6
+ * are automatically renamed with a suffix to prevent duplicate identifiers.
7
+ */
8
+ export declare const RESERVED_TYPE_NAMES: ReadonlySet<string>;
9
+ /** Default module specifier generated code imports shared runtime classes from. */
10
+ export declare const DEFAULT_RUNTIME_IMPORT_PATH = "genoc/runtime";
11
+ /**
12
+ * Sanitize a schema type name by converting dot-separated segments into
13
+ * PascalCase. Names without dots pass through unchanged.
14
+ *
15
+ * "Api.Error" → "ApiError", "Simple" → "Simple"
16
+ */
17
+ export declare function sanitizeTypeName(name: string): string;
18
+ /**
19
+ * Build a mapping of sanitized schema names → renamed names for any that
20
+ * collide with reserved type names.
21
+ */
22
+ export declare function buildSchemaRenameMap(schemaNames: Iterable<string>, reserved: ReadonlySet<string>, suffix?: string): Map<string, string>;
2
23
  /**
3
24
  * Convert a string to PascalCase, handling camelCase, kebab-case, snake_case,
4
25
  * and colon-separated segments.
@@ -21,3 +42,34 @@ export declare function getErrorType(op: AnalyzedOperation): string;
21
42
  * Generate the auto-generated header comment.
22
43
  */
23
44
  export declare function makeHeader(version: string): string;
45
+ /**
46
+ * Sanitize free-form text for safe inclusion inside a JSDoc comment.
47
+ *
48
+ * Normalizes CRLF to LF, flattens remaining newlines into single spaces,
49
+ * escapes the comment-closing sequence (asterisk + slash) as `*\/`, and
50
+ * trims leading/trailing whitespace. No other transformation is applied —
51
+ * no HTML stripping, no markdown processing, no wrapping.
52
+ */
53
+ export declare function sanitizeJsDocText(text: string): string;
54
+ /**
55
+ * Format an arbitrary metadata value for a JSDoc tag using JSON.stringify
56
+ * (no indent). Returns `null` when stringify yields `undefined` (e.g. for
57
+ * `undefined` or function values) — callers should skip the tag entirely.
58
+ */
59
+ export declare function formatJsDocValue(value: unknown): string | null;
60
+ /**
61
+ * Build the ordered JSDoc content segments for a schema-backed property.
62
+ *
63
+ * Order (frozen): description → `@deprecated` → `@default` → `@example`(s)
64
+ * → `@title`. Segments are skipped when their value is absent, empty, or
65
+ * whitespace-only. Sanitization is applied AFTER stringification for
66
+ * `@default` / `@example` values.
67
+ */
68
+ export declare function buildFieldJsDocLines(schema: SchemaObject): string[];
69
+ /**
70
+ * Render a schema's JSDoc metadata as a complete comment block.
71
+ *
72
+ * 0 segments → `''`; exactly 1 segment → single-line `/** text *\/`;
73
+ * ≥2 segments → multiline block with a blank ` *` line between segments.
74
+ */
75
+ export declare function buildTypeJsDoc(schema: SchemaObject): string;
@@ -1,3 +1,54 @@
1
+ /**
2
+ * Reserved type names used by the generated output's built-in classes,
3
+ * functions, and types. User-defined schema names that collide with these
4
+ * are automatically renamed with a suffix to prevent duplicate identifiers.
5
+ */
6
+ export const RESERVED_TYPE_NAMES = new Set([
7
+ // Classes in contracts.ts
8
+ 'StreamResponse',
9
+ 'ErrorResponse',
10
+ 'ApiError',
11
+ 'UnspecifiedApiError',
12
+ 'DefaultApiError',
13
+ 'RequesterFailError',
14
+ // Types / functions in client.ts
15
+ 'Requester',
16
+ 'isDefinedError',
17
+ 'decorateWithErrors',
18
+ 'ApiClient',
19
+ // Interface in contracts.ts (when file uploads present)
20
+ 'FileInput',
21
+ ]);
22
+ /** Default module specifier generated code imports shared runtime classes from. */
23
+ export const DEFAULT_RUNTIME_IMPORT_PATH = 'genoc/runtime';
24
+ /**
25
+ * Sanitize a schema type name by converting dot-separated segments into
26
+ * PascalCase. Names without dots pass through unchanged.
27
+ *
28
+ * "Api.Error" → "ApiError", "Simple" → "Simple"
29
+ */
30
+ export function sanitizeTypeName(name) {
31
+ if (!name.includes('.'))
32
+ return name;
33
+ return name
34
+ .split('.')
35
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
36
+ .join('');
37
+ }
38
+ /**
39
+ * Build a mapping of sanitized schema names → renamed names for any that
40
+ * collide with reserved type names.
41
+ */
42
+ export function buildSchemaRenameMap(schemaNames, reserved, suffix = 'Model') {
43
+ const renameMap = new Map();
44
+ for (const name of schemaNames) {
45
+ const sanitized = sanitizeTypeName(name);
46
+ if (reserved.has(sanitized)) {
47
+ renameMap.set(sanitized, `${sanitized}${suffix}`);
48
+ }
49
+ }
50
+ return renameMap;
51
+ }
1
52
  /**
2
53
  * Convert a string to PascalCase, handling camelCase, kebab-case, snake_case,
3
54
  * and colon-separated segments.
@@ -64,3 +115,78 @@ export function getErrorType(op) {
64
115
  export function makeHeader(version) {
65
116
  return `// Auto-generated by genoc from OpenAPI ${version} spec. DO NOT EDIT.`;
66
117
  }
118
+ /**
119
+ * Sanitize free-form text for safe inclusion inside a JSDoc comment.
120
+ *
121
+ * Normalizes CRLF to LF, flattens remaining newlines into single spaces,
122
+ * escapes the comment-closing sequence (asterisk + slash) as `*\/`, and
123
+ * trims leading/trailing whitespace. No other transformation is applied —
124
+ * no HTML stripping, no markdown processing, no wrapping.
125
+ */
126
+ export function sanitizeJsDocText(text) {
127
+ return text.replace(/\r\n/g, '\n').replace(/\n/g, ' ').replace(/\*\//g, '*\\/').trim();
128
+ }
129
+ /**
130
+ * Format an arbitrary metadata value for a JSDoc tag using JSON.stringify
131
+ * (no indent). Returns `null` when stringify yields `undefined` (e.g. for
132
+ * `undefined` or function values) — callers should skip the tag entirely.
133
+ */
134
+ export function formatJsDocValue(value) {
135
+ const json = JSON.stringify(value);
136
+ return json === undefined ? null : json;
137
+ }
138
+ /**
139
+ * Build the ordered JSDoc content segments for a schema-backed property.
140
+ *
141
+ * Order (frozen): description → `@deprecated` → `@default` → `@example`(s)
142
+ * → `@title`. Segments are skipped when their value is absent, empty, or
143
+ * whitespace-only. Sanitization is applied AFTER stringification for
144
+ * `@default` / `@example` values.
145
+ */
146
+ export function buildFieldJsDocLines(schema) {
147
+ const lines = [];
148
+ if (schema.description !== undefined && schema.description.trim() !== '') {
149
+ lines.push(sanitizeJsDocText(schema.description));
150
+ }
151
+ if (schema.deprecated === true) {
152
+ lines.push('@deprecated');
153
+ }
154
+ if (schema.default !== undefined) {
155
+ const formatted = formatJsDocValue(schema.default);
156
+ if (formatted !== null) {
157
+ lines.push(`@default ${sanitizeJsDocText(formatted)}`);
158
+ }
159
+ }
160
+ if (schema.example !== undefined) {
161
+ const formatted = formatJsDocValue(schema.example);
162
+ if (formatted !== null) {
163
+ lines.push(`@example ${sanitizeJsDocText(formatted)}`);
164
+ }
165
+ }
166
+ if (schema.examples !== undefined) {
167
+ for (const value of schema.examples) {
168
+ const formatted = formatJsDocValue(value);
169
+ if (formatted !== null) {
170
+ lines.push(`@example ${sanitizeJsDocText(formatted)}`);
171
+ }
172
+ }
173
+ }
174
+ if (schema.title) {
175
+ lines.push(`@title ${sanitizeJsDocText(schema.title)}`);
176
+ }
177
+ return lines;
178
+ }
179
+ /**
180
+ * Render a schema's JSDoc metadata as a complete comment block.
181
+ *
182
+ * 0 segments → `''`; exactly 1 segment → single-line `/** text *\/`;
183
+ * ≥2 segments → multiline block with a blank ` *` line between segments.
184
+ */
185
+ export function buildTypeJsDoc(schema) {
186
+ const lines = buildFieldJsDocLines(schema);
187
+ if (lines.length === 0)
188
+ return '';
189
+ if (lines.length === 1)
190
+ return `/** ${lines[0]} */`;
191
+ return `/**\n${lines.map((line) => ` * ${line}`).join('\n *\n')}\n */`;
192
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genoc",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Generate typed HTTP clients from OpenAPI 3.0 / 3.1 specifications",
5
5
  "keywords": [
6
6
  "client",
@@ -28,6 +28,10 @@
28
28
  ".": {
29
29
  "import": "./dist/index.js",
30
30
  "types": "./dist/index.d.ts"
31
+ },
32
+ "./runtime": {
33
+ "import": "./dist/runtime/index.js",
34
+ "types": "./dist/runtime/index.d.ts"
31
35
  }
32
36
  },
33
37
  "scripts": {
@@ -39,7 +43,7 @@
39
43
  "format": "oxfmt --write",
40
44
  "format:check": "oxfmt --check",
41
45
  "changeset": "changeset",
42
- "version-packages": "changeset version",
46
+ "version-packages": "changeset version && oxfmt --write",
43
47
  "release": "bun run build && changeset publish"
44
48
  },
45
49
  "dependencies": {