@kubb/adapter-oas 5.14.0 → 5.14.2
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 +468 -225
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +469 -226
- 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.2";
|
|
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,28 @@ 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
|
+
const PARAMETER_STYLES = /* @__PURE__ */ new Set([
|
|
1295
|
+
"matrix",
|
|
1296
|
+
"label",
|
|
1297
|
+
"form",
|
|
1298
|
+
"simple",
|
|
1299
|
+
"spaceDelimited",
|
|
1300
|
+
"pipeDelimited",
|
|
1301
|
+
"deepObject"
|
|
1302
|
+
]);
|
|
1303
|
+
/** The `encoding` of a media type, reduced to the entries that set a field Kubb carries. A `style` outside the OpenAPI set is dropped. */
|
|
1304
|
+
function getEncoding(encoding) {
|
|
1305
|
+
const entries = Object.entries(encoding ?? {}).flatMap(([property, { contentType, style, explode, allowReserved }]) => {
|
|
1306
|
+
const node = {
|
|
1307
|
+
...contentType !== void 0 ? { contentType } : {},
|
|
1308
|
+
...style !== void 0 && PARAMETER_STYLES.has(style) ? { style } : {},
|
|
1309
|
+
...explode !== void 0 ? { explode } : {},
|
|
1310
|
+
...allowReserved !== void 0 ? { allowReserved } : {}
|
|
1311
|
+
};
|
|
1312
|
+
return Object.keys(node).length ? [[property, node]] : [];
|
|
1313
|
+
});
|
|
1314
|
+
return entries.length ? Object.fromEntries(entries) : void 0;
|
|
1315
|
+
}
|
|
1159
1316
|
/** Schema for one media type of a `content` map, with the binary fallback for an emptied non-JSON entry. */
|
|
1160
1317
|
function getBodySchema({ content, contentType, refs }) {
|
|
1161
1318
|
const entry = contentType ? content?.[contentType] : void 0;
|
|
@@ -1210,7 +1367,7 @@ function parseParameter({ param, pointer, parentName, options, parseSchema }) {
|
|
|
1210
1367
|
name: schemaName
|
|
1211
1368
|
}) : ast.factory.createSchema({ type: options.unknownType });
|
|
1212
1369
|
if (rawSchema) scanSchema(schema, contentType ? `${pointer}/content/${escapePointerToken(contentType)}/schema` : `${pointer}/schema`);
|
|
1213
|
-
const style = param.style;
|
|
1370
|
+
const style = param.style !== void 0 && PARAMETER_STYLES.has(param.style) ? param.style : void 0;
|
|
1214
1371
|
const explode = param.explode;
|
|
1215
1372
|
const allowReserved = param.allowReserved;
|
|
1216
1373
|
return ast.factory.createParameter({
|
|
@@ -1263,10 +1420,12 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1263
1420
|
name: requestBodyName
|
|
1264
1421
|
});
|
|
1265
1422
|
scanSchema(node, `${pointer}/requestBody/content/${escapePointerToken(ct)}/schema`);
|
|
1423
|
+
const encoding = getEncoding(body?.content?.[ct]?.encoding);
|
|
1266
1424
|
return [ast.factory.createContent({
|
|
1267
1425
|
contentType: ct,
|
|
1268
1426
|
schema: ast.optionality(node, bodyRequired),
|
|
1269
|
-
keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
|
|
1427
|
+
keysToOmit: collectPropertyKeysByFlag(schema, "readOnly"),
|
|
1428
|
+
...encoding && { encoding }
|
|
1270
1429
|
})];
|
|
1271
1430
|
});
|
|
1272
1431
|
const requestBody = content.length > 0 || body?.description ? {
|
|
@@ -1363,6 +1522,42 @@ function createNode({ schema, name, nullable, defaultValue }, extras) {
|
|
|
1363
1522
|
//#endregion
|
|
1364
1523
|
//#region src/emit/discriminator/preserve.ts
|
|
1365
1524
|
/**
|
|
1525
|
+
* Turns string mapping keys into the discriminator property's type when they parse cleanly.
|
|
1526
|
+
*/
|
|
1527
|
+
function typedKeys({ keys, propertyType }) {
|
|
1528
|
+
return keys.map((key) => {
|
|
1529
|
+
if (propertyType === "integer" || propertyType === "number") {
|
|
1530
|
+
const value = Number(key);
|
|
1531
|
+
return key.trim() !== "" && Number.isFinite(value) ? value : key;
|
|
1532
|
+
}
|
|
1533
|
+
if (propertyType === "boolean") {
|
|
1534
|
+
const lower = key.toLowerCase();
|
|
1535
|
+
return lower === "true" ? true : lower === "false" ? false : key;
|
|
1536
|
+
}
|
|
1537
|
+
return key;
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Reads the declared `type` of `propertyName` on a schema, following `$ref`s and `allOf` members.
|
|
1542
|
+
* A nullable array such as `['integer', 'null']` reads as its one non-null type.
|
|
1543
|
+
*/
|
|
1544
|
+
function propertyTypeOf({ schema, propertyName, refs, seen = /* @__PURE__ */ new Set() }) {
|
|
1545
|
+
const deref = (value) => value && isReference(value) ? refs.resolve(value.$ref, { report: false }) : value;
|
|
1546
|
+
if (schema && isReference(schema)) {
|
|
1547
|
+
if (seen.has(schema.$ref)) return void 0;
|
|
1548
|
+
seen.add(schema.$ref);
|
|
1549
|
+
}
|
|
1550
|
+
const resolved = deref(schema);
|
|
1551
|
+
const type = deref(resolved?.properties?.[propertyName])?.type ?? resolved?.allOf?.map((member) => propertyTypeOf({
|
|
1552
|
+
schema: member,
|
|
1553
|
+
propertyName,
|
|
1554
|
+
refs,
|
|
1555
|
+
seen
|
|
1556
|
+
})).find(Boolean);
|
|
1557
|
+
const nonNull = Array.isArray(type) ? type.filter((t) => t !== "null") : [];
|
|
1558
|
+
return nonNull.length === 1 ? nonNull[0] : type;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1366
1561
|
* Creates a single-property object schema used as a discriminator literal.
|
|
1367
1562
|
*
|
|
1368
1563
|
* @example
|
|
@@ -1379,7 +1574,7 @@ function createDiscriminantNode({ propertyName, values }) {
|
|
|
1379
1574
|
name: propertyName,
|
|
1380
1575
|
schema: ast.factory.createSchema({
|
|
1381
1576
|
type: "enum",
|
|
1382
|
-
primitive:
|
|
1577
|
+
primitive: enumPrimitive(values),
|
|
1383
1578
|
enumValues: values
|
|
1384
1579
|
}),
|
|
1385
1580
|
required: true
|
|
@@ -1394,21 +1589,61 @@ function createDiscriminantNode({ propertyName, values }) {
|
|
|
1394
1589
|
*
|
|
1395
1590
|
* @example
|
|
1396
1591
|
* ```ts
|
|
1397
|
-
* findDiscriminators({ dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // ['dog', 'hound']
|
|
1592
|
+
* findDiscriminators({ mapping: { dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, ref: '#/components/schemas/Dog' }) // ['dog', 'hound']
|
|
1398
1593
|
* ```
|
|
1399
1594
|
*/
|
|
1400
|
-
function findDiscriminators(mapping, ref) {
|
|
1595
|
+
function findDiscriminators({ mapping, ref }) {
|
|
1401
1596
|
if (!mapping || !ref) return [];
|
|
1402
1597
|
return Object.entries(mapping).filter(([, value]) => value === ref).map(([key]) => key);
|
|
1403
1598
|
}
|
|
1404
1599
|
/**
|
|
1600
|
+
* Finds the discriminator a schema declares itself or through an inline `allOf` member.
|
|
1601
|
+
*/
|
|
1602
|
+
function getDiscriminator(schema) {
|
|
1603
|
+
return (isDiscriminator(schema) ? schema : schema.allOf?.find(isDiscriminator))?.discriminator;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Lists the mapping keys of a discriminator plus the keys of the nested discriminators its mapped
|
|
1607
|
+
* schemas declare on the same property, so a root `$type` accepts values from every level.
|
|
1608
|
+
*/
|
|
1609
|
+
function getMappingKeys({ discriminator: { propertyName, mapping }, refs, seen = /* @__PURE__ */ new Set() }) {
|
|
1610
|
+
return Object.entries(mapping ?? {}).flatMap(([key, target]) => {
|
|
1611
|
+
if (seen.has(target)) return [key];
|
|
1612
|
+
seen.add(target);
|
|
1613
|
+
const resolved = refs.resolve(target, { report: false });
|
|
1614
|
+
const nested = resolved && getDiscriminator(resolved);
|
|
1615
|
+
return nested?.propertyName === propertyName ? [key, ...getMappingKeys({
|
|
1616
|
+
discriminator: nested,
|
|
1617
|
+
refs,
|
|
1618
|
+
seen
|
|
1619
|
+
})] : [key];
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Drops the discriminator from an inline `allOf` member when a `$ref` sibling already declares one on
|
|
1624
|
+
* the same property. That mapping names the schema's own children, and keeping it pins the property
|
|
1625
|
+
* to those keys next to the parent's, which leaves `never`.
|
|
1626
|
+
*/
|
|
1627
|
+
function removeInheritedDiscriminator({ members, refs }) {
|
|
1628
|
+
const inherited = new Set(members.flatMap((member) => {
|
|
1629
|
+
const parent = isReference(member) ? refs.resolve(member.$ref, { report: false }) : null;
|
|
1630
|
+
return parent && getDiscriminator(parent)?.propertyName || [];
|
|
1631
|
+
}));
|
|
1632
|
+
return members.map((member) => {
|
|
1633
|
+
if (isReference(member) || !isDiscriminator(member) || member.oneOf || member.anyOf) return member;
|
|
1634
|
+
if (!inherited.has(member.discriminator.propertyName)) return member;
|
|
1635
|
+
const { discriminator: _discriminator, ...rest } = member;
|
|
1636
|
+
return rest;
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1405
1640
|
* Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
|
|
1406
1641
|
* node with either the shared-properties slice carrying that value, or a synthetic discriminant
|
|
1407
1642
|
* literal. The referenced child schema's own definition is left untouched — narrowing happens only
|
|
1408
1643
|
* at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
|
|
1409
1644
|
* which additionally patches the child schema's own definition in a post-pass).
|
|
1410
1645
|
*/
|
|
1411
|
-
function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, name, refs }) {
|
|
1646
|
+
function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parentType, parse, name, refs }) {
|
|
1412
1647
|
function pickDiscriminatorPropertyNode(node, propertyName) {
|
|
1413
1648
|
const discriminatorProperty = ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
|
|
1414
1649
|
if (!discriminatorProperty) return null;
|
|
@@ -1444,9 +1679,20 @@ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode,
|
|
|
1444
1679
|
}
|
|
1445
1680
|
return unionMembers.map((s) => {
|
|
1446
1681
|
const ref = isReference(s) ? s.$ref : void 0;
|
|
1447
|
-
const mappedValues = findDiscriminators(
|
|
1682
|
+
const mappedValues = findDiscriminators({
|
|
1683
|
+
mapping: discriminator?.mapping,
|
|
1684
|
+
ref
|
|
1685
|
+
});
|
|
1448
1686
|
const implicitValue = mappedValues.length ? null : implicitDiscriminantValue(s);
|
|
1449
|
-
const
|
|
1687
|
+
const propertyType = propertyTypeOf({
|
|
1688
|
+
schema: s,
|
|
1689
|
+
propertyName: discriminator?.propertyName ?? "",
|
|
1690
|
+
refs
|
|
1691
|
+
}) ?? parentType;
|
|
1692
|
+
const discriminatorValues = typedKeys({
|
|
1693
|
+
keys: mappedValues.length ? mappedValues : implicitValue ? [implicitValue] : [],
|
|
1694
|
+
propertyType
|
|
1695
|
+
});
|
|
1450
1696
|
const memberNode = parse({
|
|
1451
1697
|
schema: s,
|
|
1452
1698
|
name
|
|
@@ -1478,21 +1724,34 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
|
|
|
1478
1724
|
members: allOfMembers.filter((item) => {
|
|
1479
1725
|
if (!isReference(item) || !name) return true;
|
|
1480
1726
|
const deref = refs.resolve(item.$ref);
|
|
1481
|
-
|
|
1482
|
-
const parentUnion = deref
|
|
1483
|
-
if (!parentUnion) return true;
|
|
1727
|
+
const discriminator = deref && getDiscriminator(deref);
|
|
1728
|
+
const parentUnion = deref?.oneOf ?? deref?.anyOf;
|
|
1729
|
+
if (!deref || !discriminator || isDiscriminator(deref) && !parentUnion) return true;
|
|
1484
1730
|
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1731
|
+
const values = findDiscriminators({
|
|
1732
|
+
mapping: discriminator.mapping,
|
|
1733
|
+
ref: childRef
|
|
1734
|
+
});
|
|
1735
|
+
if (values.length) {
|
|
1736
|
+
const { propertyName } = discriminator;
|
|
1737
|
+
const propertyType = propertyTypeOf({
|
|
1738
|
+
schema: deref,
|
|
1739
|
+
propertyName,
|
|
1740
|
+
refs
|
|
1741
|
+
}) ?? propertyTypeOf({
|
|
1742
|
+
schema: { $ref: childRef },
|
|
1743
|
+
propertyName,
|
|
1744
|
+
refs
|
|
1745
|
+
});
|
|
1746
|
+
discriminantValues.push({
|
|
1747
|
+
propertyName,
|
|
1748
|
+
values: typedKeys({
|
|
1749
|
+
keys: values,
|
|
1750
|
+
propertyType
|
|
1751
|
+
})
|
|
1492
1752
|
});
|
|
1493
|
-
return false;
|
|
1494
1753
|
}
|
|
1495
|
-
return
|
|
1754
|
+
return !(parentUnion && (values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
|
|
1496
1755
|
}),
|
|
1497
1756
|
discriminantValues
|
|
1498
1757
|
};
|
|
@@ -1619,10 +1878,9 @@ function resolveUnionMembers(members, siblingProperties) {
|
|
|
1619
1878
|
});
|
|
1620
1879
|
}
|
|
1621
1880
|
/**
|
|
1622
|
-
* Enriches any `oneOf` or `anyOf` branches on
|
|
1881
|
+
* Enriches a schema object, and any `oneOf` or `anyOf` branches on it, with properties from sibling schemas.
|
|
1623
1882
|
*/
|
|
1624
1883
|
function resolveUnionSchema(schema, siblingProperties) {
|
|
1625
|
-
if (!schema.oneOf && !schema.anyOf) return schema;
|
|
1626
1884
|
const next = { ...resolveUnionMembers([schema], siblingProperties)[0] ?? schema };
|
|
1627
1885
|
if (schema.oneOf) next.oneOf = resolveUnionMembers(schema.oneOf, siblingProperties);
|
|
1628
1886
|
if (schema.anyOf) next.anyOf = resolveUnionMembers(schema.anyOf, siblingProperties);
|
|
@@ -1646,6 +1904,17 @@ function convertRef({ schema, name, nullable, defaultValue, document, parse, ref
|
|
|
1646
1904
|
defaultValue
|
|
1647
1905
|
};
|
|
1648
1906
|
if (refPath && document.components && !refs.resolve(refPath, { report: false })) return createNode(ctx, { type: "unknown" });
|
|
1907
|
+
if (refPath && !refPath.startsWith("#/components/schemas/") && resolvedSchema) return ast.factory.createSchema({
|
|
1908
|
+
...resolvedSchema,
|
|
1909
|
+
title: schema.title ?? resolvedSchema.title,
|
|
1910
|
+
description: schema.description ?? resolvedSchema.description,
|
|
1911
|
+
deprecated: schema.deprecated ?? resolvedSchema.deprecated,
|
|
1912
|
+
readOnly: schema.readOnly ?? resolvedSchema.readOnly,
|
|
1913
|
+
writeOnly: schema.writeOnly ?? resolvedSchema.writeOnly,
|
|
1914
|
+
default: defaultValue ?? resolvedSchema.default,
|
|
1915
|
+
name: name ?? resolvedSchema.name,
|
|
1916
|
+
nullable: nullable ?? resolvedSchema.nullable
|
|
1917
|
+
});
|
|
1649
1918
|
const targetName = renames?.get(schema.$ref);
|
|
1650
1919
|
return createNode(ctx, {
|
|
1651
1920
|
type: "ref",
|
|
@@ -1677,9 +1946,41 @@ function convertNot({ schema, name, nullable, defaultValue, parse }) {
|
|
|
1677
1946
|
});
|
|
1678
1947
|
}
|
|
1679
1948
|
/**
|
|
1949
|
+
* Keywords that describe a schema or conditionally constrain it, but add no structure.
|
|
1950
|
+
*/
|
|
1951
|
+
const annotationKeys = /* @__PURE__ */ new Set([
|
|
1952
|
+
"description",
|
|
1953
|
+
"title",
|
|
1954
|
+
"example",
|
|
1955
|
+
"examples",
|
|
1956
|
+
"default",
|
|
1957
|
+
"deprecated",
|
|
1958
|
+
"readOnly",
|
|
1959
|
+
"writeOnly",
|
|
1960
|
+
"if",
|
|
1961
|
+
"then",
|
|
1962
|
+
"else"
|
|
1963
|
+
]);
|
|
1964
|
+
/**
|
|
1965
|
+
* Moves `allOf` members that hold only annotations or `if`/`then`/`else` onto the parent, so they do
|
|
1966
|
+
* not become `unknown`. The parent's own values win, and the conditional keywords are not enforced.
|
|
1967
|
+
*/
|
|
1968
|
+
function mergeAnnotations(schema) {
|
|
1969
|
+
const members = schema.allOf;
|
|
1970
|
+
const isAnnotation = (member) => !isReference(member) && Object.keys(member).every((key) => annotationKeys.has(key));
|
|
1971
|
+
const annotations = members.filter(isAnnotation);
|
|
1972
|
+
const rest = members.filter((member) => !isAnnotation(member));
|
|
1973
|
+
return annotations.length && rest.length ? {
|
|
1974
|
+
...Object.assign({}, ...annotations),
|
|
1975
|
+
...schema,
|
|
1976
|
+
allOf: rest
|
|
1977
|
+
} : schema;
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1680
1980
|
* Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
|
|
1681
1981
|
*/
|
|
1682
|
-
function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
|
|
1982
|
+
function convertAllOf({ schema: original, name, nullable, defaultValue, parse, refs }) {
|
|
1983
|
+
const schema = mergeAnnotations(original);
|
|
1683
1984
|
if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
|
|
1684
1985
|
const [memberSchema] = schema.allOf;
|
|
1685
1986
|
const memberNode = parse({
|
|
@@ -1705,7 +2006,10 @@ function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
|
|
|
1705
2006
|
});
|
|
1706
2007
|
}
|
|
1707
2008
|
const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
|
|
1708
|
-
allOfMembers:
|
|
2009
|
+
allOfMembers: removeInheritedDiscriminator({
|
|
2010
|
+
members: schema.allOf,
|
|
2011
|
+
refs
|
|
2012
|
+
}),
|
|
1709
2013
|
name,
|
|
1710
2014
|
refs
|
|
1711
2015
|
});
|
|
@@ -1785,6 +2089,11 @@ function convertUnion(context) {
|
|
|
1785
2089
|
unionMembers,
|
|
1786
2090
|
discriminator,
|
|
1787
2091
|
sharedPropertiesNode,
|
|
2092
|
+
parentType: discriminator && propertyTypeOf({
|
|
2093
|
+
schema,
|
|
2094
|
+
propertyName: discriminator.propertyName,
|
|
2095
|
+
refs
|
|
2096
|
+
}),
|
|
1788
2097
|
parse,
|
|
1789
2098
|
name,
|
|
1790
2099
|
refs
|
|
@@ -2048,7 +2357,7 @@ function nameEnums(node, options) {
|
|
|
2048
2357
|
/**
|
|
2049
2358
|
* Converts an object-like schema into an `ObjectSchemaNode`.
|
|
2050
2359
|
*/
|
|
2051
|
-
function convertObject({ schema, name, nullable, defaultValue, options, parse }) {
|
|
2360
|
+
function convertObject({ schema, name, nullable, defaultValue, options, parse, refs }) {
|
|
2052
2361
|
const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
2053
2362
|
const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
|
|
2054
2363
|
const resolvedPropSchema = propSchema;
|
|
@@ -2065,7 +2374,7 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
|
|
|
2065
2374
|
name: propName,
|
|
2066
2375
|
schema: {
|
|
2067
2376
|
...schemaNode,
|
|
2068
|
-
nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
|
|
2377
|
+
nullable: schemaNode.type === "null" ? void 0 : propNullable || schemaNode.nullable || void 0
|
|
2069
2378
|
},
|
|
2070
2379
|
required
|
|
2071
2380
|
});
|
|
@@ -2105,7 +2414,17 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
|
|
|
2105
2414
|
});
|
|
2106
2415
|
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
2107
2416
|
const discPropName = schema.discriminator.propertyName;
|
|
2108
|
-
const values =
|
|
2417
|
+
const values = typedKeys({
|
|
2418
|
+
keys: getMappingKeys({
|
|
2419
|
+
discriminator: schema.discriminator,
|
|
2420
|
+
refs
|
|
2421
|
+
}),
|
|
2422
|
+
propertyType: propertyTypeOf({
|
|
2423
|
+
schema,
|
|
2424
|
+
propertyName: discPropName,
|
|
2425
|
+
refs
|
|
2426
|
+
})
|
|
2427
|
+
});
|
|
2109
2428
|
const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : void 0;
|
|
2110
2429
|
return ast.applyMacros(objectNode, [macroDiscriminatorEnum({
|
|
2111
2430
|
propertyName: discPropName,
|
|
@@ -2163,8 +2482,9 @@ function convertArray({ schema, name, nullable, defaultValue, options, parse })
|
|
|
2163
2482
|
//#region src/emit/parseSchema.ts
|
|
2164
2483
|
/**
|
|
2165
2484
|
* Ordered schema rule table. Order is significant: composition keywords (`not`, `$ref`, `allOf`,
|
|
2166
|
-
* `oneOf`/`anyOf`) take precedence over `const
|
|
2167
|
-
*
|
|
2485
|
+
* `oneOf`/`anyOf`) take precedence over `const` (except a `$ref` with a sibling `const`, where the
|
|
2486
|
+
* `const` wins), which takes precedence over a genuine OAS 3.1 multi-type array (more than one
|
|
2487
|
+
* non-`null` type), which takes precedence over `format`/`type`.
|
|
2168
2488
|
* A multi-type array must split into its per-type members before `format` runs, otherwise
|
|
2169
2489
|
* `format` collapses the whole schema to one type (e.g. `type: ['null', 'integer', 'string'],
|
|
2170
2490
|
* format: 'int32'` would drop `string` and emit just `integer`); each split-off member still
|
|
@@ -2178,7 +2498,7 @@ const schemaRules = [
|
|
|
2178
2498
|
convert: convertNot
|
|
2179
2499
|
},
|
|
2180
2500
|
{
|
|
2181
|
-
match: ({ schema }) => isReference(schema),
|
|
2501
|
+
match: ({ schema }) => isReference(schema) && !("const" in schema && schema.const !== void 0),
|
|
2182
2502
|
convert: convertRef
|
|
2183
2503
|
},
|
|
2184
2504
|
{
|
|
@@ -2346,114 +2666,6 @@ function refPromotedEnums(node, promoted) {
|
|
|
2346
2666
|
} });
|
|
2347
2667
|
}
|
|
2348
2668
|
//#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
2669
|
//#region src/sourceLocator.ts
|
|
2458
2670
|
let cached;
|
|
2459
2671
|
const toRange = ([start, end]) => ({
|
|
@@ -2687,7 +2899,7 @@ const adapterOas = defineExtension({
|
|
|
2687
2899
|
};
|
|
2688
2900
|
parsedByName.set(name, node);
|
|
2689
2901
|
scanSchema(node);
|
|
2690
|
-
if (node.type === "ref" && node.name
|
|
2902
|
+
if (node.type === "ref" && node.name) refAliasMap.set(name, node);
|
|
2691
2903
|
if (ast.narrowSchema(node, "enum") && node.name) enumNames.push(node.name);
|
|
2692
2904
|
if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
|
|
2693
2905
|
}
|
|
@@ -2702,6 +2914,36 @@ const adapterOas = defineExtension({
|
|
|
2702
2914
|
}));
|
|
2703
2915
|
const promotedEnums = enums === "root" ? collectInlineEnums([...parsedByName.values(), ...operationNodes], new Set(Object.keys(schemas))) : null;
|
|
2704
2916
|
if (promotedEnums) for (const name of promotedEnums.keys()) enumNames.push(name);
|
|
2917
|
+
const aliasTargetOf = (name) => {
|
|
2918
|
+
const node = refAliasMap.get(name);
|
|
2919
|
+
return node?.ref ? ast.refName(node.ref) : node?.name;
|
|
2920
|
+
};
|
|
2921
|
+
function isAliasCycle(name) {
|
|
2922
|
+
const seen = /* @__PURE__ */ new Set([name]);
|
|
2923
|
+
for (let next = aliasTargetOf(name); next; next = aliasTargetOf(next)) {
|
|
2924
|
+
if (next === name) return true;
|
|
2925
|
+
if (seen.has(next)) return false;
|
|
2926
|
+
seen.add(next);
|
|
2927
|
+
}
|
|
2928
|
+
return false;
|
|
2929
|
+
}
|
|
2930
|
+
function createCycleNode(name, pointer) {
|
|
2931
|
+
Diagnostics.report({
|
|
2932
|
+
code: Diagnostics.code.aliasCycle,
|
|
2933
|
+
severity: "warning",
|
|
2934
|
+
message: `${name} only refers to other schemas that lead back to it, so it is resolved to unknown.`,
|
|
2935
|
+
help: "Give one of the schemas in the loop a real definition, or remove the `$ref` that closes it.",
|
|
2936
|
+
location: {
|
|
2937
|
+
kind: "schema",
|
|
2938
|
+
pointer: pointer ?? `#/components/schemas/${name}`
|
|
2939
|
+
}
|
|
2940
|
+
});
|
|
2941
|
+
return ast.factory.createSchema({
|
|
2942
|
+
type: "unknown",
|
|
2943
|
+
name,
|
|
2944
|
+
pointer
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2705
2947
|
const schemaNodes = promotedEnums ? [...promotedEnums.values()] : [];
|
|
2706
2948
|
for (const name of Object.keys(schemas)) {
|
|
2707
2949
|
const aliasTarget = refAliasMap.get(name)?.name;
|
|
@@ -2709,11 +2951,12 @@ const adapterOas = defineExtension({
|
|
|
2709
2951
|
const parsed = parsedByName.get(name);
|
|
2710
2952
|
const child = discriminatorChildMap?.get(name);
|
|
2711
2953
|
const base = child ? patchDiscriminatorNode(parsed, child) : parsed;
|
|
2712
|
-
const
|
|
2954
|
+
const resolved = aliased ? {
|
|
2713
2955
|
...aliased,
|
|
2714
2956
|
name,
|
|
2715
2957
|
pointer: parsed.pointer
|
|
2716
2958
|
} : base;
|
|
2959
|
+
const node = isAliasCycle(name) ? createCycleNode(name, parsed.pointer) : resolved;
|
|
2717
2960
|
schemaNodes.push(promotedEnums ? refPromotedEnums(node, promotedEnums) : node);
|
|
2718
2961
|
}
|
|
2719
2962
|
const operations = promotedEnums ? operationNodes.map((node) => refPromotedEnums(node, promotedEnums)) : operationNodes;
|