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
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# genoc
|
|
2
2
|
|
|
3
3
|
Generate TypeScript HTTP clients from OpenAPI 3.0 / 3.1 specifications.
|
|
4
|
-
|
|
4
|
+
Generated code depends only on the tiny `genoc/runtime` module. Full type safety. Bring your own HTTP client.
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/genoc)
|
|
7
7
|
[](https://nodejs.org/)
|
|
@@ -13,6 +13,7 @@ The generated code has zero runtime dependencies. Full type safety. Bring your o
|
|
|
13
13
|
- Full OpenAPI 3.0 and 3.1 specification support with automatic version detection
|
|
14
14
|
- End-to-end type safety — requests, responses, and errors are fully typed
|
|
15
15
|
- HTTP-client agnostic — adapter pattern lets you plug in fetch, axios, or anything else
|
|
16
|
+
- Shared runtime contract — `genoc/runtime` exports the `Requester` type and response/error classes, so one requester implementation works with every generated client
|
|
16
17
|
- Error types with per-status-code narrowing and type guards
|
|
17
18
|
- File and binary upload/download with stream handling
|
|
18
19
|
- Flexible method naming strategies (path-based, operationId, operationId-with-fallback)
|
|
@@ -23,9 +24,12 @@ The generated code has zero runtime dependencies. Full type safety. Bring your o
|
|
|
23
24
|
Install:
|
|
24
25
|
|
|
25
26
|
```bash
|
|
26
|
-
npm install
|
|
27
|
+
npm install genoc
|
|
27
28
|
```
|
|
28
29
|
|
|
30
|
+
Generated code imports from `genoc/runtime`, so `genoc` is a runtime
|
|
31
|
+
dependency (not just `devDependencies`).
|
|
32
|
+
|
|
29
33
|
Generate:
|
|
30
34
|
|
|
31
35
|
```bash
|
|
@@ -40,8 +44,13 @@ This creates two files in `./src/api`:
|
|
|
40
44
|
## Usage
|
|
41
45
|
|
|
42
46
|
The generated client requires a `Requester` implementation — a function that
|
|
43
|
-
performs the actual HTTP call and returns the result.
|
|
44
|
-
|
|
47
|
+
performs the actual HTTP call and returns the result. The type lives in
|
|
48
|
+
`genoc/runtime`, so you can write and compile a requester before generating
|
|
49
|
+
anything:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import type { Requester } from 'genoc/runtime';
|
|
53
|
+
```
|
|
45
54
|
|
|
46
55
|
```typescript
|
|
47
56
|
type Requester = <TResponse>(
|
|
@@ -60,7 +69,8 @@ type Requester = <TResponse>(
|
|
|
60
69
|
|
|
61
70
|
```typescript
|
|
62
71
|
import { createClient } from './client.js';
|
|
63
|
-
import {
|
|
72
|
+
import type { Requester } from 'genoc/runtime';
|
|
73
|
+
import { RequesterFailError, ErrorResponse } from 'genoc/runtime';
|
|
64
74
|
|
|
65
75
|
const baseUrl = 'https://api.example.com';
|
|
66
76
|
|
|
@@ -85,7 +95,7 @@ const requester: Requester = async (method, path, options) => {
|
|
|
85
95
|
return new ErrorResponse(
|
|
86
96
|
response.status,
|
|
87
97
|
await response.json(),
|
|
88
|
-
response.headers,
|
|
98
|
+
Object.fromEntries(response.headers.entries()),
|
|
89
99
|
response.statusText
|
|
90
100
|
);
|
|
91
101
|
}
|
|
@@ -101,6 +111,27 @@ const pets = await client.getPets({ limit: 10 });
|
|
|
101
111
|
|
|
102
112
|
See [Binary / File Responses](#binary--file-responses) for handling `expectStream: true`.
|
|
103
113
|
|
|
114
|
+
## Shared Runtime (`genoc/runtime`)
|
|
115
|
+
|
|
116
|
+
Generated clients import their response and error classes from `genoc/runtime`
|
|
117
|
+
instead of declaring inline copies (the generated `contracts.ts` re-exports
|
|
118
|
+
them, so existing imports keep working). This gives every generated client the
|
|
119
|
+
same class identity — `instanceof` checks work across clients, and you can
|
|
120
|
+
implement **one shared requester** typed against the package:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// common-requester.ts — reusable across all generated clients,
|
|
124
|
+
// no generated imports needed
|
|
125
|
+
import type { Requester } from 'genoc/runtime';
|
|
126
|
+
|
|
127
|
+
export const requester: Requester = async (method, path, options) => {
|
|
128
|
+
// ...your fetch/axios/etc. implementation
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
To pin a specific version or point at a mirror, override the import specifier
|
|
133
|
+
via `--runtime-import-path` (CLI) or `runtimeImportPath` (programmatic config).
|
|
134
|
+
|
|
104
135
|
## Binary / File Responses
|
|
105
136
|
|
|
106
137
|
When your spec defines binary responses (e.g. `format: binary`,
|
|
@@ -109,14 +140,14 @@ When your spec defines binary responses (e.g. `format: binary`,
|
|
|
109
140
|
`StreamResponse` in that case:
|
|
110
141
|
|
|
111
142
|
```typescript
|
|
112
|
-
import { StreamResponse } from '
|
|
143
|
+
import { StreamResponse } from 'genoc/runtime';
|
|
113
144
|
|
|
114
145
|
// Inside your Requester implementation:
|
|
115
146
|
if (options.expectStream === true) {
|
|
116
147
|
return new StreamResponse(
|
|
117
148
|
response.body as ReadableStream<Uint8Array>,
|
|
118
149
|
getFilename(response.headers), // extract from Content-Disposition
|
|
119
|
-
response.headers
|
|
150
|
+
Object.fromEntries(response.headers.entries())
|
|
120
151
|
);
|
|
121
152
|
}
|
|
122
153
|
```
|
|
@@ -131,17 +162,6 @@ class StreamResponse {
|
|
|
131
162
|
}
|
|
132
163
|
```
|
|
133
164
|
|
|
134
|
-
### Response Helpers
|
|
135
|
-
|
|
136
|
-
The generated `contracts.ts` includes helper functions for constructing
|
|
137
|
-
responses in your `Requester` implementation:
|
|
138
|
-
|
|
139
|
-
- `streamResponse(data, filename?, headers?)` — Creates a `StreamResponse` instance
|
|
140
|
-
- `errorResponse(status, data, headers?, message?)` — Creates an `ErrorResponse` instance
|
|
141
|
-
|
|
142
|
-
These are convenience wrappers around the `StreamResponse` and `ErrorResponse`
|
|
143
|
-
constructors.
|
|
144
|
-
|
|
145
165
|
## CLI Reference
|
|
146
166
|
|
|
147
167
|
```bash
|
|
@@ -150,12 +170,13 @@ genoc <spec> [flags]
|
|
|
150
170
|
|
|
151
171
|
`<spec>` — Path or URL to an OpenAPI 3.0 / 3.1 spec (JSON or YAML).
|
|
152
172
|
|
|
153
|
-
| Flag | Default
|
|
154
|
-
| ------------------------ |
|
|
155
|
-
| `--output-dir` | (required)
|
|
156
|
-
| `--method-name-strategy` | `path-based`
|
|
157
|
-
| `--spec-version` | auto-detect
|
|
158
|
-
| `--strict-version` | `true`
|
|
173
|
+
| Flag | Default | Description |
|
|
174
|
+
| ------------------------ | --------------- | ------------------------------------------------------------ |
|
|
175
|
+
| `--output-dir` | (required) | Output directory for generated files |
|
|
176
|
+
| `--method-name-strategy` | `path-based` | Method naming strategy |
|
|
177
|
+
| `--spec-version` | auto-detect | Override version detection (`"3.0"` or `"3.1"`) |
|
|
178
|
+
| `--strict-version` | `true` | Warn if `--spec-version` mismatches detected version |
|
|
179
|
+
| `--runtime-import-path` | `genoc/runtime` | Module specifier generated code imports runtime classes from |
|
|
159
180
|
|
|
160
181
|
## Method Naming Strategies
|
|
161
182
|
|
|
@@ -168,6 +189,15 @@ genoc <spec> [flags]
|
|
|
168
189
|
- **`operationId-with-fallback`** — Use `operationId` if present, otherwise
|
|
169
190
|
fall back to path-based naming.
|
|
170
191
|
|
|
192
|
+
## JSDoc Documentation Output
|
|
193
|
+
|
|
194
|
+
`description`, `deprecated`, `default`, `example`/`examples`, and `title`
|
|
195
|
+
fields from your spec become JSDoc comments in the generated code — on object
|
|
196
|
+
type properties, named schemas, and client methods. Object types are always
|
|
197
|
+
emitted multi-line, so per-property JSDoc stays aligned at each nesting depth.
|
|
198
|
+
`@deprecated` tags give you editor strikethrough on deprecated fields and
|
|
199
|
+
methods for free.
|
|
200
|
+
|
|
171
201
|
## Programmatic API
|
|
172
202
|
|
|
173
203
|
```typescript
|
|
@@ -194,7 +224,7 @@ union, and `isDefinedError` narrows a caught error to that union:
|
|
|
194
224
|
|
|
195
225
|
```typescript
|
|
196
226
|
import { UnspecifiedApiError, RequesterFailError } from './contracts.js';
|
|
197
|
-
import { isDefinedError } from '
|
|
227
|
+
import { isDefinedError } from 'genoc/runtime';
|
|
198
228
|
|
|
199
229
|
try {
|
|
200
230
|
const result = await client.getPets();
|
|
@@ -9,6 +9,7 @@ export interface AnalyzedParameter {
|
|
|
9
9
|
tsType: string;
|
|
10
10
|
description?: string;
|
|
11
11
|
deprecated?: boolean;
|
|
12
|
+
example?: unknown;
|
|
12
13
|
}
|
|
13
14
|
export interface AnalyzedRequestBody {
|
|
14
15
|
required: boolean;
|
|
@@ -16,6 +17,7 @@ export interface AnalyzedRequestBody {
|
|
|
16
17
|
schema: SchemaObject | ReferenceObject | undefined;
|
|
17
18
|
tsType: string;
|
|
18
19
|
isMultipart: boolean;
|
|
20
|
+
description?: string;
|
|
19
21
|
}
|
|
20
22
|
export interface AnalyzedResponse {
|
|
21
23
|
statusCode: string;
|
|
@@ -78,6 +78,7 @@ function analyzeParameter(param, resolver) {
|
|
|
78
78
|
tsType: schemaToTsType(param.schema, resolver),
|
|
79
79
|
description: param.description,
|
|
80
80
|
deprecated: param.deprecated,
|
|
81
|
+
example: param.example,
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
84
|
function mergeParameters(pathItemParams, operationParams, resolver) {
|
|
@@ -120,6 +121,7 @@ function analyzeRequestBody(body, resolver) {
|
|
|
120
121
|
schema,
|
|
121
122
|
tsType,
|
|
122
123
|
isMultipart,
|
|
124
|
+
description: resolved.description,
|
|
123
125
|
};
|
|
124
126
|
}
|
|
125
127
|
function analyzeResponses(responses, resolver) {
|
|
@@ -44,5 +44,13 @@ export declare class SchemaMapper {
|
|
|
44
44
|
private mapDiscriminatedUnion;
|
|
45
45
|
private resolveDiscriminantValue;
|
|
46
46
|
private mapArray;
|
|
47
|
+
/**
|
|
48
|
+
* Render the per-property JSDoc comment lines for an object property.
|
|
49
|
+
*
|
|
50
|
+
* Metadata comes from the property's own schema node; for a `$ref` property
|
|
51
|
+
* the resolved target's own node metadata is used (one level only — never
|
|
52
|
+
* recursed), so self-referential schemas cannot loop here.
|
|
53
|
+
*/
|
|
54
|
+
private renderPropertyJsDoc;
|
|
47
55
|
private mapObject;
|
|
48
56
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { RefResolver } from '../parser/ref-resolver.js';
|
|
2
2
|
import { formatToBrandTypeName } from '../utils/case.js';
|
|
3
|
-
import { sanitizeTypeName } from '../utils/generator-helpers.js';
|
|
3
|
+
import { buildTypeJsDoc, sanitizeTypeName } from '../utils/generator-helpers.js';
|
|
4
4
|
import { quoteKey } from '../utils/string.js';
|
|
5
|
+
/** Indentation contract for multi-line types — 2-space unit pinned from the generated ServerParams interface; binding for downstream generators. */
|
|
6
|
+
const INDENT_UNIT = ' ';
|
|
7
|
+
function indentBy(level) {
|
|
8
|
+
return INDENT_UNIT.repeat(level);
|
|
9
|
+
}
|
|
5
10
|
/**
|
|
6
11
|
* Default: extracts the last segment of the JSON pointer.
|
|
7
12
|
* "#/components/schemas/User" -> "User"
|
|
@@ -58,7 +63,7 @@ export class SchemaMapper {
|
|
|
58
63
|
return { tsType: schema ? 'unknown' : 'never', imports: [] };
|
|
59
64
|
}
|
|
60
65
|
const visited = new Set();
|
|
61
|
-
const result = this.mapInternal(schema, name, context, visited);
|
|
66
|
+
const result = this.mapInternal(schema, name, context, visited, 0);
|
|
62
67
|
if (name && this.discriminatorTargets.has(name)) {
|
|
63
68
|
const target = this.discriminatorTargets.get(name);
|
|
64
69
|
result.tsType += ` & { ${quoteKey(target.propertyName)}: '${target.literalValue}' }`;
|
|
@@ -75,7 +80,7 @@ export class SchemaMapper {
|
|
|
75
80
|
return null;
|
|
76
81
|
return brandName;
|
|
77
82
|
}
|
|
78
|
-
mapInternal(schema, name, context, visited) {
|
|
83
|
+
mapInternal(schema, name, context, visited, indent) {
|
|
79
84
|
if (isRefObject(schema)) {
|
|
80
85
|
const refStr = schema.$ref;
|
|
81
86
|
const refName = this.typeNameGenerator(refStr);
|
|
@@ -89,7 +94,7 @@ export class SchemaMapper {
|
|
|
89
94
|
if (resolved) {
|
|
90
95
|
const discInfo = this.resolveDiscriminatorInfo(resolved, refStr);
|
|
91
96
|
if (discInfo) {
|
|
92
|
-
const expanded = this.mapInternal(resolved, undefined, context, visited);
|
|
97
|
+
const expanded = this.mapInternal(resolved, undefined, context, visited, indent);
|
|
93
98
|
expanded.tsType += ` & { ${quoteKey(discInfo.propertyName)}: '${discInfo.literalValue}' }`;
|
|
94
99
|
return expanded;
|
|
95
100
|
}
|
|
@@ -121,7 +126,7 @@ export class SchemaMapper {
|
|
|
121
126
|
return { tsType, imports: [] };
|
|
122
127
|
}
|
|
123
128
|
if (s.allOf !== undefined && s.allOf.length > 0) {
|
|
124
|
-
const result = this.mapCombinator(s.allOf, '&', context, visited);
|
|
129
|
+
const result = this.mapCombinator(s.allOf, '&', context, visited, indent);
|
|
125
130
|
if (s.nullable === true) {
|
|
126
131
|
return {
|
|
127
132
|
tsType: needsParens(result.tsType)
|
|
@@ -134,8 +139,8 @@ export class SchemaMapper {
|
|
|
134
139
|
}
|
|
135
140
|
if (s.oneOf !== undefined && s.oneOf.length > 0) {
|
|
136
141
|
const result = s.discriminator
|
|
137
|
-
? this.mapDiscriminatedUnion(s.oneOf, s.discriminator, context, visited)
|
|
138
|
-
: this.mapCombinator(s.oneOf, '|', context, visited);
|
|
142
|
+
? this.mapDiscriminatedUnion(s.oneOf, s.discriminator, context, visited, indent)
|
|
143
|
+
: this.mapCombinator(s.oneOf, '|', context, visited, indent);
|
|
139
144
|
if (s.nullable === true) {
|
|
140
145
|
return {
|
|
141
146
|
tsType: needsParens(result.tsType)
|
|
@@ -148,8 +153,8 @@ export class SchemaMapper {
|
|
|
148
153
|
}
|
|
149
154
|
if (s.anyOf !== undefined && s.anyOf.length > 0) {
|
|
150
155
|
const result = s.discriminator
|
|
151
|
-
? this.mapDiscriminatedUnion(s.anyOf, s.discriminator, context, visited)
|
|
152
|
-
: this.mapCombinator(s.anyOf, '|', context, visited);
|
|
156
|
+
? this.mapDiscriminatedUnion(s.anyOf, s.discriminator, context, visited, indent)
|
|
157
|
+
: this.mapCombinator(s.anyOf, '|', context, visited, indent);
|
|
153
158
|
if (s.nullable === true) {
|
|
154
159
|
return {
|
|
155
160
|
tsType: needsParens(result.tsType)
|
|
@@ -169,7 +174,7 @@ export class SchemaMapper {
|
|
|
169
174
|
if (nonNull.length === 0) {
|
|
170
175
|
return { tsType: 'null', imports: [] };
|
|
171
176
|
}
|
|
172
|
-
const baseResult = this.mapInternal({ ...s, type: nonNull[0] }, name, context, new Set(visited));
|
|
177
|
+
const baseResult = this.mapInternal({ ...s, type: nonNull[0] }, name, context, new Set(visited), indent);
|
|
173
178
|
if (hasNull) {
|
|
174
179
|
return {
|
|
175
180
|
tsType: `${baseResult.tsType} | null`,
|
|
@@ -224,9 +229,9 @@ export class SchemaMapper {
|
|
|
224
229
|
case 'null':
|
|
225
230
|
return { tsType: 'null', imports: [] };
|
|
226
231
|
case 'array':
|
|
227
|
-
return this.mapArray(s, context, visited);
|
|
232
|
+
return this.mapArray(s, context, visited, indent);
|
|
228
233
|
case 'object':
|
|
229
|
-
return this.mapObject(s, name, context, visited);
|
|
234
|
+
return this.mapObject(s, name, context, visited, indent);
|
|
230
235
|
default:
|
|
231
236
|
return { tsType: 'unknown', imports: [] };
|
|
232
237
|
}
|
|
@@ -270,8 +275,8 @@ export class SchemaMapper {
|
|
|
270
275
|
})
|
|
271
276
|
.join(' | ');
|
|
272
277
|
}
|
|
273
|
-
mapCombinator(schemas, kind, context, visited) {
|
|
274
|
-
const results = schemas.map((s) => this.mapInternal(s, undefined, context, visited));
|
|
278
|
+
mapCombinator(schemas, kind, context, visited, indent) {
|
|
279
|
+
const results = schemas.map((s) => this.mapInternal(s, undefined, context, visited, indent));
|
|
275
280
|
const allImports = [];
|
|
276
281
|
for (const r of results) {
|
|
277
282
|
allImports.push(...r.imports);
|
|
@@ -285,14 +290,14 @@ export class SchemaMapper {
|
|
|
285
290
|
});
|
|
286
291
|
return { tsType: parts.join(separator), imports: allImports };
|
|
287
292
|
}
|
|
288
|
-
mapDiscriminatedUnion(schemas, discriminator, context, visited) {
|
|
293
|
+
mapDiscriminatedUnion(schemas, discriminator, context, visited, indent) {
|
|
289
294
|
const propertyName = discriminator.propertyName;
|
|
290
295
|
const mapping = discriminator.mapping;
|
|
291
296
|
const allImports = [];
|
|
292
297
|
const parts = [];
|
|
293
298
|
for (const schema of schemas) {
|
|
294
299
|
const discriminantValue = this.resolveDiscriminantValue(schema, propertyName, mapping);
|
|
295
|
-
const variantResult = this.mapInternal(schema, undefined, context, visited);
|
|
300
|
+
const variantResult = this.mapInternal(schema, undefined, context, visited, indent);
|
|
296
301
|
allImports.push(...variantResult.imports);
|
|
297
302
|
const quotedProp = quoteKey(propertyName);
|
|
298
303
|
parts.push(`({ ${quotedProp}: '${discriminantValue}' } & ${variantResult.tsType})`);
|
|
@@ -323,14 +328,14 @@ export class SchemaMapper {
|
|
|
323
328
|
}
|
|
324
329
|
return 'unknown';
|
|
325
330
|
}
|
|
326
|
-
mapArray(schema, context, visited) {
|
|
331
|
+
mapArray(schema, context, visited, indent) {
|
|
327
332
|
if (!schema.items) {
|
|
328
333
|
return {
|
|
329
334
|
tsType: schema.nullable === true ? 'unknown[] | null' : 'unknown[]',
|
|
330
335
|
imports: [],
|
|
331
336
|
};
|
|
332
337
|
}
|
|
333
|
-
const itemResult = this.mapInternal(schema.items, undefined, context, visited);
|
|
338
|
+
const itemResult = this.mapInternal(schema.items, undefined, context, visited, indent);
|
|
334
339
|
const tsType = isComplexType(itemResult.tsType)
|
|
335
340
|
? `Array<${itemResult.tsType}>`
|
|
336
341
|
: `${itemResult.tsType}[]`;
|
|
@@ -339,7 +344,33 @@ export class SchemaMapper {
|
|
|
339
344
|
imports: itemResult.imports,
|
|
340
345
|
};
|
|
341
346
|
}
|
|
342
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Render the per-property JSDoc comment lines for an object property.
|
|
349
|
+
*
|
|
350
|
+
* Metadata comes from the property's own schema node; for a `$ref` property
|
|
351
|
+
* the resolved target's own node metadata is used (one level only — never
|
|
352
|
+
* recursed), so self-referential schemas cannot loop here.
|
|
353
|
+
*/
|
|
354
|
+
renderPropertyJsDoc(propSchema, indentLevel) {
|
|
355
|
+
let metaSchema;
|
|
356
|
+
if (isRefObject(propSchema)) {
|
|
357
|
+
try {
|
|
358
|
+
metaSchema = this.resolver.resolveSchema(propSchema);
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
metaSchema = propSchema;
|
|
366
|
+
}
|
|
367
|
+
const block = buildTypeJsDoc(metaSchema);
|
|
368
|
+
if (block === '')
|
|
369
|
+
return [];
|
|
370
|
+
const ind = indentBy(indentLevel);
|
|
371
|
+
return block.split('\n').map((line) => `${ind}${line}`);
|
|
372
|
+
}
|
|
373
|
+
mapObject(schema, name, context, visited, indent) {
|
|
343
374
|
const properties = schema.properties ?? {};
|
|
344
375
|
const requiredSet = new Set(schema.required ?? []);
|
|
345
376
|
const propNames = Object.keys(properties);
|
|
@@ -354,7 +385,9 @@ export class SchemaMapper {
|
|
|
354
385
|
return false;
|
|
355
386
|
return true;
|
|
356
387
|
});
|
|
357
|
-
const
|
|
388
|
+
const ownIndent = indentBy(indent);
|
|
389
|
+
const memberIndent = indentBy(indent + 1);
|
|
390
|
+
const memberLines = [];
|
|
358
391
|
const allImports = [];
|
|
359
392
|
if (schema.discriminator) {
|
|
360
393
|
const discPropName = schema.discriminator.propertyName;
|
|
@@ -362,15 +395,16 @@ export class SchemaMapper {
|
|
|
362
395
|
if (discIndex !== -1) {
|
|
363
396
|
filteredPropNames.splice(discIndex, 1);
|
|
364
397
|
}
|
|
365
|
-
|
|
398
|
+
memberLines.push(`${memberIndent}"${discPropName}": string;`);
|
|
366
399
|
}
|
|
367
400
|
for (const propName of filteredPropNames) {
|
|
368
401
|
const propSchema = properties[propName];
|
|
369
|
-
const propResult = this.mapInternal(propSchema, undefined, context, visited);
|
|
402
|
+
const propResult = this.mapInternal(propSchema, undefined, context, visited, indent + 1);
|
|
370
403
|
allImports.push(...propResult.imports);
|
|
404
|
+
memberLines.push(...this.renderPropertyJsDoc(propSchema, indent + 1));
|
|
371
405
|
const optional = requiredSet.has(propName) ? '' : '?';
|
|
372
406
|
const quotedName = quoteKey(propName);
|
|
373
|
-
|
|
407
|
+
memberLines.push(`${memberIndent}${quotedName}${optional}: ${propResult.tsType};`);
|
|
374
408
|
}
|
|
375
409
|
const additionalProps = schema.additionalProperties;
|
|
376
410
|
let indexSignature = null;
|
|
@@ -381,11 +415,11 @@ export class SchemaMapper {
|
|
|
381
415
|
indexSignature = '[key: string]: unknown';
|
|
382
416
|
}
|
|
383
417
|
else if (additionalProps !== undefined && typeof additionalProps === 'object') {
|
|
384
|
-
const addPropResult = this.mapInternal(additionalProps, undefined, context, visited);
|
|
418
|
+
const addPropResult = this.mapInternal(additionalProps, undefined, context, visited, indent + 1);
|
|
385
419
|
allImports.push(...addPropResult.imports);
|
|
386
420
|
indexSignature = `[key: string]: ${addPropResult.tsType}`;
|
|
387
421
|
}
|
|
388
|
-
const hasProps =
|
|
422
|
+
const hasProps = memberLines.length > 0;
|
|
389
423
|
const hasIndex = indexSignature !== null;
|
|
390
424
|
let result;
|
|
391
425
|
if (!hasProps && !hasIndex) {
|
|
@@ -400,7 +434,7 @@ export class SchemaMapper {
|
|
|
400
434
|
const valueType = indexSignature.replace('[key: string]: ', '');
|
|
401
435
|
if (name) {
|
|
402
436
|
result = {
|
|
403
|
-
tsType: `{
|
|
437
|
+
tsType: `{\n${memberIndent}${indexSignature};\n${ownIndent}}`,
|
|
404
438
|
imports: allImports,
|
|
405
439
|
};
|
|
406
440
|
}
|
|
@@ -412,16 +446,17 @@ export class SchemaMapper {
|
|
|
412
446
|
}
|
|
413
447
|
}
|
|
414
448
|
else {
|
|
415
|
-
const
|
|
449
|
+
const propsBlock = `{\n${memberLines.join('\n')}\n${ownIndent}}`;
|
|
416
450
|
if (hasIndex) {
|
|
451
|
+
const indexBlock = `{\n${memberIndent}${indexSignature};\n${ownIndent}}`;
|
|
417
452
|
result = {
|
|
418
|
-
tsType:
|
|
453
|
+
tsType: `${propsBlock} & ${indexBlock}`,
|
|
419
454
|
imports: allImports,
|
|
420
455
|
};
|
|
421
456
|
}
|
|
422
457
|
else {
|
|
423
458
|
result = {
|
|
424
|
-
tsType:
|
|
459
|
+
tsType: propsBlock,
|
|
425
460
|
imports: allImports,
|
|
426
461
|
};
|
|
427
462
|
}
|
package/dist/cli/app.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ interface Flags {
|
|
|
3
3
|
methodNameStrategy: 'path-based' | 'operationId' | 'operationId-with-fallback';
|
|
4
4
|
specVersion?: string;
|
|
5
5
|
strictVersion: boolean;
|
|
6
|
+
runtimeImportPath?: string;
|
|
6
7
|
}
|
|
7
8
|
export type AppFlags = Flags;
|
|
8
9
|
export declare const app: import("@stricli/core").Application<import("@stricli/core").CommandContext>;
|
package/dist/cli/app.js
CHANGED
|
@@ -29,6 +29,13 @@ const command = buildCommand({
|
|
|
29
29
|
default: true,
|
|
30
30
|
brief: 'Enable strict version checking',
|
|
31
31
|
},
|
|
32
|
+
runtimeImportPath: {
|
|
33
|
+
kind: 'parsed',
|
|
34
|
+
parse: String,
|
|
35
|
+
brief: 'Module specifier for runtime imports (default: genoc/runtime)',
|
|
36
|
+
optional: true,
|
|
37
|
+
placeholder: 'module',
|
|
38
|
+
},
|
|
32
39
|
},
|
|
33
40
|
positional: {
|
|
34
41
|
kind: 'tuple',
|
package/dist/cli/impl.js
CHANGED
|
@@ -27,6 +27,7 @@ export default async function (flags, spec) {
|
|
|
27
27
|
methodNameStrategy: flags.methodNameStrategy || 'path-based',
|
|
28
28
|
specVersion: flags.specVersion,
|
|
29
29
|
strictVersion: flags.strictVersion,
|
|
30
|
+
runtimeImportPath: flags.runtimeImportPath,
|
|
30
31
|
};
|
|
31
32
|
const preserveRefSiblings = strategy.version() === '3.1';
|
|
32
33
|
this.process.stdout.write('Generating client...\n');
|
|
@@ -2,7 +2,7 @@ import { mkdir, writeFile } from 'fs/promises';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { analyzePaths } from '../analyzer/path-analyzer.js';
|
|
4
4
|
import { RefResolver } from '../parser/ref-resolver.js';
|
|
5
|
-
import { getOperationTypePrefix, getSuccessType, getErrorType, makeHeader, } from '../utils/generator-helpers.js';
|
|
5
|
+
import { DEFAULT_RUNTIME_IMPORT_PATH, getOperationTypePrefix, getSuccessType, getErrorType, makeHeader, } from '../utils/generator-helpers.js';
|
|
6
6
|
import { generateContracts } from './contracts-generator.js';
|
|
7
7
|
import { generateMethod } from './method-generator.js';
|
|
8
8
|
function collectImportTypes(operations) {
|
|
@@ -156,7 +156,7 @@ function buildClientMethodBody(op) {
|
|
|
156
156
|
}
|
|
157
157
|
return tryCatchLines.join('\n');
|
|
158
158
|
}
|
|
159
|
-
function buildClientFile(operations, version) {
|
|
159
|
+
function buildClientFile(operations, version, runtimeImportPath = DEFAULT_RUNTIME_IMPORT_PATH) {
|
|
160
160
|
const lines = [];
|
|
161
161
|
lines.push(makeHeader(version));
|
|
162
162
|
const importTypes = collectImportTypes(operations);
|
|
@@ -180,47 +180,11 @@ function buildClientFile(operations, version) {
|
|
|
180
180
|
if (hasMultipart) {
|
|
181
181
|
lines.push('/* global FormData */');
|
|
182
182
|
}
|
|
183
|
+
lines.push(`import { decorateWithErrors } from '${runtimeImportPath}';`);
|
|
184
|
+
lines.push(`import type { Requester } from '${runtimeImportPath}';`);
|
|
183
185
|
lines.push('');
|
|
184
|
-
lines.push(
|
|
185
|
-
lines.push(
|
|
186
|
-
lines.push(' runtimeErrors: unknown,');
|
|
187
|
-
lines.push('): T & { __definedErrors: E } {');
|
|
188
|
-
lines.push(' Object.defineProperty(item, "__definedErrors", {');
|
|
189
|
-
lines.push(' value: runtimeErrors,');
|
|
190
|
-
lines.push(' enumerable: false,');
|
|
191
|
-
lines.push(' configurable: true,');
|
|
192
|
-
lines.push(' writable: false,');
|
|
193
|
-
lines.push(' });');
|
|
194
|
-
lines.push(' return item as T & { __definedErrors: E };');
|
|
195
|
-
lines.push('}');
|
|
196
|
-
lines.push('');
|
|
197
|
-
lines.push('export function isDefinedError<E extends ApiError<number, unknown>>(');
|
|
198
|
-
lines.push(' err: unknown,');
|
|
199
|
-
lines.push(' fn: { __definedErrors: E },');
|
|
200
|
-
lines.push('): err is E {');
|
|
201
|
-
lines.push(' if (err instanceof UnspecifiedApiError) return false;');
|
|
202
|
-
lines.push(' if (!(err instanceof ApiError)) return false;');
|
|
203
|
-
lines.push(' return true;');
|
|
204
|
-
lines.push('}');
|
|
205
|
-
lines.push('');
|
|
206
|
-
lines.push('/**');
|
|
207
|
-
lines.push(' * Performs an HTTP request and returns the response.');
|
|
208
|
-
lines.push(' *');
|
|
209
|
-
lines.push(' * When `expectStream` is true, the implementation should return');
|
|
210
|
-
lines.push(' * a `StreamResponse` containing the stream data, filename (from');
|
|
211
|
-
lines.push(' * Content-Disposition header), and response headers.');
|
|
212
|
-
lines.push(' */');
|
|
213
|
-
lines.push('export type Requester = <TResponse>(');
|
|
214
|
-
lines.push(' method: string,');
|
|
215
|
-
lines.push(' path: string,');
|
|
216
|
-
lines.push(' options: {');
|
|
217
|
-
lines.push(' query?: Record<string, unknown>;');
|
|
218
|
-
lines.push(' body?: unknown;');
|
|
219
|
-
lines.push(' headers?: Record<string, string>;');
|
|
220
|
-
lines.push(' expectStream?: true;');
|
|
221
|
-
lines.push(' },');
|
|
222
|
-
lines.push(') => Promise<TResponse | StreamResponse | ErrorResponse>;');
|
|
223
|
-
lines.push('');
|
|
186
|
+
lines.push(`export { isDefinedError } from '${runtimeImportPath}';`);
|
|
187
|
+
lines.push(`export type { Requester } from '${runtimeImportPath}';`);
|
|
224
188
|
lines.push('');
|
|
225
189
|
lines.push('export function createClient(requester: Requester) {');
|
|
226
190
|
lines.push(' return {');
|
|
@@ -269,9 +233,10 @@ export function generateClient(doc, config, options) {
|
|
|
269
233
|
const resolver = new RefResolver(doc, undefined, {
|
|
270
234
|
preserveRefSiblings: options?.preserveRefSiblings,
|
|
271
235
|
});
|
|
272
|
-
const
|
|
236
|
+
const runtimeImportPath = config.runtimeImportPath ?? DEFAULT_RUNTIME_IMPORT_PATH;
|
|
237
|
+
const contracts = generateContracts(doc, resolver, runtimeImportPath);
|
|
273
238
|
const operations = analyzePaths(doc, resolver, config.methodNameStrategy ?? 'path-based');
|
|
274
|
-
const client = buildClientFile(operations, doc.openapi);
|
|
239
|
+
const client = buildClientFile(operations, doc.openapi, runtimeImportPath);
|
|
275
240
|
return { contracts, client };
|
|
276
241
|
}
|
|
277
242
|
/**
|
|
@@ -10,7 +10,6 @@ import type { OpenAPIDocument } from '../types/openapi.js';
|
|
|
10
10
|
* 4. Header parameter types per operation
|
|
11
11
|
* 5. Request body types per operation
|
|
12
12
|
* 6. Response / error types per operation
|
|
13
|
-
* 7.
|
|
14
|
-
* 7b. UnspecifiedApiError class
|
|
13
|
+
* 7. Runtime re-export block (shared classes from the genoc runtime package)
|
|
15
14
|
*/
|
|
16
|
-
export declare function generateContracts(doc: OpenAPIDocument, resolver: RefResolver): string;
|
|
15
|
+
export declare function generateContracts(doc: OpenAPIDocument, resolver: RefResolver, runtimeImportPath?: string): string;
|