@kubb/adapter-oas 5.0.0-beta.36 → 5.0.0-beta.38
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/index.cjs +353 -201
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +353 -201
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +73 -29
- package/src/constants.ts +8 -0
- package/src/dialect.ts +3 -3
- package/src/factory.ts +37 -8
- package/src/parser.ts +11 -16
- package/src/refs.ts +15 -1
- package/src/resolvers.ts +27 -10
- package/src/schemaDiagnostics.ts +76 -0
- package/src/stream.ts +5 -3
- package/src/types.ts +5 -5
package/dist/index.js
CHANGED
|
@@ -1,12 +1,145 @@
|
|
|
1
1
|
import "./chunk-C0LytTxp.js";
|
|
2
|
-
import
|
|
3
|
-
import { ast, createAdapter } from "@kubb/core";
|
|
2
|
+
import { DiagnosticError, Diagnostics, ast, createAdapter, diagnosticCode } from "@kubb/core";
|
|
4
3
|
import BaseOas from "oas";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { access } from "node:fs/promises";
|
|
5
6
|
import { bundle, loadConfig } from "@redocly/openapi-core";
|
|
6
7
|
import OASNormalize from "oas-normalize";
|
|
7
8
|
import swagger2openapi from "swagger2openapi";
|
|
8
9
|
import { isRef } from "oas/types";
|
|
9
10
|
import { matchesMimeType } from "oas/utils";
|
|
11
|
+
//#region src/constants.ts
|
|
12
|
+
/**
|
|
13
|
+
* Default parser options applied when no explicit options are provided.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
|
|
18
|
+
*
|
|
19
|
+
* const parser = createOasParser(oas)
|
|
20
|
+
* const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_PARSER_OPTIONS = {
|
|
24
|
+
dateType: "string",
|
|
25
|
+
integerType: "bigint",
|
|
26
|
+
unknownType: "any",
|
|
27
|
+
emptySchemaType: "any",
|
|
28
|
+
enumSuffix: "enum"
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* JSON-Pointer prefix for schemas declared under `components.schemas` in an OpenAPI document.
|
|
32
|
+
*
|
|
33
|
+
* Used when building or parsing `$ref` strings.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* `${SCHEMA_REF_PREFIX}Pet` // '#/components/schemas/Pet'
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
const SCHEMA_REF_PREFIX = "#/components/schemas/";
|
|
41
|
+
/**
|
|
42
|
+
* OpenAPI version string written into the stub document created during multi-spec merges.
|
|
43
|
+
*/
|
|
44
|
+
const MERGE_OPENAPI_VERSION = "3.0.0";
|
|
45
|
+
/**
|
|
46
|
+
* Fallback `info.title` placed in the stub document when merging multiple API files.
|
|
47
|
+
*/
|
|
48
|
+
const MERGE_DEFAULT_TITLE = "Merged API";
|
|
49
|
+
/**
|
|
50
|
+
* Fallback `info.version` placed in the stub document when merging multiple API files.
|
|
51
|
+
*/
|
|
52
|
+
const MERGE_DEFAULT_VERSION = "1.0.0";
|
|
53
|
+
/**
|
|
54
|
+
* Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
|
|
55
|
+
*
|
|
56
|
+
* A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
|
|
57
|
+
* intersection member rather than being merged into the parent.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { structuralKeys } from '@kubb/adapter-oas'
|
|
62
|
+
*
|
|
63
|
+
* const isStructural = Object.keys(fragment).some((key) => structuralKeys.has(key))
|
|
64
|
+
* // true when fragment has e.g. 'properties' or 'oneOf'
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
const structuralKeys = new Set([
|
|
68
|
+
"properties",
|
|
69
|
+
"items",
|
|
70
|
+
"additionalProperties",
|
|
71
|
+
"oneOf",
|
|
72
|
+
"anyOf",
|
|
73
|
+
"allOf",
|
|
74
|
+
"not"
|
|
75
|
+
]);
|
|
76
|
+
/**
|
|
77
|
+
* Static map from OAS `format` strings to Kubb `SchemaType` values.
|
|
78
|
+
*
|
|
79
|
+
* Only formats whose AST type differs from the OAS `type` field appear here.
|
|
80
|
+
* Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
|
|
81
|
+
* in the parser. `ipv4` and `ipv6` map to their own dedicated schema types; `hostname` and
|
|
82
|
+
* `idn-hostname` map to `'url'` as the closest generic string-format type.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* import { formatMap } from '@kubb/adapter-oas'
|
|
87
|
+
*
|
|
88
|
+
* formatMap['uuid'] // 'uuid'
|
|
89
|
+
* formatMap['binary'] // 'blob'
|
|
90
|
+
* formatMap['float'] // 'number'
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
/**
|
|
94
|
+
* Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
|
|
95
|
+
* `int64` and the date/time family. Keep this in sync with the `convertFormat`
|
|
96
|
+
* special-cases in `parser.ts`. `isHandledFormat` reads it so the
|
|
97
|
+
* `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
|
|
98
|
+
*/
|
|
99
|
+
const specialCasedFormats = new Set([
|
|
100
|
+
"int64",
|
|
101
|
+
"date-time",
|
|
102
|
+
"date",
|
|
103
|
+
"time"
|
|
104
|
+
]);
|
|
105
|
+
const formatMap = {
|
|
106
|
+
uuid: "uuid",
|
|
107
|
+
email: "email",
|
|
108
|
+
"idn-email": "email",
|
|
109
|
+
uri: "url",
|
|
110
|
+
"uri-reference": "url",
|
|
111
|
+
url: "url",
|
|
112
|
+
ipv4: "ipv4",
|
|
113
|
+
ipv6: "ipv6",
|
|
114
|
+
hostname: "url",
|
|
115
|
+
"idn-hostname": "url",
|
|
116
|
+
binary: "blob",
|
|
117
|
+
byte: "blob",
|
|
118
|
+
int32: "integer",
|
|
119
|
+
float: "number",
|
|
120
|
+
double: "number"
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Vendor extension keys that attach human-readable labels to enum values, checked in priority order.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* import { enumExtensionKeys } from '@kubb/adapter-oas'
|
|
128
|
+
*
|
|
129
|
+
* const key = enumExtensionKeys.find((k) => k in schema) // 'x-enumNames' | 'x-enum-varnames' | undefined
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
|
|
133
|
+
/**
|
|
134
|
+
* Maps `'any' | 'unknown' | 'void'` option strings to their `ScalarSchemaType` constant.
|
|
135
|
+
* Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
|
|
136
|
+
*/
|
|
137
|
+
const typeOptionMap = new Map([
|
|
138
|
+
["any", ast.schemaTypes.any],
|
|
139
|
+
["unknown", ast.schemaTypes.unknown],
|
|
140
|
+
["void", ast.schemaTypes.void]
|
|
141
|
+
]);
|
|
142
|
+
//#endregion
|
|
10
143
|
//#region ../../internals/utils/src/casing.ts
|
|
11
144
|
/**
|
|
12
145
|
* Shared implementation for camelCase and PascalCase conversion.
|
|
@@ -71,6 +204,23 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
|
71
204
|
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
72
205
|
}
|
|
73
206
|
//#endregion
|
|
207
|
+
//#region ../../internals/utils/src/fs.ts
|
|
208
|
+
/**
|
|
209
|
+
* Resolves to `true` when the file or directory at `path` exists.
|
|
210
|
+
* Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* if (await exists('./kubb.config.ts')) {
|
|
215
|
+
* const content = await read('./kubb.config.ts')
|
|
216
|
+
* }
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
async function exists(path) {
|
|
220
|
+
if (typeof Bun !== "undefined") return Bun.file(path).exists();
|
|
221
|
+
return access(path).then(() => true, () => false);
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
74
224
|
//#region ../../internals/utils/src/object.ts
|
|
75
225
|
/**
|
|
76
226
|
* Returns `true` when `value` is a plain (non-null, non-array) object.
|
|
@@ -105,30 +255,6 @@ function mergeDeep(target, source) {
|
|
|
105
255
|
return result;
|
|
106
256
|
}
|
|
107
257
|
//#endregion
|
|
108
|
-
//#region ../../internals/utils/src/promise.ts
|
|
109
|
-
/**
|
|
110
|
-
* Returns a wrapper that caches the result of the first invocation and replays
|
|
111
|
-
* it for every subsequent call, ignoring later arguments.
|
|
112
|
-
*
|
|
113
|
-
* Works for sync and async factories — for async, the cached value is the
|
|
114
|
-
* promise itself, so concurrent callers share one in-flight execution and
|
|
115
|
-
* cannot race each other.
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
* ```ts
|
|
119
|
-
* const loadDocument = once(async (path: string) => parse(await readFile(path)))
|
|
120
|
-
* const a = loadDocument('./a.yaml') // parses
|
|
121
|
-
* const b = loadDocument('./b.yaml') // returns the cached promise from the first call
|
|
122
|
-
* ```
|
|
123
|
-
*/
|
|
124
|
-
function once(factory) {
|
|
125
|
-
let cache;
|
|
126
|
-
return (...args) => {
|
|
127
|
-
if (!cache) cache = { value: factory(...args) };
|
|
128
|
-
return cache.value;
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
//#endregion
|
|
132
258
|
//#region ../../internals/utils/src/reserved.ts
|
|
133
259
|
/**
|
|
134
260
|
* JavaScript and Java reserved words.
|
|
@@ -379,126 +505,6 @@ var URLPath = class {
|
|
|
379
505
|
}
|
|
380
506
|
};
|
|
381
507
|
//#endregion
|
|
382
|
-
//#region src/constants.ts
|
|
383
|
-
/**
|
|
384
|
-
* Default parser options applied when no explicit options are provided.
|
|
385
|
-
*
|
|
386
|
-
* @example
|
|
387
|
-
* ```ts
|
|
388
|
-
* import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
|
|
389
|
-
*
|
|
390
|
-
* const parser = createOasParser(oas)
|
|
391
|
-
* const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
|
|
392
|
-
* ```
|
|
393
|
-
*/
|
|
394
|
-
const DEFAULT_PARSER_OPTIONS = {
|
|
395
|
-
dateType: "string",
|
|
396
|
-
integerType: "bigint",
|
|
397
|
-
unknownType: "any",
|
|
398
|
-
emptySchemaType: "any",
|
|
399
|
-
enumSuffix: "enum"
|
|
400
|
-
};
|
|
401
|
-
/**
|
|
402
|
-
* JSON-Pointer prefix for schemas declared under `components.schemas` in an OpenAPI document.
|
|
403
|
-
*
|
|
404
|
-
* Used when building or parsing `$ref` strings.
|
|
405
|
-
*
|
|
406
|
-
* @example
|
|
407
|
-
* ```ts
|
|
408
|
-
* `${SCHEMA_REF_PREFIX}Pet` // '#/components/schemas/Pet'
|
|
409
|
-
* ```
|
|
410
|
-
*/
|
|
411
|
-
const SCHEMA_REF_PREFIX = "#/components/schemas/";
|
|
412
|
-
/**
|
|
413
|
-
* OpenAPI version string written into the stub document created during multi-spec merges.
|
|
414
|
-
*/
|
|
415
|
-
const MERGE_OPENAPI_VERSION = "3.0.0";
|
|
416
|
-
/**
|
|
417
|
-
* Fallback `info.title` placed in the stub document when merging multiple API files.
|
|
418
|
-
*/
|
|
419
|
-
const MERGE_DEFAULT_TITLE = "Merged API";
|
|
420
|
-
/**
|
|
421
|
-
* Fallback `info.version` placed in the stub document when merging multiple API files.
|
|
422
|
-
*/
|
|
423
|
-
const MERGE_DEFAULT_VERSION = "1.0.0";
|
|
424
|
-
/**
|
|
425
|
-
* Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
|
|
426
|
-
*
|
|
427
|
-
* A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
|
|
428
|
-
* intersection member rather than being merged into the parent.
|
|
429
|
-
*
|
|
430
|
-
* @example
|
|
431
|
-
* ```ts
|
|
432
|
-
* import { structuralKeys } from '@kubb/adapter-oas'
|
|
433
|
-
*
|
|
434
|
-
* const isStructural = Object.keys(fragment).some((key) => structuralKeys.has(key))
|
|
435
|
-
* // true when fragment has e.g. 'properties' or 'oneOf'
|
|
436
|
-
* ```
|
|
437
|
-
*/
|
|
438
|
-
const structuralKeys = new Set([
|
|
439
|
-
"properties",
|
|
440
|
-
"items",
|
|
441
|
-
"additionalProperties",
|
|
442
|
-
"oneOf",
|
|
443
|
-
"anyOf",
|
|
444
|
-
"allOf",
|
|
445
|
-
"not"
|
|
446
|
-
]);
|
|
447
|
-
/**
|
|
448
|
-
* Static map from OAS `format` strings to Kubb `SchemaType` values.
|
|
449
|
-
*
|
|
450
|
-
* Only formats whose AST type differs from the OAS `type` field appear here.
|
|
451
|
-
* Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
|
|
452
|
-
* in the parser. `ipv4` and `ipv6` map to their own dedicated schema types; `hostname` and
|
|
453
|
-
* `idn-hostname` map to `'url'` as the closest generic string-format type.
|
|
454
|
-
*
|
|
455
|
-
* @example
|
|
456
|
-
* ```ts
|
|
457
|
-
* import { formatMap } from '@kubb/adapter-oas'
|
|
458
|
-
*
|
|
459
|
-
* formatMap['uuid'] // 'uuid'
|
|
460
|
-
* formatMap['binary'] // 'blob'
|
|
461
|
-
* formatMap['float'] // 'number'
|
|
462
|
-
* ```
|
|
463
|
-
*/
|
|
464
|
-
const formatMap = {
|
|
465
|
-
uuid: "uuid",
|
|
466
|
-
email: "email",
|
|
467
|
-
"idn-email": "email",
|
|
468
|
-
uri: "url",
|
|
469
|
-
"uri-reference": "url",
|
|
470
|
-
url: "url",
|
|
471
|
-
ipv4: "ipv4",
|
|
472
|
-
ipv6: "ipv6",
|
|
473
|
-
hostname: "url",
|
|
474
|
-
"idn-hostname": "url",
|
|
475
|
-
binary: "blob",
|
|
476
|
-
byte: "blob",
|
|
477
|
-
int32: "integer",
|
|
478
|
-
float: "number",
|
|
479
|
-
double: "number"
|
|
480
|
-
};
|
|
481
|
-
/**
|
|
482
|
-
* Vendor extension keys that attach human-readable labels to enum values, checked in priority order.
|
|
483
|
-
*
|
|
484
|
-
* @example
|
|
485
|
-
* ```ts
|
|
486
|
-
* import { enumExtensionKeys } from '@kubb/adapter-oas'
|
|
487
|
-
*
|
|
488
|
-
* const key = enumExtensionKeys.find((k) => k in schema) // 'x-enumNames' | 'x-enum-varnames' | undefined
|
|
489
|
-
* ```
|
|
490
|
-
*/
|
|
491
|
-
const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
|
|
492
|
-
/**
|
|
493
|
-
* Maps `'any' | 'unknown' | 'void'` option strings to their `ScalarSchemaType` constant.
|
|
494
|
-
* Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
|
|
495
|
-
*/
|
|
496
|
-
const typeOptionMap = new Map([
|
|
497
|
-
["any", ast.schemaTypes.any],
|
|
498
|
-
["unknown", ast.schemaTypes.unknown],
|
|
499
|
-
["void", ast.schemaTypes.void]
|
|
500
|
-
]);
|
|
501
|
-
//#endregion
|
|
502
508
|
//#region src/guards.ts
|
|
503
509
|
/**
|
|
504
510
|
* Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
|
|
@@ -608,7 +614,13 @@ async function mergeDocuments(pathOrApi) {
|
|
|
608
614
|
enablePaths: false,
|
|
609
615
|
canBundle: false
|
|
610
616
|
})));
|
|
611
|
-
if (documents.length === 0) throw new
|
|
617
|
+
if (documents.length === 0) throw new DiagnosticError({
|
|
618
|
+
code: diagnosticCode.inputRequired,
|
|
619
|
+
severity: "error",
|
|
620
|
+
message: "No OAS documents were provided for merging.",
|
|
621
|
+
help: "Pass at least one path or document to `input.path`.",
|
|
622
|
+
location: { kind: "config" }
|
|
623
|
+
});
|
|
612
624
|
const seed = {
|
|
613
625
|
openapi: MERGE_OPENAPI_VERSION,
|
|
614
626
|
info: {
|
|
@@ -624,9 +636,9 @@ async function mergeDocuments(pathOrApi) {
|
|
|
624
636
|
* Creates a `Document` from an `AdapterSource`.
|
|
625
637
|
*
|
|
626
638
|
* Handles all three source types:
|
|
627
|
-
* - `{ type: 'path' }`
|
|
628
|
-
* - `{ type: 'paths' }`
|
|
629
|
-
* - `{ type: 'data' }`
|
|
639
|
+
* - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
|
|
640
|
+
* - `{ type: 'paths' }` merges multiple file paths into a single document.
|
|
641
|
+
* - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
|
|
630
642
|
*
|
|
631
643
|
* @example
|
|
632
644
|
* ```ts
|
|
@@ -634,14 +646,31 @@ async function mergeDocuments(pathOrApi) {
|
|
|
634
646
|
* const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
|
|
635
647
|
* ```
|
|
636
648
|
*/
|
|
637
|
-
function parseFromConfig(source) {
|
|
649
|
+
async function parseFromConfig(source) {
|
|
638
650
|
if (source.type === "data") {
|
|
639
651
|
if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
|
|
640
652
|
return parseDocument(source.data, { canBundle: false });
|
|
641
653
|
}
|
|
642
654
|
if (source.type === "paths") return mergeDocuments(source.paths);
|
|
643
655
|
if (new URLPath(source.path).isURL) return parseDocument(source.path);
|
|
644
|
-
|
|
656
|
+
const resolved = path.resolve(path.dirname(source.path), source.path);
|
|
657
|
+
await assertInputExists(resolved);
|
|
658
|
+
return parseDocument(resolved);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
|
|
662
|
+
* URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
|
|
663
|
+
* its parse error instead.
|
|
664
|
+
*/
|
|
665
|
+
async function assertInputExists(input) {
|
|
666
|
+
if (new URLPath(input).isURL) return;
|
|
667
|
+
if (!await exists(input)) throw new DiagnosticError({
|
|
668
|
+
code: diagnosticCode.inputNotFound,
|
|
669
|
+
severity: "error",
|
|
670
|
+
message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
|
|
671
|
+
help: "Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.",
|
|
672
|
+
location: { kind: "config" }
|
|
673
|
+
});
|
|
645
674
|
}
|
|
646
675
|
/**
|
|
647
676
|
* Validates an OpenAPI document using `oas-normalize` with colorized error output.
|
|
@@ -688,7 +717,21 @@ function resolveRef(document, $ref) {
|
|
|
688
717
|
}
|
|
689
718
|
if (docCache.has($ref)) return docCache.get($ref);
|
|
690
719
|
const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
|
|
691
|
-
if (!current)
|
|
720
|
+
if (!current) {
|
|
721
|
+
const diagnostic = {
|
|
722
|
+
code: diagnosticCode.refNotFound,
|
|
723
|
+
severity: "error",
|
|
724
|
+
message: `Could not find a definition for ${origRef}.`,
|
|
725
|
+
help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
|
|
726
|
+
location: {
|
|
727
|
+
kind: "schema",
|
|
728
|
+
pointer: origRef,
|
|
729
|
+
ref: origRef
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
if (!Diagnostics.report(diagnostic)) throw new DiagnosticError(diagnostic);
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
692
735
|
docCache.set($ref, current);
|
|
693
736
|
return current;
|
|
694
737
|
}
|
|
@@ -715,13 +758,13 @@ function dereferenceWithRef(document, schema) {
|
|
|
715
758
|
//#endregion
|
|
716
759
|
//#region src/dialect.ts
|
|
717
760
|
/**
|
|
718
|
-
* The OpenAPI / Swagger dialect
|
|
761
|
+
* The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
|
|
719
762
|
*
|
|
720
763
|
* Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
|
|
721
764
|
* decisions that differ between specs (nullability, `$ref`, discriminator, binary,
|
|
722
765
|
* ref resolution) so the converter pipeline and dispatch rules stay shared. A
|
|
723
|
-
* future adapter (e.g. AsyncAPI) ships its own dialect
|
|
724
|
-
* nullability, no discriminator object, binary via `contentEncoding`
|
|
766
|
+
* future adapter (e.g. AsyncAPI) ships its own dialect, `type: ['null', …]`
|
|
767
|
+
* nullability, no discriminator object, binary via `contentEncoding` and reuses
|
|
725
768
|
* the rest unchanged.
|
|
726
769
|
*
|
|
727
770
|
* Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
|
|
@@ -763,7 +806,16 @@ function resolveServerUrl(server, overrides) {
|
|
|
763
806
|
for (const [key, variable] of Object.entries(server.variables)) {
|
|
764
807
|
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
|
|
765
808
|
if (value === void 0) continue;
|
|
766
|
-
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new
|
|
809
|
+
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new DiagnosticError({
|
|
810
|
+
code: diagnosticCode.invalidServerVariable,
|
|
811
|
+
severity: "error",
|
|
812
|
+
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
|
|
813
|
+
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
814
|
+
location: {
|
|
815
|
+
kind: "document",
|
|
816
|
+
pointer: "#/servers"
|
|
817
|
+
}
|
|
818
|
+
});
|
|
767
819
|
url = url.replaceAll(`{${key}}`, value);
|
|
768
820
|
}
|
|
769
821
|
return url;
|
|
@@ -776,6 +828,15 @@ function getSchemaType(format) {
|
|
|
776
828
|
return formatMap[format] ?? null;
|
|
777
829
|
}
|
|
778
830
|
/**
|
|
831
|
+
* Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry and the
|
|
832
|
+
* `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to the
|
|
833
|
+
* base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
|
|
834
|
+
* diagnostic in step with the parser as `formatMap` grows.
|
|
835
|
+
*/
|
|
836
|
+
function isHandledFormat(format) {
|
|
837
|
+
return getSchemaType(format) !== null || specialCasedFormats.has(format);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
779
840
|
* Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
|
|
780
841
|
* Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
|
|
781
842
|
*/
|
|
@@ -872,7 +933,7 @@ function getRequestSchema(document, operation, options = {}) {
|
|
|
872
933
|
/**
|
|
873
934
|
* Flattens a keyword-only `allOf` into its parent schema.
|
|
874
935
|
*
|
|
875
|
-
* Only flattens when every member is a plain fragment
|
|
936
|
+
* Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
|
|
876
937
|
* (see `structuralKeys`). Outer schema values take precedence over fragment values.
|
|
877
938
|
* Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
|
|
878
939
|
*
|
|
@@ -882,7 +943,7 @@ function getRequestSchema(document, operation, options = {}) {
|
|
|
882
943
|
* // { type: 'object', properties: {}, description: 'A pet' }
|
|
883
944
|
*
|
|
884
945
|
* flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
|
|
885
|
-
* // returned unchanged
|
|
946
|
+
* // returned unchanged, contains a $ref
|
|
886
947
|
* ```
|
|
887
948
|
*/
|
|
888
949
|
/**
|
|
@@ -908,7 +969,7 @@ function flattenSchema(schema) {
|
|
|
908
969
|
/**
|
|
909
970
|
* Extracts the inline schema from a media-type `content` map.
|
|
910
971
|
*
|
|
911
|
-
* Prefers `preferredContentType` when given
|
|
972
|
+
* Prefers `preferredContentType` when given, otherwise uses the first key in the map.
|
|
912
973
|
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
913
974
|
*
|
|
914
975
|
* @example
|
|
@@ -1100,9 +1161,9 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
|
1100
1161
|
/**
|
|
1101
1162
|
* Returns all request body content type keys for an operation.
|
|
1102
1163
|
*
|
|
1103
|
-
* The requestBody is dereferenced
|
|
1104
|
-
*
|
|
1105
|
-
*
|
|
1164
|
+
* The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
|
|
1165
|
+
* `getRequestSchema` already performs), so the returned list accurately reflects the
|
|
1166
|
+
* available content types even for referenced bodies.
|
|
1106
1167
|
*
|
|
1107
1168
|
* @example
|
|
1108
1169
|
* ```ts
|
|
@@ -1119,7 +1180,7 @@ function getRequestBodyContentTypes(document, operation) {
|
|
|
1119
1180
|
/**
|
|
1120
1181
|
* Returns all response content type keys for an operation at a given status code.
|
|
1121
1182
|
*
|
|
1122
|
-
* Response `$ref`s are resolved in
|
|
1183
|
+
* Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
|
|
1123
1184
|
* so the returned list reflects the available content types even for referenced responses.
|
|
1124
1185
|
*
|
|
1125
1186
|
* @example
|
|
@@ -1179,16 +1240,12 @@ function createSchemaParser(ctx, dialect = oasDialect) {
|
|
|
1179
1240
|
*/
|
|
1180
1241
|
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
1181
1242
|
/**
|
|
1182
|
-
* Cache of
|
|
1243
|
+
* Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
|
|
1183
1244
|
*
|
|
1184
|
-
* Without
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
* each triggering a fresh recursive expansion of its entire sub-tree.
|
|
1189
|
-
*
|
|
1190
|
-
* Memoizing by `$ref` path reduces the overall work from O(2^depth) to O(N)
|
|
1191
|
-
* where N is the number of unique schema names.
|
|
1245
|
+
* Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
|
|
1246
|
+
* it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
|
|
1247
|
+
* since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
|
|
1248
|
+
* Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
|
|
1192
1249
|
*/
|
|
1193
1250
|
const resolvedRefCache = /* @__PURE__ */ new Map();
|
|
1194
1251
|
/**
|
|
@@ -1726,7 +1783,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
|
|
|
1726
1783
|
/**
|
|
1727
1784
|
* Ordered schema-dispatch table. Order is significant: composition keywords (`$ref`, `allOf`,
|
|
1728
1785
|
* `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
|
|
1729
|
-
* `type`. The first matching rule that produces a node wins
|
|
1786
|
+
* `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
|
|
1730
1787
|
* match/convert/fall-through contract.
|
|
1731
1788
|
*/
|
|
1732
1789
|
const schemaRules = [
|
|
@@ -2071,6 +2128,61 @@ function patchDiscriminatorNode(node, entry) {
|
|
|
2071
2128
|
};
|
|
2072
2129
|
}
|
|
2073
2130
|
//#endregion
|
|
2131
|
+
//#region src/schemaDiagnostics.ts
|
|
2132
|
+
/**
|
|
2133
|
+
* Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
|
|
2134
|
+
* top-level schema. Walks the node the parser produced during `preScan`, threading the RFC 6901
|
|
2135
|
+
* pointer as it descends so a nested field reports against its full path
|
|
2136
|
+
* (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
|
|
2137
|
+
* resolved schema is reported under its own walk. Reports land in the active build run, are a
|
|
2138
|
+
* no-op outside one, and repeats are deduped by the build.
|
|
2139
|
+
*/
|
|
2140
|
+
function reportSchemaDiagnostics({ node, name }) {
|
|
2141
|
+
visit(node, `#/components/schemas/${escapePointerToken(name)}`);
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
|
|
2145
|
+
* property name with those characters maps to a distinct pointer instead of colliding in the dedupe.
|
|
2146
|
+
*/
|
|
2147
|
+
function escapePointerToken(token) {
|
|
2148
|
+
return token.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2149
|
+
}
|
|
2150
|
+
function visit(node, pointer) {
|
|
2151
|
+
if (node.deprecated) Diagnostics.report({
|
|
2152
|
+
code: diagnosticCode.deprecated,
|
|
2153
|
+
severity: "info",
|
|
2154
|
+
message: "This schema is marked as deprecated.",
|
|
2155
|
+
location: {
|
|
2156
|
+
kind: "schema",
|
|
2157
|
+
pointer
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
if (typeof node.format === "string" && !isHandledFormat(node.format)) Diagnostics.report({
|
|
2161
|
+
code: diagnosticCode.unsupportedFormat,
|
|
2162
|
+
severity: "warning",
|
|
2163
|
+
message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
|
|
2164
|
+
help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
|
|
2165
|
+
location: {
|
|
2166
|
+
kind: "schema",
|
|
2167
|
+
pointer
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
if (node.type === "object") {
|
|
2171
|
+
for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`);
|
|
2172
|
+
if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
if (node.type === "array") {
|
|
2176
|
+
for (const item of node.items ?? []) visit(item, `${pointer}/items`);
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
if (node.type === "tuple") {
|
|
2180
|
+
for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`);
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`);
|
|
2184
|
+
}
|
|
2185
|
+
//#endregion
|
|
2074
2186
|
//#region src/stream.ts
|
|
2075
2187
|
/**
|
|
2076
2188
|
* Builds the deduplication plan from the already-parsed top-level schema nodes plus a
|
|
@@ -2130,7 +2242,7 @@ function resolveBaseUrl({ document, serverIndex, serverVariables }) {
|
|
|
2130
2242
|
* After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
|
|
2131
2243
|
* Both are proportional to the number of aliases or discriminator parents, not total schema count.
|
|
2132
2244
|
*
|
|
2133
|
-
* Each schema is parsed again during the streaming pass
|
|
2245
|
+
* Each schema is parsed again during the streaming pass. This is intentional.
|
|
2134
2246
|
* Holding the parsed nodes in memory here would defeat the streaming memory benefit.
|
|
2135
2247
|
*
|
|
2136
2248
|
* @example
|
|
@@ -2154,6 +2266,10 @@ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions,
|
|
|
2154
2266
|
name
|
|
2155
2267
|
}, parserOptions);
|
|
2156
2268
|
allNodes.push(node);
|
|
2269
|
+
reportSchemaDiagnostics({
|
|
2270
|
+
node,
|
|
2271
|
+
name
|
|
2272
|
+
});
|
|
2157
2273
|
if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
|
|
2158
2274
|
if (ast.narrowSchema(node, ast.schemaTypes.enum) && node.name) enumNames.push(node.name);
|
|
2159
2275
|
if (discriminator === "inherit" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
|
|
@@ -2294,37 +2410,72 @@ const adapterOas = createAdapter((options) => {
|
|
|
2294
2410
|
};
|
|
2295
2411
|
let nameMapping = /* @__PURE__ */ new Map();
|
|
2296
2412
|
let parsedDocument = null;
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2413
|
+
const documentCache = /* @__PURE__ */ new WeakMap();
|
|
2414
|
+
const schemasCache = /* @__PURE__ */ new WeakMap();
|
|
2415
|
+
const baseOasCache = /* @__PURE__ */ new WeakMap();
|
|
2416
|
+
const schemaParserCache = /* @__PURE__ */ new WeakMap();
|
|
2417
|
+
const preScanCache = /* @__PURE__ */ new WeakMap();
|
|
2418
|
+
function ensureDocument(source) {
|
|
2419
|
+
const cached = documentCache.get(source);
|
|
2420
|
+
if (cached) return cached;
|
|
2421
|
+
const promise = (async () => {
|
|
2422
|
+
const fresh = await parseFromConfig(source);
|
|
2423
|
+
if (validate) await validateDocument(fresh);
|
|
2424
|
+
parsedDocument = fresh;
|
|
2425
|
+
return fresh;
|
|
2426
|
+
})();
|
|
2427
|
+
documentCache.set(source, promise);
|
|
2428
|
+
return promise;
|
|
2429
|
+
}
|
|
2430
|
+
function ensureSchemas(document) {
|
|
2431
|
+
const cached = schemasCache.get(document);
|
|
2432
|
+
if (cached) return cached;
|
|
2433
|
+
const promise = Promise.resolve().then(() => {
|
|
2434
|
+
const result = getSchemas(document, { contentType });
|
|
2435
|
+
nameMapping = result.nameMapping;
|
|
2436
|
+
return result.schemas;
|
|
2437
|
+
});
|
|
2438
|
+
schemasCache.set(document, promise);
|
|
2439
|
+
return promise;
|
|
2440
|
+
}
|
|
2441
|
+
function ensureBaseOas(document) {
|
|
2442
|
+
const cached = baseOasCache.get(document);
|
|
2443
|
+
if (cached) return cached;
|
|
2444
|
+
const baseOas = new BaseOas(document);
|
|
2445
|
+
baseOasCache.set(document, baseOas);
|
|
2446
|
+
return baseOas;
|
|
2447
|
+
}
|
|
2448
|
+
function ensureSchemaParser(document) {
|
|
2449
|
+
const cached = schemaParserCache.get(document);
|
|
2450
|
+
if (cached) return cached;
|
|
2451
|
+
const parser = createSchemaParser({
|
|
2452
|
+
document,
|
|
2453
|
+
contentType
|
|
2454
|
+
});
|
|
2455
|
+
schemaParserCache.set(document, parser);
|
|
2456
|
+
return parser;
|
|
2457
|
+
}
|
|
2458
|
+
function ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas) {
|
|
2459
|
+
const cached = preScanCache.get(document);
|
|
2460
|
+
if (cached) return cached;
|
|
2461
|
+
const result = preScan({
|
|
2462
|
+
schemas,
|
|
2463
|
+
parseSchema,
|
|
2464
|
+
parseOperation,
|
|
2465
|
+
baseOas,
|
|
2466
|
+
parserOptions,
|
|
2467
|
+
discriminator,
|
|
2468
|
+
dedupe
|
|
2469
|
+
});
|
|
2470
|
+
preScanCache.set(document, result);
|
|
2471
|
+
return result;
|
|
2472
|
+
}
|
|
2322
2473
|
async function createStream(source) {
|
|
2323
2474
|
const document = await ensureDocument(source);
|
|
2324
2475
|
const schemas = await ensureSchemas(document);
|
|
2325
2476
|
const { parseSchema, parseOperation } = ensureSchemaParser(document);
|
|
2326
2477
|
const baseOas = ensureBaseOas(document);
|
|
2327
|
-
const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(schemas, parseSchema, parseOperation, baseOas);
|
|
2478
|
+
const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas);
|
|
2328
2479
|
return createInputStream({
|
|
2329
2480
|
schemas,
|
|
2330
2481
|
parseSchema,
|
|
@@ -2370,6 +2521,7 @@ const adapterOas = createAdapter((options) => {
|
|
|
2370
2521
|
return parsedDocument;
|
|
2371
2522
|
},
|
|
2372
2523
|
async validate(input, options) {
|
|
2524
|
+
await assertInputExists(input);
|
|
2373
2525
|
await validateDocument(await parseDocument(input), options);
|
|
2374
2526
|
},
|
|
2375
2527
|
getImports(node, resolve) {
|