genoc 0.4.3 → 0.5.1
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/dist/analyzer/analyze.js +69 -27
- package/dist/analyzer/path-analyzer.d.ts +3 -1
- package/dist/analyzer/path-analyzer.js +2 -1
- package/dist/analyzer/types.d.ts +25 -0
- package/dist/cli/impl.js +3 -1
- package/dist/generator/client-generator.js +64 -4
- package/dist/generator/contracts-generator.js +7 -0
- package/dist/generator/method-generator.d.ts +9 -1
- package/dist/generator/method-generator.js +26 -0
- package/dist/parser/spec-reader.js +6 -25
- package/dist/parser/version/spec-links.d.ts +55 -0
- package/dist/parser/version/spec-links.js +65 -0
- package/dist/parser/version/v3.0/validator.d.ts +3 -1
- package/dist/parser/version/v3.0/validator.js +23 -23
- package/dist/parser/version/v3.1/validator.d.ts +3 -1
- package/dist/parser/version/v3.1/validator.js +25 -23
- package/dist/parser/version/validate-info.d.ts +9 -0
- package/dist/parser/version/validate-info.js +22 -0
- package/dist/parser/version/validate-security.d.ts +17 -0
- package/dist/parser/version/validate-security.js +218 -0
- package/dist/parser/version/version-detector.js +27 -8
- package/dist/runtime/defined-errors.d.ts +3 -2
- package/dist/runtime/defined-errors.js +6 -3
- package/dist/runtime/errors.d.ts +1 -3
- package/dist/runtime/errors.js +2 -6
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/requester.d.ts +67 -0
- package/dist/types/openapi.d.ts +5 -4
- package/dist/utils/http-methods.d.ts +8 -0
- package/dist/utils/http-methods.js +17 -0
- package/dist/utils/operation-naming.js +1 -0
- package/package.json +1 -1
package/dist/analyzer/analyze.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RefResolver } from '../parser/ref-resolver.js';
|
|
2
2
|
import { buildSchemaRenameMap, buildTypeJsDoc, sanitizeJsDocText, sanitizeTypeName, toPascalCase, } from '../utils/generator-helpers.js';
|
|
3
3
|
import { operationEmissions, RESERVED_TYPE_NAMES } from '../utils/operation-naming.js';
|
|
4
|
+
import { escapeStringLiteral } from '../utils/string.js';
|
|
4
5
|
import { buildDiscriminatorRegistry, renameAwareTypeName } from './discriminator-registry.js';
|
|
5
6
|
import { analyzePathsDetailed } from './path-analyzer.js';
|
|
6
7
|
import { SchemaMapper } from './schema-mapper.js';
|
|
@@ -151,75 +152,92 @@ function topologicalSort(entries, allNames) {
|
|
|
151
152
|
}
|
|
152
153
|
return sorted;
|
|
153
154
|
}
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Build the `key: "value"` fragments of a security scheme object — shared by
|
|
157
|
+
* the contracts TYPE renderer (`'; '` separator) and the client CONST value
|
|
158
|
+
* renderer (`', '` separator).
|
|
159
|
+
*/
|
|
160
|
+
function schemeParts(scheme, separator) {
|
|
161
|
+
const parts = [`type: '${escapeStringLiteral(scheme.type)}'`];
|
|
156
162
|
if (scheme.description) {
|
|
157
|
-
parts.push(`description:
|
|
163
|
+
parts.push(`description: '${escapeStringLiteral(scheme.description)}'`);
|
|
158
164
|
}
|
|
159
165
|
if (scheme.type === 'apiKey') {
|
|
160
166
|
if (scheme.name)
|
|
161
|
-
parts.push(`name:
|
|
167
|
+
parts.push(`name: '${escapeStringLiteral(scheme.name)}'`);
|
|
162
168
|
if (scheme.in)
|
|
163
|
-
parts.push(`in:
|
|
169
|
+
parts.push(`in: '${escapeStringLiteral(scheme.in)}'`);
|
|
164
170
|
}
|
|
165
171
|
if (scheme.type === 'http') {
|
|
166
172
|
if (scheme.scheme)
|
|
167
|
-
parts.push(`scheme:
|
|
168
|
-
if (scheme.bearerFormat)
|
|
169
|
-
parts.push(`bearerFormat:
|
|
173
|
+
parts.push(`scheme: '${escapeStringLiteral(scheme.scheme)}'`);
|
|
174
|
+
if (scheme.bearerFormat) {
|
|
175
|
+
parts.push(`bearerFormat: '${escapeStringLiteral(scheme.bearerFormat)}'`);
|
|
176
|
+
}
|
|
170
177
|
}
|
|
171
178
|
if (scheme.type === 'oauth2' && scheme.flows) {
|
|
172
179
|
const flowParts = [];
|
|
173
180
|
const flows = scheme.flows;
|
|
174
181
|
if (flows.implicit) {
|
|
175
|
-
flowParts.push(`implicit: ${oAuth2FlowToTs(flows.implicit, true)}`);
|
|
182
|
+
flowParts.push(`implicit: ${oAuth2FlowToTs(flows.implicit, true, separator)}`);
|
|
176
183
|
}
|
|
177
184
|
if (flows.password) {
|
|
178
|
-
flowParts.push(`password: ${oAuth2FlowToTs(flows.password, false)}`);
|
|
185
|
+
flowParts.push(`password: ${oAuth2FlowToTs(flows.password, false, separator)}`);
|
|
179
186
|
}
|
|
180
187
|
if (flows.clientCredentials) {
|
|
181
|
-
flowParts.push(`clientCredentials: ${oAuth2FlowToTs(flows.clientCredentials, false)}`);
|
|
188
|
+
flowParts.push(`clientCredentials: ${oAuth2FlowToTs(flows.clientCredentials, false, separator)}`);
|
|
182
189
|
}
|
|
183
190
|
if (flows.authorizationCode) {
|
|
184
|
-
flowParts.push(`authorizationCode: ${oAuth2FlowToTs(flows.authorizationCode, true)}`);
|
|
191
|
+
flowParts.push(`authorizationCode: ${oAuth2FlowToTs(flows.authorizationCode, true, separator)}`);
|
|
185
192
|
}
|
|
186
|
-
parts.push(`flows: { ${flowParts.join(
|
|
193
|
+
parts.push(`flows: { ${flowParts.join(separator)} }`);
|
|
187
194
|
}
|
|
188
195
|
if (scheme.type === 'openIdConnect' && scheme.openIdConnectUrl) {
|
|
189
|
-
parts.push(`openIdConnectUrl:
|
|
196
|
+
parts.push(`openIdConnectUrl: '${escapeStringLiteral(scheme.openIdConnectUrl)}'`);
|
|
190
197
|
}
|
|
191
|
-
return
|
|
198
|
+
return parts;
|
|
199
|
+
}
|
|
200
|
+
function securitySchemeToTsType(scheme) {
|
|
201
|
+
return `{ ${schemeParts(scheme, '; ').join('; ')} }`;
|
|
192
202
|
}
|
|
193
|
-
|
|
203
|
+
/** The scheme object as a value expression — client-file consts (`as const`). */
|
|
204
|
+
function securitySchemeConstExpression(scheme) {
|
|
205
|
+
return `{ ${schemeParts(scheme, ', ').join(', ')} } as const`;
|
|
206
|
+
}
|
|
207
|
+
function oAuth2FlowToTs(flow, hasAuthUrl, separator) {
|
|
194
208
|
const entries = [];
|
|
195
209
|
if (hasAuthUrl && flow.authorizationUrl) {
|
|
196
|
-
entries.push(`authorizationUrl:
|
|
210
|
+
entries.push(`authorizationUrl: '${escapeStringLiteral(flow.authorizationUrl)}'`);
|
|
197
211
|
}
|
|
198
212
|
if (flow.tokenUrl) {
|
|
199
|
-
entries.push(`tokenUrl:
|
|
213
|
+
entries.push(`tokenUrl: '${escapeStringLiteral(flow.tokenUrl)}'`);
|
|
200
214
|
}
|
|
201
215
|
if (flow.refreshUrl) {
|
|
202
|
-
entries.push(`refreshUrl:
|
|
216
|
+
entries.push(`refreshUrl: '${escapeStringLiteral(flow.refreshUrl)}'`);
|
|
203
217
|
}
|
|
204
218
|
const scopeEntries = Object.entries(flow.scopes)
|
|
205
|
-
.map(([k, v]) => `
|
|
206
|
-
.join(
|
|
219
|
+
.map(([k, v]) => `'${escapeStringLiteral(k)}': '${escapeStringLiteral(v)}'`)
|
|
220
|
+
.join(separator);
|
|
207
221
|
entries.push(`scopes: { ${scopeEntries} }`);
|
|
208
|
-
return `{ ${entries.join(
|
|
222
|
+
return `{ ${entries.join(separator)} }`;
|
|
209
223
|
}
|
|
210
224
|
/**
|
|
211
225
|
* Translate `components.securitySchemes` into finished `{Name}Auth` type
|
|
212
226
|
* declarations: sanitize + PascalCase the key, number collisions, render the
|
|
213
|
-
* object type text and the description JSDoc. Spec
|
|
227
|
+
* object type text, the client-file const, and the description JSDoc. Spec
|
|
228
|
+
* key order preserved; `$ref` entries resolved through the resolver.
|
|
214
229
|
*/
|
|
215
|
-
function analyzeSecuritySchemes(securitySchemes) {
|
|
230
|
+
function analyzeSecuritySchemes(securitySchemes, resolver) {
|
|
216
231
|
const entries = [];
|
|
217
232
|
if (!securitySchemes || Object.keys(securitySchemes).length === 0) {
|
|
218
233
|
return entries;
|
|
219
234
|
}
|
|
220
235
|
const usedSecurityTypeNames = new Set();
|
|
221
|
-
|
|
222
|
-
|
|
236
|
+
const usedConstNames = new Set();
|
|
237
|
+
for (const [schemeName, rawScheme] of Object.entries(securitySchemes)) {
|
|
238
|
+
const scheme = resolver.resolve(rawScheme);
|
|
239
|
+
const pascal = sanitizeTypeName(toPascalCase(schemeName));
|
|
240
|
+
const baseTypeName = `${pascal}Auth`;
|
|
223
241
|
let typeName = baseTypeName;
|
|
224
242
|
let n = 2;
|
|
225
243
|
while (usedSecurityTypeNames.has(typeName)) {
|
|
@@ -227,14 +245,36 @@ function analyzeSecuritySchemes(securitySchemes) {
|
|
|
227
245
|
n += 1;
|
|
228
246
|
}
|
|
229
247
|
usedSecurityTypeNames.add(typeName);
|
|
248
|
+
const baseConstName = `${pascal.charAt(0).toLowerCase()}${pascal.slice(1)}Scheme`;
|
|
249
|
+
let constName = baseConstName;
|
|
250
|
+
n = 2;
|
|
251
|
+
while (usedConstNames.has(constName)) {
|
|
252
|
+
constName = `${baseConstName}${n}`;
|
|
253
|
+
n += 1;
|
|
254
|
+
}
|
|
255
|
+
usedConstNames.add(constName);
|
|
230
256
|
entries.push({
|
|
257
|
+
schemeName,
|
|
231
258
|
name: typeName,
|
|
232
259
|
tsType: securitySchemeToTsType(scheme),
|
|
233
260
|
jsDoc: buildDescriptionJsDoc(scheme.description),
|
|
261
|
+
constName,
|
|
262
|
+
constExpression: securitySchemeConstExpression(scheme),
|
|
234
263
|
});
|
|
235
264
|
}
|
|
236
265
|
return entries;
|
|
237
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Resolve effective `security` requirements into per-call facts: each spec
|
|
269
|
+
* requirement object becomes one AND-list of `{ schemeName, scopes }` calls,
|
|
270
|
+
* `{}` becoming the empty (anonymous-access) alternative. Scheme names are
|
|
271
|
+
* passed through unfiltered; the client generator degrades gracefully if a
|
|
272
|
+
* name has no scheme (unreachable in practice — validation rejects unknown
|
|
273
|
+
* names before analysis runs).
|
|
274
|
+
*/
|
|
275
|
+
function analyzeSecurityRequirements(requirements) {
|
|
276
|
+
return requirements.map((requirement) => Object.entries(requirement).map(([schemeName, scopes]) => ({ schemeName, scopes })));
|
|
277
|
+
}
|
|
238
278
|
/**
|
|
239
279
|
* Translate `doc.servers` into finished server variable interface
|
|
240
280
|
* declarations, one per server that declares variables. Names are index-aware
|
|
@@ -406,9 +446,11 @@ export function analyze(doc, opts = {}) {
|
|
|
406
446
|
if (pathParamTypeNames.size > 0) {
|
|
407
447
|
op.pathParamTypeNames = [...pathParamTypeNames];
|
|
408
448
|
}
|
|
449
|
+
const effectiveSecurity = op.security ?? doc.security;
|
|
409
450
|
operations.push({
|
|
410
451
|
...op,
|
|
411
452
|
contractsLines: [],
|
|
453
|
+
securityRequirements: effectiveSecurity === undefined ? [] : analyzeSecurityRequirements(effectiveSecurity),
|
|
412
454
|
});
|
|
413
455
|
}
|
|
414
456
|
const hasFileUpload = operations.some((op) => op.requestBody?.isMultipart);
|
|
@@ -548,7 +590,7 @@ export function analyze(doc, opts = {}) {
|
|
|
548
590
|
// Branded types snapshot after every mapSchema call has run, mirroring the
|
|
549
591
|
// pre-refactor read position at the end of generateContracts.
|
|
550
592
|
const brandedTypes = Array.from(mapper.getBrandedTypes().values());
|
|
551
|
-
const securitySchemeTypes = analyzeSecuritySchemes(doc.components?.securitySchemes);
|
|
593
|
+
const securitySchemeTypes = analyzeSecuritySchemes(doc.components?.securitySchemes, resolver);
|
|
552
594
|
const serverTypes = analyzeServers(doc.servers);
|
|
553
595
|
return {
|
|
554
596
|
specVersion: doc.openapi,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RefResolver } from '../parser/ref-resolver.js';
|
|
2
2
|
import type { MethodNameStrategy } from '../types/client.js';
|
|
3
|
-
import type { OpenAPIDocument, ReferenceObject, SchemaObject } from '../types/openapi.js';
|
|
3
|
+
import type { OpenAPIDocument, ReferenceObject, SchemaObject, SecurityRequirementObject } from '../types/openapi.js';
|
|
4
4
|
export interface AnalyzedParameter {
|
|
5
5
|
name: string;
|
|
6
6
|
in: 'path' | 'query' | 'header' | 'cookie';
|
|
@@ -79,6 +79,8 @@ export interface AnalyzedOperation {
|
|
|
79
79
|
pathParamTypeNames?: string[];
|
|
80
80
|
requestBody: AnalyzedRequestBody | undefined;
|
|
81
81
|
responses: AnalyzedResponse[];
|
|
82
|
+
/** Raw operation-level `security`; `undefined` = inherit the document default. */
|
|
83
|
+
security?: SecurityRequirementObject[];
|
|
82
84
|
}
|
|
83
85
|
/**
|
|
84
86
|
* Analyzer-internal analysis result: the analyzed operations plus the raw
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { RefResolver } from '../parser/ref-resolver.js';
|
|
2
2
|
import { isBinaryContentType } from '../utils/generator-helpers.js';
|
|
3
|
+
import { HTTP_METHODS } from '../utils/http-methods.js';
|
|
3
4
|
import { getOperationTypePrefix } from '../utils/operation-naming.js';
|
|
4
5
|
import { getMethodName } from './naming.js';
|
|
5
|
-
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'];
|
|
6
6
|
/**
|
|
7
7
|
* Resolve a possibly-$ref-ed schema and check for an exact top-level `format: binary`
|
|
8
8
|
* match. Top-level only — intentionally no deep-walk into items/allOf/oneOf/anyOf.
|
|
@@ -183,6 +183,7 @@ export function analyzePathsDetailed(doc, resolver, strategy = 'path-based') {
|
|
|
183
183
|
...categorized,
|
|
184
184
|
requestBody: requestBodyResult?.requestBody,
|
|
185
185
|
responses,
|
|
186
|
+
security: operation.security,
|
|
186
187
|
});
|
|
187
188
|
requestBodySchemas.push(requestBodyResult?.rawSchema);
|
|
188
189
|
}
|
package/dist/analyzer/types.d.ts
CHANGED
|
@@ -20,12 +20,29 @@ export interface TypeDeclaration {
|
|
|
20
20
|
* and the description-only JSDoc block ('' when absent).
|
|
21
21
|
*/
|
|
22
22
|
export interface SecuritySchemeTypeDeclaration {
|
|
23
|
+
/** Spec key under `components.securitySchemes` — the requirement lookup name. */
|
|
24
|
+
schemeName: string;
|
|
23
25
|
/** Emitted `{Name}Auth` type name (sanitized, numbered on collision). */
|
|
24
26
|
name: string;
|
|
25
27
|
/** Rendered object type text — the `securitySchemeToTsType` output. */
|
|
26
28
|
tsType: string;
|
|
27
29
|
/** Description JSDoc block; '' when the scheme carries no description. */
|
|
28
30
|
jsDoc: string;
|
|
31
|
+
/**
|
|
32
|
+
* Client-file const identifier holding the full scheme literal. Inline
|
|
33
|
+
* `scheme:` values in method bodies reference it — one const per scheme
|
|
34
|
+
* caps oauth2-flows bloat at a single site.
|
|
35
|
+
*/
|
|
36
|
+
constName: string;
|
|
37
|
+
/** `{ … } as const` — the scheme object as a value expression. */
|
|
38
|
+
constExpression: string;
|
|
39
|
+
}
|
|
40
|
+
/** One scheme invocation inside an effective security requirement. */
|
|
41
|
+
export interface AnalyzedSecurityCall {
|
|
42
|
+
/** Scheme name as declared in `components.securitySchemes` (spec key). */
|
|
43
|
+
schemeName: string;
|
|
44
|
+
/** Requested scopes/roles; empty for non-oauth2/openIdConnect schemes. */
|
|
45
|
+
scopes: string[];
|
|
29
46
|
}
|
|
30
47
|
/**
|
|
31
48
|
* Finished server variable type declaration (contracts Section 1c). Names
|
|
@@ -81,6 +98,14 @@ export interface FinishedOperation extends AnalyzedOperation {
|
|
|
81
98
|
* emission). Sole fact source for the client generator's FormData appends.
|
|
82
99
|
*/
|
|
83
100
|
fileUploadProperties?: FileUploadPropertyFact[];
|
|
101
|
+
/**
|
|
102
|
+
* Effective security requirements (operation override ?? document default
|
|
103
|
+
* ?? none): an OR-list of AND-lists. Each call names its scheme by spec
|
|
104
|
+
* key; the client generator resolves it to a `securitySchemeTypes` const.
|
|
105
|
+
* `[]` = public operation (explicitly or by absence). An empty AND-list is
|
|
106
|
+
* the spec's `{}` — "anonymous access acceptable" alternative.
|
|
107
|
+
*/
|
|
108
|
+
securityRequirements: AnalyzedSecurityCall[][];
|
|
84
109
|
}
|
|
85
110
|
/**
|
|
86
111
|
* Single-pass analysis model of an OpenAPI document — the ONLY input every
|
package/dist/cli/impl.js
CHANGED
|
@@ -138,12 +138,14 @@ async function runOneTarget(target, context) {
|
|
|
138
138
|
}
|
|
139
139
|
context.process.stdout.write(`Loading spec from ${target.input}...\n`);
|
|
140
140
|
const doc = await load(target.input, { proxy: target.proxy });
|
|
141
|
-
context.process.stdout.write(`Loaded OpenAPI ${doc.openapi} spec\n`);
|
|
142
141
|
const override = target.specVersion;
|
|
143
142
|
if (override !== undefined && override !== '3.0' && override !== '3.1') {
|
|
144
143
|
throw new Error(`No strategy registered for version: ${override}`);
|
|
145
144
|
}
|
|
145
|
+
// Resolve the dialect before logging so a missing/malformed `openapi`
|
|
146
|
+
// field is reported (with its spec link) instead of logging "undefined".
|
|
146
147
|
const profile = resolveVersion(doc, override);
|
|
148
|
+
context.process.stdout.write(`Loaded OpenAPI ${doc.openapi} spec\n`);
|
|
147
149
|
if (override !== undefined &&
|
|
148
150
|
target.strictVersion !== false &&
|
|
149
151
|
profile.detected !== profile.effective) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_RUNTIME_IMPORT_PATH, makeHeader } from '../utils/generator-helpers.js';
|
|
2
2
|
import { clientImportedNames, clientValueImports, getErrorType, getSuccessType, operationEmissions, } from '../utils/operation-naming.js';
|
|
3
|
+
import { escapeStringLiteral } from '../utils/string.js';
|
|
3
4
|
import { renderContracts } from './contracts-generator.js';
|
|
4
5
|
import { generateMethod } from './method-generator.js';
|
|
5
6
|
function collectImportTypes(operations) {
|
|
@@ -12,7 +13,34 @@ function collectImportTypes(operations) {
|
|
|
12
13
|
// oxlint-disable-next-line unicorn/no-array-sort
|
|
13
14
|
return [...types].sort();
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Render the `security` option value for a call: `[[call, …], …]` — an
|
|
18
|
+
* OR-list of AND-lists, each call naming its scheme and pointing at the
|
|
19
|
+
* file-level scheme const. Returns `undefined` for public operations (option
|
|
20
|
+
* omitted) and when any scheme name is missing from the declared schemes
|
|
21
|
+
* (unreachable post-validation; degrades to omission rather than emitting a
|
|
22
|
+
* dangling reference).
|
|
23
|
+
*/
|
|
24
|
+
function buildSecurityExpression(requirements, schemeConsts) {
|
|
25
|
+
if (requirements.length === 0) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const alternatives = [];
|
|
29
|
+
for (const requirement of requirements) {
|
|
30
|
+
const calls = [];
|
|
31
|
+
for (const call of requirement) {
|
|
32
|
+
const scheme = schemeConsts.get(call.schemeName);
|
|
33
|
+
if (scheme === undefined) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const scopes = call.scopes.map((scope) => `'${escapeStringLiteral(scope)}'`).join(', ');
|
|
37
|
+
calls.push(`{ schemeName: '${escapeStringLiteral(call.schemeName)}', scopes: [${scopes}], scheme: ${scheme.constName} }`);
|
|
38
|
+
}
|
|
39
|
+
alternatives.push(`[${calls.join(', ')}]`);
|
|
40
|
+
}
|
|
41
|
+
return `[${alternatives.join(', ')}]`;
|
|
42
|
+
}
|
|
43
|
+
function buildClientMethodBody(op, schemeConsts) {
|
|
16
44
|
const successType = getSuccessType(op);
|
|
17
45
|
const emissions = operationEmissions(op);
|
|
18
46
|
let urlTemplate = op.path;
|
|
@@ -67,6 +95,10 @@ function buildClientMethodBody(op) {
|
|
|
67
95
|
if (op.headerParams.length > 0) {
|
|
68
96
|
opts.push('headers');
|
|
69
97
|
}
|
|
98
|
+
const securityExpression = buildSecurityExpression(op.securityRequirements, schemeConsts);
|
|
99
|
+
if (securityExpression !== undefined) {
|
|
100
|
+
opts.push(`security: ${securityExpression}`);
|
|
101
|
+
}
|
|
70
102
|
const lines = [];
|
|
71
103
|
if (op.requestBody?.isMultipart && op.fileUploadProperties !== undefined) {
|
|
72
104
|
const bodyRequired = op.requestBody.required;
|
|
@@ -123,7 +155,23 @@ function buildClientMethodBody(op) {
|
|
|
123
155
|
}
|
|
124
156
|
return tryCatchLines.join('\n');
|
|
125
157
|
}
|
|
126
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Collect the scheme consts actually referenced by some operation's effective
|
|
160
|
+
* security — spec declaration order preserved. Schemes no operation uses are
|
|
161
|
+
* not emitted (dead weight in the client file).
|
|
162
|
+
*/
|
|
163
|
+
function collectUsedSchemeConsts(operations, securitySchemeTypes) {
|
|
164
|
+
const used = new Set();
|
|
165
|
+
for (const op of operations) {
|
|
166
|
+
for (const requirement of op.securityRequirements) {
|
|
167
|
+
for (const call of requirement) {
|
|
168
|
+
used.add(call.schemeName);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return securitySchemeTypes.filter((scheme) => used.has(scheme.schemeName));
|
|
173
|
+
}
|
|
174
|
+
function buildClientFile(operations, securitySchemeTypes, version, runtimeImportPath = DEFAULT_RUNTIME_IMPORT_PATH) {
|
|
127
175
|
const lines = [];
|
|
128
176
|
lines.push(makeHeader(version));
|
|
129
177
|
const importTypes = collectImportTypes(operations);
|
|
@@ -142,6 +190,18 @@ function buildClientFile(operations, version, runtimeImportPath = DEFAULT_RUNTIM
|
|
|
142
190
|
lines.push('');
|
|
143
191
|
lines.push(`export { isDefinedError } from '${runtimeImportPath}';`);
|
|
144
192
|
lines.push(`export type { Requester } from '${runtimeImportPath}';`);
|
|
193
|
+
const schemeConsts = new Map(securitySchemeTypes.map((scheme) => [scheme.schemeName, scheme]));
|
|
194
|
+
const usedSchemes = collectUsedSchemeConsts(operations, securitySchemeTypes);
|
|
195
|
+
if (usedSchemes.length > 0) {
|
|
196
|
+
lines.push('');
|
|
197
|
+
lines.push('/**');
|
|
198
|
+
lines.push(' * Security schemes (from `components.securitySchemes`) referenced by operations below.');
|
|
199
|
+
lines.push(' * Passed to the requester via the `security` option — auth decisions stay in user land.');
|
|
200
|
+
lines.push(' */');
|
|
201
|
+
for (const scheme of usedSchemes) {
|
|
202
|
+
lines.push(`const ${scheme.constName} = ${scheme.constExpression};`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
145
205
|
lines.push('');
|
|
146
206
|
lines.push('export function createClient(requester: Requester) {');
|
|
147
207
|
lines.push(' return {');
|
|
@@ -155,7 +215,7 @@ function buildClientFile(operations, version, runtimeImportPath = DEFAULT_RUNTIM
|
|
|
155
215
|
.join('\n');
|
|
156
216
|
lines.push(indentedJsDoc);
|
|
157
217
|
}
|
|
158
|
-
const body = buildClientMethodBody(op);
|
|
218
|
+
const body = buildClientMethodBody(op, schemeConsts);
|
|
159
219
|
const sig = method.signature;
|
|
160
220
|
const firstParen = sig.indexOf('(');
|
|
161
221
|
const closeParenColon = sig.indexOf('): ');
|
|
@@ -202,7 +262,7 @@ function buildIndexFile(version) {
|
|
|
202
262
|
*/
|
|
203
263
|
export function renderClient(analyzed, config) {
|
|
204
264
|
const runtimeImportPath = config.runtimeImportPath ?? DEFAULT_RUNTIME_IMPORT_PATH;
|
|
205
|
-
const client = buildClientFile(analyzed.operations, analyzed.specVersion, runtimeImportPath);
|
|
265
|
+
const client = buildClientFile(analyzed.operations, analyzed.securitySchemeTypes, analyzed.specVersion, runtimeImportPath);
|
|
206
266
|
const index = buildIndexFile(analyzed.specVersion);
|
|
207
267
|
return { client, index };
|
|
208
268
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_RUNTIME_IMPORT_PATH, makeHeader } from '../utils/generator-helpers.js';
|
|
2
2
|
import { RUNTIME_CLASS_NAMES } from '../utils/operation-naming.js';
|
|
3
|
+
import { escapeStringLiteral } from '../utils/string.js';
|
|
3
4
|
/**
|
|
4
5
|
* Build the import + re-export block for the shared runtime classes.
|
|
5
6
|
* Generated code re-uses the genoc runtime as the single source of truth
|
|
@@ -53,6 +54,12 @@ export function renderContracts(analyzed, runtimeImportPath = DEFAULT_RUNTIME_IM
|
|
|
53
54
|
lines.push('');
|
|
54
55
|
lines.push(`export type ${entry.name} = ${entry.tsType};`);
|
|
55
56
|
}
|
|
57
|
+
const nameMembers = analyzed.securitySchemeTypes
|
|
58
|
+
.map((entry) => `'${escapeStringLiteral(entry.schemeName)}'`)
|
|
59
|
+
.join(' | ');
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push('/** Names of the security schemes declared in `components.securitySchemes` (spec keys). */');
|
|
62
|
+
lines.push(`export type SecuritySchemeName = ${nameMembers};`);
|
|
56
63
|
if (analyzed.securitySchemeTypes.length > 1) {
|
|
57
64
|
const unionMembers = analyzed.securitySchemeTypes.map((entry) => entry.name).join(' | ');
|
|
58
65
|
lines.push('');
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { AnalyzedOperation } from '../analyzer/path-analyzer.js';
|
|
2
|
+
import type { FinishedOperation } from '../analyzer/types.js';
|
|
2
3
|
import type { GeneratedMethod } from '../types/client.js';
|
|
4
|
+
/**
|
|
5
|
+
* Method rendering input: an analyzed operation plus the finished security
|
|
6
|
+
* facts when available (`analyze()` output; bare `AnalyzedOperation`s from
|
|
7
|
+
* path analysis render without the `@security` tag).
|
|
8
|
+
*/
|
|
9
|
+
type MethodOperation = AnalyzedOperation & Partial<Pick<FinishedOperation, 'securityRequirements'>>;
|
|
3
10
|
/**
|
|
4
11
|
* Generate a client method from an analyzed OpenAPI operation.
|
|
5
12
|
*
|
|
6
13
|
* @param op - The analyzed operation
|
|
7
14
|
* @returns Generated method with name, JSDoc, and signature
|
|
8
15
|
*/
|
|
9
|
-
export declare function generateMethod(op:
|
|
16
|
+
export declare function generateMethod(op: MethodOperation): GeneratedMethod;
|
|
17
|
+
export {};
|
|
@@ -94,6 +94,13 @@ function buildJsDoc(op) {
|
|
|
94
94
|
}
|
|
95
95
|
lines.push(` * @param body — request body`);
|
|
96
96
|
}
|
|
97
|
+
const securitySummary = renderSecuritySummary(op.securityRequirements);
|
|
98
|
+
if (securitySummary !== undefined) {
|
|
99
|
+
if (lines.length > 0) {
|
|
100
|
+
lines.push(' *');
|
|
101
|
+
}
|
|
102
|
+
lines.push(` * @security ${sanitizeJsDocText(securitySummary)}`);
|
|
103
|
+
}
|
|
97
104
|
if (op.tags && op.tags.length > 0) {
|
|
98
105
|
if (lines.length > 0) {
|
|
99
106
|
lines.push(' *');
|
|
@@ -122,6 +129,25 @@ function buildJsDoc(op) {
|
|
|
122
129
|
}
|
|
123
130
|
return `/**\n${lines.join('\n')}\n */`;
|
|
124
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Render the `@security` tag text: OR-joined alternatives, AND-joined schemes
|
|
134
|
+
* within one alternative (parenthesized), scopes as `name[scope, …]`. An
|
|
135
|
+
* empty alternative is the spec's `{}` — rendered as `anonymous`. Returns
|
|
136
|
+
* `undefined` for public operations (no tag emitted).
|
|
137
|
+
*/
|
|
138
|
+
function renderSecuritySummary(requirements) {
|
|
139
|
+
if (requirements === undefined || requirements.length === 0) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const alternatives = requirements.map((requirement) => {
|
|
143
|
+
const calls = requirement.map((call) => call.scopes.length > 0 ? `${call.schemeName}[${call.scopes.join(', ')}]` : call.schemeName);
|
|
144
|
+
if (calls.length === 0) {
|
|
145
|
+
return 'anonymous';
|
|
146
|
+
}
|
|
147
|
+
return calls.length > 1 ? `(${calls.join(' AND ')})` : (calls[0] ?? '');
|
|
148
|
+
});
|
|
149
|
+
return alternatives.join(' OR ');
|
|
150
|
+
}
|
|
125
151
|
/**
|
|
126
152
|
* Generate a client method from an analyzed OpenAPI operation.
|
|
127
153
|
*
|
|
@@ -5,23 +5,6 @@ import { fetchSpec } from '../utils/proxy.js';
|
|
|
5
5
|
import { isUrl } from '../utils/url.js';
|
|
6
6
|
const VALID_EXTENSIONS = ['.json', '.yaml', '.yml'];
|
|
7
7
|
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
8
|
-
function assertSupportedVersion(doc) {
|
|
9
|
-
if (typeof doc !== 'object' ||
|
|
10
|
-
doc === null ||
|
|
11
|
-
!('openapi' in doc) ||
|
|
12
|
-
typeof doc.openapi !== 'string') {
|
|
13
|
-
throw new Error("Invalid spec: missing or invalid 'openapi' field.");
|
|
14
|
-
}
|
|
15
|
-
const version = doc.openapi;
|
|
16
|
-
// Check for valid version format
|
|
17
|
-
if (!version.match(/^\d+\.\d+(\.\d+)?$/)) {
|
|
18
|
-
throw new Error(`Invalid OpenAPI version format: ${version}`);
|
|
19
|
-
}
|
|
20
|
-
// Check that major version is 3 (but don't restrict minor versions)
|
|
21
|
-
if (!version.startsWith('3.')) {
|
|
22
|
-
throw new Error(`Unsupported OpenAPI version: ${version}. Supported versions: 3.0, 3.1, 3.2`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
8
|
function parseContent(content, ext) {
|
|
26
9
|
if (ext === '.json') {
|
|
27
10
|
try {
|
|
@@ -69,10 +52,10 @@ export async function loadFromFile(filePath) {
|
|
|
69
52
|
throw new Error(`Failed to load spec from file: ${err.message}`, { cause: err });
|
|
70
53
|
}
|
|
71
54
|
const doc = parseContent(content, ext);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
55
|
+
// Version/dialect rules — including whether the parsed root is even an
|
|
56
|
+
// OpenAPI document — belong to the version module, which resolves the
|
|
57
|
+
// dialect and links spec violations to the specification. The cast is safe
|
|
58
|
+
// for the same I/O reason as above: callers validate via resolveVersion.
|
|
76
59
|
return doc;
|
|
77
60
|
}
|
|
78
61
|
export async function loadFromUrl(url, opts) {
|
|
@@ -103,10 +86,8 @@ export async function loadFromUrl(url, opts) {
|
|
|
103
86
|
throw new Error(`Failed to parse spec from URL as JSON or YAML: ${yamlErr.message}`, { cause: yamlErr });
|
|
104
87
|
}
|
|
105
88
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
assertSupportedVersion(doc);
|
|
89
|
+
// Version/dialect rules — including document-rootedness — belong to the
|
|
90
|
+
// version module (see loadFromFile).
|
|
110
91
|
return doc;
|
|
111
92
|
}
|
|
112
93
|
export async function load(source, opts) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of links from spec-violation errors to the section of the
|
|
3
|
+
* OpenAPI specification that states the rule being violated.
|
|
4
|
+
*
|
|
5
|
+
* Unsupported-feature errors (Swagger 2.0, OpenAPI 3.2, external `$ref`s)
|
|
6
|
+
* never use this module: the document is valid and the error cites a genoc
|
|
7
|
+
* limitation, not a specification rule.
|
|
8
|
+
*/
|
|
9
|
+
/** Latest patch release per dialect. */
|
|
10
|
+
export declare const SPEC_PAGE: Record<'3.0' | '3.1', string>;
|
|
11
|
+
/**
|
|
12
|
+
* One anchor per violated rule, shared by both dialect pages. A wrong anchor
|
|
13
|
+
* is a one-line fix here; no automated anchor checking exists.
|
|
14
|
+
*/
|
|
15
|
+
export declare const SPEC_ANCHORS: {
|
|
16
|
+
readonly 'document-root': "openapi-object";
|
|
17
|
+
readonly 'openapi-field': "oas-version";
|
|
18
|
+
readonly 'info-object': "info-object";
|
|
19
|
+
readonly 'info-title': "info-title";
|
|
20
|
+
readonly 'info-version': "info-version";
|
|
21
|
+
readonly 'paths-object': "paths-object";
|
|
22
|
+
readonly 'path-item-object': "path-item-object";
|
|
23
|
+
readonly 'components-security-schemes': "security-scheme-object";
|
|
24
|
+
readonly 'components-schemas': "components-schemas";
|
|
25
|
+
readonly 'schema-object': "schema-object";
|
|
26
|
+
readonly 'security-scheme-object': "security-scheme-object-0";
|
|
27
|
+
readonly 'security-scheme-type': "security-scheme-type";
|
|
28
|
+
readonly 'security-scheme-name': "security-scheme-name";
|
|
29
|
+
readonly 'security-scheme-in': "security-scheme-in";
|
|
30
|
+
readonly 'security-scheme-scheme': "security-scheme-scheme";
|
|
31
|
+
readonly 'security-scheme-flows': "security-scheme-flows";
|
|
32
|
+
readonly 'security-scheme-open-id-connect-url': "security-scheme-open-id-connect-url";
|
|
33
|
+
readonly 'oauth-flow-object': "oauth-flow-object";
|
|
34
|
+
readonly 'oauth-flow-scopes': "oauth-flow-scopes";
|
|
35
|
+
readonly 'reference-object': "reference-object";
|
|
36
|
+
readonly 'security-requirement-object': "security-requirement-object";
|
|
37
|
+
};
|
|
38
|
+
export type SpecLinkKey = keyof typeof SPEC_ANCHORS;
|
|
39
|
+
export declare function specLinkUrl(version: '3.0' | '3.1', key: SpecLinkKey): string;
|
|
40
|
+
/**
|
|
41
|
+
* Renders a spec-violation message with its link line appended on its own
|
|
42
|
+
* line, indented four spaces so it aligns under the ` - ` bullet that
|
|
43
|
+
* `validateSpec` puts each message in.
|
|
44
|
+
*/
|
|
45
|
+
export declare function specViolation(version: '3.0' | '3.1', key: SpecLinkKey, message: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Detection error for a document that violates the specification itself
|
|
48
|
+
* (as opposed to an unsupported-but-valid dialect). Carries the anchor key
|
|
49
|
+
* so `resolveVersion` can render the link against the dialect being applied.
|
|
50
|
+
*/
|
|
51
|
+
export declare class SpecViolationError extends Error {
|
|
52
|
+
/** Which specification section states the violated rule. */
|
|
53
|
+
linkKey: SpecLinkKey;
|
|
54
|
+
constructor(message: string, linkKey: SpecLinkKey);
|
|
55
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of links from spec-violation errors to the section of the
|
|
3
|
+
* OpenAPI specification that states the rule being violated.
|
|
4
|
+
*
|
|
5
|
+
* Unsupported-feature errors (Swagger 2.0, OpenAPI 3.2, external `$ref`s)
|
|
6
|
+
* never use this module: the document is valid and the error cites a genoc
|
|
7
|
+
* limitation, not a specification rule.
|
|
8
|
+
*/
|
|
9
|
+
/** Latest patch release per dialect. */
|
|
10
|
+
export const SPEC_PAGE = {
|
|
11
|
+
'3.0': 'https://spec.openapis.org/oas/v3.0.4.html',
|
|
12
|
+
'3.1': 'https://spec.openapis.org/oas/v3.1.1.html',
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* One anchor per violated rule, shared by both dialect pages. A wrong anchor
|
|
16
|
+
* is a one-line fix here; no automated anchor checking exists.
|
|
17
|
+
*/
|
|
18
|
+
export const SPEC_ANCHORS = {
|
|
19
|
+
'document-root': 'openapi-object',
|
|
20
|
+
'openapi-field': 'oas-version',
|
|
21
|
+
'info-object': 'info-object',
|
|
22
|
+
'info-title': 'info-title',
|
|
23
|
+
'info-version': 'info-version',
|
|
24
|
+
'paths-object': 'paths-object',
|
|
25
|
+
'path-item-object': 'path-item-object',
|
|
26
|
+
'components-security-schemes': 'security-scheme-object',
|
|
27
|
+
'components-schemas': 'components-schemas',
|
|
28
|
+
'schema-object': 'schema-object',
|
|
29
|
+
'security-scheme-object': 'security-scheme-object-0',
|
|
30
|
+
'security-scheme-type': 'security-scheme-type',
|
|
31
|
+
'security-scheme-name': 'security-scheme-name',
|
|
32
|
+
'security-scheme-in': 'security-scheme-in',
|
|
33
|
+
'security-scheme-scheme': 'security-scheme-scheme',
|
|
34
|
+
'security-scheme-flows': 'security-scheme-flows',
|
|
35
|
+
'security-scheme-open-id-connect-url': 'security-scheme-open-id-connect-url',
|
|
36
|
+
'oauth-flow-object': 'oauth-flow-object',
|
|
37
|
+
'oauth-flow-scopes': 'oauth-flow-scopes',
|
|
38
|
+
'reference-object': 'reference-object',
|
|
39
|
+
'security-requirement-object': 'security-requirement-object',
|
|
40
|
+
};
|
|
41
|
+
export function specLinkUrl(version, key) {
|
|
42
|
+
return `${SPEC_PAGE[version]}#${SPEC_ANCHORS[key]}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Renders a spec-violation message with its link line appended on its own
|
|
46
|
+
* line, indented four spaces so it aligns under the ` - ` bullet that
|
|
47
|
+
* `validateSpec` puts each message in.
|
|
48
|
+
*/
|
|
49
|
+
export function specViolation(version, key, message) {
|
|
50
|
+
return `${message}\n ${specLinkUrl(version, key)}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Detection error for a document that violates the specification itself
|
|
54
|
+
* (as opposed to an unsupported-but-valid dialect). Carries the anchor key
|
|
55
|
+
* so `resolveVersion` can render the link against the dialect being applied.
|
|
56
|
+
*/
|
|
57
|
+
export class SpecViolationError extends Error {
|
|
58
|
+
/** Which specification section states the violated rule. */
|
|
59
|
+
linkKey;
|
|
60
|
+
constructor(message, linkKey) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = 'SpecViolationError';
|
|
63
|
+
this.linkKey = linkKey;
|
|
64
|
+
}
|
|
65
|
+
}
|