genoc 0.1.3 → 0.3.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 +79 -26
- package/dist/analyzer/naming.js +3 -2
- package/dist/analyzer/path-analyzer.d.ts +4 -0
- package/dist/analyzer/path-analyzer.js +31 -1
- package/dist/analyzer/schema-mapper.d.ts +8 -0
- package/dist/analyzer/schema-mapper.js +69 -30
- package/dist/cli/app.d.ts +2 -0
- package/dist/cli/app.js +14 -0
- package/dist/cli/impl.js +11 -1
- package/dist/generator/client-generator.js +9 -44
- package/dist/generator/contracts-generator.d.ts +2 -3
- package/dist/generator/contracts-generator.js +141 -140
- package/dist/generator/method-generator.js +11 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +5 -2
- package/dist/parser/spec-reader.d.ts +3 -2
- package/dist/parser/spec-reader.js +5 -4
- 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 +2 -0
- package/dist/types/openapi.d.ts +4 -0
- package/dist/utils/generator-helpers.d.ts +60 -5
- package/dist/utils/generator-helpers.js +223 -14
- package/dist/utils/proxy.d.ts +6 -0
- package/dist/utils/proxy.js +81 -0
- package/package.json +7 -2
- package/dist/generator/error-types.d.ts +0 -23
- package/dist/generator/error-types.js +0 -87
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,14 @@ 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 |
|
|
180
|
+
| `--proxy` | (none) | HTTP(S) proxy URL for fetching specs from URLs; overrides HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars |
|
|
159
181
|
|
|
160
182
|
## Method Naming Strategies
|
|
161
183
|
|
|
@@ -168,6 +190,36 @@ genoc <spec> [flags]
|
|
|
168
190
|
- **`operationId-with-fallback`** — Use `operationId` if present, otherwise
|
|
169
191
|
fall back to path-based naming.
|
|
170
192
|
|
|
193
|
+
## Proxy Support
|
|
194
|
+
|
|
195
|
+
When fetching a spec from a URL, `genoc` can route the request through an
|
|
196
|
+
HTTP(S) proxy:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
genoc https://api.example.com/openapi.yaml --output-dir ./src/api \
|
|
200
|
+
--proxy http://user:pass@proxy.example.com:8080
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
- Credentials are supported in the proxy URL (`user:pass@host:port`).
|
|
204
|
+
- Without `--proxy`, the `HTTP_PROXY` / `HTTPS_PROXY` environment variables
|
|
205
|
+
(either casing) are respected automatically, minus `NO_PROXY` exclusions.
|
|
206
|
+
- An explicit `--proxy` flag overrides environment detection entirely.
|
|
207
|
+
- Programmatic equivalent: `proxy: 'http://proxy.example.com:8080'` in the
|
|
208
|
+
`generateClient` config, or an optional second argument `loadSpec(url, { proxy })`.
|
|
209
|
+
- SOCKS proxies and `ALL_PROXY` are not supported.
|
|
210
|
+
|
|
211
|
+
Note: if you already have `HTTP_PROXY` set in your environment, spec fetches
|
|
212
|
+
now go through it (previously ignored). Use `NO_PROXY` to opt out.
|
|
213
|
+
|
|
214
|
+
## JSDoc Documentation Output
|
|
215
|
+
|
|
216
|
+
`description`, `deprecated`, `default`, `example`/`examples`, and `title`
|
|
217
|
+
fields from your spec become JSDoc comments in the generated code — on object
|
|
218
|
+
type properties, named schemas, and client methods. Object types are always
|
|
219
|
+
emitted multi-line, so per-property JSDoc stays aligned at each nesting depth.
|
|
220
|
+
`@deprecated` tags give you editor strikethrough on deprecated fields and
|
|
221
|
+
methods for free.
|
|
222
|
+
|
|
171
223
|
## Programmatic API
|
|
172
224
|
|
|
173
225
|
```typescript
|
|
@@ -179,6 +231,7 @@ await generateClient({
|
|
|
179
231
|
methodNameStrategy: 'path-based',
|
|
180
232
|
specVersion: '3.1',
|
|
181
233
|
strictVersion: true,
|
|
234
|
+
proxy: 'http://proxy.example.com:8080',
|
|
182
235
|
});
|
|
183
236
|
```
|
|
184
237
|
|
|
@@ -194,7 +247,7 @@ union, and `isDefinedError` narrows a caught error to that union:
|
|
|
194
247
|
|
|
195
248
|
```typescript
|
|
196
249
|
import { UnspecifiedApiError, RequesterFailError } from './contracts.js';
|
|
197
|
-
import { isDefinedError } from '
|
|
250
|
+
import { isDefinedError } from 'genoc/runtime';
|
|
198
251
|
|
|
199
252
|
try {
|
|
200
253
|
const result = await client.getPets();
|
package/dist/analyzer/naming.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toPascalCaseSegment } from '../utils/case.js';
|
|
2
|
+
import { sanitizeTypeName } from '../utils/generator-helpers.js';
|
|
2
3
|
import { sanitizeIdentifier } from '../utils/string.js';
|
|
3
4
|
import { isPathParam, extractParamName } from '../utils/url.js';
|
|
4
5
|
/**
|
|
@@ -51,9 +52,9 @@ export function generateMethodName(method, path) {
|
|
|
51
52
|
}
|
|
52
53
|
const transformedSegments = segments.map((segment, index) => {
|
|
53
54
|
if (isParam[index]) {
|
|
54
|
-
return `By${toPascalCaseSegment(segment)}`;
|
|
55
|
+
return `By${sanitizeTypeName(toPascalCaseSegment(segment))}`;
|
|
55
56
|
}
|
|
56
|
-
return toPascalCaseSegment(segment);
|
|
57
|
+
return sanitizeTypeName(toPascalCaseSegment(segment));
|
|
57
58
|
});
|
|
58
59
|
return lowerMethod + transformedSegments.join('');
|
|
59
60
|
}
|
|
@@ -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;
|
|
@@ -30,6 +32,8 @@ export interface AnalyzedOperation {
|
|
|
30
32
|
path: string;
|
|
31
33
|
operationId: string | undefined;
|
|
32
34
|
methodName: string;
|
|
35
|
+
/** Deduped by analyzePaths; computed from method + path when absent. */
|
|
36
|
+
typePrefix?: string;
|
|
33
37
|
summary: string | undefined;
|
|
34
38
|
description: string | undefined;
|
|
35
39
|
deprecated: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RefResolver } from '../parser/ref-resolver.js';
|
|
2
|
-
import { sanitizeTypeName } from '../utils/generator-helpers.js';
|
|
2
|
+
import { sanitizeTypeName, getOperationTypePrefix } from '../utils/generator-helpers.js';
|
|
3
3
|
import { getMethodName } from './naming.js';
|
|
4
4
|
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'];
|
|
5
5
|
function isRef(obj) {
|
|
@@ -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) {
|
|
@@ -193,6 +195,8 @@ export function analyzePaths(doc, resolver, strategy = 'path-based') {
|
|
|
193
195
|
const operations = [];
|
|
194
196
|
if (!doc.paths)
|
|
195
197
|
return operations;
|
|
198
|
+
const usedTypePrefixes = new Set();
|
|
199
|
+
const usedMethodNames = new Set();
|
|
196
200
|
for (const [urlPath, pathItem] of Object.entries(doc.paths)) {
|
|
197
201
|
for (const method of HTTP_METHODS) {
|
|
198
202
|
const operation = pathItem[method];
|
|
@@ -219,5 +223,31 @@ export function analyzePaths(doc, resolver, strategy = 'path-based') {
|
|
|
219
223
|
});
|
|
220
224
|
}
|
|
221
225
|
}
|
|
226
|
+
// Dedupe type prefixes and method names so that distinct routes folding to
|
|
227
|
+
// the same identifier (e.g. "/weird" and "/weird-", or "/Weird" and
|
|
228
|
+
// "/weird") cannot produce duplicate exported types or client methods.
|
|
229
|
+
// NOTE: analyzePaths runs twice per generation (contracts pass + client
|
|
230
|
+
// pass); this assignment must stay deterministic - it relies on insertion
|
|
231
|
+
// order and the fixed HTTP_METHODS loop - so both passes agree.
|
|
232
|
+
for (const op of operations) {
|
|
233
|
+
const baseTypePrefix = getOperationTypePrefix(op);
|
|
234
|
+
let typePrefix = baseTypePrefix;
|
|
235
|
+
let n = 2;
|
|
236
|
+
while (usedTypePrefixes.has(typePrefix)) {
|
|
237
|
+
typePrefix = `${baseTypePrefix}${n}`;
|
|
238
|
+
n += 1;
|
|
239
|
+
}
|
|
240
|
+
usedTypePrefixes.add(typePrefix);
|
|
241
|
+
op.typePrefix = typePrefix;
|
|
242
|
+
const baseMethodName = op.methodName;
|
|
243
|
+
let methodName = baseMethodName;
|
|
244
|
+
n = 2;
|
|
245
|
+
while (usedMethodNames.has(methodName)) {
|
|
246
|
+
methodName = `${baseMethodName}${n}`;
|
|
247
|
+
n += 1;
|
|
248
|
+
}
|
|
249
|
+
usedMethodNames.add(methodName);
|
|
250
|
+
op.methodName = methodName;
|
|
251
|
+
}
|
|
222
252
|
return operations;
|
|
223
253
|
}
|
|
@@ -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"
|
|
@@ -37,6 +42,10 @@ export class SchemaMapper {
|
|
|
37
42
|
brandedTypes = new Map();
|
|
38
43
|
static nullableWarned = false;
|
|
39
44
|
constructor(resolver, typeNameGenerator, discriminatorTargets, reservedNames) {
|
|
45
|
+
// NOTE: discriminatorTargets must be keyed by names produced by the SAME
|
|
46
|
+
// (rename-aware) typeNameGenerator that resolves $refs. Passing targets
|
|
47
|
+
// keyed by raw schema names means renamed subtypes silently lose their
|
|
48
|
+
// `& { prop: 'literal' }` intersection in the generated output.
|
|
40
49
|
this.resolver = resolver;
|
|
41
50
|
this.typeNameGenerator = typeNameGenerator ?? defaultTypeNameGenerator;
|
|
42
51
|
this.discriminatorTargets = discriminatorTargets ?? new Map();
|
|
@@ -58,7 +67,7 @@ export class SchemaMapper {
|
|
|
58
67
|
return { tsType: schema ? 'unknown' : 'never', imports: [] };
|
|
59
68
|
}
|
|
60
69
|
const visited = new Set();
|
|
61
|
-
const result = this.mapInternal(schema, name, context, visited);
|
|
70
|
+
const result = this.mapInternal(schema, name, context, visited, 0);
|
|
62
71
|
if (name && this.discriminatorTargets.has(name)) {
|
|
63
72
|
const target = this.discriminatorTargets.get(name);
|
|
64
73
|
result.tsType += ` & { ${quoteKey(target.propertyName)}: '${target.literalValue}' }`;
|
|
@@ -75,7 +84,7 @@ export class SchemaMapper {
|
|
|
75
84
|
return null;
|
|
76
85
|
return brandName;
|
|
77
86
|
}
|
|
78
|
-
mapInternal(schema, name, context, visited) {
|
|
87
|
+
mapInternal(schema, name, context, visited, indent) {
|
|
79
88
|
if (isRefObject(schema)) {
|
|
80
89
|
const refStr = schema.$ref;
|
|
81
90
|
const refName = this.typeNameGenerator(refStr);
|
|
@@ -89,7 +98,7 @@ export class SchemaMapper {
|
|
|
89
98
|
if (resolved) {
|
|
90
99
|
const discInfo = this.resolveDiscriminatorInfo(resolved, refStr);
|
|
91
100
|
if (discInfo) {
|
|
92
|
-
const expanded = this.mapInternal(resolved, undefined, context, visited);
|
|
101
|
+
const expanded = this.mapInternal(resolved, undefined, context, visited, indent);
|
|
93
102
|
expanded.tsType += ` & { ${quoteKey(discInfo.propertyName)}: '${discInfo.literalValue}' }`;
|
|
94
103
|
return expanded;
|
|
95
104
|
}
|
|
@@ -121,7 +130,7 @@ export class SchemaMapper {
|
|
|
121
130
|
return { tsType, imports: [] };
|
|
122
131
|
}
|
|
123
132
|
if (s.allOf !== undefined && s.allOf.length > 0) {
|
|
124
|
-
const result = this.mapCombinator(s.allOf, '&', context, visited);
|
|
133
|
+
const result = this.mapCombinator(s.allOf, '&', context, visited, indent);
|
|
125
134
|
if (s.nullable === true) {
|
|
126
135
|
return {
|
|
127
136
|
tsType: needsParens(result.tsType)
|
|
@@ -134,8 +143,8 @@ export class SchemaMapper {
|
|
|
134
143
|
}
|
|
135
144
|
if (s.oneOf !== undefined && s.oneOf.length > 0) {
|
|
136
145
|
const result = s.discriminator
|
|
137
|
-
? this.mapDiscriminatedUnion(s.oneOf, s.discriminator, context, visited)
|
|
138
|
-
: this.mapCombinator(s.oneOf, '|', context, visited);
|
|
146
|
+
? this.mapDiscriminatedUnion(s.oneOf, s.discriminator, context, visited, indent)
|
|
147
|
+
: this.mapCombinator(s.oneOf, '|', context, visited, indent);
|
|
139
148
|
if (s.nullable === true) {
|
|
140
149
|
return {
|
|
141
150
|
tsType: needsParens(result.tsType)
|
|
@@ -148,8 +157,8 @@ export class SchemaMapper {
|
|
|
148
157
|
}
|
|
149
158
|
if (s.anyOf !== undefined && s.anyOf.length > 0) {
|
|
150
159
|
const result = s.discriminator
|
|
151
|
-
? this.mapDiscriminatedUnion(s.anyOf, s.discriminator, context, visited)
|
|
152
|
-
: this.mapCombinator(s.anyOf, '|', context, visited);
|
|
160
|
+
? this.mapDiscriminatedUnion(s.anyOf, s.discriminator, context, visited, indent)
|
|
161
|
+
: this.mapCombinator(s.anyOf, '|', context, visited, indent);
|
|
153
162
|
if (s.nullable === true) {
|
|
154
163
|
return {
|
|
155
164
|
tsType: needsParens(result.tsType)
|
|
@@ -169,7 +178,7 @@ export class SchemaMapper {
|
|
|
169
178
|
if (nonNull.length === 0) {
|
|
170
179
|
return { tsType: 'null', imports: [] };
|
|
171
180
|
}
|
|
172
|
-
const baseResult = this.mapInternal({ ...s, type: nonNull[0] }, name, context, new Set(visited));
|
|
181
|
+
const baseResult = this.mapInternal({ ...s, type: nonNull[0] }, name, context, new Set(visited), indent);
|
|
173
182
|
if (hasNull) {
|
|
174
183
|
return {
|
|
175
184
|
tsType: `${baseResult.tsType} | null`,
|
|
@@ -224,15 +233,15 @@ export class SchemaMapper {
|
|
|
224
233
|
case 'null':
|
|
225
234
|
return { tsType: 'null', imports: [] };
|
|
226
235
|
case 'array':
|
|
227
|
-
return this.mapArray(s, context, visited);
|
|
236
|
+
return this.mapArray(s, context, visited, indent);
|
|
228
237
|
case 'object':
|
|
229
|
-
return this.mapObject(s, name, context, visited);
|
|
238
|
+
return this.mapObject(s, name, context, visited, indent);
|
|
230
239
|
default:
|
|
231
240
|
return { tsType: 'unknown', imports: [] };
|
|
232
241
|
}
|
|
233
242
|
}
|
|
234
243
|
resolveDiscriminatorInfo(schema, refStr) {
|
|
235
|
-
const schemaName =
|
|
244
|
+
const schemaName = this.typeNameGenerator(refStr);
|
|
236
245
|
const target = this.discriminatorTargets.get(schemaName);
|
|
237
246
|
if (target)
|
|
238
247
|
return target;
|
|
@@ -270,8 +279,8 @@ export class SchemaMapper {
|
|
|
270
279
|
})
|
|
271
280
|
.join(' | ');
|
|
272
281
|
}
|
|
273
|
-
mapCombinator(schemas, kind, context, visited) {
|
|
274
|
-
const results = schemas.map((s) => this.mapInternal(s, undefined, context, visited));
|
|
282
|
+
mapCombinator(schemas, kind, context, visited, indent) {
|
|
283
|
+
const results = schemas.map((s) => this.mapInternal(s, undefined, context, visited, indent));
|
|
275
284
|
const allImports = [];
|
|
276
285
|
for (const r of results) {
|
|
277
286
|
allImports.push(...r.imports);
|
|
@@ -285,14 +294,14 @@ export class SchemaMapper {
|
|
|
285
294
|
});
|
|
286
295
|
return { tsType: parts.join(separator), imports: allImports };
|
|
287
296
|
}
|
|
288
|
-
mapDiscriminatedUnion(schemas, discriminator, context, visited) {
|
|
297
|
+
mapDiscriminatedUnion(schemas, discriminator, context, visited, indent) {
|
|
289
298
|
const propertyName = discriminator.propertyName;
|
|
290
299
|
const mapping = discriminator.mapping;
|
|
291
300
|
const allImports = [];
|
|
292
301
|
const parts = [];
|
|
293
302
|
for (const schema of schemas) {
|
|
294
303
|
const discriminantValue = this.resolveDiscriminantValue(schema, propertyName, mapping);
|
|
295
|
-
const variantResult = this.mapInternal(schema, undefined, context, visited);
|
|
304
|
+
const variantResult = this.mapInternal(schema, undefined, context, visited, indent);
|
|
296
305
|
allImports.push(...variantResult.imports);
|
|
297
306
|
const quotedProp = quoteKey(propertyName);
|
|
298
307
|
parts.push(`({ ${quotedProp}: '${discriminantValue}' } & ${variantResult.tsType})`);
|
|
@@ -323,14 +332,14 @@ export class SchemaMapper {
|
|
|
323
332
|
}
|
|
324
333
|
return 'unknown';
|
|
325
334
|
}
|
|
326
|
-
mapArray(schema, context, visited) {
|
|
335
|
+
mapArray(schema, context, visited, indent) {
|
|
327
336
|
if (!schema.items) {
|
|
328
337
|
return {
|
|
329
338
|
tsType: schema.nullable === true ? 'unknown[] | null' : 'unknown[]',
|
|
330
339
|
imports: [],
|
|
331
340
|
};
|
|
332
341
|
}
|
|
333
|
-
const itemResult = this.mapInternal(schema.items, undefined, context, visited);
|
|
342
|
+
const itemResult = this.mapInternal(schema.items, undefined, context, visited, indent);
|
|
334
343
|
const tsType = isComplexType(itemResult.tsType)
|
|
335
344
|
? `Array<${itemResult.tsType}>`
|
|
336
345
|
: `${itemResult.tsType}[]`;
|
|
@@ -339,7 +348,33 @@ export class SchemaMapper {
|
|
|
339
348
|
imports: itemResult.imports,
|
|
340
349
|
};
|
|
341
350
|
}
|
|
342
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Render the per-property JSDoc comment lines for an object property.
|
|
353
|
+
*
|
|
354
|
+
* Metadata comes from the property's own schema node; for a `$ref` property
|
|
355
|
+
* the resolved target's own node metadata is used (one level only — never
|
|
356
|
+
* recursed), so self-referential schemas cannot loop here.
|
|
357
|
+
*/
|
|
358
|
+
renderPropertyJsDoc(propSchema, indentLevel) {
|
|
359
|
+
let metaSchema;
|
|
360
|
+
if (isRefObject(propSchema)) {
|
|
361
|
+
try {
|
|
362
|
+
metaSchema = this.resolver.resolveSchema(propSchema);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
metaSchema = propSchema;
|
|
370
|
+
}
|
|
371
|
+
const block = buildTypeJsDoc(metaSchema);
|
|
372
|
+
if (block === '')
|
|
373
|
+
return [];
|
|
374
|
+
const ind = indentBy(indentLevel);
|
|
375
|
+
return block.split('\n').map((line) => `${ind}${line}`);
|
|
376
|
+
}
|
|
377
|
+
mapObject(schema, name, context, visited, indent) {
|
|
343
378
|
const properties = schema.properties ?? {};
|
|
344
379
|
const requiredSet = new Set(schema.required ?? []);
|
|
345
380
|
const propNames = Object.keys(properties);
|
|
@@ -354,7 +389,9 @@ export class SchemaMapper {
|
|
|
354
389
|
return false;
|
|
355
390
|
return true;
|
|
356
391
|
});
|
|
357
|
-
const
|
|
392
|
+
const ownIndent = indentBy(indent);
|
|
393
|
+
const memberIndent = indentBy(indent + 1);
|
|
394
|
+
const memberLines = [];
|
|
358
395
|
const allImports = [];
|
|
359
396
|
if (schema.discriminator) {
|
|
360
397
|
const discPropName = schema.discriminator.propertyName;
|
|
@@ -362,15 +399,16 @@ export class SchemaMapper {
|
|
|
362
399
|
if (discIndex !== -1) {
|
|
363
400
|
filteredPropNames.splice(discIndex, 1);
|
|
364
401
|
}
|
|
365
|
-
|
|
402
|
+
memberLines.push(`${memberIndent}"${discPropName}": string;`);
|
|
366
403
|
}
|
|
367
404
|
for (const propName of filteredPropNames) {
|
|
368
405
|
const propSchema = properties[propName];
|
|
369
|
-
const propResult = this.mapInternal(propSchema, undefined, context, visited);
|
|
406
|
+
const propResult = this.mapInternal(propSchema, undefined, context, visited, indent + 1);
|
|
370
407
|
allImports.push(...propResult.imports);
|
|
408
|
+
memberLines.push(...this.renderPropertyJsDoc(propSchema, indent + 1));
|
|
371
409
|
const optional = requiredSet.has(propName) ? '' : '?';
|
|
372
410
|
const quotedName = quoteKey(propName);
|
|
373
|
-
|
|
411
|
+
memberLines.push(`${memberIndent}${quotedName}${optional}: ${propResult.tsType};`);
|
|
374
412
|
}
|
|
375
413
|
const additionalProps = schema.additionalProperties;
|
|
376
414
|
let indexSignature = null;
|
|
@@ -381,11 +419,11 @@ export class SchemaMapper {
|
|
|
381
419
|
indexSignature = '[key: string]: unknown';
|
|
382
420
|
}
|
|
383
421
|
else if (additionalProps !== undefined && typeof additionalProps === 'object') {
|
|
384
|
-
const addPropResult = this.mapInternal(additionalProps, undefined, context, visited);
|
|
422
|
+
const addPropResult = this.mapInternal(additionalProps, undefined, context, visited, indent + 1);
|
|
385
423
|
allImports.push(...addPropResult.imports);
|
|
386
424
|
indexSignature = `[key: string]: ${addPropResult.tsType}`;
|
|
387
425
|
}
|
|
388
|
-
const hasProps =
|
|
426
|
+
const hasProps = memberLines.length > 0;
|
|
389
427
|
const hasIndex = indexSignature !== null;
|
|
390
428
|
let result;
|
|
391
429
|
if (!hasProps && !hasIndex) {
|
|
@@ -400,7 +438,7 @@ export class SchemaMapper {
|
|
|
400
438
|
const valueType = indexSignature.replace('[key: string]: ', '');
|
|
401
439
|
if (name) {
|
|
402
440
|
result = {
|
|
403
|
-
tsType: `{
|
|
441
|
+
tsType: `{\n${memberIndent}${indexSignature};\n${ownIndent}}`,
|
|
404
442
|
imports: allImports,
|
|
405
443
|
};
|
|
406
444
|
}
|
|
@@ -412,16 +450,17 @@ export class SchemaMapper {
|
|
|
412
450
|
}
|
|
413
451
|
}
|
|
414
452
|
else {
|
|
415
|
-
const
|
|
453
|
+
const propsBlock = `{\n${memberLines.join('\n')}\n${ownIndent}}`;
|
|
416
454
|
if (hasIndex) {
|
|
455
|
+
const indexBlock = `{\n${memberIndent}${indexSignature};\n${ownIndent}}`;
|
|
417
456
|
result = {
|
|
418
|
-
tsType:
|
|
457
|
+
tsType: `${propsBlock} & ${indexBlock}`,
|
|
419
458
|
imports: allImports,
|
|
420
459
|
};
|
|
421
460
|
}
|
|
422
461
|
else {
|
|
423
462
|
result = {
|
|
424
|
-
tsType:
|
|
463
|
+
tsType: propsBlock,
|
|
425
464
|
imports: allImports,
|
|
426
465
|
};
|
|
427
466
|
}
|
package/dist/cli/app.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ interface Flags {
|
|
|
3
3
|
methodNameStrategy: 'path-based' | 'operationId' | 'operationId-with-fallback';
|
|
4
4
|
specVersion?: string;
|
|
5
5
|
strictVersion: boolean;
|
|
6
|
+
runtimeImportPath?: string;
|
|
7
|
+
proxy?: string;
|
|
6
8
|
}
|
|
7
9
|
export type AppFlags = Flags;
|
|
8
10
|
export declare const app: import("@stricli/core").Application<import("@stricli/core").CommandContext>;
|
package/dist/cli/app.js
CHANGED
|
@@ -29,6 +29,20 @@ 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
|
+
},
|
|
39
|
+
proxy: {
|
|
40
|
+
kind: 'parsed',
|
|
41
|
+
parse: String,
|
|
42
|
+
brief: 'HTTP(S) proxy URL for fetching specs (overrides HTTP_PROXY/HTTPS_PROXY env vars)',
|
|
43
|
+
optional: true,
|
|
44
|
+
placeholder: 'url',
|
|
45
|
+
},
|
|
32
46
|
},
|
|
33
47
|
positional: {
|
|
34
48
|
kind: 'tuple',
|
package/dist/cli/impl.js
CHANGED
|
@@ -2,11 +2,20 @@ import { generateFullOutput } from '../generator/client-generator.js';
|
|
|
2
2
|
import { load } from '../parser/spec-reader.js';
|
|
3
3
|
import { validateSpec } from '../parser/validators.js';
|
|
4
4
|
import { defaultRegistry } from '../parser/version/index.js';
|
|
5
|
+
import { assertValidProxyUrl } from '../utils/proxy.js';
|
|
5
6
|
import { UserError } from './errors.js';
|
|
6
7
|
export default async function (flags, spec) {
|
|
7
8
|
try {
|
|
9
|
+
if (flags.proxy) {
|
|
10
|
+
try {
|
|
11
|
+
assertValidProxyUrl(flags.proxy);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
throw new UserError(err.message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
8
17
|
this.process.stdout.write(`Loading spec from ${spec}...\n`);
|
|
9
|
-
const doc = await load(spec);
|
|
18
|
+
const doc = await load(spec, { proxy: flags.proxy });
|
|
10
19
|
this.process.stdout.write(`Loaded OpenAPI ${doc.openapi} spec\n`);
|
|
11
20
|
const strategy = flags.specVersion
|
|
12
21
|
? defaultRegistry.get(flags.specVersion)
|
|
@@ -27,6 +36,7 @@ export default async function (flags, spec) {
|
|
|
27
36
|
methodNameStrategy: flags.methodNameStrategy || 'path-based',
|
|
28
37
|
specVersion: flags.specVersion,
|
|
29
38
|
strictVersion: flags.strictVersion,
|
|
39
|
+
runtimeImportPath: flags.runtimeImportPath,
|
|
30
40
|
};
|
|
31
41
|
const preserveRefSiblings = strategy.version() === '3.1';
|
|
32
42
|
this.process.stdout.write('Generating client...\n');
|