@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.cjs
CHANGED
|
@@ -36,7 +36,7 @@ var compatibility = {
|
|
|
36
36
|
};
|
|
37
37
|
//#endregion
|
|
38
38
|
//#region package.json
|
|
39
|
-
var version = "5.14.
|
|
39
|
+
var version = "5.14.2";
|
|
40
40
|
//#endregion
|
|
41
41
|
//#region src/constants.ts
|
|
42
42
|
/**
|
|
@@ -387,6 +387,198 @@ async function read(path) {
|
|
|
387
387
|
return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
|
|
388
388
|
}
|
|
389
389
|
//#endregion
|
|
390
|
+
//#region src/oas.ts
|
|
391
|
+
/**
|
|
392
|
+
* Returns `true` when a schema should be treated as nullable.
|
|
393
|
+
*
|
|
394
|
+
* Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
|
|
395
|
+
* `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
|
|
396
|
+
*/
|
|
397
|
+
function isNullable(schema) {
|
|
398
|
+
if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
|
|
399
|
+
const schemaType = schema?.type;
|
|
400
|
+
if (schemaType === "null") return true;
|
|
401
|
+
if (Array.isArray(schemaType)) return schemaType.includes("null");
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
|
|
406
|
+
*/
|
|
407
|
+
function pickExtensions(source) {
|
|
408
|
+
const entries = Object.entries(source).filter(([key]) => key.startsWith("x-"));
|
|
409
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
|
|
413
|
+
*/
|
|
414
|
+
function isReference(obj) {
|
|
415
|
+
return !!obj && typeof obj === "object" && "$ref" in obj;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
|
|
419
|
+
* excluding the Swagger 2 string form.
|
|
420
|
+
*/
|
|
421
|
+
function isDiscriminator(obj) {
|
|
422
|
+
const record = obj;
|
|
423
|
+
return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Returns `true` when a schema is a raw binary payload.
|
|
427
|
+
*/
|
|
428
|
+
function isBinary(schema) {
|
|
429
|
+
return schema.contentMediaType === "application/octet-stream" && schema.contentEncoding === void 0 && (schema.type === void 0 || schema.type === "string");
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Returns the binary schema for a non-JSON entry the OAS 3.1 upgrade emptied out, or `undefined`
|
|
433
|
+
* when the entry carries a schema of its own. The upgrade rewrites `format: 'binary'` into
|
|
434
|
+
* `contentMediaType` and drops it whenever it only repeats the media type key, for any binary
|
|
435
|
+
* media type (`application/octet-stream`, `application/pdf`, `image/png`, ...), not just
|
|
436
|
+
* `application/octet-stream`. Without the fallback such a body resolves to the empty schema type
|
|
437
|
+
* instead of a blob.
|
|
438
|
+
*/
|
|
439
|
+
function getBinaryFallbackSchema(mediaType, schema) {
|
|
440
|
+
if (!mediaType || isJsonMimeType(mediaType)) return void 0;
|
|
441
|
+
if (schema && Object.keys(schema).length > 0) return void 0;
|
|
442
|
+
return {
|
|
443
|
+
type: "string",
|
|
444
|
+
contentMediaType: "application/octet-stream"
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* MIME type fragments that mark a media type as JSON-like.
|
|
449
|
+
*
|
|
450
|
+
* A content type is JSON when it contains any of these substrings. The `+json` entry catches
|
|
451
|
+
* structured-syntax suffixes such as `application/vnd.api+json`.
|
|
452
|
+
*/
|
|
453
|
+
const jsonMimeFragments = [
|
|
454
|
+
"application/json",
|
|
455
|
+
"application/x-json",
|
|
456
|
+
"text/json",
|
|
457
|
+
"text/x-json",
|
|
458
|
+
"+json"
|
|
459
|
+
];
|
|
460
|
+
/**
|
|
461
|
+
* Returns `true` when a media type string is JSON-like.
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* ```ts
|
|
465
|
+
* isJsonMimeType('application/json') // true
|
|
466
|
+
* isJsonMimeType('application/vnd.api+json') // true
|
|
467
|
+
* isJsonMimeType('multipart/form-data') // false
|
|
468
|
+
* ```
|
|
469
|
+
*/
|
|
470
|
+
function isJsonMimeType(mimeType) {
|
|
471
|
+
return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
|
|
472
|
+
}
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/refs.ts
|
|
475
|
+
/**
|
|
476
|
+
* Creates the `$ref` resolution service for one document. One pointer walk backs every method.
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```ts
|
|
480
|
+
* const refs = createRefs(document)
|
|
481
|
+
* refs.resolve<SchemaObject>('#/components/schemas/Pet')
|
|
482
|
+
* refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
|
|
483
|
+
* refs.resolveNode('#/components/schemas/Pet', parseSchema)
|
|
484
|
+
* refs.deref<ResponseObject>(operation.schema.responses?.['200'])
|
|
485
|
+
* refs.derefKeepingRef(parameter)
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
488
|
+
function createRefs(document) {
|
|
489
|
+
const pointerCache = /* @__PURE__ */ new Map();
|
|
490
|
+
const resolvedNodeCache = /* @__PURE__ */ new Map();
|
|
491
|
+
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
492
|
+
function walkPointer($ref) {
|
|
493
|
+
const trimmed = $ref.trim();
|
|
494
|
+
if (trimmed === "" || !trimmed.startsWith("#")) return {
|
|
495
|
+
applicable: false,
|
|
496
|
+
value: null
|
|
497
|
+
};
|
|
498
|
+
const pointer = trimmed.substring(1);
|
|
499
|
+
if (pointerCache.has(pointer)) return {
|
|
500
|
+
applicable: true,
|
|
501
|
+
value: pointerCache.get(pointer)
|
|
502
|
+
};
|
|
503
|
+
const current = pointer.split("/").filter(Boolean).map((token) => globalThis.decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~")).reduce((obj, key) => obj?.[key], document);
|
|
504
|
+
if (current) pointerCache.set(pointer, current);
|
|
505
|
+
return {
|
|
506
|
+
applicable: true,
|
|
507
|
+
value: current ?? null
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
|
|
512
|
+
* `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
|
|
513
|
+
* outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
|
|
514
|
+
* for a speculative lookup where a missing ref is not an error.
|
|
515
|
+
*/
|
|
516
|
+
function resolve(refPath, options) {
|
|
517
|
+
const { applicable, value } = walkPointer(refPath);
|
|
518
|
+
if (!applicable || value || options?.report === false) return value;
|
|
519
|
+
const diagnostic = {
|
|
520
|
+
code: _kubb_kit.Diagnostics.code.refNotFound,
|
|
521
|
+
severity: "error",
|
|
522
|
+
message: `Could not find a definition for ${refPath}.`,
|
|
523
|
+
help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
|
|
524
|
+
location: {
|
|
525
|
+
kind: "schema",
|
|
526
|
+
pointer: refPath,
|
|
527
|
+
ref: refPath
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
if (!_kubb_kit.Diagnostics.report(diagnostic)) throw new _kubb_kit.Diagnostics.Error(diagnostic);
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
|
|
535
|
+
* instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
|
|
536
|
+
* resolved (e.g. a minimal document in a unit test).
|
|
537
|
+
*/
|
|
538
|
+
function resolveNode(refPath, parse) {
|
|
539
|
+
if (resolvingRefs.has(refPath)) return null;
|
|
540
|
+
if (!resolvedNodeCache.has(refPath)) {
|
|
541
|
+
let resolved = null;
|
|
542
|
+
try {
|
|
543
|
+
const referenced = resolve(refPath);
|
|
544
|
+
if (referenced) {
|
|
545
|
+
resolvingRefs.add(refPath);
|
|
546
|
+
resolved = parse({ schema: referenced });
|
|
547
|
+
resolvingRefs.delete(refPath);
|
|
548
|
+
}
|
|
549
|
+
} catch {}
|
|
550
|
+
resolvedNodeCache.set(refPath, resolved);
|
|
551
|
+
}
|
|
552
|
+
return resolvedNodeCache.get(refPath) ?? null;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
|
|
556
|
+
* resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
|
|
557
|
+
* `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
|
|
558
|
+
* returned as-is.
|
|
559
|
+
*/
|
|
560
|
+
function deref(value) {
|
|
561
|
+
if (!isReference(value)) return value ? value : null;
|
|
562
|
+
const resolved = resolve(value.$ref);
|
|
563
|
+
return resolved && !isReference(resolved) ? resolved : null;
|
|
564
|
+
}
|
|
565
|
+
/** Resolves a `$ref` object but keeps the `$ref` field on the result; other values pass through. */
|
|
566
|
+
function derefKeepingRef(value) {
|
|
567
|
+
if (isReference(value)) return {
|
|
568
|
+
...value,
|
|
569
|
+
...resolve(value.$ref),
|
|
570
|
+
$ref: value.$ref
|
|
571
|
+
};
|
|
572
|
+
return value;
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
resolve,
|
|
576
|
+
resolveNode,
|
|
577
|
+
deref,
|
|
578
|
+
derefKeepingRef
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
//#endregion
|
|
390
582
|
//#region src/load/source.ts
|
|
391
583
|
const urlRegExp = /^https?:\/+/i;
|
|
392
584
|
/**
|
|
@@ -507,6 +699,22 @@ function hasExternalRef(node) {
|
|
|
507
699
|
return Object.values(node).some(hasExternalRef);
|
|
508
700
|
}
|
|
509
701
|
/**
|
|
702
|
+
* Replaces each `$ref` into `#/paths/...` with a copy of its target, in place.
|
|
703
|
+
*
|
|
704
|
+
* The Swagger 2.0 upgrade moves `responses/200/schema` under `content`, so a ref to the old location
|
|
705
|
+
* would no longer resolve. A ref that points back into itself is kept, since inlining it never ends.
|
|
706
|
+
*/
|
|
707
|
+
function inlinePathRefs(node, resolve, visiting = []) {
|
|
708
|
+
if (!node || typeof node !== "object") return node;
|
|
709
|
+
const ref = node.$ref;
|
|
710
|
+
if (typeof ref === "string" && ref.startsWith("#/paths/") && !visiting.includes(ref)) {
|
|
711
|
+
const target = resolve(ref);
|
|
712
|
+
return target && typeof target === "object" ? inlinePathRefs(structuredClone(target), resolve, [...visiting, ref]) : node;
|
|
713
|
+
}
|
|
714
|
+
for (const [key, value] of Object.entries(node)) node[key] = inlinePathRefs(value, resolve, visiting);
|
|
715
|
+
return node;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
510
718
|
* Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
|
|
511
719
|
*
|
|
512
720
|
* External file schemas are hoisted into named `components.schemas` entries, so a property
|
|
@@ -564,6 +772,17 @@ async function parseFromConfig(source) {
|
|
|
564
772
|
await assertInputExists(source.path);
|
|
565
773
|
return bundleDocument(source.path);
|
|
566
774
|
})();
|
|
775
|
+
if (raw && typeof raw === "object") {
|
|
776
|
+
const document = raw;
|
|
777
|
+
for (const key of ["swagger", "openapi"]) {
|
|
778
|
+
const version = document[key];
|
|
779
|
+
if (typeof version === "number") document[key] = Number.isInteger(version) ? version.toFixed(1) : String(version);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (raw && typeof raw === "object" && "swagger" in raw) {
|
|
783
|
+
const refs = createRefs(raw);
|
|
784
|
+
inlinePathRefs(raw, (ref) => refs.resolve(ref, { report: false }));
|
|
785
|
+
}
|
|
567
786
|
try {
|
|
568
787
|
return (0, _scalar_openapi_upgrader.upgrade)(raw, "3.1");
|
|
569
788
|
} catch (error) {
|
|
@@ -660,90 +879,6 @@ async function validateDocumentInWorker(document) {
|
|
|
660
879
|
if (!message.valid) reportValidationErrors(message);
|
|
661
880
|
}
|
|
662
881
|
//#endregion
|
|
663
|
-
//#region src/oas.ts
|
|
664
|
-
/**
|
|
665
|
-
* Returns `true` when a schema should be treated as nullable.
|
|
666
|
-
*
|
|
667
|
-
* Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
|
|
668
|
-
* `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
|
|
669
|
-
*/
|
|
670
|
-
function isNullable(schema) {
|
|
671
|
-
if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
|
|
672
|
-
const schemaType = schema?.type;
|
|
673
|
-
if (schemaType === "null") return true;
|
|
674
|
-
if (Array.isArray(schemaType)) return schemaType.includes("null");
|
|
675
|
-
return false;
|
|
676
|
-
}
|
|
677
|
-
/**
|
|
678
|
-
* Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
|
|
679
|
-
*/
|
|
680
|
-
function pickExtensions(source) {
|
|
681
|
-
const entries = Object.entries(source).filter(([key]) => key.startsWith("x-"));
|
|
682
|
-
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
|
|
686
|
-
*/
|
|
687
|
-
function isReference(obj) {
|
|
688
|
-
return !!obj && typeof obj === "object" && "$ref" in obj;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
|
|
692
|
-
* excluding the Swagger 2 string form.
|
|
693
|
-
*/
|
|
694
|
-
function isDiscriminator(obj) {
|
|
695
|
-
const record = obj;
|
|
696
|
-
return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
|
|
697
|
-
}
|
|
698
|
-
/**
|
|
699
|
-
* Returns `true` when a schema is a raw binary payload.
|
|
700
|
-
*/
|
|
701
|
-
function isBinary(schema) {
|
|
702
|
-
return schema.contentMediaType === "application/octet-stream" && schema.contentEncoding === void 0 && (schema.type === void 0 || schema.type === "string");
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* Returns the binary schema for a non-JSON entry the OAS 3.1 upgrade emptied out, or `undefined`
|
|
706
|
-
* when the entry carries a schema of its own. The upgrade rewrites `format: 'binary'` into
|
|
707
|
-
* `contentMediaType` and drops it whenever it only repeats the media type key, for any binary
|
|
708
|
-
* media type (`application/octet-stream`, `application/pdf`, `image/png`, ...), not just
|
|
709
|
-
* `application/octet-stream`. Without the fallback such a body resolves to the empty schema type
|
|
710
|
-
* instead of a blob.
|
|
711
|
-
*/
|
|
712
|
-
function getBinaryFallbackSchema(mediaType, schema) {
|
|
713
|
-
if (!mediaType || isJsonMimeType(mediaType)) return void 0;
|
|
714
|
-
if (schema && Object.keys(schema).length > 0) return void 0;
|
|
715
|
-
return {
|
|
716
|
-
type: "string",
|
|
717
|
-
contentMediaType: "application/octet-stream"
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* MIME type fragments that mark a media type as JSON-like.
|
|
722
|
-
*
|
|
723
|
-
* A content type is JSON when it contains any of these substrings. The `+json` entry catches
|
|
724
|
-
* structured-syntax suffixes such as `application/vnd.api+json`.
|
|
725
|
-
*/
|
|
726
|
-
const jsonMimeFragments = [
|
|
727
|
-
"application/json",
|
|
728
|
-
"application/x-json",
|
|
729
|
-
"text/json",
|
|
730
|
-
"text/x-json",
|
|
731
|
-
"+json"
|
|
732
|
-
];
|
|
733
|
-
/**
|
|
734
|
-
* Returns `true` when a media type string is JSON-like.
|
|
735
|
-
*
|
|
736
|
-
* @example
|
|
737
|
-
* ```ts
|
|
738
|
-
* isJsonMimeType('application/json') // true
|
|
739
|
-
* isJsonMimeType('application/vnd.api+json') // true
|
|
740
|
-
* isJsonMimeType('multipart/form-data') // false
|
|
741
|
-
* ```
|
|
742
|
-
*/
|
|
743
|
-
function isJsonMimeType(mimeType) {
|
|
744
|
-
return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
|
|
745
|
-
}
|
|
746
|
-
//#endregion
|
|
747
882
|
//#region src/model/components.ts
|
|
748
883
|
/**
|
|
749
884
|
* Extracts the inline schema from a media-type `content` map, with the media type it sits under.
|
|
@@ -1172,6 +1307,28 @@ function getParameters({ operation, refs }) {
|
|
|
1172
1307
|
for (const entry of [...pathLevelParams, ...operationParams]) if (entry.param.name && entry.param.in) paramMap.set(`${entry.param.in}:${entry.param.name}`, entry);
|
|
1173
1308
|
return Array.from(paramMap.values());
|
|
1174
1309
|
}
|
|
1310
|
+
const PARAMETER_STYLES = /* @__PURE__ */ new Set([
|
|
1311
|
+
"matrix",
|
|
1312
|
+
"label",
|
|
1313
|
+
"form",
|
|
1314
|
+
"simple",
|
|
1315
|
+
"spaceDelimited",
|
|
1316
|
+
"pipeDelimited",
|
|
1317
|
+
"deepObject"
|
|
1318
|
+
]);
|
|
1319
|
+
/** The `encoding` of a media type, reduced to the entries that set a field Kubb carries. A `style` outside the OpenAPI set is dropped. */
|
|
1320
|
+
function getEncoding(encoding) {
|
|
1321
|
+
const entries = Object.entries(encoding ?? {}).flatMap(([property, { contentType, style, explode, allowReserved }]) => {
|
|
1322
|
+
const node = {
|
|
1323
|
+
...contentType !== void 0 ? { contentType } : {},
|
|
1324
|
+
...style !== void 0 && PARAMETER_STYLES.has(style) ? { style } : {},
|
|
1325
|
+
...explode !== void 0 ? { explode } : {},
|
|
1326
|
+
...allowReserved !== void 0 ? { allowReserved } : {}
|
|
1327
|
+
};
|
|
1328
|
+
return Object.keys(node).length ? [[property, node]] : [];
|
|
1329
|
+
});
|
|
1330
|
+
return entries.length ? Object.fromEntries(entries) : void 0;
|
|
1331
|
+
}
|
|
1175
1332
|
/** Schema for one media type of a `content` map, with the binary fallback for an emptied non-JSON entry. */
|
|
1176
1333
|
function getBodySchema({ content, contentType, refs }) {
|
|
1177
1334
|
const entry = contentType ? content?.[contentType] : void 0;
|
|
@@ -1226,7 +1383,7 @@ function parseParameter({ param, pointer, parentName, options, parseSchema }) {
|
|
|
1226
1383
|
name: schemaName
|
|
1227
1384
|
}) : _kubb_kit.ast.factory.createSchema({ type: options.unknownType });
|
|
1228
1385
|
if (rawSchema) scanSchema(schema, contentType ? `${pointer}/content/${escapePointerToken(contentType)}/schema` : `${pointer}/schema`);
|
|
1229
|
-
const style = param.style;
|
|
1386
|
+
const style = param.style !== void 0 && PARAMETER_STYLES.has(param.style) ? param.style : void 0;
|
|
1230
1387
|
const explode = param.explode;
|
|
1231
1388
|
const allowReserved = param.allowReserved;
|
|
1232
1389
|
return _kubb_kit.ast.factory.createParameter({
|
|
@@ -1279,10 +1436,12 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1279
1436
|
name: requestBodyName
|
|
1280
1437
|
});
|
|
1281
1438
|
scanSchema(node, `${pointer}/requestBody/content/${escapePointerToken(ct)}/schema`);
|
|
1439
|
+
const encoding = getEncoding(body?.content?.[ct]?.encoding);
|
|
1282
1440
|
return [_kubb_kit.ast.factory.createContent({
|
|
1283
1441
|
contentType: ct,
|
|
1284
1442
|
schema: _kubb_kit.ast.optionality(node, bodyRequired),
|
|
1285
|
-
keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
|
|
1443
|
+
keysToOmit: collectPropertyKeysByFlag(schema, "readOnly"),
|
|
1444
|
+
...encoding && { encoding }
|
|
1286
1445
|
})];
|
|
1287
1446
|
});
|
|
1288
1447
|
const requestBody = content.length > 0 || body?.description ? {
|
|
@@ -1379,6 +1538,42 @@ function createNode({ schema, name, nullable, defaultValue }, extras) {
|
|
|
1379
1538
|
//#endregion
|
|
1380
1539
|
//#region src/emit/discriminator/preserve.ts
|
|
1381
1540
|
/**
|
|
1541
|
+
* Turns string mapping keys into the discriminator property's type when they parse cleanly.
|
|
1542
|
+
*/
|
|
1543
|
+
function typedKeys({ keys, propertyType }) {
|
|
1544
|
+
return keys.map((key) => {
|
|
1545
|
+
if (propertyType === "integer" || propertyType === "number") {
|
|
1546
|
+
const value = Number(key);
|
|
1547
|
+
return key.trim() !== "" && Number.isFinite(value) ? value : key;
|
|
1548
|
+
}
|
|
1549
|
+
if (propertyType === "boolean") {
|
|
1550
|
+
const lower = key.toLowerCase();
|
|
1551
|
+
return lower === "true" ? true : lower === "false" ? false : key;
|
|
1552
|
+
}
|
|
1553
|
+
return key;
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Reads the declared `type` of `propertyName` on a schema, following `$ref`s and `allOf` members.
|
|
1558
|
+
* A nullable array such as `['integer', 'null']` reads as its one non-null type.
|
|
1559
|
+
*/
|
|
1560
|
+
function propertyTypeOf({ schema, propertyName, refs, seen = /* @__PURE__ */ new Set() }) {
|
|
1561
|
+
const deref = (value) => value && isReference(value) ? refs.resolve(value.$ref, { report: false }) : value;
|
|
1562
|
+
if (schema && isReference(schema)) {
|
|
1563
|
+
if (seen.has(schema.$ref)) return void 0;
|
|
1564
|
+
seen.add(schema.$ref);
|
|
1565
|
+
}
|
|
1566
|
+
const resolved = deref(schema);
|
|
1567
|
+
const type = deref(resolved?.properties?.[propertyName])?.type ?? resolved?.allOf?.map((member) => propertyTypeOf({
|
|
1568
|
+
schema: member,
|
|
1569
|
+
propertyName,
|
|
1570
|
+
refs,
|
|
1571
|
+
seen
|
|
1572
|
+
})).find(Boolean);
|
|
1573
|
+
const nonNull = Array.isArray(type) ? type.filter((t) => t !== "null") : [];
|
|
1574
|
+
return nonNull.length === 1 ? nonNull[0] : type;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1382
1577
|
* Creates a single-property object schema used as a discriminator literal.
|
|
1383
1578
|
*
|
|
1384
1579
|
* @example
|
|
@@ -1395,7 +1590,7 @@ function createDiscriminantNode({ propertyName, values }) {
|
|
|
1395
1590
|
name: propertyName,
|
|
1396
1591
|
schema: _kubb_kit.ast.factory.createSchema({
|
|
1397
1592
|
type: "enum",
|
|
1398
|
-
primitive:
|
|
1593
|
+
primitive: (0, _kubb_kit.enumPrimitive)(values),
|
|
1399
1594
|
enumValues: values
|
|
1400
1595
|
}),
|
|
1401
1596
|
required: true
|
|
@@ -1410,21 +1605,61 @@ function createDiscriminantNode({ propertyName, values }) {
|
|
|
1410
1605
|
*
|
|
1411
1606
|
* @example
|
|
1412
1607
|
* ```ts
|
|
1413
|
-
* findDiscriminators({ dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // ['dog', 'hound']
|
|
1608
|
+
* findDiscriminators({ mapping: { dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, ref: '#/components/schemas/Dog' }) // ['dog', 'hound']
|
|
1414
1609
|
* ```
|
|
1415
1610
|
*/
|
|
1416
|
-
function findDiscriminators(mapping, ref) {
|
|
1611
|
+
function findDiscriminators({ mapping, ref }) {
|
|
1417
1612
|
if (!mapping || !ref) return [];
|
|
1418
1613
|
return Object.entries(mapping).filter(([, value]) => value === ref).map(([key]) => key);
|
|
1419
1614
|
}
|
|
1420
1615
|
/**
|
|
1616
|
+
* Finds the discriminator a schema declares itself or through an inline `allOf` member.
|
|
1617
|
+
*/
|
|
1618
|
+
function getDiscriminator(schema) {
|
|
1619
|
+
return (isDiscriminator(schema) ? schema : schema.allOf?.find(isDiscriminator))?.discriminator;
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Lists the mapping keys of a discriminator plus the keys of the nested discriminators its mapped
|
|
1623
|
+
* schemas declare on the same property, so a root `$type` accepts values from every level.
|
|
1624
|
+
*/
|
|
1625
|
+
function getMappingKeys({ discriminator: { propertyName, mapping }, refs, seen = /* @__PURE__ */ new Set() }) {
|
|
1626
|
+
return Object.entries(mapping ?? {}).flatMap(([key, target]) => {
|
|
1627
|
+
if (seen.has(target)) return [key];
|
|
1628
|
+
seen.add(target);
|
|
1629
|
+
const resolved = refs.resolve(target, { report: false });
|
|
1630
|
+
const nested = resolved && getDiscriminator(resolved);
|
|
1631
|
+
return nested?.propertyName === propertyName ? [key, ...getMappingKeys({
|
|
1632
|
+
discriminator: nested,
|
|
1633
|
+
refs,
|
|
1634
|
+
seen
|
|
1635
|
+
})] : [key];
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Drops the discriminator from an inline `allOf` member when a `$ref` sibling already declares one on
|
|
1640
|
+
* the same property. That mapping names the schema's own children, and keeping it pins the property
|
|
1641
|
+
* to those keys next to the parent's, which leaves `never`.
|
|
1642
|
+
*/
|
|
1643
|
+
function removeInheritedDiscriminator({ members, refs }) {
|
|
1644
|
+
const inherited = new Set(members.flatMap((member) => {
|
|
1645
|
+
const parent = isReference(member) ? refs.resolve(member.$ref, { report: false }) : null;
|
|
1646
|
+
return parent && getDiscriminator(parent)?.propertyName || [];
|
|
1647
|
+
}));
|
|
1648
|
+
return members.map((member) => {
|
|
1649
|
+
if (isReference(member) || !isDiscriminator(member) || member.oneOf || member.anyOf) return member;
|
|
1650
|
+
if (!inherited.has(member.discriminator.propertyName)) return member;
|
|
1651
|
+
const { discriminator: _discriminator, ...rest } = member;
|
|
1652
|
+
return rest;
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1421
1656
|
* Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
|
|
1422
1657
|
* node with either the shared-properties slice carrying that value, or a synthetic discriminant
|
|
1423
1658
|
* literal. The referenced child schema's own definition is left untouched — narrowing happens only
|
|
1424
1659
|
* at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
|
|
1425
1660
|
* which additionally patches the child schema's own definition in a post-pass).
|
|
1426
1661
|
*/
|
|
1427
|
-
function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, name, refs }) {
|
|
1662
|
+
function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parentType, parse, name, refs }) {
|
|
1428
1663
|
function pickDiscriminatorPropertyNode(node, propertyName) {
|
|
1429
1664
|
const discriminatorProperty = _kubb_kit.ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
|
|
1430
1665
|
if (!discriminatorProperty) return null;
|
|
@@ -1460,9 +1695,20 @@ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode,
|
|
|
1460
1695
|
}
|
|
1461
1696
|
return unionMembers.map((s) => {
|
|
1462
1697
|
const ref = isReference(s) ? s.$ref : void 0;
|
|
1463
|
-
const mappedValues = findDiscriminators(
|
|
1698
|
+
const mappedValues = findDiscriminators({
|
|
1699
|
+
mapping: discriminator?.mapping,
|
|
1700
|
+
ref
|
|
1701
|
+
});
|
|
1464
1702
|
const implicitValue = mappedValues.length ? null : implicitDiscriminantValue(s);
|
|
1465
|
-
const
|
|
1703
|
+
const propertyType = propertyTypeOf({
|
|
1704
|
+
schema: s,
|
|
1705
|
+
propertyName: discriminator?.propertyName ?? "",
|
|
1706
|
+
refs
|
|
1707
|
+
}) ?? parentType;
|
|
1708
|
+
const discriminatorValues = typedKeys({
|
|
1709
|
+
keys: mappedValues.length ? mappedValues : implicitValue ? [implicitValue] : [],
|
|
1710
|
+
propertyType
|
|
1711
|
+
});
|
|
1466
1712
|
const memberNode = parse({
|
|
1467
1713
|
schema: s,
|
|
1468
1714
|
name
|
|
@@ -1494,21 +1740,34 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
|
|
|
1494
1740
|
members: allOfMembers.filter((item) => {
|
|
1495
1741
|
if (!isReference(item) || !name) return true;
|
|
1496
1742
|
const deref = refs.resolve(item.$ref);
|
|
1497
|
-
|
|
1498
|
-
const parentUnion = deref
|
|
1499
|
-
if (!parentUnion) return true;
|
|
1743
|
+
const discriminator = deref && getDiscriminator(deref);
|
|
1744
|
+
const parentUnion = deref?.oneOf ?? deref?.anyOf;
|
|
1745
|
+
if (!deref || !discriminator || isDiscriminator(deref) && !parentUnion) return true;
|
|
1500
1746
|
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1501
|
-
const
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1747
|
+
const values = findDiscriminators({
|
|
1748
|
+
mapping: discriminator.mapping,
|
|
1749
|
+
ref: childRef
|
|
1750
|
+
});
|
|
1751
|
+
if (values.length) {
|
|
1752
|
+
const { propertyName } = discriminator;
|
|
1753
|
+
const propertyType = propertyTypeOf({
|
|
1754
|
+
schema: deref,
|
|
1755
|
+
propertyName,
|
|
1756
|
+
refs
|
|
1757
|
+
}) ?? propertyTypeOf({
|
|
1758
|
+
schema: { $ref: childRef },
|
|
1759
|
+
propertyName,
|
|
1760
|
+
refs
|
|
1761
|
+
});
|
|
1762
|
+
discriminantValues.push({
|
|
1763
|
+
propertyName,
|
|
1764
|
+
values: typedKeys({
|
|
1765
|
+
keys: values,
|
|
1766
|
+
propertyType
|
|
1767
|
+
})
|
|
1508
1768
|
});
|
|
1509
|
-
return false;
|
|
1510
1769
|
}
|
|
1511
|
-
return
|
|
1770
|
+
return !(parentUnion && (values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
|
|
1512
1771
|
}),
|
|
1513
1772
|
discriminantValues
|
|
1514
1773
|
};
|
|
@@ -1635,10 +1894,9 @@ function resolveUnionMembers(members, siblingProperties) {
|
|
|
1635
1894
|
});
|
|
1636
1895
|
}
|
|
1637
1896
|
/**
|
|
1638
|
-
* Enriches any `oneOf` or `anyOf` branches on
|
|
1897
|
+
* Enriches a schema object, and any `oneOf` or `anyOf` branches on it, with properties from sibling schemas.
|
|
1639
1898
|
*/
|
|
1640
1899
|
function resolveUnionSchema(schema, siblingProperties) {
|
|
1641
|
-
if (!schema.oneOf && !schema.anyOf) return schema;
|
|
1642
1900
|
const next = { ...resolveUnionMembers([schema], siblingProperties)[0] ?? schema };
|
|
1643
1901
|
if (schema.oneOf) next.oneOf = resolveUnionMembers(schema.oneOf, siblingProperties);
|
|
1644
1902
|
if (schema.anyOf) next.anyOf = resolveUnionMembers(schema.anyOf, siblingProperties);
|
|
@@ -1662,6 +1920,17 @@ function convertRef({ schema, name, nullable, defaultValue, document, parse, ref
|
|
|
1662
1920
|
defaultValue
|
|
1663
1921
|
};
|
|
1664
1922
|
if (refPath && document.components && !refs.resolve(refPath, { report: false })) return createNode(ctx, { type: "unknown" });
|
|
1923
|
+
if (refPath && !refPath.startsWith("#/components/schemas/") && resolvedSchema) return _kubb_kit.ast.factory.createSchema({
|
|
1924
|
+
...resolvedSchema,
|
|
1925
|
+
title: schema.title ?? resolvedSchema.title,
|
|
1926
|
+
description: schema.description ?? resolvedSchema.description,
|
|
1927
|
+
deprecated: schema.deprecated ?? resolvedSchema.deprecated,
|
|
1928
|
+
readOnly: schema.readOnly ?? resolvedSchema.readOnly,
|
|
1929
|
+
writeOnly: schema.writeOnly ?? resolvedSchema.writeOnly,
|
|
1930
|
+
default: defaultValue ?? resolvedSchema.default,
|
|
1931
|
+
name: name ?? resolvedSchema.name,
|
|
1932
|
+
nullable: nullable ?? resolvedSchema.nullable
|
|
1933
|
+
});
|
|
1665
1934
|
const targetName = renames?.get(schema.$ref);
|
|
1666
1935
|
return createNode(ctx, {
|
|
1667
1936
|
type: "ref",
|
|
@@ -1693,9 +1962,41 @@ function convertNot({ schema, name, nullable, defaultValue, parse }) {
|
|
|
1693
1962
|
});
|
|
1694
1963
|
}
|
|
1695
1964
|
/**
|
|
1965
|
+
* Keywords that describe a schema or conditionally constrain it, but add no structure.
|
|
1966
|
+
*/
|
|
1967
|
+
const annotationKeys = /* @__PURE__ */ new Set([
|
|
1968
|
+
"description",
|
|
1969
|
+
"title",
|
|
1970
|
+
"example",
|
|
1971
|
+
"examples",
|
|
1972
|
+
"default",
|
|
1973
|
+
"deprecated",
|
|
1974
|
+
"readOnly",
|
|
1975
|
+
"writeOnly",
|
|
1976
|
+
"if",
|
|
1977
|
+
"then",
|
|
1978
|
+
"else"
|
|
1979
|
+
]);
|
|
1980
|
+
/**
|
|
1981
|
+
* Moves `allOf` members that hold only annotations or `if`/`then`/`else` onto the parent, so they do
|
|
1982
|
+
* not become `unknown`. The parent's own values win, and the conditional keywords are not enforced.
|
|
1983
|
+
*/
|
|
1984
|
+
function mergeAnnotations(schema) {
|
|
1985
|
+
const members = schema.allOf;
|
|
1986
|
+
const isAnnotation = (member) => !isReference(member) && Object.keys(member).every((key) => annotationKeys.has(key));
|
|
1987
|
+
const annotations = members.filter(isAnnotation);
|
|
1988
|
+
const rest = members.filter((member) => !isAnnotation(member));
|
|
1989
|
+
return annotations.length && rest.length ? {
|
|
1990
|
+
...Object.assign({}, ...annotations),
|
|
1991
|
+
...schema,
|
|
1992
|
+
allOf: rest
|
|
1993
|
+
} : schema;
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1696
1996
|
* Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
|
|
1697
1997
|
*/
|
|
1698
|
-
function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
|
|
1998
|
+
function convertAllOf({ schema: original, name, nullable, defaultValue, parse, refs }) {
|
|
1999
|
+
const schema = mergeAnnotations(original);
|
|
1699
2000
|
if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
|
|
1700
2001
|
const [memberSchema] = schema.allOf;
|
|
1701
2002
|
const memberNode = parse({
|
|
@@ -1721,7 +2022,10 @@ function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
|
|
|
1721
2022
|
});
|
|
1722
2023
|
}
|
|
1723
2024
|
const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
|
|
1724
|
-
allOfMembers:
|
|
2025
|
+
allOfMembers: removeInheritedDiscriminator({
|
|
2026
|
+
members: schema.allOf,
|
|
2027
|
+
refs
|
|
2028
|
+
}),
|
|
1725
2029
|
name,
|
|
1726
2030
|
refs
|
|
1727
2031
|
});
|
|
@@ -1801,6 +2105,11 @@ function convertUnion(context) {
|
|
|
1801
2105
|
unionMembers,
|
|
1802
2106
|
discriminator,
|
|
1803
2107
|
sharedPropertiesNode,
|
|
2108
|
+
parentType: discriminator && propertyTypeOf({
|
|
2109
|
+
schema,
|
|
2110
|
+
propertyName: discriminator.propertyName,
|
|
2111
|
+
refs
|
|
2112
|
+
}),
|
|
1804
2113
|
parse,
|
|
1805
2114
|
name,
|
|
1806
2115
|
refs
|
|
@@ -2064,7 +2373,7 @@ function nameEnums(node, options) {
|
|
|
2064
2373
|
/**
|
|
2065
2374
|
* Converts an object-like schema into an `ObjectSchemaNode`.
|
|
2066
2375
|
*/
|
|
2067
|
-
function convertObject({ schema, name, nullable, defaultValue, options, parse }) {
|
|
2376
|
+
function convertObject({ schema, name, nullable, defaultValue, options, parse, refs }) {
|
|
2068
2377
|
const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
2069
2378
|
const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
|
|
2070
2379
|
const resolvedPropSchema = propSchema;
|
|
@@ -2081,7 +2390,7 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
|
|
|
2081
2390
|
name: propName,
|
|
2082
2391
|
schema: {
|
|
2083
2392
|
...schemaNode,
|
|
2084
|
-
nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
|
|
2393
|
+
nullable: schemaNode.type === "null" ? void 0 : propNullable || schemaNode.nullable || void 0
|
|
2085
2394
|
},
|
|
2086
2395
|
required
|
|
2087
2396
|
});
|
|
@@ -2121,7 +2430,17 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
|
|
|
2121
2430
|
});
|
|
2122
2431
|
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
2123
2432
|
const discPropName = schema.discriminator.propertyName;
|
|
2124
|
-
const values =
|
|
2433
|
+
const values = typedKeys({
|
|
2434
|
+
keys: getMappingKeys({
|
|
2435
|
+
discriminator: schema.discriminator,
|
|
2436
|
+
refs
|
|
2437
|
+
}),
|
|
2438
|
+
propertyType: propertyTypeOf({
|
|
2439
|
+
schema,
|
|
2440
|
+
propertyName: discPropName,
|
|
2441
|
+
refs
|
|
2442
|
+
})
|
|
2443
|
+
});
|
|
2125
2444
|
const enumName = name ? (0, _kubb_kit.enumPropName)(name, discPropName, options.enumSuffix) : void 0;
|
|
2126
2445
|
return _kubb_kit.ast.applyMacros(objectNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
|
|
2127
2446
|
propertyName: discPropName,
|
|
@@ -2179,8 +2498,9 @@ function convertArray({ schema, name, nullable, defaultValue, options, parse })
|
|
|
2179
2498
|
//#region src/emit/parseSchema.ts
|
|
2180
2499
|
/**
|
|
2181
2500
|
* Ordered schema rule table. Order is significant: composition keywords (`not`, `$ref`, `allOf`,
|
|
2182
|
-
* `oneOf`/`anyOf`) take precedence over `const
|
|
2183
|
-
*
|
|
2501
|
+
* `oneOf`/`anyOf`) take precedence over `const` (except a `$ref` with a sibling `const`, where the
|
|
2502
|
+
* `const` wins), which takes precedence over a genuine OAS 3.1 multi-type array (more than one
|
|
2503
|
+
* non-`null` type), which takes precedence over `format`/`type`.
|
|
2184
2504
|
* A multi-type array must split into its per-type members before `format` runs, otherwise
|
|
2185
2505
|
* `format` collapses the whole schema to one type (e.g. `type: ['null', 'integer', 'string'],
|
|
2186
2506
|
* format: 'int32'` would drop `string` and emit just `integer`); each split-off member still
|
|
@@ -2194,7 +2514,7 @@ const schemaRules = [
|
|
|
2194
2514
|
convert: convertNot
|
|
2195
2515
|
},
|
|
2196
2516
|
{
|
|
2197
|
-
match: ({ schema }) => isReference(schema),
|
|
2517
|
+
match: ({ schema }) => isReference(schema) && !("const" in schema && schema.const !== void 0),
|
|
2198
2518
|
convert: convertRef
|
|
2199
2519
|
},
|
|
2200
2520
|
{
|
|
@@ -2362,114 +2682,6 @@ function refPromotedEnums(node, promoted) {
|
|
|
2362
2682
|
} });
|
|
2363
2683
|
}
|
|
2364
2684
|
//#endregion
|
|
2365
|
-
//#region src/refs.ts
|
|
2366
|
-
/**
|
|
2367
|
-
* Creates the `$ref` resolution service for one document. One pointer walk backs every method.
|
|
2368
|
-
*
|
|
2369
|
-
* @example
|
|
2370
|
-
* ```ts
|
|
2371
|
-
* const refs = createRefs(document)
|
|
2372
|
-
* refs.resolve<SchemaObject>('#/components/schemas/Pet')
|
|
2373
|
-
* refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
|
|
2374
|
-
* refs.resolveNode('#/components/schemas/Pet', parseSchema)
|
|
2375
|
-
* refs.deref<ResponseObject>(operation.schema.responses?.['200'])
|
|
2376
|
-
* refs.derefKeepingRef(parameter)
|
|
2377
|
-
* ```
|
|
2378
|
-
*/
|
|
2379
|
-
function createRefs(document) {
|
|
2380
|
-
const pointerCache = /* @__PURE__ */ new Map();
|
|
2381
|
-
const resolvedNodeCache = /* @__PURE__ */ new Map();
|
|
2382
|
-
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
2383
|
-
function walkPointer($ref) {
|
|
2384
|
-
const trimmed = $ref.trim();
|
|
2385
|
-
if (trimmed === "" || !trimmed.startsWith("#")) return {
|
|
2386
|
-
applicable: false,
|
|
2387
|
-
value: null
|
|
2388
|
-
};
|
|
2389
|
-
const pointer = trimmed.substring(1);
|
|
2390
|
-
if (pointerCache.has(pointer)) return {
|
|
2391
|
-
applicable: true,
|
|
2392
|
-
value: pointerCache.get(pointer)
|
|
2393
|
-
};
|
|
2394
|
-
const current = pointer.split("/").filter(Boolean).map((token) => globalThis.decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~")).reduce((obj, key) => obj?.[key], document);
|
|
2395
|
-
if (current) pointerCache.set(pointer, current);
|
|
2396
|
-
return {
|
|
2397
|
-
applicable: true,
|
|
2398
|
-
value: current ?? null
|
|
2399
|
-
};
|
|
2400
|
-
}
|
|
2401
|
-
/**
|
|
2402
|
-
* Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
|
|
2403
|
-
* `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
|
|
2404
|
-
* outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
|
|
2405
|
-
* for a speculative lookup where a missing ref is not an error.
|
|
2406
|
-
*/
|
|
2407
|
-
function resolve(refPath, options) {
|
|
2408
|
-
const { applicable, value } = walkPointer(refPath);
|
|
2409
|
-
if (!applicable || value || options?.report === false) return value;
|
|
2410
|
-
const diagnostic = {
|
|
2411
|
-
code: _kubb_kit.Diagnostics.code.refNotFound,
|
|
2412
|
-
severity: "error",
|
|
2413
|
-
message: `Could not find a definition for ${refPath}.`,
|
|
2414
|
-
help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
|
|
2415
|
-
location: {
|
|
2416
|
-
kind: "schema",
|
|
2417
|
-
pointer: refPath,
|
|
2418
|
-
ref: refPath
|
|
2419
|
-
}
|
|
2420
|
-
};
|
|
2421
|
-
if (!_kubb_kit.Diagnostics.report(diagnostic)) throw new _kubb_kit.Diagnostics.Error(diagnostic);
|
|
2422
|
-
return null;
|
|
2423
|
-
}
|
|
2424
|
-
/**
|
|
2425
|
-
* Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
|
|
2426
|
-
* instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
|
|
2427
|
-
* resolved (e.g. a minimal document in a unit test).
|
|
2428
|
-
*/
|
|
2429
|
-
function resolveNode(refPath, parse) {
|
|
2430
|
-
if (resolvingRefs.has(refPath)) return null;
|
|
2431
|
-
if (!resolvedNodeCache.has(refPath)) {
|
|
2432
|
-
let resolved = null;
|
|
2433
|
-
try {
|
|
2434
|
-
const referenced = resolve(refPath);
|
|
2435
|
-
if (referenced) {
|
|
2436
|
-
resolvingRefs.add(refPath);
|
|
2437
|
-
resolved = parse({ schema: referenced });
|
|
2438
|
-
resolvingRefs.delete(refPath);
|
|
2439
|
-
}
|
|
2440
|
-
} catch {}
|
|
2441
|
-
resolvedNodeCache.set(refPath, resolved);
|
|
2442
|
-
}
|
|
2443
|
-
return resolvedNodeCache.get(refPath) ?? null;
|
|
2444
|
-
}
|
|
2445
|
-
/**
|
|
2446
|
-
* Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
|
|
2447
|
-
* resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
|
|
2448
|
-
* `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
|
|
2449
|
-
* returned as-is.
|
|
2450
|
-
*/
|
|
2451
|
-
function deref(value) {
|
|
2452
|
-
if (!isReference(value)) return value ? value : null;
|
|
2453
|
-
const resolved = resolve(value.$ref);
|
|
2454
|
-
return resolved && !isReference(resolved) ? resolved : null;
|
|
2455
|
-
}
|
|
2456
|
-
/** Resolves a `$ref` object but keeps the `$ref` field on the result; other values pass through. */
|
|
2457
|
-
function derefKeepingRef(value) {
|
|
2458
|
-
if (isReference(value)) return {
|
|
2459
|
-
...value,
|
|
2460
|
-
...resolve(value.$ref),
|
|
2461
|
-
$ref: value.$ref
|
|
2462
|
-
};
|
|
2463
|
-
return value;
|
|
2464
|
-
}
|
|
2465
|
-
return {
|
|
2466
|
-
resolve,
|
|
2467
|
-
resolveNode,
|
|
2468
|
-
deref,
|
|
2469
|
-
derefKeepingRef
|
|
2470
|
-
};
|
|
2471
|
-
}
|
|
2472
|
-
//#endregion
|
|
2473
2685
|
//#region src/sourceLocator.ts
|
|
2474
2686
|
let cached;
|
|
2475
2687
|
const toRange = ([start, end]) => ({
|
|
@@ -2702,7 +2914,7 @@ const adapterOas = (0, _kubb_core.defineExtension)({
|
|
|
2702
2914
|
};
|
|
2703
2915
|
parsedByName.set(name, node);
|
|
2704
2916
|
scanSchema(node);
|
|
2705
|
-
if (node.type === "ref" && node.name
|
|
2917
|
+
if (node.type === "ref" && node.name) refAliasMap.set(name, node);
|
|
2706
2918
|
if (_kubb_kit.ast.narrowSchema(node, "enum") && node.name) enumNames.push(node.name);
|
|
2707
2919
|
if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
|
|
2708
2920
|
}
|
|
@@ -2717,6 +2929,36 @@ const adapterOas = (0, _kubb_core.defineExtension)({
|
|
|
2717
2929
|
}));
|
|
2718
2930
|
const promotedEnums = enums === "root" ? collectInlineEnums([...parsedByName.values(), ...operationNodes], new Set(Object.keys(schemas))) : null;
|
|
2719
2931
|
if (promotedEnums) for (const name of promotedEnums.keys()) enumNames.push(name);
|
|
2932
|
+
const aliasTargetOf = (name) => {
|
|
2933
|
+
const node = refAliasMap.get(name);
|
|
2934
|
+
return node?.ref ? _kubb_kit.ast.refName(node.ref) : node?.name;
|
|
2935
|
+
};
|
|
2936
|
+
function isAliasCycle(name) {
|
|
2937
|
+
const seen = /* @__PURE__ */ new Set([name]);
|
|
2938
|
+
for (let next = aliasTargetOf(name); next; next = aliasTargetOf(next)) {
|
|
2939
|
+
if (next === name) return true;
|
|
2940
|
+
if (seen.has(next)) return false;
|
|
2941
|
+
seen.add(next);
|
|
2942
|
+
}
|
|
2943
|
+
return false;
|
|
2944
|
+
}
|
|
2945
|
+
function createCycleNode(name, pointer) {
|
|
2946
|
+
_kubb_kit.Diagnostics.report({
|
|
2947
|
+
code: _kubb_kit.Diagnostics.code.aliasCycle,
|
|
2948
|
+
severity: "warning",
|
|
2949
|
+
message: `${name} only refers to other schemas that lead back to it, so it is resolved to unknown.`,
|
|
2950
|
+
help: "Give one of the schemas in the loop a real definition, or remove the `$ref` that closes it.",
|
|
2951
|
+
location: {
|
|
2952
|
+
kind: "schema",
|
|
2953
|
+
pointer: pointer ?? `#/components/schemas/${name}`
|
|
2954
|
+
}
|
|
2955
|
+
});
|
|
2956
|
+
return _kubb_kit.ast.factory.createSchema({
|
|
2957
|
+
type: "unknown",
|
|
2958
|
+
name,
|
|
2959
|
+
pointer
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2720
2962
|
const schemaNodes = promotedEnums ? [...promotedEnums.values()] : [];
|
|
2721
2963
|
for (const name of Object.keys(schemas)) {
|
|
2722
2964
|
const aliasTarget = refAliasMap.get(name)?.name;
|
|
@@ -2724,11 +2966,12 @@ const adapterOas = (0, _kubb_core.defineExtension)({
|
|
|
2724
2966
|
const parsed = parsedByName.get(name);
|
|
2725
2967
|
const child = discriminatorChildMap?.get(name);
|
|
2726
2968
|
const base = child ? patchDiscriminatorNode(parsed, child) : parsed;
|
|
2727
|
-
const
|
|
2969
|
+
const resolved = aliased ? {
|
|
2728
2970
|
...aliased,
|
|
2729
2971
|
name,
|
|
2730
2972
|
pointer: parsed.pointer
|
|
2731
2973
|
} : base;
|
|
2974
|
+
const node = isAliasCycle(name) ? createCycleNode(name, parsed.pointer) : resolved;
|
|
2732
2975
|
schemaNodes.push(promotedEnums ? refPromotedEnums(node, promotedEnums) : node);
|
|
2733
2976
|
}
|
|
2734
2977
|
const operations = promotedEnums ? operationNodes.map((node) => refPromotedEnums(node, promotedEnums)) : operationNodes;
|