@kubb/adapter-oas 5.0.0-beta.99 → 5.0.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 +10 -8
- package/dist/index.cjs +1217 -934
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.js +1209 -926
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/dist/index.cjs
CHANGED
|
@@ -16,20 +16,20 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
16
|
}
|
|
17
17
|
return to;
|
|
18
18
|
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
20
|
value: mod,
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
24
|
let _kubb_ast = require("@kubb/ast");
|
|
25
25
|
let _kubb_core = require("@kubb/core");
|
|
26
|
+
let node_fs_promises = require("node:fs/promises");
|
|
26
27
|
let node_path = require("node:path");
|
|
27
28
|
node_path = __toESM(node_path, 1);
|
|
28
|
-
let
|
|
29
|
-
let _readme_openapi_parser = require("@readme/openapi-parser");
|
|
29
|
+
let yaml = require("yaml");
|
|
30
30
|
let _scalar_openapi_upgrader = require("@scalar/openapi-upgrader");
|
|
31
31
|
let api_ref_bundler = require("api-ref-bundler");
|
|
32
|
-
let
|
|
32
|
+
let _kubb_kit = require("@kubb/kit");
|
|
33
33
|
//#region src/constants.ts
|
|
34
34
|
/**
|
|
35
35
|
* Default parser options applied when no explicit options are provided.
|
|
@@ -37,8 +37,8 @@ let yaml = require("yaml");
|
|
|
37
37
|
const DEFAULT_PARSER_OPTIONS = {
|
|
38
38
|
dateType: "string",
|
|
39
39
|
integerType: "bigint",
|
|
40
|
-
unknownType: "
|
|
41
|
-
emptySchemaType: "
|
|
40
|
+
unknownType: "unknown",
|
|
41
|
+
emptySchemaType: "unknown",
|
|
42
42
|
enumSuffix: "enum"
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
@@ -83,17 +83,32 @@ const structuralKeys = /* @__PURE__ */ new Set([
|
|
|
83
83
|
]);
|
|
84
84
|
/**
|
|
85
85
|
* Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
|
|
86
|
-
* `int64` and the date/time family. Keep this in sync with the `convertFormat`
|
|
86
|
+
* `int64`, `uint64` and the date/time family. Keep this in sync with the `convertFormat`
|
|
87
87
|
* special-cases in `parser.ts`. `isHandledFormat` reads it so the
|
|
88
88
|
* `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
|
|
89
89
|
*/
|
|
90
90
|
const specialCasedFormats = /* @__PURE__ */ new Set([
|
|
91
91
|
"int64",
|
|
92
|
+
"uint64",
|
|
92
93
|
"date-time",
|
|
93
94
|
"date",
|
|
94
95
|
"time"
|
|
95
96
|
]);
|
|
96
97
|
/**
|
|
98
|
+
* Formats that describe a number, whether they resolve through `formatMap` or through the
|
|
99
|
+
* `convertFormat` special cases. On a `type: 'string'` schema these do not make the value a
|
|
100
|
+
* number: gRPC-gateway and other ProtoJSON producers send 64-bit integers as JSON strings.
|
|
101
|
+
*
|
|
102
|
+
* @see https://protobuf.dev/programming-guides/json/#int64-strings
|
|
103
|
+
*/
|
|
104
|
+
const numericFormats = /* @__PURE__ */ new Set([
|
|
105
|
+
"int32",
|
|
106
|
+
"int64",
|
|
107
|
+
"uint64",
|
|
108
|
+
"float",
|
|
109
|
+
"double"
|
|
110
|
+
]);
|
|
111
|
+
/**
|
|
97
112
|
* Static map from OAS `format` strings to Kubb `SchemaType` values.
|
|
98
113
|
*
|
|
99
114
|
* Only formats whose AST type differs from the OAS `type` field appear here.
|
|
@@ -127,7 +142,7 @@ const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
|
|
|
127
142
|
*/
|
|
128
143
|
const enumDescriptionKeys = ["x-enumDescriptions", "x-enum-descriptions"];
|
|
129
144
|
//#endregion
|
|
130
|
-
//#region src/discriminator.ts
|
|
145
|
+
//#region src/emit/discriminator/propagate.ts
|
|
131
146
|
/**
|
|
132
147
|
* Maps each child schema name to its discriminator patch data by scanning the given
|
|
133
148
|
* top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
|
|
@@ -203,42 +218,6 @@ function patchDiscriminatorNode(node, entry) {
|
|
|
203
218
|
properties: newProperties
|
|
204
219
|
};
|
|
205
220
|
}
|
|
206
|
-
/**
|
|
207
|
-
* Creates a single-property object schema used as a discriminator literal.
|
|
208
|
-
*
|
|
209
|
-
* @example
|
|
210
|
-
* ```ts
|
|
211
|
-
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
212
|
-
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
213
|
-
* ```
|
|
214
|
-
*/
|
|
215
|
-
function createDiscriminantNode({ propertyName, value }) {
|
|
216
|
-
return _kubb_ast.ast.factory.createSchema({
|
|
217
|
-
type: "object",
|
|
218
|
-
primitive: "object",
|
|
219
|
-
properties: [_kubb_ast.ast.factory.createProperty({
|
|
220
|
-
name: propertyName,
|
|
221
|
-
schema: _kubb_ast.ast.factory.createSchema({
|
|
222
|
-
type: "enum",
|
|
223
|
-
primitive: "string",
|
|
224
|
-
enumValues: [value]
|
|
225
|
-
}),
|
|
226
|
-
required: true
|
|
227
|
-
})]
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
|
|
232
|
-
*
|
|
233
|
-
* @example
|
|
234
|
-
* ```ts
|
|
235
|
-
* findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
|
|
236
|
-
* ```
|
|
237
|
-
*/
|
|
238
|
-
function findDiscriminator(mapping, ref) {
|
|
239
|
-
if (!mapping || !ref) return null;
|
|
240
|
-
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
|
|
241
|
-
}
|
|
242
221
|
//#endregion
|
|
243
222
|
//#region ../../internals/utils/src/casing.ts
|
|
244
223
|
/**
|
|
@@ -267,6 +246,20 @@ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
|
|
|
267
246
|
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
268
247
|
}
|
|
269
248
|
//#endregion
|
|
249
|
+
//#region ../../internals/utils/src/errors.ts
|
|
250
|
+
/**
|
|
251
|
+
* Extracts a human-readable message from any thrown value.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```ts
|
|
255
|
+
* getErrorMessage(new Error('oops')) // 'oops'
|
|
256
|
+
* getErrorMessage('plain string') // 'plain string'
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
function getErrorMessage(value) {
|
|
260
|
+
return value instanceof Error ? value.message : String(value);
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
270
263
|
//#region ../../internals/utils/src/runtime.ts
|
|
271
264
|
/**
|
|
272
265
|
* Detects the JavaScript runtime executing the current process and exposes its name and version.
|
|
@@ -367,21 +360,104 @@ async function read(path) {
|
|
|
367
360
|
return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
|
|
368
361
|
}
|
|
369
362
|
//#endregion
|
|
370
|
-
//#region src/
|
|
363
|
+
//#region src/load/source.ts
|
|
371
364
|
const urlRegExp = /^https?:\/+/i;
|
|
365
|
+
/**
|
|
366
|
+
* Node reports every connection failure as `TypeError: fetch failed` and keeps the useful part
|
|
367
|
+
* (`connect ECONNREFUSED 127.0.0.1:8000`) on `cause`, one level deeper again when a host resolves
|
|
368
|
+
* to several addresses and the attempts collect into an `AggregateError`.
|
|
369
|
+
*/
|
|
370
|
+
function describeFetchFailure(error) {
|
|
371
|
+
if (error instanceof AggregateError && error.errors.length > 0) return describeFetchFailure(error.errors[0]);
|
|
372
|
+
if (error instanceof Error && error.cause instanceof Error) return describeFetchFailure(error.cause) || error.message;
|
|
373
|
+
return getErrorMessage(error);
|
|
374
|
+
}
|
|
375
|
+
function helpForStatus(status) {
|
|
376
|
+
if (status === 401 || status === 403) return "The server refused the request. Kubb sends no credentials, so serve the document without authentication or download it and set `input` to the local file.";
|
|
377
|
+
if (status === 404) return "Check the URL. Open it in a browser or with `curl` to confirm it serves the OpenAPI document.";
|
|
378
|
+
if (status >= 500) return "The server failed while serving the document. Check that it is healthy, then run Kubb again.";
|
|
379
|
+
return "Open the URL in a browser or with `curl` to see what the server returns, then point `input` at a URL that serves the OpenAPI document.";
|
|
380
|
+
}
|
|
381
|
+
async function fetchSource(url) {
|
|
382
|
+
try {
|
|
383
|
+
return await fetch(url);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
throw new _kubb_core.Diagnostics.Error({
|
|
386
|
+
code: _kubb_core.Diagnostics.code.inputUnreachable,
|
|
387
|
+
severity: "error",
|
|
388
|
+
message: `Cannot reach ${url.href}: ${describeFetchFailure(error)}`,
|
|
389
|
+
help: "Check that the host is running and reachable from this machine. For a local server, start it and confirm the port matches the one in `input`.",
|
|
390
|
+
location: { kind: "config" },
|
|
391
|
+
cause: error instanceof Error ? error : void 0
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
372
395
|
async function readSource(sourcePath) {
|
|
373
396
|
if (urlRegExp.test(sourcePath)) {
|
|
374
397
|
const url = new URL(sourcePath);
|
|
375
|
-
const response = await
|
|
376
|
-
if (!response.ok)
|
|
398
|
+
const response = await fetchSource(url);
|
|
399
|
+
if (!response.ok) {
|
|
400
|
+
const status = response.statusText ? `${response.status} ${response.statusText}` : String(response.status);
|
|
401
|
+
throw new _kubb_core.Diagnostics.Error({
|
|
402
|
+
code: _kubb_core.Diagnostics.code.inputRequestFailed,
|
|
403
|
+
severity: "error",
|
|
404
|
+
message: `The server at ${url.href} answered with HTTP ${status} instead of the OpenAPI document.`,
|
|
405
|
+
help: helpForStatus(response.status),
|
|
406
|
+
location: { kind: "config" }
|
|
407
|
+
});
|
|
408
|
+
}
|
|
377
409
|
return response.text();
|
|
378
410
|
}
|
|
379
411
|
return read(sourcePath);
|
|
380
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Reads and parses one source file or URL referenced during bundling: YAML/JSON is parsed into an
|
|
415
|
+
* object, Markdown is returned as-is (bundled inline rather than dereferenced).
|
|
416
|
+
*
|
|
417
|
+
* JSON is valid YAML, so `yaml`'s `parse` handles both, but its general-purpose parser (comments,
|
|
418
|
+
* anchors, block scalars, multi-document streams) does much more work than `JSON.parse` needs to.
|
|
419
|
+
* `JSON.parse` runs first and fails fast on the first non-JSON character, so a real YAML document
|
|
420
|
+
* falls through to `parse` at negligible cost.
|
|
421
|
+
*/
|
|
381
422
|
async function resolveSource(sourcePath) {
|
|
382
423
|
const data = await readSource(sourcePath);
|
|
383
424
|
if (sourcePath.toLowerCase().endsWith(".md")) return data;
|
|
384
|
-
|
|
425
|
+
try {
|
|
426
|
+
return JSON.parse(data);
|
|
427
|
+
} catch {
|
|
428
|
+
return (0, yaml.parse)(data);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
|
|
433
|
+
* URLs are skipped: a remote input reports `KUBB_INPUT_REQUEST_FAILED` or `KUBB_INPUT_UNREACHABLE`
|
|
434
|
+
* from the request itself. A malformed but readable file is left for `parseDocument` to surface
|
|
435
|
+
* its parse error instead.
|
|
436
|
+
*/
|
|
437
|
+
async function assertInputExists(input) {
|
|
438
|
+
if (URL.canParse(input)) return;
|
|
439
|
+
if (!await exists(input)) throw new _kubb_core.Diagnostics.Error({
|
|
440
|
+
code: _kubb_core.Diagnostics.code.inputNotFound,
|
|
441
|
+
severity: "error",
|
|
442
|
+
message: `Cannot read the file set as \`input\` (or via \`kubb generate PATH\`): ${input}`,
|
|
443
|
+
help: "Check that the path exists and is readable, then set it as `input` or pass it as `kubb generate PATH`.",
|
|
444
|
+
location: { kind: "config" }
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/load/normalize.ts
|
|
449
|
+
/**
|
|
450
|
+
* True when `node` contains a `$ref` pointing outside the current document (a relative path,
|
|
451
|
+
* absolute path, or URL). An internal `#/...` fragment does not count.
|
|
452
|
+
*
|
|
453
|
+
* `Object.values` reads array elements and object property values alike, so the same recursion
|
|
454
|
+
* walks both without a separate array branch.
|
|
455
|
+
*/
|
|
456
|
+
function hasExternalRef(node) {
|
|
457
|
+
if (!node || typeof node !== "object") return false;
|
|
458
|
+
const ref = node.$ref;
|
|
459
|
+
if (typeof ref === "string" && !ref.startsWith("#")) return true;
|
|
460
|
+
return Object.values(node).some(hasExternalRef);
|
|
385
461
|
}
|
|
386
462
|
/**
|
|
387
463
|
* Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
|
|
@@ -391,6 +467,11 @@ async function resolveSource(sourcePath) {
|
|
|
391
467
|
* can then emit a named type with an import instead of inlining the shape. Sources are read with
|
|
392
468
|
* the Bun-aware `read` util for local YAML and JSON files, and with `fetch` for HTTP(S) URLs.
|
|
393
469
|
*
|
|
470
|
+
* A document with no `$ref` outside itself has nothing to bundle, so it skips `api-ref-bundler`
|
|
471
|
+
* and returns as parsed. `bundle` only rewrites external refs into internal ones; on an
|
|
472
|
+
* all-internal document it is a no-op that still walks the whole tree to confirm that, which
|
|
473
|
+
* costs real time on a large spec.
|
|
474
|
+
*
|
|
394
475
|
* @example Local file
|
|
395
476
|
* `const document = await bundleDocument('./openapi.yaml')`
|
|
396
477
|
*
|
|
@@ -407,7 +488,8 @@ async function bundleDocument(pathOrUrl) {
|
|
|
407
488
|
cache.set(key, result);
|
|
408
489
|
return result;
|
|
409
490
|
};
|
|
410
|
-
await resolver(pathOrUrl);
|
|
491
|
+
const root = await resolver(pathOrUrl);
|
|
492
|
+
if (typeof root === "object" && root !== null && !hasExternalRef(root)) return root;
|
|
411
493
|
return await (0, api_ref_bundler.bundle)(pathOrUrl, resolver);
|
|
412
494
|
}
|
|
413
495
|
/**
|
|
@@ -448,17 +530,20 @@ async function parseFromConfig(source) {
|
|
|
448
530
|
return parseDocument(resolved);
|
|
449
531
|
}
|
|
450
532
|
/**
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
533
|
+
* Asserts the parsed input is an OpenAPI or Swagger document.
|
|
534
|
+
*
|
|
535
|
+
* {@link validateDocument} keeps spec violations non-fatal so imperfect but usable documents still
|
|
536
|
+
* generate. That leniency also swallowed input that is not a document at all, which then produced
|
|
537
|
+
* an empty build with a success exit code. A missing version field is the one failure that cannot
|
|
538
|
+
* be a usable document, so it is fatal regardless of the `validate` option.
|
|
539
|
+
*/
|
|
540
|
+
function assertDocument(document) {
|
|
541
|
+
if (document && ("openapi" in document || "swagger" in document)) return;
|
|
542
|
+
throw new _kubb_core.Diagnostics.Error({
|
|
543
|
+
code: _kubb_core.Diagnostics.code.invalidDocument,
|
|
459
544
|
severity: "error",
|
|
460
|
-
message:
|
|
461
|
-
help: "
|
|
545
|
+
message: "The resolved `input` is not an OpenAPI or Swagger document: it declares no `openapi` or `swagger` version.",
|
|
546
|
+
help: "Point `input` at a document that declares `openapi` or `swagger`. If you pass an object, pass the spec itself rather than a wrapper such as `{ path }` or `{ data }`.",
|
|
462
547
|
location: { kind: "config" }
|
|
463
548
|
});
|
|
464
549
|
}
|
|
@@ -471,9 +556,10 @@ async function assertInputExists(input) {
|
|
|
471
556
|
* ```
|
|
472
557
|
*/
|
|
473
558
|
async function validateDocument(document, { throwOnError = false } = {}) {
|
|
559
|
+
const { compileErrors, validate } = await import("@readme/openapi-parser");
|
|
474
560
|
try {
|
|
475
|
-
const result = await
|
|
476
|
-
if (!result.valid) throw new Error(
|
|
561
|
+
const result = await validate(structuredClone(document), { validate: { errors: { colorize: true } } });
|
|
562
|
+
if (!result.valid) throw new Error(compileErrors(result));
|
|
477
563
|
} catch (error) {
|
|
478
564
|
if (throwOnError) throw error;
|
|
479
565
|
}
|
|
@@ -539,97 +625,232 @@ const jsonMimeFragments = [
|
|
|
539
625
|
function isJsonMimeType(mimeType) {
|
|
540
626
|
return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
|
|
541
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* Picks a media-type entry from a `content` map: the first JSON-like media type, falling back to
|
|
630
|
+
* the first declared one. Returns `false` when `content` has no entries.
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* ```ts
|
|
634
|
+
* pickContentEntry({ 'application/xml': xmlEntry, 'application/json': jsonEntry })
|
|
635
|
+
* // ['application/json', jsonEntry]
|
|
636
|
+
* ```
|
|
637
|
+
*/
|
|
638
|
+
function pickContentEntry(content) {
|
|
639
|
+
const mediaTypes = Object.keys(content);
|
|
640
|
+
const available = mediaTypes.find(isJsonMimeType) ?? mediaTypes[0];
|
|
641
|
+
return available ? [available, content[available]] : false;
|
|
642
|
+
}
|
|
542
643
|
//#endregion
|
|
543
|
-
//#region src/
|
|
544
|
-
const _refCache = /* @__PURE__ */ new WeakMap();
|
|
644
|
+
//#region src/model/components.ts
|
|
545
645
|
/**
|
|
546
|
-
*
|
|
646
|
+
* Extracts the inline schema from a media-type `content` map.
|
|
547
647
|
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
* build there is no sink to collect it, so it throws instead.
|
|
648
|
+
* Prefers `preferredContentType` when given, otherwise uses the first key in the map.
|
|
649
|
+
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
551
650
|
*
|
|
552
651
|
* @example
|
|
553
652
|
* ```ts
|
|
554
|
-
*
|
|
653
|
+
* extractSchemaFromContent(operation.content, 'application/json')
|
|
654
|
+
* // SchemaObject | null
|
|
555
655
|
* ```
|
|
556
656
|
*/
|
|
557
|
-
function
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
657
|
+
function extractSchemaFromContent(content, preferredContentType) {
|
|
658
|
+
if (!content) return null;
|
|
659
|
+
const firstContentType = Object.keys(content)[0] ?? "application/json";
|
|
660
|
+
const schema = content[preferredContentType ?? firstContentType]?.schema;
|
|
661
|
+
if (isReference(schema)) return null;
|
|
662
|
+
return schema ?? null;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
666
|
+
*/
|
|
667
|
+
function* collectRefs(schema) {
|
|
668
|
+
if (Array.isArray(schema)) {
|
|
669
|
+
for (const item of schema) yield* collectRefs(item);
|
|
670
|
+
return;
|
|
567
671
|
}
|
|
568
|
-
if (
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
pointer: origRef,
|
|
579
|
-
ref: origRef
|
|
580
|
-
}
|
|
581
|
-
};
|
|
582
|
-
if (!_kubb_core.Diagnostics.report(diagnostic)) throw new _kubb_core.Diagnostics.Error(diagnostic);
|
|
583
|
-
return null;
|
|
672
|
+
if (schema && typeof schema === "object") for (const key in schema) {
|
|
673
|
+
const value = schema[key];
|
|
674
|
+
if (!(key === "$ref" && typeof value === "string")) {
|
|
675
|
+
yield* collectRefs(value);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (value.startsWith("#/components/schemas/")) {
|
|
679
|
+
const name = value.slice(21);
|
|
680
|
+
if (name) yield name;
|
|
681
|
+
}
|
|
584
682
|
}
|
|
585
|
-
docCache.set($ref, current);
|
|
586
|
-
return current;
|
|
587
683
|
}
|
|
588
684
|
/**
|
|
589
|
-
*
|
|
685
|
+
* Returns a copy of `schemas` topologically sorted by `$ref` dependency.
|
|
590
686
|
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
687
|
+
* Referenced schemas appear before the schemas that depend on them, so code generators
|
|
688
|
+
* can emit types in the correct order. Cycles are silently skipped.
|
|
593
689
|
*
|
|
594
690
|
* @example
|
|
595
691
|
* ```ts
|
|
596
|
-
*
|
|
597
|
-
* //
|
|
692
|
+
* const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
|
|
693
|
+
* // Pet appears before Order when Order.$ref points at Pet
|
|
598
694
|
* ```
|
|
599
695
|
*/
|
|
600
|
-
function
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
696
|
+
function sortSchemas(schemas) {
|
|
697
|
+
const deps = /* @__PURE__ */ new Map();
|
|
698
|
+
for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
|
|
699
|
+
const sorted = [];
|
|
700
|
+
const visited = /* @__PURE__ */ new Set();
|
|
701
|
+
function visit(name, stack) {
|
|
702
|
+
if (visited.has(name) || stack.has(name)) return;
|
|
703
|
+
stack.add(name);
|
|
704
|
+
for (const child of deps.get(name) ?? []) if (deps.has(child)) visit(child, stack);
|
|
705
|
+
stack.delete(name);
|
|
706
|
+
visited.add(name);
|
|
707
|
+
sorted.push(name);
|
|
708
|
+
}
|
|
709
|
+
for (const name of Object.keys(schemas)) visit(name, /* @__PURE__ */ new Set());
|
|
710
|
+
const result = {};
|
|
711
|
+
for (const name of sorted) result[name] = schemas[name];
|
|
712
|
+
return result;
|
|
713
|
+
}
|
|
714
|
+
const semanticSuffixes = {
|
|
715
|
+
schemas: "Schema",
|
|
716
|
+
responses: "Response",
|
|
717
|
+
requestBodies: "Request"
|
|
718
|
+
};
|
|
719
|
+
/**
|
|
720
|
+
* Picks the collision suffix for one name-colliding schema: none when the name is unique,
|
|
721
|
+
* a semantic suffix (`Schema`, `Response`, `Request`) when the collision spans sources, otherwise
|
|
722
|
+
* a numeric suffix (`2`, `3`, …) for same-source collisions.
|
|
723
|
+
*/
|
|
724
|
+
function collisionSuffix({ isSingle, hasMultipleSources, source, index }) {
|
|
725
|
+
if (isSingle) return "";
|
|
726
|
+
if (hasMultipleSources) return semanticSuffixes[source];
|
|
727
|
+
if (index === 0) return "";
|
|
728
|
+
return String(index + 1);
|
|
607
729
|
}
|
|
608
730
|
/**
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
731
|
+
* Collects component schemas from one or more sources and resolves name collisions.
|
|
732
|
+
*
|
|
733
|
+
* Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
|
|
734
|
+
* topologically sorted by `$ref` dependency so generators emit types in the correct order.
|
|
735
|
+
*
|
|
736
|
+
* When two or more schemas normalize to the same PascalCase name:
|
|
737
|
+
* - Same source → numeric suffix (`2`, `3`, …).
|
|
738
|
+
* - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
|
|
612
739
|
*
|
|
613
740
|
* @example
|
|
614
741
|
* ```ts
|
|
615
|
-
*
|
|
742
|
+
* const { schemas, renames } = getSchemas(document, { contentType: 'application/json' }, refs)
|
|
616
743
|
* ```
|
|
617
744
|
*/
|
|
618
|
-
function
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
745
|
+
function getSchemas(document, { contentType }, refs) {
|
|
746
|
+
const components = document.components;
|
|
747
|
+
function resolveSchemaRef(schema) {
|
|
748
|
+
if (!isReference(schema)) return schema;
|
|
749
|
+
const resolved = refs.resolve(schema.$ref);
|
|
750
|
+
return resolved && !isReference(resolved) ? resolved : schema;
|
|
751
|
+
}
|
|
752
|
+
const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
|
|
753
|
+
schema: resolveSchemaRef(schema),
|
|
754
|
+
source: "schemas",
|
|
755
|
+
originalName: name
|
|
756
|
+
})), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
|
|
757
|
+
const schema = extractSchemaFromContent(item.content, contentType);
|
|
758
|
+
return schema ? [{
|
|
759
|
+
schema: resolveSchemaRef(schema),
|
|
760
|
+
source,
|
|
761
|
+
originalName: name
|
|
762
|
+
}] : [];
|
|
763
|
+
}))];
|
|
764
|
+
const normalizedNames = /* @__PURE__ */ new Map();
|
|
765
|
+
for (const item of candidates) {
|
|
766
|
+
const key = pascalCase(item.originalName);
|
|
767
|
+
const bucket = normalizedNames.get(key) ?? [];
|
|
768
|
+
bucket.push(item);
|
|
769
|
+
normalizedNames.set(key, bucket);
|
|
770
|
+
}
|
|
771
|
+
const schemas = {};
|
|
772
|
+
const renames = /* @__PURE__ */ new Map();
|
|
773
|
+
for (const [, items] of normalizedNames) {
|
|
774
|
+
const isSingle = items.length === 1;
|
|
775
|
+
const hasMultipleSources = !isSingle && new Set(items.map((item) => item.source)).size > 1;
|
|
776
|
+
items.forEach((item, index) => {
|
|
777
|
+
const suffix = collisionSuffix({
|
|
778
|
+
isSingle,
|
|
779
|
+
hasMultipleSources,
|
|
780
|
+
source: item.source,
|
|
781
|
+
index
|
|
782
|
+
});
|
|
783
|
+
const uniqueName = item.originalName + suffix;
|
|
784
|
+
schemas[uniqueName] = item.schema;
|
|
785
|
+
if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
schemas: sortSchemas(schemas),
|
|
790
|
+
renames
|
|
791
|
+
};
|
|
624
792
|
}
|
|
625
793
|
//#endregion
|
|
626
|
-
//#region src/
|
|
794
|
+
//#region src/model/server.ts
|
|
627
795
|
/**
|
|
628
|
-
*
|
|
629
|
-
*
|
|
796
|
+
* Reads the server URL from the document's `servers` array at `server.index`,
|
|
797
|
+
* interpolating any `server.variables` into the URL template.
|
|
798
|
+
*
|
|
799
|
+
* Returns `null` when `server.index` is omitted or out of range.
|
|
800
|
+
*
|
|
801
|
+
* @example Resolve the first server
|
|
802
|
+
* `resolveBaseUrl({ document, server: { index: 0 } })`
|
|
803
|
+
*
|
|
804
|
+
* @example Override a path variable
|
|
805
|
+
* `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
|
|
630
806
|
*/
|
|
631
|
-
function
|
|
632
|
-
|
|
807
|
+
function resolveBaseUrl({ document, server }) {
|
|
808
|
+
const index = server?.index;
|
|
809
|
+
const entry = index !== void 0 ? document.servers?.at(index) : void 0;
|
|
810
|
+
return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
|
|
814
|
+
* Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
|
|
815
|
+
* Throws if an override value is not in the variable's `enum` list.
|
|
816
|
+
*
|
|
817
|
+
* @example
|
|
818
|
+
* ```ts
|
|
819
|
+
* resolveServerUrl(
|
|
820
|
+
* { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
|
|
821
|
+
* { env: 'prod' },
|
|
822
|
+
* )
|
|
823
|
+
* // 'https://prod.api.example.com'
|
|
824
|
+
* ```
|
|
825
|
+
*/
|
|
826
|
+
function resolveServerUrl(server, overrides) {
|
|
827
|
+
if (!server.variables) return server.url;
|
|
828
|
+
let url = server.url;
|
|
829
|
+
for (const [key, variable] of Object.entries(server.variables)) {
|
|
830
|
+
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
|
|
831
|
+
if (value === void 0) continue;
|
|
832
|
+
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new _kubb_core.Diagnostics.Error({
|
|
833
|
+
code: _kubb_core.Diagnostics.code.invalidServerVariable,
|
|
834
|
+
severity: "error",
|
|
835
|
+
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
|
|
836
|
+
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
837
|
+
location: {
|
|
838
|
+
kind: "document",
|
|
839
|
+
pointer: "#/servers"
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
url = url.replaceAll(`{${key}}`, value);
|
|
843
|
+
}
|
|
844
|
+
return url;
|
|
845
|
+
}
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/operation.ts
|
|
848
|
+
/**
|
|
849
|
+
* Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
|
|
850
|
+
* with no leading or trailing dash.
|
|
851
|
+
*/
|
|
852
|
+
function slugify(value) {
|
|
853
|
+
return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
633
854
|
}
|
|
634
855
|
/**
|
|
635
856
|
* Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
|
|
@@ -648,26 +869,28 @@ function getResponseStatusCodes({ schema }) {
|
|
|
648
869
|
return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
|
|
649
870
|
}
|
|
650
871
|
/**
|
|
651
|
-
* Returns the response object for a status code, resolving a `$ref`
|
|
872
|
+
* Returns the response object for a status code, resolving a `$ref` through `refs`. `false` when absent.
|
|
652
873
|
*/
|
|
653
|
-
function getResponseByStatusCode({
|
|
874
|
+
function getResponseByStatusCode({ operation, refs, statusCode }) {
|
|
654
875
|
const responses = operation.schema.responses;
|
|
655
876
|
if (!responses || isReference(responses)) return false;
|
|
656
|
-
return
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
877
|
+
return refs.deref(responses[statusCode]) ?? false;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Resolves the operation's request body, dereferencing a `$ref` through `refs`. Returns `null`
|
|
881
|
+
* when the operation has no request body or it cannot be resolved.
|
|
882
|
+
*/
|
|
883
|
+
function getRequestBody({ operation, refs }) {
|
|
884
|
+
return refs.deref(operation.schema.requestBody);
|
|
661
885
|
}
|
|
662
886
|
/**
|
|
663
|
-
* Resolves the request body (
|
|
887
|
+
* Resolves the request body (a `$ref` through `refs`) and returns its content map, or
|
|
664
888
|
* `undefined` when the operation has no request body.
|
|
665
889
|
*/
|
|
666
|
-
function getRequestBodyContent({
|
|
667
|
-
return
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
key: "requestBody"
|
|
890
|
+
function getRequestBodyContent({ operation, refs }) {
|
|
891
|
+
return getRequestBody({
|
|
892
|
+
operation,
|
|
893
|
+
refs
|
|
671
894
|
})?.content;
|
|
672
895
|
}
|
|
673
896
|
/**
|
|
@@ -675,25 +898,23 @@ function getRequestBodyContent({ document, operation }) {
|
|
|
675
898
|
* Otherwise picks the first JSON-like media type, then the first declared one, as a
|
|
676
899
|
* `[mediaType, object]` tuple.
|
|
677
900
|
*/
|
|
678
|
-
function getRequestContent({
|
|
901
|
+
function getRequestContent({ operation, refs, mediaType }) {
|
|
679
902
|
const content = getRequestBodyContent({
|
|
680
|
-
|
|
681
|
-
|
|
903
|
+
operation,
|
|
904
|
+
refs
|
|
682
905
|
});
|
|
683
906
|
if (!content) return false;
|
|
684
907
|
if (mediaType) return mediaType in content ? content[mediaType] : false;
|
|
685
|
-
|
|
686
|
-
const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0];
|
|
687
|
-
return available ? [available, content[available]] : false;
|
|
908
|
+
return pickContentEntry(content);
|
|
688
909
|
}
|
|
689
910
|
/**
|
|
690
911
|
* Returns the primary request content type. Prefers a JSON-like media type (the last one wins
|
|
691
912
|
* when several are declared), then the first declared one, defaulting to `'application/json'`.
|
|
692
913
|
*/
|
|
693
|
-
function getRequestContentType({
|
|
914
|
+
function getRequestContentType({ operation, refs }) {
|
|
694
915
|
const content = getRequestBodyContent({
|
|
695
|
-
|
|
696
|
-
|
|
916
|
+
operation,
|
|
917
|
+
refs
|
|
697
918
|
});
|
|
698
919
|
const mediaTypes = content ? Object.keys(content) : [];
|
|
699
920
|
let result = mediaTypes[0] ?? "application/json";
|
|
@@ -706,22 +927,18 @@ function getRequestContentType({ document, operation }) {
|
|
|
706
927
|
*
|
|
707
928
|
* @example
|
|
708
929
|
* ```ts
|
|
709
|
-
* for (const operation of getOperations(document)) {
|
|
930
|
+
* for (const operation of getOperations(document, refs)) {
|
|
710
931
|
* parseOperation(options, operation)
|
|
711
932
|
* }
|
|
712
933
|
* ```
|
|
713
934
|
*/
|
|
714
|
-
function getOperations(document) {
|
|
935
|
+
function getOperations(document, refs) {
|
|
715
936
|
const operations = [];
|
|
716
937
|
const paths = document.paths;
|
|
717
938
|
if (!paths) return operations;
|
|
718
939
|
for (const path of Object.keys(paths)) {
|
|
719
940
|
if (path.startsWith("x-")) continue;
|
|
720
|
-
const pathItem =
|
|
721
|
-
document,
|
|
722
|
-
container: paths,
|
|
723
|
-
key: path
|
|
724
|
-
});
|
|
941
|
+
const pathItem = refs.deref(paths[path]);
|
|
725
942
|
if (!pathItem) continue;
|
|
726
943
|
const item = pathItem;
|
|
727
944
|
for (const method of Object.keys(item)) {
|
|
@@ -731,75 +948,25 @@ function getOperations(document) {
|
|
|
731
948
|
operations.push({
|
|
732
949
|
path,
|
|
733
950
|
method,
|
|
734
|
-
schema
|
|
951
|
+
schema,
|
|
952
|
+
pathItem
|
|
735
953
|
});
|
|
736
954
|
}
|
|
737
955
|
}
|
|
738
956
|
return operations;
|
|
739
957
|
}
|
|
740
958
|
//#endregion
|
|
741
|
-
//#region src/
|
|
742
|
-
/**
|
|
743
|
-
* Reads the server URL from the document's `servers` array at `server.index`,
|
|
744
|
-
* interpolating any `server.variables` into the URL template.
|
|
745
|
-
*
|
|
746
|
-
* Returns `null` when `server.index` is omitted or out of range.
|
|
747
|
-
*
|
|
748
|
-
* @example Resolve the first server
|
|
749
|
-
* `resolveBaseUrl({ document, server: { index: 0 } })`
|
|
750
|
-
*
|
|
751
|
-
* @example Override a path variable
|
|
752
|
-
* `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
|
|
753
|
-
*/
|
|
754
|
-
function resolveBaseUrl({ document, server }) {
|
|
755
|
-
const index = server?.index;
|
|
756
|
-
const entry = index !== void 0 ? document.servers?.at(index) : void 0;
|
|
757
|
-
return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
|
|
758
|
-
}
|
|
759
|
-
/**
|
|
760
|
-
* Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
|
|
761
|
-
* Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
|
|
762
|
-
* Throws if an override value is not in the variable's `enum` list.
|
|
763
|
-
*
|
|
764
|
-
* @example
|
|
765
|
-
* ```ts
|
|
766
|
-
* resolveServerUrl(
|
|
767
|
-
* { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
|
|
768
|
-
* { env: 'prod' },
|
|
769
|
-
* )
|
|
770
|
-
* // 'https://prod.api.example.com'
|
|
771
|
-
* ```
|
|
772
|
-
*/
|
|
773
|
-
function resolveServerUrl(server, overrides) {
|
|
774
|
-
if (!server.variables) return server.url;
|
|
775
|
-
let url = server.url;
|
|
776
|
-
for (const [key, variable] of Object.entries(server.variables)) {
|
|
777
|
-
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
|
|
778
|
-
if (value === void 0) continue;
|
|
779
|
-
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new _kubb_core.Diagnostics.Error({
|
|
780
|
-
code: _kubb_core.Diagnostics.code.invalidServerVariable,
|
|
781
|
-
severity: "error",
|
|
782
|
-
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
|
|
783
|
-
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
784
|
-
location: {
|
|
785
|
-
kind: "document",
|
|
786
|
-
pointer: "#/servers"
|
|
787
|
-
}
|
|
788
|
-
});
|
|
789
|
-
url = url.replaceAll(`{${key}}`, value);
|
|
790
|
-
}
|
|
791
|
-
return url;
|
|
792
|
-
}
|
|
959
|
+
//#region src/emit/schemaShape.ts
|
|
793
960
|
/**
|
|
794
961
|
* Returns the Kubb `SchemaType` for a given OAS `format` string, or `null` if not found.
|
|
795
|
-
* Formats not in `formatMap` (e.g., `int64`, `date-time`) are handled separately by parser options.
|
|
962
|
+
* Formats not in `formatMap` (e.g., `int64`, `uint64`, `date-time`) are handled separately by parser options.
|
|
796
963
|
*/
|
|
797
964
|
function getSchemaType(format) {
|
|
798
965
|
return formatMap[format] ?? null;
|
|
799
966
|
}
|
|
800
967
|
/**
|
|
801
968
|
* Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry, plus the
|
|
802
|
-
* `specialCasedFormats` that `convertFormat` handles directly. False means the format falls back to
|
|
969
|
+
* `specialCasedFormats` that `convertFormat` handles directly (int64, uint64, date-time, date, time). False means the format falls back to
|
|
803
970
|
* the base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
|
|
804
971
|
* diagnostic in step with the parser as `formatMap` grows.
|
|
805
972
|
*/
|
|
@@ -816,96 +983,57 @@ function getPrimitiveType(type) {
|
|
|
816
983
|
return "string";
|
|
817
984
|
}
|
|
818
985
|
/**
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
* Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
|
|
822
|
-
*
|
|
823
|
-
* @example
|
|
824
|
-
* ```ts
|
|
825
|
-
* getParameters(document, operation)
|
|
826
|
-
* // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
|
|
827
|
-
* ```
|
|
986
|
+
* Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
|
|
987
|
+
* Returns `null` when `dateType: false`, so the format falls through to `string`.
|
|
828
988
|
*/
|
|
829
|
-
function
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
989
|
+
function getDateType(options, format) {
|
|
990
|
+
if (!options.dateType) return null;
|
|
991
|
+
if (format === "date-time") {
|
|
992
|
+
if (options.dateType === "date") return {
|
|
993
|
+
type: "date",
|
|
994
|
+
representation: "date"
|
|
995
|
+
};
|
|
996
|
+
if (options.dateType === "stringOffset") return {
|
|
997
|
+
type: "datetime",
|
|
998
|
+
offset: true
|
|
999
|
+
};
|
|
1000
|
+
if (options.dateType === "stringLocal") return {
|
|
1001
|
+
type: "datetime",
|
|
1002
|
+
local: true
|
|
1003
|
+
};
|
|
1004
|
+
return {
|
|
1005
|
+
type: "datetime",
|
|
1006
|
+
offset: false
|
|
1007
|
+
};
|
|
847
1008
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
for (const key in responses) derefInPlace({
|
|
857
|
-
document,
|
|
858
|
-
container: responses,
|
|
859
|
-
key
|
|
860
|
-
});
|
|
1009
|
+
if (format === "date") return {
|
|
1010
|
+
type: "date",
|
|
1011
|
+
representation: options.dateType === "date" ? "date" : "string"
|
|
1012
|
+
};
|
|
1013
|
+
return {
|
|
1014
|
+
type: "time",
|
|
1015
|
+
representation: options.dateType === "date" ? "date" : "string"
|
|
1016
|
+
};
|
|
861
1017
|
}
|
|
862
1018
|
/**
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
* @example
|
|
868
|
-
* ```ts
|
|
869
|
-
* getResponseSchema(document, operation, 200) // SchemaObject
|
|
870
|
-
* getResponseSchema(document, operation, '4XX') // {}
|
|
871
|
-
* ```
|
|
1019
|
+
* Reads a schema's numeric `exclusiveMinimum`/`exclusiveMaximum` bounds (the OAS 3.1 numeric
|
|
1020
|
+
* form). Either key is `undefined` when absent or, for the legacy OAS 3.0 boolean form, not a
|
|
1021
|
+
* number.
|
|
872
1022
|
*/
|
|
873
|
-
function
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
statusCode
|
|
879
|
-
}), options.contentType);
|
|
880
|
-
if (responseBody === false) return {};
|
|
881
|
-
const schema = responseBody.schema;
|
|
882
|
-
if (!schema) return {};
|
|
883
|
-
return dereferenceWithRef(document, schema);
|
|
1023
|
+
function getExclusiveBounds(schema) {
|
|
1024
|
+
return {
|
|
1025
|
+
exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
|
|
1026
|
+
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
|
|
1027
|
+
};
|
|
884
1028
|
}
|
|
885
1029
|
/**
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
* ```ts
|
|
890
|
-
* getRequestSchema(document, operation) // SchemaObject | null
|
|
891
|
-
* ```
|
|
1030
|
+
* Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
|
|
1031
|
+
* labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
|
|
1032
|
+
* 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
|
|
892
1033
|
*/
|
|
893
|
-
function
|
|
894
|
-
if (
|
|
895
|
-
|
|
896
|
-
document,
|
|
897
|
-
operation,
|
|
898
|
-
mediaType: options.contentType
|
|
899
|
-
});
|
|
900
|
-
if (requestBody === false) return null;
|
|
901
|
-
const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
|
|
902
|
-
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
|
|
903
|
-
if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
|
|
904
|
-
type: "string",
|
|
905
|
-
contentMediaType: "application/octet-stream"
|
|
906
|
-
};
|
|
907
|
-
if (!schema) return null;
|
|
908
|
-
return dereferenceWithRef(document, schema);
|
|
1034
|
+
function extractExamples(schema) {
|
|
1035
|
+
if (Array.isArray(schema.examples)) return schema.examples;
|
|
1036
|
+
return schema.example !== void 0 ? [schema.example] : void 0;
|
|
909
1037
|
}
|
|
910
1038
|
/**
|
|
911
1039
|
* Returns `true` when `fragment` carries any JSON Schema keyword that makes it
|
|
@@ -914,8 +1042,7 @@ function getRequestSchema(document, operation, options = {}) {
|
|
|
914
1042
|
* A fragment with a structural keyword can't be safely merged into a parent schema.
|
|
915
1043
|
*/
|
|
916
1044
|
function hasStructuralKeywords(fragment) {
|
|
917
|
-
|
|
918
|
-
return false;
|
|
1045
|
+
return Object.keys(fragment).some((key) => structuralKeys.has(key));
|
|
919
1046
|
}
|
|
920
1047
|
/**
|
|
921
1048
|
* Flattens a keyword-only `allOf` into its parent schema.
|
|
@@ -946,192 +1073,15 @@ function flattenSchema(schema) {
|
|
|
946
1073
|
for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) merged[key] ??= value;
|
|
947
1074
|
return merged;
|
|
948
1075
|
}
|
|
1076
|
+
//#endregion
|
|
1077
|
+
//#region src/emit/createNode.ts
|
|
949
1078
|
/**
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
954
|
-
*
|
|
955
|
-
* @example
|
|
956
|
-
* ```ts
|
|
957
|
-
* extractSchemaFromContent(operation.content, 'application/json')
|
|
958
|
-
* // SchemaObject | null
|
|
959
|
-
* ```
|
|
960
|
-
*/
|
|
961
|
-
function extractSchemaFromContent(content, preferredContentType) {
|
|
962
|
-
if (!content) return null;
|
|
963
|
-
const firstContentType = Object.keys(content)[0] ?? "application/json";
|
|
964
|
-
const schema = content[preferredContentType ?? firstContentType]?.schema;
|
|
965
|
-
if (schema && "$ref" in schema) return null;
|
|
966
|
-
return schema ?? null;
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
970
|
-
*/
|
|
971
|
-
function* collectRefs(schema) {
|
|
972
|
-
if (Array.isArray(schema)) {
|
|
973
|
-
for (const item of schema) yield* collectRefs(item);
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
if (schema && typeof schema === "object") for (const key in schema) {
|
|
977
|
-
const value = schema[key];
|
|
978
|
-
if (!(key === "$ref" && typeof value === "string")) {
|
|
979
|
-
yield* collectRefs(value);
|
|
980
|
-
continue;
|
|
981
|
-
}
|
|
982
|
-
if (value.startsWith("#/components/schemas/")) {
|
|
983
|
-
const name = value.slice(21);
|
|
984
|
-
if (name) yield name;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
/**
|
|
989
|
-
* Returns a copy of `schemas` topologically sorted by `$ref` dependency.
|
|
990
|
-
*
|
|
991
|
-
* Referenced schemas appear before the schemas that depend on them, so code generators
|
|
992
|
-
* can emit types in the correct order. Cycles are silently skipped.
|
|
993
|
-
*
|
|
994
|
-
* @example
|
|
995
|
-
* ```ts
|
|
996
|
-
* const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
|
|
997
|
-
* // Pet appears before Order when Order.$ref points at Pet
|
|
998
|
-
* ```
|
|
999
|
-
*/
|
|
1000
|
-
function sortSchemas(schemas) {
|
|
1001
|
-
const deps = /* @__PURE__ */ new Map();
|
|
1002
|
-
for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
|
|
1003
|
-
const sorted = [];
|
|
1004
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1005
|
-
function visit(name, stack) {
|
|
1006
|
-
if (visited.has(name) || stack.has(name)) return;
|
|
1007
|
-
stack.add(name);
|
|
1008
|
-
for (const child of deps.get(name) ?? []) if (deps.has(child)) visit(child, stack);
|
|
1009
|
-
stack.delete(name);
|
|
1010
|
-
visited.add(name);
|
|
1011
|
-
sorted.push(name);
|
|
1012
|
-
}
|
|
1013
|
-
for (const name of Object.keys(schemas)) visit(name, /* @__PURE__ */ new Set());
|
|
1014
|
-
const result = {};
|
|
1015
|
-
for (const name of sorted) result[name] = schemas[name];
|
|
1016
|
-
return result;
|
|
1017
|
-
}
|
|
1018
|
-
const semanticSuffixes = {
|
|
1019
|
-
schemas: "Schema",
|
|
1020
|
-
responses: "Response",
|
|
1021
|
-
requestBodies: "Request"
|
|
1022
|
-
};
|
|
1023
|
-
function resolveSchemaRef(document, schema) {
|
|
1024
|
-
if (!isReference(schema)) return schema;
|
|
1025
|
-
const resolved = resolveRef(document, schema.$ref);
|
|
1026
|
-
return resolved && !isReference(resolved) ? resolved : schema;
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Collects component schemas from one or more sources and resolves name collisions.
|
|
1030
|
-
*
|
|
1031
|
-
* Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
|
|
1032
|
-
* topologically sorted by `$ref` dependency so generators emit types in the correct order.
|
|
1033
|
-
*
|
|
1034
|
-
* When two or more schemas normalize to the same PascalCase name:
|
|
1035
|
-
* - Same source → numeric suffix (`2`, `3`, …).
|
|
1036
|
-
* - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
|
|
1037
|
-
*
|
|
1038
|
-
* @example
|
|
1039
|
-
* ```ts
|
|
1040
|
-
* const { schemas, renames } = getSchemas(document, { contentType: 'application/json' })
|
|
1041
|
-
* ```
|
|
1042
|
-
*/
|
|
1043
|
-
function getSchemas(document, { contentType }) {
|
|
1044
|
-
const components = document.components;
|
|
1045
|
-
const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
|
|
1046
|
-
schema: resolveSchemaRef(document, schema),
|
|
1047
|
-
source: "schemas",
|
|
1048
|
-
originalName: name
|
|
1049
|
-
})), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
|
|
1050
|
-
const schema = extractSchemaFromContent(item.content, contentType);
|
|
1051
|
-
return schema ? [{
|
|
1052
|
-
schema: resolveSchemaRef(document, schema),
|
|
1053
|
-
source,
|
|
1054
|
-
originalName: name
|
|
1055
|
-
}] : [];
|
|
1056
|
-
}))];
|
|
1057
|
-
const normalizedNames = /* @__PURE__ */ new Map();
|
|
1058
|
-
for (const item of candidates) {
|
|
1059
|
-
const key = pascalCase(item.originalName);
|
|
1060
|
-
const bucket = normalizedNames.get(key) ?? [];
|
|
1061
|
-
bucket.push(item);
|
|
1062
|
-
normalizedNames.set(key, bucket);
|
|
1063
|
-
}
|
|
1064
|
-
const schemas = {};
|
|
1065
|
-
const renames = /* @__PURE__ */ new Map();
|
|
1066
|
-
for (const [, items] of normalizedNames) {
|
|
1067
|
-
const isSingle = items.length === 1;
|
|
1068
|
-
let hasMultipleSources = false;
|
|
1069
|
-
if (!isSingle) {
|
|
1070
|
-
const firstSource = items[0].source;
|
|
1071
|
-
for (const item of items) if (item.source !== firstSource) {
|
|
1072
|
-
hasMultipleSources = true;
|
|
1073
|
-
break;
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
items.forEach((item, index) => {
|
|
1077
|
-
const suffix = isSingle ? "" : hasMultipleSources ? semanticSuffixes[item.source] : index === 0 ? "" : String(index + 1);
|
|
1078
|
-
const uniqueName = item.originalName + suffix;
|
|
1079
|
-
schemas[uniqueName] = item.schema;
|
|
1080
|
-
if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1083
|
-
return {
|
|
1084
|
-
schemas: sortSchemas(schemas),
|
|
1085
|
-
renames
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
/**
|
|
1089
|
-
* Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
|
|
1090
|
-
* Returns `null` when `dateType: false`, so the format falls through to `string`.
|
|
1091
|
-
*/
|
|
1092
|
-
function getDateType(options, format) {
|
|
1093
|
-
if (!options.dateType) return null;
|
|
1094
|
-
if (format === "date-time") {
|
|
1095
|
-
if (options.dateType === "date") return {
|
|
1096
|
-
type: "date",
|
|
1097
|
-
representation: "date"
|
|
1098
|
-
};
|
|
1099
|
-
if (options.dateType === "stringOffset") return {
|
|
1100
|
-
type: "datetime",
|
|
1101
|
-
offset: true
|
|
1102
|
-
};
|
|
1103
|
-
if (options.dateType === "stringLocal") return {
|
|
1104
|
-
type: "datetime",
|
|
1105
|
-
local: true
|
|
1106
|
-
};
|
|
1107
|
-
return {
|
|
1108
|
-
type: "datetime",
|
|
1109
|
-
offset: false
|
|
1110
|
-
};
|
|
1111
|
-
}
|
|
1112
|
-
if (format === "date") return {
|
|
1113
|
-
type: "date",
|
|
1114
|
-
representation: options.dateType === "date" ? "date" : "string"
|
|
1115
|
-
};
|
|
1116
|
-
return {
|
|
1117
|
-
type: "time",
|
|
1118
|
-
representation: options.dateType === "date" ? "date" : "string"
|
|
1119
|
-
};
|
|
1120
|
-
}
|
|
1121
|
-
/**
|
|
1122
|
-
* Collects the shared metadata fields passed to every `createSchema` call.
|
|
1123
|
-
*/
|
|
1124
|
-
/**
|
|
1125
|
-
* Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
|
|
1126
|
-
* labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
|
|
1127
|
-
* 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
|
|
1079
|
+
* Builds a schema node from a converter's base context plus its type-specific fields. Every
|
|
1080
|
+
* converter needs the same metadata fields (`title`, `description`, `examples`, ...) alongside
|
|
1081
|
+
* whatever makes its node distinct; this folds both into one call.
|
|
1128
1082
|
*/
|
|
1129
|
-
function
|
|
1130
|
-
|
|
1131
|
-
return schema.example !== void 0 ? [schema.example] : void 0;
|
|
1132
|
-
}
|
|
1133
|
-
function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
1134
|
-
return {
|
|
1083
|
+
function createNode({ schema, name, nullable, defaultValue }, extras) {
|
|
1084
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1135
1085
|
name,
|
|
1136
1086
|
nullable,
|
|
1137
1087
|
title: schema.title,
|
|
@@ -1141,125 +1091,166 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
|
1141
1091
|
writeOnly: schema.writeOnly,
|
|
1142
1092
|
default: defaultValue,
|
|
1143
1093
|
examples: extractExamples(schema),
|
|
1144
|
-
format: schema.format
|
|
1145
|
-
|
|
1094
|
+
format: schema.format,
|
|
1095
|
+
...extras
|
|
1096
|
+
});
|
|
1146
1097
|
}
|
|
1098
|
+
//#endregion
|
|
1099
|
+
//#region src/emit/discriminator/preserve.ts
|
|
1147
1100
|
/**
|
|
1148
|
-
*
|
|
1149
|
-
*
|
|
1150
|
-
* The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
|
|
1151
|
-
* `getRequestSchema` already performs), so the returned list accurately reflects the
|
|
1152
|
-
* available content types even for referenced bodies.
|
|
1101
|
+
* Creates a single-property object schema used as a discriminator literal.
|
|
1153
1102
|
*
|
|
1154
1103
|
* @example
|
|
1155
1104
|
* ```ts
|
|
1156
|
-
*
|
|
1157
|
-
* // ['
|
|
1105
|
+
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
1106
|
+
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
1158
1107
|
* ```
|
|
1159
1108
|
*/
|
|
1160
|
-
function
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1109
|
+
function createDiscriminantNode({ propertyName, value }) {
|
|
1110
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1111
|
+
type: "object",
|
|
1112
|
+
primitive: "object",
|
|
1113
|
+
properties: [_kubb_ast.ast.factory.createProperty({
|
|
1114
|
+
name: propertyName,
|
|
1115
|
+
schema: _kubb_ast.ast.factory.createSchema({
|
|
1116
|
+
type: "enum",
|
|
1117
|
+
primitive: "string",
|
|
1118
|
+
enumValues: [value]
|
|
1119
|
+
}),
|
|
1120
|
+
required: true
|
|
1121
|
+
})]
|
|
1122
|
+
});
|
|
1165
1123
|
}
|
|
1166
1124
|
/**
|
|
1167
|
-
* Returns
|
|
1168
|
-
*
|
|
1169
|
-
* Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
|
|
1170
|
-
* so the returned list reflects the available content types even for referenced responses.
|
|
1125
|
+
* Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
|
|
1171
1126
|
*
|
|
1172
1127
|
* @example
|
|
1173
1128
|
* ```ts
|
|
1174
|
-
*
|
|
1175
|
-
* // ['application/json', 'application/xml']
|
|
1129
|
+
* findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
|
|
1176
1130
|
* ```
|
|
1177
1131
|
*/
|
|
1178
|
-
function
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
document,
|
|
1182
|
-
operation,
|
|
1183
|
-
statusCode
|
|
1184
|
-
});
|
|
1185
|
-
if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
|
|
1186
|
-
const body = responseObj;
|
|
1187
|
-
return body.content ? Object.keys(body.content) : [];
|
|
1188
|
-
}
|
|
1189
|
-
//#endregion
|
|
1190
|
-
//#region src/converters.ts
|
|
1191
|
-
/**
|
|
1192
|
-
* Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
|
|
1193
|
-
*
|
|
1194
|
-
* This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
|
|
1195
|
-
* from the array to its items sub-schema, so they are valid for downstream processing.
|
|
1196
|
-
*
|
|
1197
|
-
* @note A defensive measure for non-compliant specs.
|
|
1198
|
-
*/
|
|
1199
|
-
function normalizeArrayEnum(schema) {
|
|
1200
|
-
const normalizedItems = {
|
|
1201
|
-
...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
|
|
1202
|
-
enum: schema.enum
|
|
1203
|
-
};
|
|
1204
|
-
const { enum: _enum, ...schemaWithoutEnum } = schema;
|
|
1205
|
-
return {
|
|
1206
|
-
...schemaWithoutEnum,
|
|
1207
|
-
items: normalizedItems
|
|
1208
|
-
};
|
|
1132
|
+
function findDiscriminator(mapping, ref) {
|
|
1133
|
+
if (!mapping || !ref) return null;
|
|
1134
|
+
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
|
|
1209
1135
|
}
|
|
1210
1136
|
/**
|
|
1211
|
-
*
|
|
1212
|
-
*
|
|
1137
|
+
* Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
|
|
1138
|
+
* node with either the shared-properties slice carrying that value, or a synthetic discriminant
|
|
1139
|
+
* literal. The referenced child schema's own definition is left untouched — narrowing happens only
|
|
1140
|
+
* at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
|
|
1141
|
+
* which additionally patches the child schema's own definition in a post-pass).
|
|
1213
1142
|
*/
|
|
1214
|
-
function
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1143
|
+
function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, rawOptions, name, refs }) {
|
|
1144
|
+
function pickDiscriminatorPropertyNode(node, propertyName) {
|
|
1145
|
+
const discriminatorProperty = _kubb_ast.ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
|
|
1146
|
+
if (!discriminatorProperty) return null;
|
|
1147
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1148
|
+
type: "object",
|
|
1149
|
+
primitive: "object",
|
|
1150
|
+
properties: [discriminatorProperty]
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
function implicitDiscriminantValue(member) {
|
|
1154
|
+
if (!discriminator || discriminator.mapping || !isReference(member)) return null;
|
|
1155
|
+
const value = (0, _kubb_kit.extractRefName)(member.$ref);
|
|
1156
|
+
if (!value) return null;
|
|
1157
|
+
const variant = refs.resolve(member.$ref, { report: false });
|
|
1158
|
+
if (!variant) return null;
|
|
1159
|
+
const propertyName = discriminator.propertyName;
|
|
1160
|
+
const seen = /* @__PURE__ */ new Set([member.$ref]);
|
|
1161
|
+
function constrains(v) {
|
|
1162
|
+
const prop = v.properties?.[propertyName];
|
|
1163
|
+
const resolved = prop && isReference(prop) ? refs.resolve(prop.$ref, { report: false }) : prop;
|
|
1164
|
+
if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
|
|
1165
|
+
const composition = v.allOf ?? v.oneOf ?? v.anyOf;
|
|
1166
|
+
if (!composition) return false;
|
|
1167
|
+
return composition.some((m) => {
|
|
1168
|
+
if (!isReference(m)) return constrains(m);
|
|
1169
|
+
if (seen.has(m.$ref)) return false;
|
|
1170
|
+
seen.add(m.$ref);
|
|
1171
|
+
const r = refs.resolve(m.$ref, { report: false });
|
|
1172
|
+
return r ? constrains(r) : false;
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
return constrains(variant) ? null : value;
|
|
1176
|
+
}
|
|
1177
|
+
return unionMembers.map((s) => {
|
|
1178
|
+
const ref = isReference(s) ? s.$ref : void 0;
|
|
1179
|
+
const discriminatorValue = findDiscriminator(discriminator?.mapping, ref) ?? implicitDiscriminantValue(s);
|
|
1180
|
+
const memberNode = parse({
|
|
1181
|
+
schema: s,
|
|
1182
|
+
name
|
|
1183
|
+
}, rawOptions);
|
|
1184
|
+
if (!discriminatorValue || !discriminator) return memberNode;
|
|
1185
|
+
const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(_kubb_ast.ast.applyMacros(sharedPropertiesNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
|
|
1186
|
+
propertyName: discriminator.propertyName,
|
|
1187
|
+
values: [discriminatorValue]
|
|
1188
|
+
})], { depth: "shallow" }), discriminator.propertyName) : void 0;
|
|
1189
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1190
|
+
type: "intersection",
|
|
1191
|
+
members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
|
|
1192
|
+
propertyName: discriminator.propertyName,
|
|
1193
|
+
value: discriminatorValue
|
|
1194
|
+
})]
|
|
1195
|
+
});
|
|
1224
1196
|
});
|
|
1225
1197
|
}
|
|
1226
1198
|
/**
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1199
|
+
* Filters the discriminated members out of an `allOf` list: an `allOf` member that `$ref`s a
|
|
1200
|
+
* discriminated union's parent, where this schema is itself one of that union's children, is
|
|
1201
|
+
* dropped from `members` and its discriminant value collected instead — the same synthetic
|
|
1202
|
+
* literal `narrowUnionMembers` produces, so the emitted node stays a plain intersection rather
|
|
1203
|
+
* than nesting the whole parent union one level deeper.
|
|
1229
1204
|
*/
|
|
1230
|
-
function
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1205
|
+
function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
|
|
1206
|
+
const discriminantValues = [];
|
|
1207
|
+
return {
|
|
1208
|
+
members: allOfMembers.filter((item) => {
|
|
1209
|
+
if (!isReference(item) || !name) return true;
|
|
1210
|
+
const deref = refs.resolve(item.$ref);
|
|
1211
|
+
if (!deref || !isDiscriminator(deref)) return true;
|
|
1212
|
+
const parentUnion = deref.oneOf ?? deref.anyOf;
|
|
1213
|
+
if (!parentUnion) return true;
|
|
1214
|
+
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1215
|
+
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
|
|
1216
|
+
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
|
|
1217
|
+
if (inOneOf || inMapping) {
|
|
1218
|
+
const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
|
|
1219
|
+
if (discriminatorValue) discriminantValues.push({
|
|
1220
|
+
propertyName: deref.discriminator.propertyName,
|
|
1221
|
+
value: discriminatorValue
|
|
1222
|
+
});
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
return true;
|
|
1226
|
+
}),
|
|
1227
|
+
discriminantValues
|
|
1228
|
+
};
|
|
1242
1229
|
}
|
|
1230
|
+
//#endregion
|
|
1231
|
+
//#region src/emit/converters/composition.ts
|
|
1243
1232
|
/**
|
|
1244
1233
|
* Converts a `$ref` schema into a `RefSchemaNode`.
|
|
1245
1234
|
*
|
|
1246
1235
|
* The resolved schema is stored in `node.schema`. Usage-site sibling fields
|
|
1247
1236
|
* (description, readOnly, nullable, etc.) are stored directly on the ref node.
|
|
1248
1237
|
* Use `syncSchemaRef(node)` in printers to get a merged view of both.
|
|
1249
|
-
* Circular refs are detected in `
|
|
1238
|
+
* Circular refs are detected in `refs.resolveNode` and leave `schema` as `null`.
|
|
1250
1239
|
*/
|
|
1251
|
-
function convertRef({ schema, name, nullable, defaultValue, rawOptions, document,
|
|
1240
|
+
function convertRef({ schema, name, nullable, defaultValue, rawOptions, document, parse, refs, renames }) {
|
|
1252
1241
|
const refPath = schema.$ref;
|
|
1253
|
-
const resolvedSchema = refPath ?
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1242
|
+
const resolvedSchema = refPath ? refs.resolveNode(refPath, parse, rawOptions) : null;
|
|
1243
|
+
const ctx = {
|
|
1244
|
+
schema,
|
|
1245
|
+
name,
|
|
1246
|
+
nullable,
|
|
1247
|
+
defaultValue
|
|
1248
|
+
};
|
|
1249
|
+
if (refPath && document.components && !refs.exists(refPath)) return createNode(ctx, { type: "unknown" });
|
|
1258
1250
|
const targetName = renames?.get(schema.$ref);
|
|
1259
|
-
return
|
|
1260
|
-
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1251
|
+
return createNode(ctx, {
|
|
1261
1252
|
type: "ref",
|
|
1262
|
-
name: (0,
|
|
1253
|
+
name: (0, _kubb_kit.extractRefName)(schema.$ref),
|
|
1263
1254
|
ref: schema.$ref,
|
|
1264
1255
|
...targetName ? { targetName } : {},
|
|
1265
1256
|
schema: resolvedSchema
|
|
@@ -1268,7 +1259,7 @@ function convertRef({ schema, name, nullable, defaultValue, rawOptions, document
|
|
|
1268
1259
|
/**
|
|
1269
1260
|
* Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
|
|
1270
1261
|
*/
|
|
1271
|
-
function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
1262
|
+
function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse, refs }) {
|
|
1272
1263
|
if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
|
|
1273
1264
|
const [memberSchema] = schema.allOf;
|
|
1274
1265
|
const memberNode = parse({
|
|
@@ -1293,26 +1284,12 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1293
1284
|
format: schema.format ?? memberNode.format
|
|
1294
1285
|
});
|
|
1295
1286
|
}
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
if (!parentUnion) return true;
|
|
1303
|
-
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1304
|
-
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
|
|
1305
|
-
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
|
|
1306
|
-
if (inOneOf || inMapping) {
|
|
1307
|
-
const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
|
|
1308
|
-
if (discriminatorValue) filteredDiscriminantValues.push({
|
|
1309
|
-
propertyName: deref.discriminator.propertyName,
|
|
1310
|
-
value: discriminatorValue
|
|
1311
|
-
});
|
|
1312
|
-
return false;
|
|
1313
|
-
}
|
|
1314
|
-
return true;
|
|
1315
|
-
}).map((s) => parse({
|
|
1287
|
+
const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
|
|
1288
|
+
allOfMembers: schema.allOf,
|
|
1289
|
+
name,
|
|
1290
|
+
refs
|
|
1291
|
+
});
|
|
1292
|
+
const allOfMembers = discriminatedAllOf.map((s) => parse({
|
|
1316
1293
|
schema: s,
|
|
1317
1294
|
name
|
|
1318
1295
|
}, rawOptions));
|
|
@@ -1323,7 +1300,7 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1323
1300
|
if (missingRequired.length) {
|
|
1324
1301
|
const resolvedMembers = schema.allOf.flatMap((item) => {
|
|
1325
1302
|
if (!isReference(item)) return [item];
|
|
1326
|
-
const deref =
|
|
1303
|
+
const deref = refs.resolve(item.$ref);
|
|
1327
1304
|
return deref && !isReference(deref) ? [deref] : [];
|
|
1328
1305
|
});
|
|
1329
1306
|
for (const key of missingRequired) for (const resolved of resolvedMembers) {
|
|
@@ -1346,61 +1323,33 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1346
1323
|
const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
|
|
1347
1324
|
allOfMembers.push(parse({ schema: schemaWithoutAllOf }, rawOptions));
|
|
1348
1325
|
}
|
|
1349
|
-
for (const { propertyName, value } of
|
|
1326
|
+
for (const { propertyName, value } of discriminantValues) allOfMembers.push(createDiscriminantNode({
|
|
1350
1327
|
propertyName,
|
|
1351
1328
|
value
|
|
1352
1329
|
}));
|
|
1353
|
-
return
|
|
1330
|
+
return createNode({
|
|
1331
|
+
schema,
|
|
1332
|
+
name,
|
|
1333
|
+
nullable,
|
|
1334
|
+
defaultValue
|
|
1335
|
+
}, {
|
|
1354
1336
|
type: "intersection",
|
|
1355
|
-
members: [...(0,
|
|
1356
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1337
|
+
members: [...(0, _kubb_kit.mergeAdjacentObjectsLazy)(allOfMembers.slice(0, syntheticStart)), ...(0, _kubb_kit.mergeAdjacentObjectsLazy)(allOfMembers.slice(syntheticStart))]
|
|
1357
1338
|
});
|
|
1358
1339
|
}
|
|
1359
1340
|
/**
|
|
1360
1341
|
* Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
|
|
1361
1342
|
*/
|
|
1362
|
-
function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
properties: [discriminatorProperty]
|
|
1370
|
-
});
|
|
1371
|
-
}
|
|
1372
|
-
function resolveRefSilent($ref) {
|
|
1373
|
-
if (!$ref.startsWith("#")) return null;
|
|
1374
|
-
return decodeURIComponent($ref.substring(1)).split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document) ?? null;
|
|
1375
|
-
}
|
|
1376
|
-
function implicitDiscriminantValue(member) {
|
|
1377
|
-
if (!discriminator || discriminator.mapping || !isReference(member)) return null;
|
|
1378
|
-
const value = (0, _kubb_ast.extractRefName)(member.$ref);
|
|
1379
|
-
if (!value) return null;
|
|
1380
|
-
const variant = resolveRefSilent(member.$ref);
|
|
1381
|
-
if (!variant) return null;
|
|
1382
|
-
const propertyName = discriminator.propertyName;
|
|
1383
|
-
const seen = /* @__PURE__ */ new Set([member.$ref]);
|
|
1384
|
-
function constrains(v) {
|
|
1385
|
-
const prop = v.properties?.[propertyName];
|
|
1386
|
-
const resolved = prop && isReference(prop) ? resolveRefSilent(prop.$ref) : prop;
|
|
1387
|
-
if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
|
|
1388
|
-
const composition = v.allOf ?? v.oneOf ?? v.anyOf;
|
|
1389
|
-
if (!composition) return false;
|
|
1390
|
-
return composition.some((m) => {
|
|
1391
|
-
if (!isReference(m)) return constrains(m);
|
|
1392
|
-
if (seen.has(m.$ref)) return false;
|
|
1393
|
-
seen.add(m.$ref);
|
|
1394
|
-
const r = resolveRefSilent(m.$ref);
|
|
1395
|
-
return r ? constrains(r) : false;
|
|
1396
|
-
});
|
|
1397
|
-
}
|
|
1398
|
-
return constrains(variant) ? null : value;
|
|
1399
|
-
}
|
|
1343
|
+
function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse, refs }) {
|
|
1344
|
+
const ctx = {
|
|
1345
|
+
schema,
|
|
1346
|
+
name,
|
|
1347
|
+
nullable,
|
|
1348
|
+
defaultValue
|
|
1349
|
+
};
|
|
1400
1350
|
const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
|
|
1401
1351
|
const strategy = schema.oneOf ? "one" : "any";
|
|
1402
|
-
const
|
|
1403
|
-
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1352
|
+
const unionExtras = {
|
|
1404
1353
|
discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
|
|
1405
1354
|
strategy
|
|
1406
1355
|
};
|
|
@@ -1411,47 +1360,99 @@ function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1411
1360
|
name
|
|
1412
1361
|
}, rawOptions) : void 0;
|
|
1413
1362
|
if (sharedPropertiesNode || discriminator) {
|
|
1414
|
-
const members =
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(_kubb_ast.ast.applyMacros(sharedPropertiesNode, [(0, _kubb_ast.macroDiscriminatorEnum)({
|
|
1423
|
-
propertyName: discriminator.propertyName,
|
|
1424
|
-
values: [discriminatorValue]
|
|
1425
|
-
})], { depth: "shallow" }), discriminator.propertyName) : void 0;
|
|
1426
|
-
return _kubb_ast.ast.factory.createSchema({
|
|
1427
|
-
type: "intersection",
|
|
1428
|
-
members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
|
|
1429
|
-
propertyName: discriminator.propertyName,
|
|
1430
|
-
value: discriminatorValue
|
|
1431
|
-
})]
|
|
1432
|
-
});
|
|
1363
|
+
const members = narrowUnionMembers({
|
|
1364
|
+
unionMembers,
|
|
1365
|
+
discriminator,
|
|
1366
|
+
sharedPropertiesNode,
|
|
1367
|
+
parse,
|
|
1368
|
+
rawOptions,
|
|
1369
|
+
name,
|
|
1370
|
+
refs
|
|
1433
1371
|
});
|
|
1434
|
-
const unionNode =
|
|
1372
|
+
const unionNode = createNode(ctx, {
|
|
1435
1373
|
type: "union",
|
|
1436
|
-
...
|
|
1374
|
+
...unionExtras,
|
|
1437
1375
|
members
|
|
1438
1376
|
});
|
|
1439
1377
|
if (!sharedPropertiesNode) return unionNode;
|
|
1440
|
-
return
|
|
1378
|
+
return createNode(ctx, {
|
|
1441
1379
|
type: "intersection",
|
|
1442
|
-
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1443
1380
|
members: [unionNode, sharedPropertiesNode]
|
|
1444
1381
|
});
|
|
1445
1382
|
}
|
|
1446
|
-
const unionNode =
|
|
1383
|
+
const unionNode = createNode(ctx, {
|
|
1447
1384
|
type: "union",
|
|
1448
|
-
...
|
|
1385
|
+
...unionExtras,
|
|
1449
1386
|
members: unionMembers.map((s) => parse({
|
|
1450
1387
|
schema: s,
|
|
1451
1388
|
name
|
|
1452
1389
|
}, rawOptions))
|
|
1453
1390
|
});
|
|
1454
|
-
return _kubb_ast.ast.applyMacros(unionNode, [
|
|
1391
|
+
return _kubb_ast.ast.applyMacros(unionNode, [_kubb_kit.macroSimplifyUnion], { depth: "shallow" });
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
|
|
1395
|
+
* Only called once the multi-type rule's `match` has confirmed more than one non-`null` type
|
|
1396
|
+
* remains; a single remaining type (e.g. `['string', 'null']`) is handled as that type instead,
|
|
1397
|
+
* with nullability already folded in.
|
|
1398
|
+
*/
|
|
1399
|
+
function convertMultiType({ schema, name, nullable, defaultValue, rawOptions, parse }) {
|
|
1400
|
+
const types = schema.type;
|
|
1401
|
+
const nonNullTypes = types.filter((t) => t !== "null");
|
|
1402
|
+
return createNode({
|
|
1403
|
+
schema,
|
|
1404
|
+
name,
|
|
1405
|
+
nullable: types.includes("null") || nullable || void 0,
|
|
1406
|
+
defaultValue
|
|
1407
|
+
}, {
|
|
1408
|
+
type: "union",
|
|
1409
|
+
members: nonNullTypes.map((t) => {
|
|
1410
|
+
return parse({
|
|
1411
|
+
schema: {
|
|
1412
|
+
...schema,
|
|
1413
|
+
type: t
|
|
1414
|
+
},
|
|
1415
|
+
name
|
|
1416
|
+
}, rawOptions);
|
|
1417
|
+
})
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
//#endregion
|
|
1421
|
+
//#region src/emit/converters/scalar.ts
|
|
1422
|
+
/**
|
|
1423
|
+
* Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
|
|
1424
|
+
*
|
|
1425
|
+
* This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
|
|
1426
|
+
* from the array to its items sub-schema, so they are valid for downstream processing.
|
|
1427
|
+
*
|
|
1428
|
+
* @note A defensive measure for non-compliant specs.
|
|
1429
|
+
*/
|
|
1430
|
+
function normalizeArrayEnum(schema) {
|
|
1431
|
+
const normalizedItems = {
|
|
1432
|
+
...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
|
|
1433
|
+
enum: schema.enum
|
|
1434
|
+
};
|
|
1435
|
+
const { enum: _enum, ...schemaWithoutEnum } = schema;
|
|
1436
|
+
return {
|
|
1437
|
+
...schemaWithoutEnum,
|
|
1438
|
+
items: normalizedItems
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
|
|
1443
|
+
* and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
|
|
1444
|
+
*/
|
|
1445
|
+
function createNullNode(schema, name, nullable) {
|
|
1446
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1447
|
+
type: "null",
|
|
1448
|
+
primitive: "null",
|
|
1449
|
+
name,
|
|
1450
|
+
title: schema.title,
|
|
1451
|
+
description: schema.description,
|
|
1452
|
+
deprecated: schema.deprecated,
|
|
1453
|
+
nullable,
|
|
1454
|
+
format: schema.format
|
|
1455
|
+
});
|
|
1455
1456
|
}
|
|
1456
1457
|
/**
|
|
1457
1458
|
* Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
|
|
@@ -1460,54 +1461,57 @@ function convertConst({ schema, name, nullable, defaultValue }) {
|
|
|
1460
1461
|
const constValue = schema.const;
|
|
1461
1462
|
if (constValue === null) return createNullNode(schema, name);
|
|
1462
1463
|
const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
|
|
1463
|
-
return
|
|
1464
|
+
return createNode({
|
|
1465
|
+
schema,
|
|
1466
|
+
name,
|
|
1467
|
+
nullable,
|
|
1468
|
+
defaultValue
|
|
1469
|
+
}, {
|
|
1464
1470
|
type: "enum",
|
|
1465
1471
|
primitive: constPrimitive,
|
|
1466
|
-
enumValues: [constValue]
|
|
1467
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1472
|
+
enumValues: [constValue]
|
|
1468
1473
|
});
|
|
1469
1474
|
}
|
|
1470
1475
|
/**
|
|
1471
|
-
* Converts a format-annotated schema into a special-type `SchemaNode`.
|
|
1472
|
-
*
|
|
1476
|
+
* Converts a format-annotated schema into a special-type `SchemaNode`. Only called once the
|
|
1477
|
+
* `format` rule's `match` has confirmed the format is handled (see `isHandledFormat`) and, for
|
|
1478
|
+
* a date-ish format, that `dateType` is not `false`.
|
|
1473
1479
|
*/
|
|
1474
|
-
function convertFormat(
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1480
|
+
function convertFormat(context) {
|
|
1481
|
+
const { schema, name, nullable, defaultValue, options, type } = context;
|
|
1482
|
+
const ctx = {
|
|
1483
|
+
schema,
|
|
1484
|
+
name,
|
|
1485
|
+
nullable,
|
|
1486
|
+
defaultValue
|
|
1487
|
+
};
|
|
1488
|
+
if (type === "string" && numericFormats.has(schema.format)) return convertString(context);
|
|
1489
|
+
if (schema.format === "int64" || schema.format === "uint64") return createNode(ctx, {
|
|
1477
1490
|
type: options.integerType === "bigint" ? "bigint" : "integer",
|
|
1478
1491
|
primitive: "integer",
|
|
1479
|
-
...base,
|
|
1480
1492
|
min: schema.minimum,
|
|
1481
1493
|
max: schema.maximum,
|
|
1482
|
-
|
|
1483
|
-
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
|
|
1494
|
+
...getExclusiveBounds(schema)
|
|
1484
1495
|
});
|
|
1485
1496
|
if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
|
|
1486
1497
|
const dateType = getDateType(options, schema.format);
|
|
1487
|
-
if (
|
|
1488
|
-
if (dateType.type === "datetime") return _kubb_ast.ast.factory.createSchema({
|
|
1489
|
-
...base,
|
|
1498
|
+
if (dateType.type === "datetime") return createNode(ctx, {
|
|
1490
1499
|
primitive: "string",
|
|
1491
1500
|
type: "datetime",
|
|
1492
1501
|
offset: dateType.offset,
|
|
1493
1502
|
local: dateType.local
|
|
1494
1503
|
});
|
|
1495
|
-
return
|
|
1496
|
-
...base,
|
|
1504
|
+
return createNode(ctx, {
|
|
1497
1505
|
primitive: "string",
|
|
1498
1506
|
type: dateType.type,
|
|
1499
1507
|
representation: dateType.representation
|
|
1500
1508
|
});
|
|
1501
1509
|
}
|
|
1502
1510
|
const specialType = getSchemaType(schema.format);
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const hasLength = specialType === "url" || specialType === "uuid" || specialType === "email";
|
|
1506
|
-
return _kubb_ast.ast.factory.createSchema({
|
|
1507
|
-
...base,
|
|
1508
|
-
primitive: specialPrimitive,
|
|
1511
|
+
return createNode(ctx, {
|
|
1512
|
+
primitive: specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string",
|
|
1509
1513
|
type: specialType,
|
|
1510
|
-
...
|
|
1514
|
+
...specialType === "url" || specialType === "uuid" || specialType === "email" ? {
|
|
1511
1515
|
min: schema.minLength,
|
|
1512
1516
|
max: schema.maxLength
|
|
1513
1517
|
} : {}
|
|
@@ -1527,21 +1531,28 @@ function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
|
|
|
1527
1531
|
const enumNullable = nullable || nullInEnum || void 0;
|
|
1528
1532
|
const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
|
|
1529
1533
|
const enumPrimitive = getPrimitiveType(type);
|
|
1530
|
-
const
|
|
1534
|
+
const ctx = {
|
|
1535
|
+
schema,
|
|
1536
|
+
name,
|
|
1537
|
+
nullable: enumNullable,
|
|
1538
|
+
defaultValue: enumDefault
|
|
1539
|
+
};
|
|
1540
|
+
const enumExtras = {
|
|
1531
1541
|
type: "enum",
|
|
1532
|
-
primitive: enumPrimitive
|
|
1533
|
-
...buildSchemaNode(schema, name, enumNullable, enumDefault)
|
|
1542
|
+
primitive: enumPrimitive
|
|
1534
1543
|
};
|
|
1535
1544
|
const extensionKey = enumExtensionKeys.find((key) => key in schema);
|
|
1536
1545
|
const descriptionKey = enumDescriptionKeys.find((key) => key in schema);
|
|
1537
1546
|
if (extensionKey || descriptionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
|
|
1538
|
-
|
|
1547
|
+
let enumPrimitiveType = "string";
|
|
1548
|
+
if (enumPrimitive === "number" || enumPrimitive === "integer") enumPrimitiveType = "number";
|
|
1549
|
+
else if (enumPrimitive === "boolean") enumPrimitiveType = "boolean";
|
|
1539
1550
|
const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
|
|
1540
1551
|
const rawEnumDescriptions = descriptionKey ? schema[descriptionKey] : void 0;
|
|
1541
1552
|
const uniqueValues = [...new Set(filteredValues)];
|
|
1542
1553
|
const seenNames = /* @__PURE__ */ new Set();
|
|
1543
|
-
return
|
|
1544
|
-
...
|
|
1554
|
+
return createNode(ctx, {
|
|
1555
|
+
...enumExtras,
|
|
1545
1556
|
primitive: enumPrimitiveType,
|
|
1546
1557
|
namedEnumValues: uniqueValues.map((value, index) => ({
|
|
1547
1558
|
name: String(rawEnumNames?.[index] ?? value),
|
|
@@ -1555,12 +1566,103 @@ function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
|
|
|
1555
1566
|
})
|
|
1556
1567
|
});
|
|
1557
1568
|
}
|
|
1558
|
-
return
|
|
1559
|
-
...
|
|
1569
|
+
return createNode(ctx, {
|
|
1570
|
+
...enumExtras,
|
|
1560
1571
|
enumValues: [...new Set(filteredValues)]
|
|
1561
1572
|
});
|
|
1562
1573
|
}
|
|
1563
1574
|
/**
|
|
1575
|
+
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
1576
|
+
*/
|
|
1577
|
+
function convertString({ schema, name, nullable, defaultValue }) {
|
|
1578
|
+
return createNode({
|
|
1579
|
+
schema,
|
|
1580
|
+
name,
|
|
1581
|
+
nullable,
|
|
1582
|
+
defaultValue
|
|
1583
|
+
}, {
|
|
1584
|
+
type: "string",
|
|
1585
|
+
primitive: "string",
|
|
1586
|
+
min: schema.minLength,
|
|
1587
|
+
max: schema.maxLength,
|
|
1588
|
+
pattern: schema.pattern
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Converts a `type: 'number'` or `type: 'integer'` schema.
|
|
1593
|
+
*/
|
|
1594
|
+
function convertNumeric({ schema, name, nullable, defaultValue }, type) {
|
|
1595
|
+
return createNode({
|
|
1596
|
+
schema,
|
|
1597
|
+
name,
|
|
1598
|
+
nullable,
|
|
1599
|
+
defaultValue
|
|
1600
|
+
}, {
|
|
1601
|
+
type,
|
|
1602
|
+
primitive: type,
|
|
1603
|
+
min: schema.minimum,
|
|
1604
|
+
max: schema.maximum,
|
|
1605
|
+
...getExclusiveBounds(schema),
|
|
1606
|
+
multipleOf: schema.multipleOf
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Converts a `type: 'boolean'` schema.
|
|
1611
|
+
*/
|
|
1612
|
+
function convertBoolean({ schema, name, nullable, defaultValue }) {
|
|
1613
|
+
return createNode({
|
|
1614
|
+
schema,
|
|
1615
|
+
name,
|
|
1616
|
+
nullable,
|
|
1617
|
+
defaultValue
|
|
1618
|
+
}, {
|
|
1619
|
+
type: "boolean",
|
|
1620
|
+
primitive: "boolean"
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
|
|
1625
|
+
* into a `blob` node.
|
|
1626
|
+
*/
|
|
1627
|
+
function convertBinary({ schema, name, nullable, defaultValue }) {
|
|
1628
|
+
return createNode({
|
|
1629
|
+
schema,
|
|
1630
|
+
name,
|
|
1631
|
+
nullable,
|
|
1632
|
+
defaultValue
|
|
1633
|
+
}, {
|
|
1634
|
+
type: "blob",
|
|
1635
|
+
primitive: "string"
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
//#endregion
|
|
1639
|
+
//#region src/emit/converters/structural.ts
|
|
1640
|
+
/**
|
|
1641
|
+
* Resolves a `true` or empty-object map schema (`additionalProperties`/`patternProperties`) to
|
|
1642
|
+
* `options.unknownType`, otherwise parses it as a regular schema.
|
|
1643
|
+
*/
|
|
1644
|
+
function resolveMapSchema(mapSchema, options, parse, rawOptions) {
|
|
1645
|
+
if (mapSchema === true || typeof mapSchema === "object" && Object.keys(mapSchema).length === 0) return _kubb_ast.ast.factory.createSchema({ type: options.unknownType });
|
|
1646
|
+
return parse({ schema: mapSchema }, rawOptions);
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Names the inline enums on a property's schema, and on each item when the property is a tuple, from
|
|
1650
|
+
* the parent and property name. Wraps `macroEnumName` at the property construction site.
|
|
1651
|
+
*/
|
|
1652
|
+
function nameEnums(node, options) {
|
|
1653
|
+
const macro = (0, _kubb_kit.macroEnumName)(options);
|
|
1654
|
+
const named = _kubb_ast.ast.applyMacros(node, [macro], { depth: "shallow" });
|
|
1655
|
+
const tupleNode = _kubb_ast.ast.narrowSchema(named, "tuple");
|
|
1656
|
+
if (tupleNode?.items) {
|
|
1657
|
+
const namedItems = tupleNode.items.map((item) => _kubb_ast.ast.applyMacros(item, [macro], { depth: "shallow" }));
|
|
1658
|
+
if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
|
|
1659
|
+
...tupleNode,
|
|
1660
|
+
items: namedItems
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
return named;
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1564
1666
|
* Converts an object-like schema into an `ObjectSchemaNode`.
|
|
1565
1667
|
*/
|
|
1566
1668
|
function convertObject({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
@@ -1570,7 +1672,7 @@ function convertObject({ schema, name, nullable, defaultValue, rawOptions, optio
|
|
|
1570
1672
|
const propNullable = isNullable(resolvedPropSchema);
|
|
1571
1673
|
const schemaNode = nameEnums(parse({
|
|
1572
1674
|
schema: resolvedPropSchema,
|
|
1573
|
-
name: (0,
|
|
1675
|
+
name: (0, _kubb_kit.childName)(name, propName)
|
|
1574
1676
|
}, rawOptions), {
|
|
1575
1677
|
parentName: name,
|
|
1576
1678
|
propName,
|
|
@@ -1586,146 +1688,84 @@ function convertObject({ schema, name, nullable, defaultValue, rawOptions, optio
|
|
|
1586
1688
|
});
|
|
1587
1689
|
}) : [];
|
|
1588
1690
|
const additionalProperties = schema.additionalProperties;
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
if (additionalProperties) return _kubb_ast.ast.factory.createSchema({ type: options.unknownType });
|
|
1594
|
-
})();
|
|
1691
|
+
let additionalPropertiesNode;
|
|
1692
|
+
if (additionalProperties === true) additionalPropertiesNode = true;
|
|
1693
|
+
else if (additionalProperties) additionalPropertiesNode = resolveMapSchema(additionalProperties, options, parse, rawOptions);
|
|
1694
|
+
else additionalPropertiesNode = additionalProperties;
|
|
1595
1695
|
const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
|
|
1596
|
-
const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern,
|
|
1597
|
-
const objectNode =
|
|
1696
|
+
const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, resolveMapSchema(patternSchema, options, parse, rawOptions)])) : void 0;
|
|
1697
|
+
const objectNode = createNode({
|
|
1698
|
+
schema,
|
|
1699
|
+
name,
|
|
1700
|
+
nullable,
|
|
1701
|
+
defaultValue
|
|
1702
|
+
}, {
|
|
1598
1703
|
type: "object",
|
|
1599
1704
|
primitive: "object",
|
|
1600
1705
|
properties,
|
|
1601
1706
|
additionalProperties: additionalPropertiesNode,
|
|
1602
1707
|
patternProperties,
|
|
1603
1708
|
minProperties: schema.minProperties,
|
|
1604
|
-
maxProperties: schema.maxProperties
|
|
1605
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1709
|
+
maxProperties: schema.maxProperties
|
|
1606
1710
|
});
|
|
1607
1711
|
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
1608
|
-
const discPropName = schema.discriminator.propertyName;
|
|
1609
|
-
const values = Object.keys(schema.discriminator.mapping);
|
|
1610
|
-
const enumName = name ? (0,
|
|
1611
|
-
return _kubb_ast.ast.applyMacros(objectNode, [(0,
|
|
1612
|
-
propertyName: discPropName,
|
|
1613
|
-
values,
|
|
1614
|
-
enumName
|
|
1615
|
-
})], { depth: "shallow" });
|
|
1616
|
-
}
|
|
1617
|
-
return objectNode;
|
|
1618
|
-
}
|
|
1619
|
-
/**
|
|
1620
|
-
* Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
|
|
1621
|
-
*/
|
|
1622
|
-
function convertTuple({ schema, name, nullable, defaultValue, rawOptions, parse }) {
|
|
1623
|
-
const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
|
|
1624
|
-
const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? _kubb_ast.ast.factory.createSchema({ type: "any" }) : parse({ schema: schema.items }, rawOptions);
|
|
1625
|
-
return _kubb_ast.ast.factory.createSchema({
|
|
1626
|
-
type: "tuple",
|
|
1627
|
-
primitive: "array",
|
|
1628
|
-
items: tupleItems,
|
|
1629
|
-
rest,
|
|
1630
|
-
min: schema.minItems,
|
|
1631
|
-
max: schema.maxItems,
|
|
1632
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1633
|
-
});
|
|
1634
|
-
}
|
|
1635
|
-
/**
|
|
1636
|
-
* Converts a `type: 'array'` schema into an `ArraySchemaNode`.
|
|
1637
|
-
*/
|
|
1638
|
-
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
1639
|
-
const rawItems = schema.items;
|
|
1640
|
-
const itemName = rawItems?.enum?.length && name ? (0, _kubb_ast.enumPropName)(null, name, options.enumSuffix) : name;
|
|
1641
|
-
const items = rawItems ? [parse({
|
|
1642
|
-
schema: rawItems,
|
|
1643
|
-
name: itemName
|
|
1644
|
-
}, rawOptions)] : [];
|
|
1645
|
-
return _kubb_ast.ast.factory.createSchema({
|
|
1646
|
-
type: "array",
|
|
1647
|
-
primitive: "array",
|
|
1648
|
-
items,
|
|
1649
|
-
min: schema.minItems,
|
|
1650
|
-
max: schema.maxItems,
|
|
1651
|
-
unique: schema.uniqueItems ?? void 0,
|
|
1652
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
/**
|
|
1656
|
-
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
1657
|
-
*/
|
|
1658
|
-
function convertString({ schema, name, nullable, defaultValue }) {
|
|
1659
|
-
return _kubb_ast.ast.factory.createSchema({
|
|
1660
|
-
type: "string",
|
|
1661
|
-
primitive: "string",
|
|
1662
|
-
min: schema.minLength,
|
|
1663
|
-
max: schema.maxLength,
|
|
1664
|
-
pattern: schema.pattern,
|
|
1665
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1666
|
-
});
|
|
1667
|
-
}
|
|
1668
|
-
/**
|
|
1669
|
-
* Converts a `type: 'number'` or `type: 'integer'` schema.
|
|
1670
|
-
*/
|
|
1671
|
-
function convertNumeric({ schema, name, nullable, defaultValue }, type) {
|
|
1672
|
-
return _kubb_ast.ast.factory.createSchema({
|
|
1673
|
-
type,
|
|
1674
|
-
primitive: type,
|
|
1675
|
-
min: schema.minimum,
|
|
1676
|
-
max: schema.maximum,
|
|
1677
|
-
exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
|
|
1678
|
-
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0,
|
|
1679
|
-
multipleOf: schema.multipleOf,
|
|
1680
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1681
|
-
});
|
|
1682
|
-
}
|
|
1683
|
-
/**
|
|
1684
|
-
* Converts a `type: 'boolean'` schema.
|
|
1685
|
-
*/
|
|
1686
|
-
function convertBoolean({ schema, name, nullable, defaultValue }) {
|
|
1687
|
-
return _kubb_ast.ast.factory.createSchema({
|
|
1688
|
-
type: "boolean",
|
|
1689
|
-
primitive: "boolean",
|
|
1690
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1691
|
-
});
|
|
1712
|
+
const discPropName = schema.discriminator.propertyName;
|
|
1713
|
+
const values = Object.keys(schema.discriminator.mapping);
|
|
1714
|
+
const enumName = name ? (0, _kubb_kit.enumPropName)(name, discPropName, options.enumSuffix) : void 0;
|
|
1715
|
+
return _kubb_ast.ast.applyMacros(objectNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
|
|
1716
|
+
propertyName: discPropName,
|
|
1717
|
+
values,
|
|
1718
|
+
enumName
|
|
1719
|
+
})], { depth: "shallow" });
|
|
1720
|
+
}
|
|
1721
|
+
return objectNode;
|
|
1692
1722
|
}
|
|
1693
1723
|
/**
|
|
1694
|
-
* Converts
|
|
1695
|
-
* into a `blob` node.
|
|
1724
|
+
* Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
|
|
1696
1725
|
*/
|
|
1697
|
-
function
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1726
|
+
function convertTuple({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
1727
|
+
const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
|
|
1728
|
+
const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? _kubb_ast.ast.factory.createSchema({ type: options.unknownType }) : parse({ schema: schema.items }, rawOptions);
|
|
1729
|
+
return createNode({
|
|
1730
|
+
schema,
|
|
1731
|
+
name,
|
|
1732
|
+
nullable,
|
|
1733
|
+
defaultValue
|
|
1734
|
+
}, {
|
|
1735
|
+
type: "tuple",
|
|
1736
|
+
primitive: "array",
|
|
1737
|
+
items: tupleItems,
|
|
1738
|
+
rest,
|
|
1739
|
+
min: schema.minItems,
|
|
1740
|
+
max: schema.maxItems
|
|
1702
1741
|
});
|
|
1703
1742
|
}
|
|
1704
1743
|
/**
|
|
1705
|
-
* Converts
|
|
1706
|
-
*
|
|
1707
|
-
* Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parse`
|
|
1708
|
-
* falls through and handles it as that single type with nullability already folded in.
|
|
1744
|
+
* Converts a `type: 'array'` schema into an `ArraySchemaNode`.
|
|
1709
1745
|
*/
|
|
1710
|
-
function
|
|
1711
|
-
const
|
|
1712
|
-
const
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1746
|
+
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
1747
|
+
const rawItems = schema.items;
|
|
1748
|
+
const itemName = rawItems?.enum?.length && name ? (0, _kubb_kit.enumPropName)(null, name, options.enumSuffix) : name;
|
|
1749
|
+
const items = rawItems ? [parse({
|
|
1750
|
+
schema: rawItems,
|
|
1751
|
+
name: itemName
|
|
1752
|
+
}, rawOptions)] : [];
|
|
1753
|
+
return createNode({
|
|
1754
|
+
schema,
|
|
1755
|
+
name,
|
|
1756
|
+
nullable,
|
|
1757
|
+
defaultValue
|
|
1758
|
+
}, {
|
|
1759
|
+
type: "array",
|
|
1760
|
+
primitive: "array",
|
|
1761
|
+
items,
|
|
1762
|
+
min: schema.minItems,
|
|
1763
|
+
max: schema.maxItems,
|
|
1764
|
+
unique: schema.uniqueItems ?? void 0
|
|
1727
1765
|
});
|
|
1728
1766
|
}
|
|
1767
|
+
//#endregion
|
|
1768
|
+
//#region src/emit/parseSchema.ts
|
|
1729
1769
|
/**
|
|
1730
1770
|
* Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
|
|
1731
1771
|
* `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
|
|
@@ -1750,7 +1790,11 @@ const schemaRules = [
|
|
|
1750
1790
|
convert: convertConst
|
|
1751
1791
|
},
|
|
1752
1792
|
{
|
|
1753
|
-
match: ({ schema }) =>
|
|
1793
|
+
match: ({ schema, options }) => {
|
|
1794
|
+
if (!schema.format) return false;
|
|
1795
|
+
if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") return options.dateType !== false;
|
|
1796
|
+
return isHandledFormat(schema.format);
|
|
1797
|
+
},
|
|
1754
1798
|
convert: convertFormat
|
|
1755
1799
|
},
|
|
1756
1800
|
{
|
|
@@ -1758,7 +1802,7 @@ const schemaRules = [
|
|
|
1758
1802
|
convert: convertBinary
|
|
1759
1803
|
},
|
|
1760
1804
|
{
|
|
1761
|
-
match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1,
|
|
1805
|
+
match: ({ schema }) => Array.isArray(schema.type) && schema.type.filter((t) => t !== "null").length > 1,
|
|
1762
1806
|
convert: convertMultiType
|
|
1763
1807
|
},
|
|
1764
1808
|
{
|
|
@@ -1807,72 +1851,306 @@ const schemaRules = [
|
|
|
1807
1851
|
}
|
|
1808
1852
|
];
|
|
1809
1853
|
//#endregion
|
|
1810
|
-
//#region src/
|
|
1854
|
+
//#region src/refs.ts
|
|
1855
|
+
const _refCache = /* @__PURE__ */ new WeakMap();
|
|
1811
1856
|
/**
|
|
1812
|
-
*
|
|
1857
|
+
* Walks a local `#/...` JSON pointer against `document`, memoized per document. `applicable` is
|
|
1858
|
+
* `false` for an empty or non-local ref (the caller should not treat that as a failed lookup).
|
|
1859
|
+
* Shared by `resolveRef`'s reporting walk and `createRefs().resolve`'s silent walk, so both use
|
|
1860
|
+
* the same trimming and caching instead of two separate implementations.
|
|
1861
|
+
*/
|
|
1862
|
+
function walkPointer(document, $ref) {
|
|
1863
|
+
const trimmed = $ref.trim();
|
|
1864
|
+
if (trimmed === "" || !trimmed.startsWith("#")) return {
|
|
1865
|
+
applicable: false,
|
|
1866
|
+
value: null
|
|
1867
|
+
};
|
|
1868
|
+
const pointer = globalThis.decodeURIComponent(trimmed.substring(1));
|
|
1869
|
+
let docCache = _refCache.get(document);
|
|
1870
|
+
if (!docCache) {
|
|
1871
|
+
docCache = /* @__PURE__ */ new Map();
|
|
1872
|
+
_refCache.set(document, docCache);
|
|
1873
|
+
}
|
|
1874
|
+
if (docCache.has(pointer)) return {
|
|
1875
|
+
applicable: true,
|
|
1876
|
+
value: docCache.get(pointer)
|
|
1877
|
+
};
|
|
1878
|
+
const current = pointer.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
|
|
1879
|
+
if (current) docCache.set(pointer, current);
|
|
1880
|
+
return {
|
|
1881
|
+
applicable: true,
|
|
1882
|
+
value: current ?? null
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Resolves a local JSON pointer reference from a document.
|
|
1813
1887
|
*
|
|
1814
|
-
*
|
|
1815
|
-
*
|
|
1816
|
-
*
|
|
1817
|
-
* `parse` function passed to it, so this file only wires state to the converters.
|
|
1888
|
+
* Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
|
|
1889
|
+
* resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
|
|
1890
|
+
* build there is no sink to collect it, so it throws instead.
|
|
1818
1891
|
*
|
|
1819
|
-
* @
|
|
1892
|
+
* @example
|
|
1893
|
+
* ```ts
|
|
1894
|
+
* resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
|
|
1895
|
+
* ```
|
|
1820
1896
|
*/
|
|
1821
|
-
function
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1897
|
+
function resolveRef(document, $ref) {
|
|
1898
|
+
const { applicable, value } = walkPointer(document, $ref);
|
|
1899
|
+
if (!applicable) return null;
|
|
1900
|
+
if (value) return value;
|
|
1901
|
+
const diagnostic = {
|
|
1902
|
+
code: _kubb_core.Diagnostics.code.refNotFound,
|
|
1903
|
+
severity: "error",
|
|
1904
|
+
message: `Could not find a definition for ${$ref}.`,
|
|
1905
|
+
help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
|
|
1906
|
+
location: {
|
|
1907
|
+
kind: "schema",
|
|
1908
|
+
pointer: $ref,
|
|
1909
|
+
ref: $ref
|
|
1910
|
+
}
|
|
1911
|
+
};
|
|
1912
|
+
if (!_kubb_core.Diagnostics.report(diagnostic)) throw new _kubb_core.Diagnostics.Error(diagnostic);
|
|
1913
|
+
return null;
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Resolves a `$ref` object while preserving the original `$ref` field on the result.
|
|
1917
|
+
*
|
|
1918
|
+
* Useful for parser flows that need both dereferenced fields and pointer
|
|
1919
|
+
* identity (for naming/import purposes). Non-reference values are returned as-is.
|
|
1920
|
+
*
|
|
1921
|
+
* @example
|
|
1922
|
+
* ```ts
|
|
1923
|
+
* dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
|
|
1924
|
+
* // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
|
|
1925
|
+
* ```
|
|
1926
|
+
*/
|
|
1927
|
+
function dereferenceWithRef(document, schema) {
|
|
1928
|
+
if (isReference(schema)) return {
|
|
1929
|
+
...schema,
|
|
1930
|
+
...resolveRef(document, schema.$ref),
|
|
1931
|
+
$ref: schema.$ref
|
|
1932
|
+
};
|
|
1933
|
+
return schema;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Creates the `$ref` resolution service for one document.
|
|
1937
|
+
*
|
|
1938
|
+
* Replaces what used to be six overlapping resolvers (a reporting walk, a silent walk, an
|
|
1939
|
+
* existence check, and a resolve-then-parse-into-a-node step, each with its own cache) with one
|
|
1940
|
+
* pointer walk and one explicit `report` contract for a missing ref: `report: true` (the default)
|
|
1941
|
+
* reports a `refNotFound` diagnostic (or throws outside a build), `report: false` resolves to
|
|
1942
|
+
* `null` silently for a speculative lookup.
|
|
1943
|
+
*
|
|
1944
|
+
* @example
|
|
1945
|
+
* ```ts
|
|
1946
|
+
* const refs = createRefs(document)
|
|
1947
|
+
* refs.resolve<SchemaObject>('#/components/schemas/Pet')
|
|
1948
|
+
* refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
|
|
1949
|
+
* refs.exists('#/components/schemas/Pet')
|
|
1950
|
+
* refs.resolveNode('#/components/schemas/Pet', parseSchema)
|
|
1951
|
+
* refs.deref<ResponseObject>(operation.schema.responses?.['200'])
|
|
1952
|
+
* ```
|
|
1953
|
+
*/
|
|
1954
|
+
function createRefs(document) {
|
|
1955
|
+
const resolvedNodeCache = /* @__PURE__ */ new Map();
|
|
1956
|
+
const existenceCache = /* @__PURE__ */ new Map();
|
|
1827
1957
|
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
1828
1958
|
/**
|
|
1829
|
-
*
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1832
|
-
*
|
|
1833
|
-
* since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
|
|
1834
|
-
* Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
|
|
1959
|
+
* Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
|
|
1960
|
+
* `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
|
|
1961
|
+
* outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
|
|
1962
|
+
* for a speculative lookup where a missing ref is not an error.
|
|
1835
1963
|
*/
|
|
1836
|
-
|
|
1964
|
+
function resolve(refPath, options) {
|
|
1965
|
+
if (options?.report === false) {
|
|
1966
|
+
const { applicable, value } = walkPointer(document, refPath);
|
|
1967
|
+
return applicable ? value : null;
|
|
1968
|
+
}
|
|
1969
|
+
return resolveRef(document, refPath);
|
|
1970
|
+
}
|
|
1837
1971
|
/**
|
|
1838
|
-
*
|
|
1972
|
+
* Returns `true` when a `$ref` path resolves to a component the document actually defines.
|
|
1839
1973
|
* A circular ref still resolves to an existing target, so this stays `true` for cycles and only
|
|
1840
|
-
* goes `false` for a `$ref` that points at a component the spec never declares.
|
|
1974
|
+
* goes `false` for a `$ref` that points at a component the spec never declares. Memoized.
|
|
1841
1975
|
*/
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
let exists = false;
|
|
1846
|
-
try {
|
|
1847
|
-
exists = !!resolveRef(document, refPath);
|
|
1848
|
-
} catch {
|
|
1849
|
-
exists = false;
|
|
1850
|
-
}
|
|
1851
|
-
refExistence.set(refPath, exists);
|
|
1852
|
-
}
|
|
1853
|
-
return refExistence.get(refPath) ?? false;
|
|
1976
|
+
function exists(refPath) {
|
|
1977
|
+
if (!existenceCache.has(refPath)) existenceCache.set(refPath, !!resolve(refPath, { report: false }));
|
|
1978
|
+
return existenceCache.get(refPath) ?? false;
|
|
1854
1979
|
}
|
|
1855
1980
|
/**
|
|
1856
|
-
* Resolves a `$ref` to its parsed node
|
|
1857
|
-
* Returns `null` when the ref is currently being resolved (a cycle) or cannot be
|
|
1858
|
-
* (e.g. a minimal document in a unit test).
|
|
1981
|
+
* Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
|
|
1982
|
+
* instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
|
|
1983
|
+
* resolved (e.g. a minimal document in a unit test).
|
|
1859
1984
|
*/
|
|
1860
|
-
function
|
|
1985
|
+
function resolveNode(refPath, parse, rawOptions) {
|
|
1861
1986
|
if (resolvingRefs.has(refPath)) return null;
|
|
1862
|
-
if (!
|
|
1987
|
+
if (!resolvedNodeCache.has(refPath)) {
|
|
1863
1988
|
let resolved = null;
|
|
1864
1989
|
try {
|
|
1865
|
-
const referenced =
|
|
1990
|
+
const referenced = resolve(refPath);
|
|
1866
1991
|
if (referenced) {
|
|
1867
1992
|
resolvingRefs.add(refPath);
|
|
1868
|
-
resolved =
|
|
1993
|
+
resolved = parse({ schema: referenced }, rawOptions);
|
|
1869
1994
|
resolvingRefs.delete(refPath);
|
|
1870
1995
|
}
|
|
1871
1996
|
} catch {}
|
|
1872
|
-
|
|
1997
|
+
resolvedNodeCache.set(refPath, resolved);
|
|
1873
1998
|
}
|
|
1874
|
-
return
|
|
1999
|
+
return resolvedNodeCache.get(refPath) ?? null;
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
|
|
2003
|
+
* resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
|
|
2004
|
+
* `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
|
|
2005
|
+
* returned as-is.
|
|
2006
|
+
*
|
|
2007
|
+
* @example
|
|
2008
|
+
* ```ts
|
|
2009
|
+
* refs.deref<ResponseObject>(operation.schema.responses?.['200'])
|
|
2010
|
+
* ```
|
|
2011
|
+
*/
|
|
2012
|
+
function deref(value) {
|
|
2013
|
+
if (!isReference(value)) return value ? value : null;
|
|
2014
|
+
const resolved = resolve(value.$ref);
|
|
2015
|
+
return resolved && !isReference(resolved) ? resolved : null;
|
|
1875
2016
|
}
|
|
2017
|
+
return {
|
|
2018
|
+
resolve,
|
|
2019
|
+
exists,
|
|
2020
|
+
resolveNode,
|
|
2021
|
+
deref
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
//#endregion
|
|
2025
|
+
//#region src/model/operations.ts
|
|
2026
|
+
/**
|
|
2027
|
+
* Returns all parameters for an operation, merging path-level and operation-level entries.
|
|
2028
|
+
* Operation-level parameters override path-level ones with the same `in:name` key.
|
|
2029
|
+
* Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
|
|
2030
|
+
*
|
|
2031
|
+
* @example
|
|
2032
|
+
* ```ts
|
|
2033
|
+
* getParameters({ document, operation })
|
|
2034
|
+
* // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
|
|
2035
|
+
* ```
|
|
2036
|
+
*/
|
|
2037
|
+
function getParameters({ document, operation }) {
|
|
2038
|
+
const resolveParams = (params) => params.map((p) => dereferenceWithRef(document, p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
|
|
2039
|
+
const operationParams = resolveParams(operation.schema?.parameters || []);
|
|
2040
|
+
const pathLevelParams = resolveParams(operation.pathItem.parameters ?? []);
|
|
2041
|
+
const paramMap = /* @__PURE__ */ new Map();
|
|
2042
|
+
for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
|
|
2043
|
+
for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
|
|
2044
|
+
return Array.from(paramMap.values());
|
|
2045
|
+
}
|
|
2046
|
+
function getResponseBody(responseBody, contentType) {
|
|
2047
|
+
if (!responseBody) return false;
|
|
2048
|
+
if (isReference(responseBody)) return false;
|
|
2049
|
+
const body = responseBody;
|
|
2050
|
+
if (!body.content) return false;
|
|
2051
|
+
if (contentType) return contentType in body.content ? body.content[contentType] : false;
|
|
2052
|
+
const picked = pickContentEntry(body.content);
|
|
2053
|
+
return picked ? picked[1] : false;
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* Returns the response schema for a given operation and HTTP status code.
|
|
2057
|
+
*
|
|
2058
|
+
* Returns an empty object `{}` when no response body schema is available.
|
|
2059
|
+
*
|
|
2060
|
+
* @example
|
|
2061
|
+
* ```ts
|
|
2062
|
+
* getResponseSchema({ document, operation, refs, statusCode: 200 }) // SchemaObject
|
|
2063
|
+
* getResponseSchema({ document, operation, refs, statusCode: '4XX' }) // {}
|
|
2064
|
+
* ```
|
|
2065
|
+
*/
|
|
2066
|
+
function getResponseSchema({ document, operation, refs, statusCode, options = {} }) {
|
|
2067
|
+
const responseBody = getResponseBody(getResponseByStatusCode({
|
|
2068
|
+
operation,
|
|
2069
|
+
refs,
|
|
2070
|
+
statusCode
|
|
2071
|
+
}), options.contentType);
|
|
2072
|
+
if (responseBody === false) return {};
|
|
2073
|
+
const schema = responseBody.schema;
|
|
2074
|
+
if (!schema) return {};
|
|
2075
|
+
return dereferenceWithRef(document, schema);
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Returns the request body schema for an operation, or `null` when absent.
|
|
2079
|
+
*
|
|
2080
|
+
* @example
|
|
2081
|
+
* ```ts
|
|
2082
|
+
* getRequestSchema({ document, operation, refs }) // SchemaObject | null
|
|
2083
|
+
* ```
|
|
2084
|
+
*/
|
|
2085
|
+
function getRequestSchema({ document, operation, refs, options = {} }) {
|
|
2086
|
+
const requestBody = getRequestContent({
|
|
2087
|
+
operation,
|
|
2088
|
+
refs,
|
|
2089
|
+
mediaType: options.contentType
|
|
2090
|
+
});
|
|
2091
|
+
if (requestBody === false) return null;
|
|
2092
|
+
const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
|
|
2093
|
+
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
|
|
2094
|
+
if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
|
|
2095
|
+
type: "string",
|
|
2096
|
+
contentMediaType: "application/octet-stream"
|
|
2097
|
+
};
|
|
2098
|
+
if (!schema) return null;
|
|
2099
|
+
return dereferenceWithRef(document, schema);
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Returns all request body content type keys for an operation, resolving a `$ref` requestBody
|
|
2103
|
+
* through `refs`.
|
|
2104
|
+
*
|
|
2105
|
+
* @example
|
|
2106
|
+
* ```ts
|
|
2107
|
+
* getRequestBodyContentTypes(operation, refs)
|
|
2108
|
+
* // ['application/json', 'multipart/form-data']
|
|
2109
|
+
* ```
|
|
2110
|
+
*/
|
|
2111
|
+
function getRequestBodyContentTypes(operation, refs) {
|
|
2112
|
+
const body = getRequestBody({
|
|
2113
|
+
operation,
|
|
2114
|
+
refs
|
|
2115
|
+
});
|
|
2116
|
+
return body?.content ? Object.keys(body.content) : [];
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Returns all response content type keys for an operation at a given status code, resolving the
|
|
2120
|
+
* response `$ref` through `refs`.
|
|
2121
|
+
*
|
|
2122
|
+
* @example
|
|
2123
|
+
* ```ts
|
|
2124
|
+
* getResponseBodyContentTypes(operation, refs, 200)
|
|
2125
|
+
* // ['application/json', 'application/xml']
|
|
2126
|
+
* ```
|
|
2127
|
+
*/
|
|
2128
|
+
function getResponseBodyContentTypes(operation, refs, statusCode) {
|
|
2129
|
+
const responseObj = getResponseByStatusCode({
|
|
2130
|
+
operation,
|
|
2131
|
+
refs,
|
|
2132
|
+
statusCode
|
|
2133
|
+
});
|
|
2134
|
+
if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
|
|
2135
|
+
const body = responseObj;
|
|
2136
|
+
return body.content ? Object.keys(body.content) : [];
|
|
2137
|
+
}
|
|
2138
|
+
//#endregion
|
|
2139
|
+
//#region src/parser.ts
|
|
2140
|
+
/**
|
|
2141
|
+
* Creates the schema and operation converters bound to one OpenAPI document.
|
|
2142
|
+
*
|
|
2143
|
+
* Takes the `$ref` service for this document (shared with the rest of the pipeline, see
|
|
2144
|
+
* `adapter.ts`) and owns the `parseSchema` recursion seam, then dispatches each schema through
|
|
2145
|
+
* the ordered `schemaRules` table from `emit/parseSchema.ts`. Every converter is a standalone
|
|
2146
|
+
* function that recurses through the `parse` function passed to it, so this file only wires
|
|
2147
|
+
* state to the converters.
|
|
2148
|
+
*
|
|
2149
|
+
* @internal
|
|
2150
|
+
*/
|
|
2151
|
+
function createSchemaParser(ctx) {
|
|
2152
|
+
const document = ctx.document;
|
|
2153
|
+
const refs = ctx.refs;
|
|
1876
2154
|
/**
|
|
1877
2155
|
* Converts an OAS `SchemaObject` into a `SchemaNode`.
|
|
1878
2156
|
*
|
|
@@ -1901,15 +2179,10 @@ function createSchemaParser(ctx) {
|
|
|
1901
2179
|
options,
|
|
1902
2180
|
parse: parseSchema,
|
|
1903
2181
|
document,
|
|
1904
|
-
|
|
1905
|
-
refExists,
|
|
2182
|
+
refs,
|
|
1906
2183
|
renames: ctx.renames
|
|
1907
2184
|
};
|
|
1908
|
-
for (const rule of schemaRules)
|
|
1909
|
-
if (!rule.match(context)) continue;
|
|
1910
|
-
const node = rule.convert(context);
|
|
1911
|
-
if (node) return node;
|
|
1912
|
-
}
|
|
2185
|
+
for (const rule of schemaRules) if (rule.match(context)) return rule.convert(context);
|
|
1913
2186
|
const emptyType = options.emptySchemaType;
|
|
1914
2187
|
return _kubb_ast.ast.factory.createSchema({
|
|
1915
2188
|
type: emptyType,
|
|
@@ -1946,10 +2219,14 @@ function createSchemaParser(ctx) {
|
|
|
1946
2219
|
}
|
|
1947
2220
|
/**
|
|
1948
2221
|
* Reads the inline `requestBody` metadata (description / required) that OAS exposes
|
|
1949
|
-
* outside the schema itself
|
|
2222
|
+
* outside the schema itself, resolving a `$ref` requestBody through `refs`. Returns an
|
|
2223
|
+
* empty object when the request body is missing or cannot be resolved.
|
|
1950
2224
|
*/
|
|
1951
2225
|
function getRequestBodyMeta(operation) {
|
|
1952
|
-
const body =
|
|
2226
|
+
const body = getRequestBody({
|
|
2227
|
+
operation,
|
|
2228
|
+
refs
|
|
2229
|
+
});
|
|
1953
2230
|
if (!body) return { required: false };
|
|
1954
2231
|
return {
|
|
1955
2232
|
description: body.description,
|
|
@@ -1975,12 +2252,20 @@ function createSchemaParser(ctx) {
|
|
|
1975
2252
|
function parseOperation(options, operation) {
|
|
1976
2253
|
const operationId = getOperationId(operation);
|
|
1977
2254
|
const operationName = operationId ? pascalCase(operationId) : void 0;
|
|
1978
|
-
const parameters = getParameters(
|
|
1979
|
-
|
|
2255
|
+
const parameters = getParameters({
|
|
2256
|
+
document,
|
|
2257
|
+
operation
|
|
2258
|
+
}).map((param) => parseParameter(options, param, operationName));
|
|
2259
|
+
const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(operation, refs);
|
|
1980
2260
|
const requestBodyMeta = getRequestBodyMeta(operation);
|
|
1981
2261
|
const requestBodyName = operationName ? `${operationName}Request` : void 0;
|
|
1982
2262
|
const content = allContentTypes.flatMap((ct) => {
|
|
1983
|
-
const schema = getRequestSchema(
|
|
2263
|
+
const schema = getRequestSchema({
|
|
2264
|
+
document,
|
|
2265
|
+
operation,
|
|
2266
|
+
refs,
|
|
2267
|
+
options: { contentType: ct }
|
|
2268
|
+
});
|
|
1984
2269
|
if (!schema) return [];
|
|
1985
2270
|
return [_kubb_ast.ast.factory.createContent({
|
|
1986
2271
|
contentType: ct,
|
|
@@ -1998,14 +2283,20 @@ function createSchemaParser(ctx) {
|
|
|
1998
2283
|
} : void 0;
|
|
1999
2284
|
const responses = getResponseStatusCodes(operation).map((statusCode) => {
|
|
2000
2285
|
const responseObj = getResponseByStatusCode({
|
|
2001
|
-
document,
|
|
2002
2286
|
operation,
|
|
2287
|
+
refs,
|
|
2003
2288
|
statusCode
|
|
2004
2289
|
});
|
|
2005
2290
|
const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
|
|
2006
2291
|
const description = typeof responseObj === "object" && responseObj !== null ? responseObj.description : void 0;
|
|
2007
2292
|
const parseEntrySchema = (contentType) => {
|
|
2008
|
-
const raw = getResponseSchema(
|
|
2293
|
+
const raw = getResponseSchema({
|
|
2294
|
+
document,
|
|
2295
|
+
operation,
|
|
2296
|
+
refs,
|
|
2297
|
+
statusCode,
|
|
2298
|
+
options: { contentType }
|
|
2299
|
+
});
|
|
2009
2300
|
return {
|
|
2010
2301
|
schema: raw && Object.keys(raw).length > 0 ? parseSchema({
|
|
2011
2302
|
schema: raw,
|
|
@@ -2014,14 +2305,14 @@ function createSchemaParser(ctx) {
|
|
|
2014
2305
|
keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
|
|
2015
2306
|
};
|
|
2016
2307
|
};
|
|
2017
|
-
const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(
|
|
2308
|
+
const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(operation, refs, statusCode)).map((contentType) => _kubb_ast.ast.factory.createContent({
|
|
2018
2309
|
contentType,
|
|
2019
2310
|
...parseEntrySchema(contentType)
|
|
2020
2311
|
}));
|
|
2021
2312
|
if (content.length === 0) content.push(_kubb_ast.ast.factory.createContent({
|
|
2022
2313
|
contentType: getRequestContentType({
|
|
2023
|
-
|
|
2024
|
-
|
|
2314
|
+
operation,
|
|
2315
|
+
refs
|
|
2025
2316
|
}) || "application/json",
|
|
2026
2317
|
...parseEntrySchema(ctx.contentType)
|
|
2027
2318
|
}));
|
|
@@ -2031,12 +2322,10 @@ function createSchemaParser(ctx) {
|
|
|
2031
2322
|
content
|
|
2032
2323
|
});
|
|
2033
2324
|
});
|
|
2034
|
-
const pathItem = document.paths?.[operation.path];
|
|
2035
|
-
const pathItemDoc = pathItem && !isReference(pathItem) ? pathItem : void 0;
|
|
2036
2325
|
const pickDoc = (key) => {
|
|
2037
2326
|
const own = operation.schema[key];
|
|
2038
2327
|
if (typeof own === "string") return own;
|
|
2039
|
-
const fallback =
|
|
2328
|
+
const fallback = operation.pathItem[key];
|
|
2040
2329
|
return typeof fallback === "string" ? fallback : void 0;
|
|
2041
2330
|
};
|
|
2042
2331
|
return _kubb_ast.ast.factory.createOperation({
|
|
@@ -2109,15 +2398,20 @@ function refPromotedEnums(node, promoted) {
|
|
|
2109
2398
|
//#endregion
|
|
2110
2399
|
//#region src/schemaDiagnostics.ts
|
|
2111
2400
|
/**
|
|
2112
|
-
*
|
|
2113
|
-
*
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2116
|
-
*
|
|
2117
|
-
*
|
|
2401
|
+
* Scans one freshly converted top-level schema in a single walk, so the post-convert pass never
|
|
2402
|
+
* sweeps the same nodes twice. It reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`,
|
|
2403
|
+
* `KUBB_DEPRECATED`) and returns the names of every schema the node references, ready to feed the
|
|
2404
|
+
* circular-dependency graph.
|
|
2405
|
+
*
|
|
2406
|
+
* Walks the node the parser produced, threading the RFC 6901 pointer as it descends so a nested
|
|
2407
|
+
* field reports against its full path (`#/components/schemas/Pet/properties/owner/properties/name`).
|
|
2408
|
+
* Refs are recorded by name and not followed, so the resolved schema is reported under its own walk.
|
|
2409
|
+
* Reports land in the active build run, are a no-op outside one, and repeats are deduped by the build.
|
|
2118
2410
|
*/
|
|
2119
|
-
function
|
|
2120
|
-
|
|
2411
|
+
function scanSchema({ node, name }) {
|
|
2412
|
+
const refs = /* @__PURE__ */ new Set();
|
|
2413
|
+
visit(node, `#/components/schemas/${escapePointerToken(name)}`, refs);
|
|
2414
|
+
return refs;
|
|
2121
2415
|
}
|
|
2122
2416
|
/**
|
|
2123
2417
|
* Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
|
|
@@ -2126,7 +2420,11 @@ function reportSchemaDiagnostics({ node, name }) {
|
|
|
2126
2420
|
function escapePointerToken(token) {
|
|
2127
2421
|
return token.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2128
2422
|
}
|
|
2129
|
-
function visit(node, pointer) {
|
|
2423
|
+
function visit(node, pointer, refs) {
|
|
2424
|
+
if (node.type === "ref") {
|
|
2425
|
+
const refName = (0, _kubb_ast.resolveRefName)(node);
|
|
2426
|
+
if (refName) refs.add(refName);
|
|
2427
|
+
}
|
|
2130
2428
|
if (node.deprecated) _kubb_core.Diagnostics.report({
|
|
2131
2429
|
code: _kubb_core.Diagnostics.code.deprecated,
|
|
2132
2430
|
severity: "info",
|
|
@@ -2147,19 +2445,19 @@ function visit(node, pointer) {
|
|
|
2147
2445
|
}
|
|
2148
2446
|
});
|
|
2149
2447
|
if (node.type === "object") {
|
|
2150
|
-
for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}
|
|
2151
|
-
if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties
|
|
2448
|
+
for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`, refs);
|
|
2449
|
+
if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`, refs);
|
|
2152
2450
|
return;
|
|
2153
2451
|
}
|
|
2154
2452
|
if (node.type === "array") {
|
|
2155
|
-
for (const item of node.items ?? []) visit(item, `${pointer}/items
|
|
2453
|
+
for (const item of node.items ?? []) visit(item, `${pointer}/items`, refs);
|
|
2156
2454
|
return;
|
|
2157
2455
|
}
|
|
2158
2456
|
if (node.type === "tuple") {
|
|
2159
|
-
for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}
|
|
2457
|
+
for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`, refs);
|
|
2160
2458
|
return;
|
|
2161
2459
|
}
|
|
2162
|
-
if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}
|
|
2460
|
+
if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`, refs);
|
|
2163
2461
|
}
|
|
2164
2462
|
//#endregion
|
|
2165
2463
|
//#region src/adapter.ts
|
|
@@ -2205,63 +2503,33 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
|
|
|
2205
2503
|
enumSuffix
|
|
2206
2504
|
};
|
|
2207
2505
|
let parsedDocument = null;
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
const schemaParserCache = /* @__PURE__ */ new WeakMap();
|
|
2211
|
-
function ensureDocument(source) {
|
|
2212
|
-
const cached = documentCache.get(source);
|
|
2213
|
-
if (cached) return cached;
|
|
2214
|
-
const promise = (async () => {
|
|
2215
|
-
const fresh = await parseFromConfig(source);
|
|
2216
|
-
if (validate) await validateDocument(fresh);
|
|
2217
|
-
parsedDocument = fresh;
|
|
2218
|
-
return fresh;
|
|
2219
|
-
})();
|
|
2220
|
-
documentCache.set(source, promise);
|
|
2221
|
-
return promise;
|
|
2222
|
-
}
|
|
2223
|
-
function ensureSchemas(document) {
|
|
2224
|
-
const cached = schemasCache.get(document);
|
|
2225
|
-
if (cached) return cached;
|
|
2226
|
-
const result = getSchemas(document, { contentType });
|
|
2227
|
-
schemasCache.set(document, result);
|
|
2228
|
-
return result;
|
|
2229
|
-
}
|
|
2230
|
-
function ensureSchemaParser({ document, renames }) {
|
|
2231
|
-
const cached = schemaParserCache.get(document);
|
|
2232
|
-
if (cached) return cached;
|
|
2233
|
-
const parser = createSchemaParser({
|
|
2234
|
-
document,
|
|
2235
|
-
contentType,
|
|
2236
|
-
renames
|
|
2237
|
-
});
|
|
2238
|
-
schemaParserCache.set(document, parser);
|
|
2239
|
-
return parser;
|
|
2240
|
-
}
|
|
2241
|
-
function parseInput({ document, schemas, parser }) {
|
|
2506
|
+
const inputCache = /* @__PURE__ */ new WeakMap();
|
|
2507
|
+
function parseInput({ document, refs, schemas, parser }) {
|
|
2242
2508
|
const { parseSchema, parseOperation } = parser;
|
|
2243
2509
|
const parsedByName = /* @__PURE__ */ new Map();
|
|
2244
2510
|
const refAliasMap = /* @__PURE__ */ new Map();
|
|
2245
2511
|
const enumNames = [];
|
|
2246
2512
|
const discriminatorParentNodes = [];
|
|
2513
|
+
const refGraph = /* @__PURE__ */ new Map();
|
|
2247
2514
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
2248
2515
|
const node = parseSchema({
|
|
2249
2516
|
schema,
|
|
2250
2517
|
name
|
|
2251
2518
|
}, parserOptions);
|
|
2252
2519
|
parsedByName.set(name, node);
|
|
2253
|
-
|
|
2520
|
+
const refs = scanSchema({
|
|
2254
2521
|
node,
|
|
2255
2522
|
name
|
|
2256
2523
|
});
|
|
2524
|
+
if (node.name) refGraph.set(node.name, refs);
|
|
2257
2525
|
if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
|
|
2258
2526
|
if ((0, _kubb_ast.narrowSchema)(node, "enum") && node.name) enumNames.push(node.name);
|
|
2259
2527
|
if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
|
|
2260
2528
|
}
|
|
2261
|
-
const circularNames = [...(0, _kubb_ast.
|
|
2529
|
+
const circularNames = [...(0, _kubb_ast.findCircularSchemasFromGraph)(refGraph)];
|
|
2262
2530
|
const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
|
|
2263
2531
|
const operationNodes = [];
|
|
2264
|
-
for (const operation of getOperations(document)) {
|
|
2532
|
+
for (const operation of getOperations(document, refs)) {
|
|
2265
2533
|
const operationNode = parseOperation(parserOptions, operation);
|
|
2266
2534
|
if (operationNode) operationNodes.push(operationNode);
|
|
2267
2535
|
}
|
|
@@ -2323,19 +2591,34 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
|
|
|
2323
2591
|
},
|
|
2324
2592
|
async validate(input, options) {
|
|
2325
2593
|
await assertInputExists(input);
|
|
2326
|
-
|
|
2594
|
+
const document = await parseDocument(input);
|
|
2595
|
+
assertDocument(document);
|
|
2596
|
+
await validateDocument(document, options);
|
|
2327
2597
|
},
|
|
2328
2598
|
async parse(source) {
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
document
|
|
2333
|
-
|
|
2334
|
-
|
|
2599
|
+
const cached = inputCache.get(source);
|
|
2600
|
+
if (cached) return cached;
|
|
2601
|
+
const promise = (async () => {
|
|
2602
|
+
const document = await parseFromConfig(source);
|
|
2603
|
+
assertDocument(document);
|
|
2604
|
+
if (validate) await validateDocument(document);
|
|
2605
|
+
parsedDocument = document;
|
|
2606
|
+
const refs = createRefs(document);
|
|
2607
|
+
const { schemas, renames } = getSchemas(document, { contentType }, refs);
|
|
2608
|
+
return parseInput({
|
|
2335
2609
|
document,
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2610
|
+
refs,
|
|
2611
|
+
schemas,
|
|
2612
|
+
parser: createSchemaParser({
|
|
2613
|
+
document,
|
|
2614
|
+
refs,
|
|
2615
|
+
contentType,
|
|
2616
|
+
renames
|
|
2617
|
+
})
|
|
2618
|
+
});
|
|
2619
|
+
})();
|
|
2620
|
+
inputCache.set(source, promise);
|
|
2621
|
+
return promise;
|
|
2339
2622
|
}
|
|
2340
2623
|
};
|
|
2341
2624
|
});
|