@kubb/adapter-oas 5.14.0 → 5.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +454 -224
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +455 -225
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineExtension } from "@kubb/core";
|
|
2
|
-
import { Diagnostics, ast, childName, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion, mergeAdjacentObjectsLazy } from "@kubb/kit";
|
|
2
|
+
import { Diagnostics, ast, childName, enumPrimitive, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion, mergeAdjacentObjectsLazy } from "@kubb/kit";
|
|
3
3
|
import { access, readFile } from "node:fs/promises";
|
|
4
4
|
import { upgrade } from "@scalar/openapi-upgrader";
|
|
5
5
|
import { bundle } from "api-ref-bundler";
|
|
@@ -20,7 +20,7 @@ var compatibility = {
|
|
|
20
20
|
};
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region package.json
|
|
23
|
-
var version = "5.14.
|
|
23
|
+
var version = "5.14.1";
|
|
24
24
|
//#endregion
|
|
25
25
|
//#region src/constants.ts
|
|
26
26
|
/**
|
|
@@ -371,6 +371,198 @@ async function read(path) {
|
|
|
371
371
|
return readFile(path, { encoding: "utf8" });
|
|
372
372
|
}
|
|
373
373
|
//#endregion
|
|
374
|
+
//#region src/oas.ts
|
|
375
|
+
/**
|
|
376
|
+
* Returns `true` when a schema should be treated as nullable.
|
|
377
|
+
*
|
|
378
|
+
* Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
|
|
379
|
+
* `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
|
|
380
|
+
*/
|
|
381
|
+
function isNullable(schema) {
|
|
382
|
+
if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
|
|
383
|
+
const schemaType = schema?.type;
|
|
384
|
+
if (schemaType === "null") return true;
|
|
385
|
+
if (Array.isArray(schemaType)) return schemaType.includes("null");
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
|
|
390
|
+
*/
|
|
391
|
+
function pickExtensions(source) {
|
|
392
|
+
const entries = Object.entries(source).filter(([key]) => key.startsWith("x-"));
|
|
393
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
|
|
397
|
+
*/
|
|
398
|
+
function isReference(obj) {
|
|
399
|
+
return !!obj && typeof obj === "object" && "$ref" in obj;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
|
|
403
|
+
* excluding the Swagger 2 string form.
|
|
404
|
+
*/
|
|
405
|
+
function isDiscriminator(obj) {
|
|
406
|
+
const record = obj;
|
|
407
|
+
return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Returns `true` when a schema is a raw binary payload.
|
|
411
|
+
*/
|
|
412
|
+
function isBinary(schema) {
|
|
413
|
+
return schema.contentMediaType === "application/octet-stream" && schema.contentEncoding === void 0 && (schema.type === void 0 || schema.type === "string");
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Returns the binary schema for a non-JSON entry the OAS 3.1 upgrade emptied out, or `undefined`
|
|
417
|
+
* when the entry carries a schema of its own. The upgrade rewrites `format: 'binary'` into
|
|
418
|
+
* `contentMediaType` and drops it whenever it only repeats the media type key, for any binary
|
|
419
|
+
* media type (`application/octet-stream`, `application/pdf`, `image/png`, ...), not just
|
|
420
|
+
* `application/octet-stream`. Without the fallback such a body resolves to the empty schema type
|
|
421
|
+
* instead of a blob.
|
|
422
|
+
*/
|
|
423
|
+
function getBinaryFallbackSchema(mediaType, schema) {
|
|
424
|
+
if (!mediaType || isJsonMimeType(mediaType)) return void 0;
|
|
425
|
+
if (schema && Object.keys(schema).length > 0) return void 0;
|
|
426
|
+
return {
|
|
427
|
+
type: "string",
|
|
428
|
+
contentMediaType: "application/octet-stream"
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* MIME type fragments that mark a media type as JSON-like.
|
|
433
|
+
*
|
|
434
|
+
* A content type is JSON when it contains any of these substrings. The `+json` entry catches
|
|
435
|
+
* structured-syntax suffixes such as `application/vnd.api+json`.
|
|
436
|
+
*/
|
|
437
|
+
const jsonMimeFragments = [
|
|
438
|
+
"application/json",
|
|
439
|
+
"application/x-json",
|
|
440
|
+
"text/json",
|
|
441
|
+
"text/x-json",
|
|
442
|
+
"+json"
|
|
443
|
+
];
|
|
444
|
+
/**
|
|
445
|
+
* Returns `true` when a media type string is JSON-like.
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
* ```ts
|
|
449
|
+
* isJsonMimeType('application/json') // true
|
|
450
|
+
* isJsonMimeType('application/vnd.api+json') // true
|
|
451
|
+
* isJsonMimeType('multipart/form-data') // false
|
|
452
|
+
* ```
|
|
453
|
+
*/
|
|
454
|
+
function isJsonMimeType(mimeType) {
|
|
455
|
+
return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
|
|
456
|
+
}
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/refs.ts
|
|
459
|
+
/**
|
|
460
|
+
* Creates the `$ref` resolution service for one document. One pointer walk backs every method.
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* ```ts
|
|
464
|
+
* const refs = createRefs(document)
|
|
465
|
+
* refs.resolve<SchemaObject>('#/components/schemas/Pet')
|
|
466
|
+
* refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
|
|
467
|
+
* refs.resolveNode('#/components/schemas/Pet', parseSchema)
|
|
468
|
+
* refs.deref<ResponseObject>(operation.schema.responses?.['200'])
|
|
469
|
+
* refs.derefKeepingRef(parameter)
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
function createRefs(document) {
|
|
473
|
+
const pointerCache = /* @__PURE__ */ new Map();
|
|
474
|
+
const resolvedNodeCache = /* @__PURE__ */ new Map();
|
|
475
|
+
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
476
|
+
function walkPointer($ref) {
|
|
477
|
+
const trimmed = $ref.trim();
|
|
478
|
+
if (trimmed === "" || !trimmed.startsWith("#")) return {
|
|
479
|
+
applicable: false,
|
|
480
|
+
value: null
|
|
481
|
+
};
|
|
482
|
+
const pointer = trimmed.substring(1);
|
|
483
|
+
if (pointerCache.has(pointer)) return {
|
|
484
|
+
applicable: true,
|
|
485
|
+
value: pointerCache.get(pointer)
|
|
486
|
+
};
|
|
487
|
+
const current = pointer.split("/").filter(Boolean).map((token) => globalThis.decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~")).reduce((obj, key) => obj?.[key], document);
|
|
488
|
+
if (current) pointerCache.set(pointer, current);
|
|
489
|
+
return {
|
|
490
|
+
applicable: true,
|
|
491
|
+
value: current ?? null
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
|
|
496
|
+
* `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
|
|
497
|
+
* outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
|
|
498
|
+
* for a speculative lookup where a missing ref is not an error.
|
|
499
|
+
*/
|
|
500
|
+
function resolve(refPath, options) {
|
|
501
|
+
const { applicable, value } = walkPointer(refPath);
|
|
502
|
+
if (!applicable || value || options?.report === false) return value;
|
|
503
|
+
const diagnostic = {
|
|
504
|
+
code: Diagnostics.code.refNotFound,
|
|
505
|
+
severity: "error",
|
|
506
|
+
message: `Could not find a definition for ${refPath}.`,
|
|
507
|
+
help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
|
|
508
|
+
location: {
|
|
509
|
+
kind: "schema",
|
|
510
|
+
pointer: refPath,
|
|
511
|
+
ref: refPath
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
|
|
519
|
+
* instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
|
|
520
|
+
* resolved (e.g. a minimal document in a unit test).
|
|
521
|
+
*/
|
|
522
|
+
function resolveNode(refPath, parse) {
|
|
523
|
+
if (resolvingRefs.has(refPath)) return null;
|
|
524
|
+
if (!resolvedNodeCache.has(refPath)) {
|
|
525
|
+
let resolved = null;
|
|
526
|
+
try {
|
|
527
|
+
const referenced = resolve(refPath);
|
|
528
|
+
if (referenced) {
|
|
529
|
+
resolvingRefs.add(refPath);
|
|
530
|
+
resolved = parse({ schema: referenced });
|
|
531
|
+
resolvingRefs.delete(refPath);
|
|
532
|
+
}
|
|
533
|
+
} catch {}
|
|
534
|
+
resolvedNodeCache.set(refPath, resolved);
|
|
535
|
+
}
|
|
536
|
+
return resolvedNodeCache.get(refPath) ?? null;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
|
|
540
|
+
* resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
|
|
541
|
+
* `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
|
|
542
|
+
* returned as-is.
|
|
543
|
+
*/
|
|
544
|
+
function deref(value) {
|
|
545
|
+
if (!isReference(value)) return value ? value : null;
|
|
546
|
+
const resolved = resolve(value.$ref);
|
|
547
|
+
return resolved && !isReference(resolved) ? resolved : null;
|
|
548
|
+
}
|
|
549
|
+
/** Resolves a `$ref` object but keeps the `$ref` field on the result; other values pass through. */
|
|
550
|
+
function derefKeepingRef(value) {
|
|
551
|
+
if (isReference(value)) return {
|
|
552
|
+
...value,
|
|
553
|
+
...resolve(value.$ref),
|
|
554
|
+
$ref: value.$ref
|
|
555
|
+
};
|
|
556
|
+
return value;
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
resolve,
|
|
560
|
+
resolveNode,
|
|
561
|
+
deref,
|
|
562
|
+
derefKeepingRef
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
//#endregion
|
|
374
566
|
//#region src/load/source.ts
|
|
375
567
|
const urlRegExp = /^https?:\/+/i;
|
|
376
568
|
/**
|
|
@@ -491,6 +683,22 @@ function hasExternalRef(node) {
|
|
|
491
683
|
return Object.values(node).some(hasExternalRef);
|
|
492
684
|
}
|
|
493
685
|
/**
|
|
686
|
+
* Replaces each `$ref` into `#/paths/...` with a copy of its target, in place.
|
|
687
|
+
*
|
|
688
|
+
* The Swagger 2.0 upgrade moves `responses/200/schema` under `content`, so a ref to the old location
|
|
689
|
+
* would no longer resolve. A ref that points back into itself is kept, since inlining it never ends.
|
|
690
|
+
*/
|
|
691
|
+
function inlinePathRefs(node, resolve, visiting = []) {
|
|
692
|
+
if (!node || typeof node !== "object") return node;
|
|
693
|
+
const ref = node.$ref;
|
|
694
|
+
if (typeof ref === "string" && ref.startsWith("#/paths/") && !visiting.includes(ref)) {
|
|
695
|
+
const target = resolve(ref);
|
|
696
|
+
return target && typeof target === "object" ? inlinePathRefs(structuredClone(target), resolve, [...visiting, ref]) : node;
|
|
697
|
+
}
|
|
698
|
+
for (const [key, value] of Object.entries(node)) node[key] = inlinePathRefs(value, resolve, visiting);
|
|
699
|
+
return node;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
494
702
|
* Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
|
|
495
703
|
*
|
|
496
704
|
* External file schemas are hoisted into named `components.schemas` entries, so a property
|
|
@@ -548,6 +756,17 @@ async function parseFromConfig(source) {
|
|
|
548
756
|
await assertInputExists(source.path);
|
|
549
757
|
return bundleDocument(source.path);
|
|
550
758
|
})();
|
|
759
|
+
if (raw && typeof raw === "object") {
|
|
760
|
+
const document = raw;
|
|
761
|
+
for (const key of ["swagger", "openapi"]) {
|
|
762
|
+
const version = document[key];
|
|
763
|
+
if (typeof version === "number") document[key] = Number.isInteger(version) ? version.toFixed(1) : String(version);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (raw && typeof raw === "object" && "swagger" in raw) {
|
|
767
|
+
const refs = createRefs(raw);
|
|
768
|
+
inlinePathRefs(raw, (ref) => refs.resolve(ref, { report: false }));
|
|
769
|
+
}
|
|
551
770
|
try {
|
|
552
771
|
return upgrade(raw, "3.1");
|
|
553
772
|
} catch (error) {
|
|
@@ -644,90 +863,6 @@ async function validateDocumentInWorker(document) {
|
|
|
644
863
|
if (!message.valid) reportValidationErrors(message);
|
|
645
864
|
}
|
|
646
865
|
//#endregion
|
|
647
|
-
//#region src/oas.ts
|
|
648
|
-
/**
|
|
649
|
-
* Returns `true` when a schema should be treated as nullable.
|
|
650
|
-
*
|
|
651
|
-
* Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
|
|
652
|
-
* `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
|
|
653
|
-
*/
|
|
654
|
-
function isNullable(schema) {
|
|
655
|
-
if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
|
|
656
|
-
const schemaType = schema?.type;
|
|
657
|
-
if (schemaType === "null") return true;
|
|
658
|
-
if (Array.isArray(schemaType)) return schemaType.includes("null");
|
|
659
|
-
return false;
|
|
660
|
-
}
|
|
661
|
-
/**
|
|
662
|
-
* Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
|
|
663
|
-
*/
|
|
664
|
-
function pickExtensions(source) {
|
|
665
|
-
const entries = Object.entries(source).filter(([key]) => key.startsWith("x-"));
|
|
666
|
-
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
|
|
670
|
-
*/
|
|
671
|
-
function isReference(obj) {
|
|
672
|
-
return !!obj && typeof obj === "object" && "$ref" in obj;
|
|
673
|
-
}
|
|
674
|
-
/**
|
|
675
|
-
* Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
|
|
676
|
-
* excluding the Swagger 2 string form.
|
|
677
|
-
*/
|
|
678
|
-
function isDiscriminator(obj) {
|
|
679
|
-
const record = obj;
|
|
680
|
-
return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
|
|
681
|
-
}
|
|
682
|
-
/**
|
|
683
|
-
* Returns `true` when a schema is a raw binary payload.
|
|
684
|
-
*/
|
|
685
|
-
function isBinary(schema) {
|
|
686
|
-
return schema.contentMediaType === "application/octet-stream" && schema.contentEncoding === void 0 && (schema.type === void 0 || schema.type === "string");
|
|
687
|
-
}
|
|
688
|
-
/**
|
|
689
|
-
* Returns the binary schema for a non-JSON entry the OAS 3.1 upgrade emptied out, or `undefined`
|
|
690
|
-
* when the entry carries a schema of its own. The upgrade rewrites `format: 'binary'` into
|
|
691
|
-
* `contentMediaType` and drops it whenever it only repeats the media type key, for any binary
|
|
692
|
-
* media type (`application/octet-stream`, `application/pdf`, `image/png`, ...), not just
|
|
693
|
-
* `application/octet-stream`. Without the fallback such a body resolves to the empty schema type
|
|
694
|
-
* instead of a blob.
|
|
695
|
-
*/
|
|
696
|
-
function getBinaryFallbackSchema(mediaType, schema) {
|
|
697
|
-
if (!mediaType || isJsonMimeType(mediaType)) return void 0;
|
|
698
|
-
if (schema && Object.keys(schema).length > 0) return void 0;
|
|
699
|
-
return {
|
|
700
|
-
type: "string",
|
|
701
|
-
contentMediaType: "application/octet-stream"
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* MIME type fragments that mark a media type as JSON-like.
|
|
706
|
-
*
|
|
707
|
-
* A content type is JSON when it contains any of these substrings. The `+json` entry catches
|
|
708
|
-
* structured-syntax suffixes such as `application/vnd.api+json`.
|
|
709
|
-
*/
|
|
710
|
-
const jsonMimeFragments = [
|
|
711
|
-
"application/json",
|
|
712
|
-
"application/x-json",
|
|
713
|
-
"text/json",
|
|
714
|
-
"text/x-json",
|
|
715
|
-
"+json"
|
|
716
|
-
];
|
|
717
|
-
/**
|
|
718
|
-
* Returns `true` when a media type string is JSON-like.
|
|
719
|
-
*
|
|
720
|
-
* @example
|
|
721
|
-
* ```ts
|
|
722
|
-
* isJsonMimeType('application/json') // true
|
|
723
|
-
* isJsonMimeType('application/vnd.api+json') // true
|
|
724
|
-
* isJsonMimeType('multipart/form-data') // false
|
|
725
|
-
* ```
|
|
726
|
-
*/
|
|
727
|
-
function isJsonMimeType(mimeType) {
|
|
728
|
-
return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
|
|
729
|
-
}
|
|
730
|
-
//#endregion
|
|
731
866
|
//#region src/model/components.ts
|
|
732
867
|
/**
|
|
733
868
|
* Extracts the inline schema from a media-type `content` map, with the media type it sits under.
|
|
@@ -1156,6 +1291,19 @@ function getParameters({ operation, refs }) {
|
|
|
1156
1291
|
for (const entry of [...pathLevelParams, ...operationParams]) if (entry.param.name && entry.param.in) paramMap.set(`${entry.param.in}:${entry.param.name}`, entry);
|
|
1157
1292
|
return Array.from(paramMap.values());
|
|
1158
1293
|
}
|
|
1294
|
+
/** The `encoding` of a media type, reduced to the entries that set a field Kubb carries. */
|
|
1295
|
+
function getEncoding(encoding) {
|
|
1296
|
+
const entries = Object.entries(encoding ?? {}).flatMap(([property, { contentType, style, explode, allowReserved }]) => {
|
|
1297
|
+
const node = {
|
|
1298
|
+
...contentType !== void 0 ? { contentType } : {},
|
|
1299
|
+
...style !== void 0 ? { style } : {},
|
|
1300
|
+
...explode !== void 0 ? { explode } : {},
|
|
1301
|
+
...allowReserved !== void 0 ? { allowReserved } : {}
|
|
1302
|
+
};
|
|
1303
|
+
return Object.keys(node).length ? [[property, node]] : [];
|
|
1304
|
+
});
|
|
1305
|
+
return entries.length ? Object.fromEntries(entries) : void 0;
|
|
1306
|
+
}
|
|
1159
1307
|
/** Schema for one media type of a `content` map, with the binary fallback for an emptied non-JSON entry. */
|
|
1160
1308
|
function getBodySchema({ content, contentType, refs }) {
|
|
1161
1309
|
const entry = contentType ? content?.[contentType] : void 0;
|
|
@@ -1263,10 +1411,12 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1263
1411
|
name: requestBodyName
|
|
1264
1412
|
});
|
|
1265
1413
|
scanSchema(node, `${pointer}/requestBody/content/${escapePointerToken(ct)}/schema`);
|
|
1414
|
+
const encoding = getEncoding(body?.content?.[ct]?.encoding);
|
|
1266
1415
|
return [ast.factory.createContent({
|
|
1267
1416
|
contentType: ct,
|
|
1268
1417
|
schema: ast.optionality(node, bodyRequired),
|
|
1269
|
-
keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
|
|
1418
|
+
keysToOmit: collectPropertyKeysByFlag(schema, "readOnly"),
|
|
1419
|
+
...encoding && { encoding }
|
|
1270
1420
|
})];
|
|
1271
1421
|
});
|
|
1272
1422
|
const requestBody = content.length > 0 || body?.description ? {
|
|
@@ -1363,6 +1513,42 @@ function createNode({ schema, name, nullable, defaultValue }, extras) {
|
|
|
1363
1513
|
//#endregion
|
|
1364
1514
|
//#region src/emit/discriminator/preserve.ts
|
|
1365
1515
|
/**
|
|
1516
|
+
* Turns string mapping keys into the discriminator property's type when they parse cleanly.
|
|
1517
|
+
*/
|
|
1518
|
+
function typedKeys({ keys, propertyType }) {
|
|
1519
|
+
return keys.map((key) => {
|
|
1520
|
+
if (propertyType === "integer" || propertyType === "number") {
|
|
1521
|
+
const value = Number(key);
|
|
1522
|
+
return key.trim() !== "" && Number.isFinite(value) ? value : key;
|
|
1523
|
+
}
|
|
1524
|
+
if (propertyType === "boolean") {
|
|
1525
|
+
const lower = key.toLowerCase();
|
|
1526
|
+
return lower === "true" ? true : lower === "false" ? false : key;
|
|
1527
|
+
}
|
|
1528
|
+
return key;
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Reads the declared `type` of `propertyName` on a schema, following `$ref`s and `allOf` members.
|
|
1533
|
+
* A nullable array such as `['integer', 'null']` reads as its one non-null type.
|
|
1534
|
+
*/
|
|
1535
|
+
function propertyTypeOf({ schema, propertyName, refs, seen = /* @__PURE__ */ new Set() }) {
|
|
1536
|
+
const deref = (value) => value && isReference(value) ? refs.resolve(value.$ref, { report: false }) : value;
|
|
1537
|
+
if (schema && isReference(schema)) {
|
|
1538
|
+
if (seen.has(schema.$ref)) return void 0;
|
|
1539
|
+
seen.add(schema.$ref);
|
|
1540
|
+
}
|
|
1541
|
+
const resolved = deref(schema);
|
|
1542
|
+
const type = deref(resolved?.properties?.[propertyName])?.type ?? resolved?.allOf?.map((member) => propertyTypeOf({
|
|
1543
|
+
schema: member,
|
|
1544
|
+
propertyName,
|
|
1545
|
+
refs,
|
|
1546
|
+
seen
|
|
1547
|
+
})).find(Boolean);
|
|
1548
|
+
const nonNull = Array.isArray(type) ? type.filter((t) => t !== "null") : [];
|
|
1549
|
+
return nonNull.length === 1 ? nonNull[0] : type;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1366
1552
|
* Creates a single-property object schema used as a discriminator literal.
|
|
1367
1553
|
*
|
|
1368
1554
|
* @example
|
|
@@ -1379,7 +1565,7 @@ function createDiscriminantNode({ propertyName, values }) {
|
|
|
1379
1565
|
name: propertyName,
|
|
1380
1566
|
schema: ast.factory.createSchema({
|
|
1381
1567
|
type: "enum",
|
|
1382
|
-
primitive:
|
|
1568
|
+
primitive: enumPrimitive(values),
|
|
1383
1569
|
enumValues: values
|
|
1384
1570
|
}),
|
|
1385
1571
|
required: true
|
|
@@ -1394,21 +1580,61 @@ function createDiscriminantNode({ propertyName, values }) {
|
|
|
1394
1580
|
*
|
|
1395
1581
|
* @example
|
|
1396
1582
|
* ```ts
|
|
1397
|
-
* findDiscriminators({ dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // ['dog', 'hound']
|
|
1583
|
+
* findDiscriminators({ mapping: { dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, ref: '#/components/schemas/Dog' }) // ['dog', 'hound']
|
|
1398
1584
|
* ```
|
|
1399
1585
|
*/
|
|
1400
|
-
function findDiscriminators(mapping, ref) {
|
|
1586
|
+
function findDiscriminators({ mapping, ref }) {
|
|
1401
1587
|
if (!mapping || !ref) return [];
|
|
1402
1588
|
return Object.entries(mapping).filter(([, value]) => value === ref).map(([key]) => key);
|
|
1403
1589
|
}
|
|
1404
1590
|
/**
|
|
1591
|
+
* Finds the discriminator a schema declares itself or through an inline `allOf` member.
|
|
1592
|
+
*/
|
|
1593
|
+
function getDiscriminator(schema) {
|
|
1594
|
+
return (isDiscriminator(schema) ? schema : schema.allOf?.find(isDiscriminator))?.discriminator;
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Lists the mapping keys of a discriminator plus the keys of the nested discriminators its mapped
|
|
1598
|
+
* schemas declare on the same property, so a root `$type` accepts values from every level.
|
|
1599
|
+
*/
|
|
1600
|
+
function getMappingKeys({ discriminator: { propertyName, mapping }, refs, seen = /* @__PURE__ */ new Set() }) {
|
|
1601
|
+
return Object.entries(mapping ?? {}).flatMap(([key, target]) => {
|
|
1602
|
+
if (seen.has(target)) return [key];
|
|
1603
|
+
seen.add(target);
|
|
1604
|
+
const resolved = refs.resolve(target, { report: false });
|
|
1605
|
+
const nested = resolved && getDiscriminator(resolved);
|
|
1606
|
+
return nested?.propertyName === propertyName ? [key, ...getMappingKeys({
|
|
1607
|
+
discriminator: nested,
|
|
1608
|
+
refs,
|
|
1609
|
+
seen
|
|
1610
|
+
})] : [key];
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Drops the discriminator from an inline `allOf` member when a `$ref` sibling already declares one on
|
|
1615
|
+
* the same property. That mapping names the schema's own children, and keeping it pins the property
|
|
1616
|
+
* to those keys next to the parent's, which leaves `never`.
|
|
1617
|
+
*/
|
|
1618
|
+
function removeInheritedDiscriminator({ members, refs }) {
|
|
1619
|
+
const inherited = new Set(members.flatMap((member) => {
|
|
1620
|
+
const parent = isReference(member) ? refs.resolve(member.$ref, { report: false }) : null;
|
|
1621
|
+
return parent && getDiscriminator(parent)?.propertyName || [];
|
|
1622
|
+
}));
|
|
1623
|
+
return members.map((member) => {
|
|
1624
|
+
if (isReference(member) || !isDiscriminator(member) || member.oneOf || member.anyOf) return member;
|
|
1625
|
+
if (!inherited.has(member.discriminator.propertyName)) return member;
|
|
1626
|
+
const { discriminator: _discriminator, ...rest } = member;
|
|
1627
|
+
return rest;
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1405
1631
|
* Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
|
|
1406
1632
|
* node with either the shared-properties slice carrying that value, or a synthetic discriminant
|
|
1407
1633
|
* literal. The referenced child schema's own definition is left untouched — narrowing happens only
|
|
1408
1634
|
* at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
|
|
1409
1635
|
* which additionally patches the child schema's own definition in a post-pass).
|
|
1410
1636
|
*/
|
|
1411
|
-
function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, name, refs }) {
|
|
1637
|
+
function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parentType, parse, name, refs }) {
|
|
1412
1638
|
function pickDiscriminatorPropertyNode(node, propertyName) {
|
|
1413
1639
|
const discriminatorProperty = ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
|
|
1414
1640
|
if (!discriminatorProperty) return null;
|
|
@@ -1444,9 +1670,20 @@ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode,
|
|
|
1444
1670
|
}
|
|
1445
1671
|
return unionMembers.map((s) => {
|
|
1446
1672
|
const ref = isReference(s) ? s.$ref : void 0;
|
|
1447
|
-
const mappedValues = findDiscriminators(
|
|
1673
|
+
const mappedValues = findDiscriminators({
|
|
1674
|
+
mapping: discriminator?.mapping,
|
|
1675
|
+
ref
|
|
1676
|
+
});
|
|
1448
1677
|
const implicitValue = mappedValues.length ? null : implicitDiscriminantValue(s);
|
|
1449
|
-
const
|
|
1678
|
+
const propertyType = propertyTypeOf({
|
|
1679
|
+
schema: s,
|
|
1680
|
+
propertyName: discriminator?.propertyName ?? "",
|
|
1681
|
+
refs
|
|
1682
|
+
}) ?? parentType;
|
|
1683
|
+
const discriminatorValues = typedKeys({
|
|
1684
|
+
keys: mappedValues.length ? mappedValues : implicitValue ? [implicitValue] : [],
|
|
1685
|
+
propertyType
|
|
1686
|
+
});
|
|
1450
1687
|
const memberNode = parse({
|
|
1451
1688
|
schema: s,
|
|
1452
1689
|
name
|
|
@@ -1478,21 +1715,34 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
|
|
|
1478
1715
|
members: allOfMembers.filter((item) => {
|
|
1479
1716
|
if (!isReference(item) || !name) return true;
|
|
1480
1717
|
const deref = refs.resolve(item.$ref);
|
|
1481
|
-
|
|
1482
|
-
const parentUnion = deref
|
|
1483
|
-
if (!parentUnion) return true;
|
|
1718
|
+
const discriminator = deref && getDiscriminator(deref);
|
|
1719
|
+
const parentUnion = deref?.oneOf ?? deref?.anyOf;
|
|
1720
|
+
if (!deref || !discriminator || isDiscriminator(deref) && !parentUnion) return true;
|
|
1484
1721
|
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1722
|
+
const values = findDiscriminators({
|
|
1723
|
+
mapping: discriminator.mapping,
|
|
1724
|
+
ref: childRef
|
|
1725
|
+
});
|
|
1726
|
+
if (values.length) {
|
|
1727
|
+
const { propertyName } = discriminator;
|
|
1728
|
+
const propertyType = propertyTypeOf({
|
|
1729
|
+
schema: deref,
|
|
1730
|
+
propertyName,
|
|
1731
|
+
refs
|
|
1732
|
+
}) ?? propertyTypeOf({
|
|
1733
|
+
schema: { $ref: childRef },
|
|
1734
|
+
propertyName,
|
|
1735
|
+
refs
|
|
1736
|
+
});
|
|
1737
|
+
discriminantValues.push({
|
|
1738
|
+
propertyName,
|
|
1739
|
+
values: typedKeys({
|
|
1740
|
+
keys: values,
|
|
1741
|
+
propertyType
|
|
1742
|
+
})
|
|
1492
1743
|
});
|
|
1493
|
-
return false;
|
|
1494
1744
|
}
|
|
1495
|
-
return
|
|
1745
|
+
return !(parentUnion && (values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
|
|
1496
1746
|
}),
|
|
1497
1747
|
discriminantValues
|
|
1498
1748
|
};
|
|
@@ -1619,10 +1869,9 @@ function resolveUnionMembers(members, siblingProperties) {
|
|
|
1619
1869
|
});
|
|
1620
1870
|
}
|
|
1621
1871
|
/**
|
|
1622
|
-
* Enriches any `oneOf` or `anyOf` branches on
|
|
1872
|
+
* Enriches a schema object, and any `oneOf` or `anyOf` branches on it, with properties from sibling schemas.
|
|
1623
1873
|
*/
|
|
1624
1874
|
function resolveUnionSchema(schema, siblingProperties) {
|
|
1625
|
-
if (!schema.oneOf && !schema.anyOf) return schema;
|
|
1626
1875
|
const next = { ...resolveUnionMembers([schema], siblingProperties)[0] ?? schema };
|
|
1627
1876
|
if (schema.oneOf) next.oneOf = resolveUnionMembers(schema.oneOf, siblingProperties);
|
|
1628
1877
|
if (schema.anyOf) next.anyOf = resolveUnionMembers(schema.anyOf, siblingProperties);
|
|
@@ -1646,6 +1895,17 @@ function convertRef({ schema, name, nullable, defaultValue, document, parse, ref
|
|
|
1646
1895
|
defaultValue
|
|
1647
1896
|
};
|
|
1648
1897
|
if (refPath && document.components && !refs.resolve(refPath, { report: false })) return createNode(ctx, { type: "unknown" });
|
|
1898
|
+
if (refPath && !refPath.startsWith("#/components/schemas/") && resolvedSchema) return ast.factory.createSchema({
|
|
1899
|
+
...resolvedSchema,
|
|
1900
|
+
title: schema.title ?? resolvedSchema.title,
|
|
1901
|
+
description: schema.description ?? resolvedSchema.description,
|
|
1902
|
+
deprecated: schema.deprecated ?? resolvedSchema.deprecated,
|
|
1903
|
+
readOnly: schema.readOnly ?? resolvedSchema.readOnly,
|
|
1904
|
+
writeOnly: schema.writeOnly ?? resolvedSchema.writeOnly,
|
|
1905
|
+
default: defaultValue ?? resolvedSchema.default,
|
|
1906
|
+
name: name ?? resolvedSchema.name,
|
|
1907
|
+
nullable: nullable ?? resolvedSchema.nullable
|
|
1908
|
+
});
|
|
1649
1909
|
const targetName = renames?.get(schema.$ref);
|
|
1650
1910
|
return createNode(ctx, {
|
|
1651
1911
|
type: "ref",
|
|
@@ -1677,9 +1937,41 @@ function convertNot({ schema, name, nullable, defaultValue, parse }) {
|
|
|
1677
1937
|
});
|
|
1678
1938
|
}
|
|
1679
1939
|
/**
|
|
1940
|
+
* Keywords that describe a schema or conditionally constrain it, but add no structure.
|
|
1941
|
+
*/
|
|
1942
|
+
const annotationKeys = /* @__PURE__ */ new Set([
|
|
1943
|
+
"description",
|
|
1944
|
+
"title",
|
|
1945
|
+
"example",
|
|
1946
|
+
"examples",
|
|
1947
|
+
"default",
|
|
1948
|
+
"deprecated",
|
|
1949
|
+
"readOnly",
|
|
1950
|
+
"writeOnly",
|
|
1951
|
+
"if",
|
|
1952
|
+
"then",
|
|
1953
|
+
"else"
|
|
1954
|
+
]);
|
|
1955
|
+
/**
|
|
1956
|
+
* Moves `allOf` members that hold only annotations or `if`/`then`/`else` onto the parent, so they do
|
|
1957
|
+
* not become `unknown`. The parent's own values win, and the conditional keywords are not enforced.
|
|
1958
|
+
*/
|
|
1959
|
+
function mergeAnnotations(schema) {
|
|
1960
|
+
const members = schema.allOf;
|
|
1961
|
+
const isAnnotation = (member) => !isReference(member) && Object.keys(member).every((key) => annotationKeys.has(key));
|
|
1962
|
+
const annotations = members.filter(isAnnotation);
|
|
1963
|
+
const rest = members.filter((member) => !isAnnotation(member));
|
|
1964
|
+
return annotations.length && rest.length ? {
|
|
1965
|
+
...Object.assign({}, ...annotations),
|
|
1966
|
+
...schema,
|
|
1967
|
+
allOf: rest
|
|
1968
|
+
} : schema;
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1680
1971
|
* Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
|
|
1681
1972
|
*/
|
|
1682
|
-
function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
|
|
1973
|
+
function convertAllOf({ schema: original, name, nullable, defaultValue, parse, refs }) {
|
|
1974
|
+
const schema = mergeAnnotations(original);
|
|
1683
1975
|
if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
|
|
1684
1976
|
const [memberSchema] = schema.allOf;
|
|
1685
1977
|
const memberNode = parse({
|
|
@@ -1705,7 +1997,10 @@ function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
|
|
|
1705
1997
|
});
|
|
1706
1998
|
}
|
|
1707
1999
|
const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
|
|
1708
|
-
allOfMembers:
|
|
2000
|
+
allOfMembers: removeInheritedDiscriminator({
|
|
2001
|
+
members: schema.allOf,
|
|
2002
|
+
refs
|
|
2003
|
+
}),
|
|
1709
2004
|
name,
|
|
1710
2005
|
refs
|
|
1711
2006
|
});
|
|
@@ -1785,6 +2080,11 @@ function convertUnion(context) {
|
|
|
1785
2080
|
unionMembers,
|
|
1786
2081
|
discriminator,
|
|
1787
2082
|
sharedPropertiesNode,
|
|
2083
|
+
parentType: discriminator && propertyTypeOf({
|
|
2084
|
+
schema,
|
|
2085
|
+
propertyName: discriminator.propertyName,
|
|
2086
|
+
refs
|
|
2087
|
+
}),
|
|
1788
2088
|
parse,
|
|
1789
2089
|
name,
|
|
1790
2090
|
refs
|
|
@@ -2048,7 +2348,7 @@ function nameEnums(node, options) {
|
|
|
2048
2348
|
/**
|
|
2049
2349
|
* Converts an object-like schema into an `ObjectSchemaNode`.
|
|
2050
2350
|
*/
|
|
2051
|
-
function convertObject({ schema, name, nullable, defaultValue, options, parse }) {
|
|
2351
|
+
function convertObject({ schema, name, nullable, defaultValue, options, parse, refs }) {
|
|
2052
2352
|
const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
2053
2353
|
const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
|
|
2054
2354
|
const resolvedPropSchema = propSchema;
|
|
@@ -2065,7 +2365,7 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
|
|
|
2065
2365
|
name: propName,
|
|
2066
2366
|
schema: {
|
|
2067
2367
|
...schemaNode,
|
|
2068
|
-
nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
|
|
2368
|
+
nullable: schemaNode.type === "null" ? void 0 : propNullable || schemaNode.nullable || void 0
|
|
2069
2369
|
},
|
|
2070
2370
|
required
|
|
2071
2371
|
});
|
|
@@ -2105,7 +2405,17 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
|
|
|
2105
2405
|
});
|
|
2106
2406
|
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
2107
2407
|
const discPropName = schema.discriminator.propertyName;
|
|
2108
|
-
const values =
|
|
2408
|
+
const values = typedKeys({
|
|
2409
|
+
keys: getMappingKeys({
|
|
2410
|
+
discriminator: schema.discriminator,
|
|
2411
|
+
refs
|
|
2412
|
+
}),
|
|
2413
|
+
propertyType: propertyTypeOf({
|
|
2414
|
+
schema,
|
|
2415
|
+
propertyName: discPropName,
|
|
2416
|
+
refs
|
|
2417
|
+
})
|
|
2418
|
+
});
|
|
2109
2419
|
const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : void 0;
|
|
2110
2420
|
return ast.applyMacros(objectNode, [macroDiscriminatorEnum({
|
|
2111
2421
|
propertyName: discPropName,
|
|
@@ -2163,8 +2473,9 @@ function convertArray({ schema, name, nullable, defaultValue, options, parse })
|
|
|
2163
2473
|
//#region src/emit/parseSchema.ts
|
|
2164
2474
|
/**
|
|
2165
2475
|
* Ordered schema rule table. Order is significant: composition keywords (`not`, `$ref`, `allOf`,
|
|
2166
|
-
* `oneOf`/`anyOf`) take precedence over `const
|
|
2167
|
-
*
|
|
2476
|
+
* `oneOf`/`anyOf`) take precedence over `const` (except a `$ref` with a sibling `const`, where the
|
|
2477
|
+
* `const` wins), which takes precedence over a genuine OAS 3.1 multi-type array (more than one
|
|
2478
|
+
* non-`null` type), which takes precedence over `format`/`type`.
|
|
2168
2479
|
* A multi-type array must split into its per-type members before `format` runs, otherwise
|
|
2169
2480
|
* `format` collapses the whole schema to one type (e.g. `type: ['null', 'integer', 'string'],
|
|
2170
2481
|
* format: 'int32'` would drop `string` and emit just `integer`); each split-off member still
|
|
@@ -2178,7 +2489,7 @@ const schemaRules = [
|
|
|
2178
2489
|
convert: convertNot
|
|
2179
2490
|
},
|
|
2180
2491
|
{
|
|
2181
|
-
match: ({ schema }) => isReference(schema),
|
|
2492
|
+
match: ({ schema }) => isReference(schema) && !("const" in schema && schema.const !== void 0),
|
|
2182
2493
|
convert: convertRef
|
|
2183
2494
|
},
|
|
2184
2495
|
{
|
|
@@ -2346,114 +2657,6 @@ function refPromotedEnums(node, promoted) {
|
|
|
2346
2657
|
} });
|
|
2347
2658
|
}
|
|
2348
2659
|
//#endregion
|
|
2349
|
-
//#region src/refs.ts
|
|
2350
|
-
/**
|
|
2351
|
-
* Creates the `$ref` resolution service for one document. One pointer walk backs every method.
|
|
2352
|
-
*
|
|
2353
|
-
* @example
|
|
2354
|
-
* ```ts
|
|
2355
|
-
* const refs = createRefs(document)
|
|
2356
|
-
* refs.resolve<SchemaObject>('#/components/schemas/Pet')
|
|
2357
|
-
* refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
|
|
2358
|
-
* refs.resolveNode('#/components/schemas/Pet', parseSchema)
|
|
2359
|
-
* refs.deref<ResponseObject>(operation.schema.responses?.['200'])
|
|
2360
|
-
* refs.derefKeepingRef(parameter)
|
|
2361
|
-
* ```
|
|
2362
|
-
*/
|
|
2363
|
-
function createRefs(document) {
|
|
2364
|
-
const pointerCache = /* @__PURE__ */ new Map();
|
|
2365
|
-
const resolvedNodeCache = /* @__PURE__ */ new Map();
|
|
2366
|
-
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
2367
|
-
function walkPointer($ref) {
|
|
2368
|
-
const trimmed = $ref.trim();
|
|
2369
|
-
if (trimmed === "" || !trimmed.startsWith("#")) return {
|
|
2370
|
-
applicable: false,
|
|
2371
|
-
value: null
|
|
2372
|
-
};
|
|
2373
|
-
const pointer = trimmed.substring(1);
|
|
2374
|
-
if (pointerCache.has(pointer)) return {
|
|
2375
|
-
applicable: true,
|
|
2376
|
-
value: pointerCache.get(pointer)
|
|
2377
|
-
};
|
|
2378
|
-
const current = pointer.split("/").filter(Boolean).map((token) => globalThis.decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~")).reduce((obj, key) => obj?.[key], document);
|
|
2379
|
-
if (current) pointerCache.set(pointer, current);
|
|
2380
|
-
return {
|
|
2381
|
-
applicable: true,
|
|
2382
|
-
value: current ?? null
|
|
2383
|
-
};
|
|
2384
|
-
}
|
|
2385
|
-
/**
|
|
2386
|
-
* Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
|
|
2387
|
-
* `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
|
|
2388
|
-
* outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
|
|
2389
|
-
* for a speculative lookup where a missing ref is not an error.
|
|
2390
|
-
*/
|
|
2391
|
-
function resolve(refPath, options) {
|
|
2392
|
-
const { applicable, value } = walkPointer(refPath);
|
|
2393
|
-
if (!applicable || value || options?.report === false) return value;
|
|
2394
|
-
const diagnostic = {
|
|
2395
|
-
code: Diagnostics.code.refNotFound,
|
|
2396
|
-
severity: "error",
|
|
2397
|
-
message: `Could not find a definition for ${refPath}.`,
|
|
2398
|
-
help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
|
|
2399
|
-
location: {
|
|
2400
|
-
kind: "schema",
|
|
2401
|
-
pointer: refPath,
|
|
2402
|
-
ref: refPath
|
|
2403
|
-
}
|
|
2404
|
-
};
|
|
2405
|
-
if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
|
|
2406
|
-
return null;
|
|
2407
|
-
}
|
|
2408
|
-
/**
|
|
2409
|
-
* Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
|
|
2410
|
-
* instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
|
|
2411
|
-
* resolved (e.g. a minimal document in a unit test).
|
|
2412
|
-
*/
|
|
2413
|
-
function resolveNode(refPath, parse) {
|
|
2414
|
-
if (resolvingRefs.has(refPath)) return null;
|
|
2415
|
-
if (!resolvedNodeCache.has(refPath)) {
|
|
2416
|
-
let resolved = null;
|
|
2417
|
-
try {
|
|
2418
|
-
const referenced = resolve(refPath);
|
|
2419
|
-
if (referenced) {
|
|
2420
|
-
resolvingRefs.add(refPath);
|
|
2421
|
-
resolved = parse({ schema: referenced });
|
|
2422
|
-
resolvingRefs.delete(refPath);
|
|
2423
|
-
}
|
|
2424
|
-
} catch {}
|
|
2425
|
-
resolvedNodeCache.set(refPath, resolved);
|
|
2426
|
-
}
|
|
2427
|
-
return resolvedNodeCache.get(refPath) ?? null;
|
|
2428
|
-
}
|
|
2429
|
-
/**
|
|
2430
|
-
* Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
|
|
2431
|
-
* resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
|
|
2432
|
-
* `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
|
|
2433
|
-
* returned as-is.
|
|
2434
|
-
*/
|
|
2435
|
-
function deref(value) {
|
|
2436
|
-
if (!isReference(value)) return value ? value : null;
|
|
2437
|
-
const resolved = resolve(value.$ref);
|
|
2438
|
-
return resolved && !isReference(resolved) ? resolved : null;
|
|
2439
|
-
}
|
|
2440
|
-
/** Resolves a `$ref` object but keeps the `$ref` field on the result; other values pass through. */
|
|
2441
|
-
function derefKeepingRef(value) {
|
|
2442
|
-
if (isReference(value)) return {
|
|
2443
|
-
...value,
|
|
2444
|
-
...resolve(value.$ref),
|
|
2445
|
-
$ref: value.$ref
|
|
2446
|
-
};
|
|
2447
|
-
return value;
|
|
2448
|
-
}
|
|
2449
|
-
return {
|
|
2450
|
-
resolve,
|
|
2451
|
-
resolveNode,
|
|
2452
|
-
deref,
|
|
2453
|
-
derefKeepingRef
|
|
2454
|
-
};
|
|
2455
|
-
}
|
|
2456
|
-
//#endregion
|
|
2457
2660
|
//#region src/sourceLocator.ts
|
|
2458
2661
|
let cached;
|
|
2459
2662
|
const toRange = ([start, end]) => ({
|
|
@@ -2687,7 +2890,7 @@ const adapterOas = defineExtension({
|
|
|
2687
2890
|
};
|
|
2688
2891
|
parsedByName.set(name, node);
|
|
2689
2892
|
scanSchema(node);
|
|
2690
|
-
if (node.type === "ref" && node.name
|
|
2893
|
+
if (node.type === "ref" && node.name) refAliasMap.set(name, node);
|
|
2691
2894
|
if (ast.narrowSchema(node, "enum") && node.name) enumNames.push(node.name);
|
|
2692
2895
|
if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
|
|
2693
2896
|
}
|
|
@@ -2702,6 +2905,32 @@ const adapterOas = defineExtension({
|
|
|
2702
2905
|
}));
|
|
2703
2906
|
const promotedEnums = enums === "root" ? collectInlineEnums([...parsedByName.values(), ...operationNodes], new Set(Object.keys(schemas))) : null;
|
|
2704
2907
|
if (promotedEnums) for (const name of promotedEnums.keys()) enumNames.push(name);
|
|
2908
|
+
function isAliasCycle(name) {
|
|
2909
|
+
const seen = /* @__PURE__ */ new Set([name]);
|
|
2910
|
+
for (let next = refAliasMap.get(name)?.name; next; next = refAliasMap.get(next)?.name) {
|
|
2911
|
+
if (next === name) return true;
|
|
2912
|
+
if (seen.has(next)) return false;
|
|
2913
|
+
seen.add(next);
|
|
2914
|
+
}
|
|
2915
|
+
return false;
|
|
2916
|
+
}
|
|
2917
|
+
function createCycleNode(name, pointer) {
|
|
2918
|
+
Diagnostics.report({
|
|
2919
|
+
code: Diagnostics.code.aliasCycle,
|
|
2920
|
+
severity: "warning",
|
|
2921
|
+
message: `${name} only refers to other schemas that lead back to it, so it is resolved to unknown.`,
|
|
2922
|
+
help: "Give one of the schemas in the loop a real definition, or remove the `$ref` that closes it.",
|
|
2923
|
+
location: {
|
|
2924
|
+
kind: "schema",
|
|
2925
|
+
pointer: pointer ?? `#/components/schemas/${name}`
|
|
2926
|
+
}
|
|
2927
|
+
});
|
|
2928
|
+
return ast.factory.createSchema({
|
|
2929
|
+
type: "unknown",
|
|
2930
|
+
name,
|
|
2931
|
+
pointer
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2705
2934
|
const schemaNodes = promotedEnums ? [...promotedEnums.values()] : [];
|
|
2706
2935
|
for (const name of Object.keys(schemas)) {
|
|
2707
2936
|
const aliasTarget = refAliasMap.get(name)?.name;
|
|
@@ -2709,11 +2938,12 @@ const adapterOas = defineExtension({
|
|
|
2709
2938
|
const parsed = parsedByName.get(name);
|
|
2710
2939
|
const child = discriminatorChildMap?.get(name);
|
|
2711
2940
|
const base = child ? patchDiscriminatorNode(parsed, child) : parsed;
|
|
2712
|
-
const
|
|
2941
|
+
const resolved = aliased ? {
|
|
2713
2942
|
...aliased,
|
|
2714
2943
|
name,
|
|
2715
2944
|
pointer: parsed.pointer
|
|
2716
2945
|
} : base;
|
|
2946
|
+
const node = isAliasCycle(name) ? createCycleNode(name, parsed.pointer) : resolved;
|
|
2717
2947
|
schemaNodes.push(promotedEnums ? refPromotedEnums(node, promotedEnums) : node);
|
|
2718
2948
|
}
|
|
2719
2949
|
const operations = promotedEnums ? operationNodes.map((node) => refPromotedEnums(node, promotedEnums)) : operationNodes;
|