schema-components 1.22.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/core/adapter.d.mts +97 -3
- package/dist/core/adapter.mjs +260 -111
- package/dist/core/constraints.d.mts +2 -2
- package/dist/core/constraints.mjs +0 -7
- package/dist/core/cssClasses.d.mts +52 -0
- package/dist/core/cssClasses.mjs +51 -0
- package/dist/core/diagnostics.d.mts +1 -1
- package/dist/core/errors.d.mts +1 -1
- package/dist/core/errors.mjs +5 -13
- package/dist/core/fieldOrder.d.mts +1 -1
- package/dist/core/formats.d.mts +9 -2
- package/dist/core/formats.mjs +12 -1
- package/dist/core/idPath.d.mts +54 -0
- package/dist/core/idPath.mjs +66 -0
- package/dist/core/merge.d.mts +10 -1
- package/dist/core/merge.mjs +49 -10
- package/dist/core/normalise.d.mts +14 -3
- package/dist/core/normalise.mjs +2 -2
- package/dist/core/openapi30.d.mts +15 -1
- package/dist/core/openapi30.mjs +2 -2
- package/dist/core/openapiConstants.d.mts +67 -0
- package/dist/core/openapiConstants.mjs +90 -0
- package/dist/core/ref.d.mts +2 -2
- package/dist/core/ref.mjs +84 -6
- package/dist/core/refChain.d.mts +70 -0
- package/dist/core/refChain.mjs +44 -0
- package/dist/core/renderer.d.mts +1 -1
- package/dist/core/swagger2.d.mts +1 -1
- package/dist/core/swagger2.mjs +1 -1
- package/dist/core/typeInference.d.mts +982 -2
- package/dist/core/types.d.mts +1 -1
- package/dist/core/unionMatch.d.mts +36 -0
- package/dist/core/unionMatch.mjs +53 -0
- package/dist/core/version.d.mts +1 -1
- package/dist/core/version.mjs +29 -17
- package/dist/core/walkBuilders.d.mts +23 -4
- package/dist/core/walkBuilders.mjs +27 -7
- package/dist/core/walker.d.mts +1 -1
- package/dist/core/walker.mjs +44 -45
- package/dist/{diagnostics-D0QCYGv0.d.mts → diagnostics-BS2kaUyE.d.mts} +1 -1
- package/dist/{errors-DpFwqs5C.d.mts → errors-g_MCTQel.d.mts} +9 -15
- package/dist/html/a11y.d.mts +9 -4
- package/dist/html/a11y.mjs +10 -19
- package/dist/html/renderToHtml.d.mts +2 -2
- package/dist/html/renderToHtmlStream.d.mts +2 -2
- package/dist/html/renderToHtmlStream.mjs +12 -1
- package/dist/html/renderers.d.mts +43 -8
- package/dist/html/renderers.mjs +136 -111
- package/dist/html/streamRenderers.d.mts +4 -5
- package/dist/html/streamRenderers.mjs +40 -61
- package/dist/{normalise-DVEJQmF7.mjs → normalise-DCYp06Sr.mjs} +352 -162
- package/dist/openapi/ApiCallbacks.d.mts +1 -1
- package/dist/openapi/ApiLinks.d.mts +1 -1
- package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
- package/dist/openapi/ApiSecurity.d.mts +1 -1
- package/dist/openapi/components.d.mts +116 -37
- package/dist/openapi/components.mjs +54 -37
- package/dist/openapi/parser.d.mts +9 -8
- package/dist/openapi/parser.mjs +234 -84
- package/dist/openapi/resolve.d.mts +20 -11
- package/dist/openapi/resolve.mjs +133 -73
- package/dist/react/SchemaComponent.d.mts +32 -7
- package/dist/react/SchemaComponent.mjs +45 -21
- package/dist/react/SchemaView.d.mts +30 -10
- package/dist/react/a11y.d.mts +21 -0
- package/dist/react/a11y.mjs +24 -0
- package/dist/react/fieldPath.d.mts +1 -1
- package/dist/react/headless.d.mts +1 -1
- package/dist/react/headlessRenderers.d.mts +8 -9
- package/dist/react/headlessRenderers.mjs +41 -72
- package/dist/{ref-D-_JBZkF.d.mts → ref-DjLEKa_E.d.mts} +38 -3
- package/dist/{renderer-BaRlQIuN.d.mts → renderer-CXJ8y0qw.d.mts} +1 -1
- package/dist/themes/mantine.d.mts +1 -1
- package/dist/themes/mui.d.mts +1 -1
- package/dist/themes/radix.d.mts +1 -1
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/themes/shadcn.mjs +2 -1
- package/dist/{types-BrRMV0en.d.mts → types-BTB73MB8.d.mts} +32 -4
- package/dist/{version-D2jfdX6E.d.mts → version-BFTVLsdb.d.mts} +7 -1
- package/package.json +1 -1
- package/dist/typeInference-DkcUHfaM.d.mts +0 -982
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { isObject } from "./core/guards.mjs";
|
|
2
2
|
import { appendPointer, emitDiagnostic } from "./core/diagnostics.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { RECURSIVE_ANCHOR_SENTINEL } from "./core/ref.mjs";
|
|
4
|
+
import { isOpenApi30, isOpenApi31, isSwagger2, matchJsonSchemaDraftUri, readJsonSchemaDialect } from "./core/version.mjs";
|
|
5
|
+
import { SWAGGER_2_METHODS, rewriteSwaggerRefPrefix } from "./core/openapiConstants.mjs";
|
|
6
|
+
import { resolveRefChain } from "./core/refChain.mjs";
|
|
4
7
|
//#region src/core/openapi30.ts
|
|
5
8
|
/**
|
|
6
9
|
* OpenAPI 3.0.x schema normalisation.
|
|
@@ -10,6 +13,29 @@ import { isOpenApi30, isOpenApi31, isSwagger2, readJsonSchemaDialect } from "./c
|
|
|
10
13
|
* responses, headers, callbacks, links, examples) to apply normalisation.
|
|
11
14
|
*/
|
|
12
15
|
/**
|
|
16
|
+
* Lift OpenAPI 3.x singular `example` onto the plural `examples` key.
|
|
17
|
+
*
|
|
18
|
+
* Two output shapes are spec-correct depending on the parent object type:
|
|
19
|
+
* - `"array"` — Schema Object: `examples: [example]` (Draft 2020-12 plural).
|
|
20
|
+
* - `"map"` — Parameter / Header / Media Type Object: an Examples Map
|
|
21
|
+
* keyed by name. The single value is wrapped under the
|
|
22
|
+
* synthetic key `default` to produce a valid one-entry map
|
|
23
|
+
* of one Example Object.
|
|
24
|
+
*
|
|
25
|
+
* When both `example` and `examples` coexist the spec declares them mutually
|
|
26
|
+
* exclusive — `example` is dropped and `examples` wins.
|
|
27
|
+
*/
|
|
28
|
+
function liftExampleToExamples(node, shape) {
|
|
29
|
+
if (!("example" in node)) return;
|
|
30
|
+
if ("examples" in node) {
|
|
31
|
+
delete node.example;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (shape === "array") node.examples = [node.example];
|
|
35
|
+
else node.examples = { default: { value: node.example } };
|
|
36
|
+
delete node.example;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
13
39
|
* Normalise OpenAPI 3.0.x `nullable` keyword to `anyOf [T, null]`.
|
|
14
40
|
*
|
|
15
41
|
* OpenAPI 3.0 uses `nullable: true` instead of the JSON Schema standard
|
|
@@ -20,24 +46,32 @@ import { isOpenApi30, isOpenApi31, isSwagger2, readJsonSchemaDialect } from "./c
|
|
|
20
46
|
* absent is the default and requires no transformation.
|
|
21
47
|
*/
|
|
22
48
|
function normaliseOpenApi30Node(node) {
|
|
23
|
-
|
|
24
|
-
node.examples = [node.example];
|
|
25
|
-
delete node.example;
|
|
26
|
-
} else if ("example" in node) delete node.example;
|
|
49
|
+
liftExampleToExamples(node, "array");
|
|
27
50
|
if (node.nullable !== true) {
|
|
28
51
|
if ("nullable" in node) delete node.nullable;
|
|
29
52
|
return node;
|
|
30
53
|
}
|
|
31
54
|
const nullOption = { type: "null" };
|
|
32
|
-
if (typeof node.$ref === "string")
|
|
33
|
-
|
|
55
|
+
if (typeof node.$ref === "string") {
|
|
56
|
+
const wrapper = { anyOf: [{ $ref: node.$ref }, nullOption] };
|
|
57
|
+
for (const key of REF_DOC_SIBLINGS) if (key in node) wrapper[key] = node[key];
|
|
58
|
+
return wrapper;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(node.enum)) {
|
|
61
|
+
if (node.enum.includes(null)) {
|
|
62
|
+
delete node.nullable;
|
|
63
|
+
return node;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
34
66
|
if (Array.isArray(node.anyOf)) {
|
|
35
|
-
|
|
67
|
+
const existing = node.anyOf;
|
|
68
|
+
node.anyOf = compositeAlreadyAllowsNull(existing) ? existing : [...existing, nullOption];
|
|
36
69
|
delete node.nullable;
|
|
37
70
|
return node;
|
|
38
71
|
}
|
|
39
72
|
if (Array.isArray(node.oneOf)) {
|
|
40
|
-
|
|
73
|
+
const existing = node.oneOf;
|
|
74
|
+
node.anyOf = compositeAlreadyAllowsNull(existing) ? existing : [...existing, nullOption];
|
|
41
75
|
delete node.oneOf;
|
|
42
76
|
delete node.nullable;
|
|
43
77
|
return node;
|
|
@@ -53,6 +87,39 @@ function normaliseOpenApi30Node(node) {
|
|
|
53
87
|
return { anyOf: [wrapper, nullOption] };
|
|
54
88
|
}
|
|
55
89
|
/**
|
|
90
|
+
* Documentary keys that may legitimately sit alongside a `$ref` in an
|
|
91
|
+
* OpenAPI 3.0 Schema Object. They carry author-facing metadata, not
|
|
92
|
+
* validation semantics, so lifting them onto the `anyOf` wrapper
|
|
93
|
+
* preserves authorial intent without violating the spec rule that a
|
|
94
|
+
* Reference Object itself only carry `$ref`.
|
|
95
|
+
*/
|
|
96
|
+
const REF_DOC_SIBLINGS = [
|
|
97
|
+
"description",
|
|
98
|
+
"summary",
|
|
99
|
+
"title",
|
|
100
|
+
"deprecated",
|
|
101
|
+
"readOnly",
|
|
102
|
+
"writeOnly",
|
|
103
|
+
"example",
|
|
104
|
+
"examples",
|
|
105
|
+
"default"
|
|
106
|
+
];
|
|
107
|
+
/**
|
|
108
|
+
* Returns `true` when at least one option in a composite (`anyOf` /
|
|
109
|
+
* `oneOf`) already permits `null` — either a literal `{ type: "null" }`
|
|
110
|
+
* branch or an `enum` containing `null`. Used to dedup the synthetic
|
|
111
|
+
* null option appended when normalising `nullable: true`.
|
|
112
|
+
*/
|
|
113
|
+
function compositeAlreadyAllowsNull(options) {
|
|
114
|
+
for (const option of options) {
|
|
115
|
+
if (!isObject(option)) continue;
|
|
116
|
+
if (option.type === "null") return true;
|
|
117
|
+
if (Array.isArray(option.type) && option.type.includes("null")) return true;
|
|
118
|
+
if (Array.isArray(option.enum) && option.enum.includes(null)) return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
56
123
|
* Normalise OpenAPI 3.0.x `discriminator` keyword by injecting `const`
|
|
57
124
|
* values into each `oneOf`/`anyOf` option's discriminator property.
|
|
58
125
|
*
|
|
@@ -80,7 +147,7 @@ function normaliseOpenApi30Discriminator(node) {
|
|
|
80
147
|
normalisedComposite.push(option);
|
|
81
148
|
continue;
|
|
82
149
|
}
|
|
83
|
-
const props = isObject(option.properties) ?
|
|
150
|
+
const props = isObject(option.properties) ? option.properties : void 0;
|
|
84
151
|
const discProp = props?.[propertyName];
|
|
85
152
|
if (isObject(discProp) && "const" in discProp) {
|
|
86
153
|
normalisedComposite.push(option);
|
|
@@ -102,7 +169,7 @@ function normaliseOpenApi30Discriminator(node) {
|
|
|
102
169
|
if (entry !== void 0) constValue = entry[0];
|
|
103
170
|
}
|
|
104
171
|
if (constValue !== void 0) {
|
|
105
|
-
const normalisedProps = props
|
|
172
|
+
const normalisedProps = { ...props };
|
|
106
173
|
normalisedProps[propertyName] = {
|
|
107
174
|
...isObject(discProp) ? discProp : {},
|
|
108
175
|
const: constValue
|
|
@@ -487,10 +554,7 @@ function normaliseParameter(param, normaliseSchema) {
|
|
|
487
554
|
if (isObject(schema)) result.schema = normaliseSchema(schema);
|
|
488
555
|
const content = param.content;
|
|
489
556
|
if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
|
|
490
|
-
|
|
491
|
-
result.examples = { default: { value: result.example } };
|
|
492
|
-
delete result.example;
|
|
493
|
-
} else if ("example" in result) delete result.example;
|
|
557
|
+
liftExampleToExamples(result, "map");
|
|
494
558
|
return result;
|
|
495
559
|
}
|
|
496
560
|
function normaliseRequestBody(requestBody, normaliseSchema) {
|
|
@@ -513,10 +577,7 @@ function normaliseHeader(header, normaliseSchema) {
|
|
|
513
577
|
if (isObject(schema)) result.schema = normaliseSchema(schema);
|
|
514
578
|
const content = header.content;
|
|
515
579
|
if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
|
|
516
|
-
|
|
517
|
-
result.examples = { default: { value: result.example } };
|
|
518
|
-
delete result.example;
|
|
519
|
-
} else if ("example" in result) delete result.example;
|
|
580
|
+
liftExampleToExamples(result, "map");
|
|
520
581
|
return result;
|
|
521
582
|
}
|
|
522
583
|
/**
|
|
@@ -539,10 +600,7 @@ function normaliseContentMap(content, normaliseSchema) {
|
|
|
539
600
|
if (isObject(schema)) normalised.schema = normaliseSchema(schema);
|
|
540
601
|
const encoding = mediaObj.encoding;
|
|
541
602
|
if (isObject(encoding)) normalised.encoding = mapObjectValues(encoding, (enc) => isObject(enc) ? normaliseEncoding(enc, normaliseSchema) : enc);
|
|
542
|
-
|
|
543
|
-
normalised.examples = { default: { value: normalised.example } };
|
|
544
|
-
delete normalised.example;
|
|
545
|
-
} else if ("example" in normalised) delete normalised.example;
|
|
603
|
+
liftExampleToExamples(normalised, "map");
|
|
546
604
|
result[mediaType] = normalised;
|
|
547
605
|
}
|
|
548
606
|
return result;
|
|
@@ -639,16 +697,34 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
|
|
|
639
697
|
const resolved = resolution.param;
|
|
640
698
|
const location = resolved.in;
|
|
641
699
|
if (location === "body") {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
700
|
+
const paramPointer = appendPointer(appendPointer("", "parameters"), name);
|
|
701
|
+
let bodyContentTypes;
|
|
702
|
+
if (consumesResolution.source === "synthesised") {
|
|
703
|
+
bodyContentTypes = globalConsumes;
|
|
704
|
+
emitDiagnostic(diagnostics, {
|
|
705
|
+
code: "swagger-missing-consumes",
|
|
706
|
+
message: "Global body parameter declared but document-level `consumes` is absent; defaulting to application/json",
|
|
707
|
+
pointer: paramPointer,
|
|
708
|
+
detail: {
|
|
709
|
+
level: "document",
|
|
710
|
+
name
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
} else if (globalConsumes.length === 0) {
|
|
714
|
+
bodyContentTypes = [];
|
|
715
|
+
emitDiagnostic(diagnostics, {
|
|
716
|
+
code: "swagger-missing-consumes",
|
|
717
|
+
message: "Global body parameter declared but document-level `consumes` is an explicit empty array; preserving an empty content map",
|
|
718
|
+
pointer: paramPointer,
|
|
719
|
+
detail: {
|
|
720
|
+
level: "document",
|
|
721
|
+
name,
|
|
722
|
+
reason: "explicitly-cleared",
|
|
723
|
+
source: consumesResolution.source
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
} else bodyContentTypes = globalConsumes;
|
|
727
|
+
requestBodies[name] = buildRequestBody(resolved, bodyContentTypes);
|
|
652
728
|
} else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]), formDataContentTypes(globalConsumes));
|
|
653
729
|
else {
|
|
654
730
|
const normalised = normaliseSwaggerParameter(resolved, doc, diagnostics, appendPointer(appendPointer("", "parameters"), name));
|
|
@@ -668,7 +744,11 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
|
|
|
668
744
|
const securityDefinitions = doc.securityDefinitions;
|
|
669
745
|
if (isObject(securityDefinitions)) {
|
|
670
746
|
const translated = {};
|
|
671
|
-
|
|
747
|
+
const securityDefinitionsPointer = appendPointer("", "securityDefinitions");
|
|
748
|
+
for (const [name, scheme] of Object.entries(securityDefinitions)) {
|
|
749
|
+
const schemePointer = appendPointer(securityDefinitionsPointer, name);
|
|
750
|
+
translated[name] = isObject(scheme) ? translateSwaggerSecurityScheme(scheme, diagnostics, schemePointer, name) : scheme;
|
|
751
|
+
}
|
|
672
752
|
components.securitySchemes = translated;
|
|
673
753
|
}
|
|
674
754
|
if (Object.keys(components).length > 0) result.components = components;
|
|
@@ -676,7 +756,7 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
|
|
|
676
756
|
if (isObject(doc.externalDocs)) result.externalDocs = doc.externalDocs;
|
|
677
757
|
if (Array.isArray(doc.security)) result.security = doc.security;
|
|
678
758
|
rewriteSwaggerRefs(result);
|
|
679
|
-
if (
|
|
759
|
+
if (documentContainsKeyword(doc.definitions, "xml") || documentContainsKeyword(doc.paths, "xml") || documentContainsKeyword(doc.parameters, "xml") || documentContainsKeyword(doc.responses, "xml")) emitDiagnostic(diagnostics, {
|
|
680
760
|
code: "dropped-swagger-feature",
|
|
681
761
|
message: "Swagger 2.0 xml markup is not supported and will be dropped",
|
|
682
762
|
pointer: "",
|
|
@@ -686,22 +766,13 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
|
|
|
686
766
|
}
|
|
687
767
|
function normaliseSwaggerPaths(paths, doc, diagnostics) {
|
|
688
768
|
const result = {};
|
|
689
|
-
const METHODS = [
|
|
690
|
-
"get",
|
|
691
|
-
"post",
|
|
692
|
-
"put",
|
|
693
|
-
"patch",
|
|
694
|
-
"delete",
|
|
695
|
-
"head",
|
|
696
|
-
"options"
|
|
697
|
-
];
|
|
698
769
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
699
770
|
if (!isObject(pathItem)) {
|
|
700
771
|
result[path] = pathItem;
|
|
701
772
|
continue;
|
|
702
773
|
}
|
|
703
774
|
const normalisedPath = {};
|
|
704
|
-
for (const method of
|
|
775
|
+
for (const method of SWAGGER_2_METHODS) {
|
|
705
776
|
const operation = pathItem[method];
|
|
706
777
|
if (!isObject(operation)) continue;
|
|
707
778
|
normalisedPath[method] = normaliseSwaggerOperation(operation, doc, path, method, diagnostics);
|
|
@@ -783,16 +854,34 @@ function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
|
|
|
783
854
|
}
|
|
784
855
|
if (nonBodyParams.length > 0) result.parameters = nonBodyParams;
|
|
785
856
|
if (bodyParam !== void 0) {
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
857
|
+
const operationPointer = appendPointer(appendPointer(appendPointer("", "paths"), path), method);
|
|
858
|
+
let bodyContentTypes;
|
|
859
|
+
if (usesFormData) bodyContentTypes = formDataContentTypes(consumes);
|
|
860
|
+
else if (consumesResolution.source === "synthesised") {
|
|
861
|
+
bodyContentTypes = consumes;
|
|
862
|
+
emitDiagnostic(diagnostics, {
|
|
863
|
+
code: "swagger-missing-consumes",
|
|
864
|
+
message: "Operation declares a body parameter but neither operation-level nor document-level `consumes` is set; defaulting to application/json",
|
|
865
|
+
pointer: operationPointer,
|
|
866
|
+
detail: {
|
|
867
|
+
level: "operation",
|
|
868
|
+
method
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
} else if (consumes.length === 0) {
|
|
872
|
+
bodyContentTypes = [];
|
|
873
|
+
emitDiagnostic(diagnostics, {
|
|
874
|
+
code: "swagger-missing-consumes",
|
|
875
|
+
message: "Operation declares a body parameter but `consumes` is an explicit empty array; preserving an empty content map",
|
|
876
|
+
pointer: operationPointer,
|
|
877
|
+
detail: {
|
|
878
|
+
level: "operation",
|
|
879
|
+
method,
|
|
880
|
+
reason: "explicitly-cleared",
|
|
881
|
+
source: consumesResolution.source
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
} else bodyContentTypes = consumes;
|
|
796
885
|
result.requestBody = buildRequestBody(bodyParam, bodyContentTypes);
|
|
797
886
|
}
|
|
798
887
|
}
|
|
@@ -895,34 +984,37 @@ function buildSchemaFromSwaggerParameterShape(node) {
|
|
|
895
984
|
* decide how to surface the failure. Non-ref parameters resolve to
|
|
896
985
|
* themselves; ref targets that don't exist also resolve to the input
|
|
897
986
|
* (the caller treats unknown refs the same as bare parameters).
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
987
|
+
*
|
|
988
|
+
* Uses the shared {@link resolveRefChain} helper so cycle detection
|
|
989
|
+
* and hop accounting stay consistent with the other OpenAPI $ref
|
|
990
|
+
* walkers.
|
|
991
|
+
*/
|
|
992
|
+
function resolveSwaggerParameter(param, doc) {
|
|
993
|
+
let cyclicRef;
|
|
994
|
+
const finalNode = resolveRefChain(param, {
|
|
995
|
+
extractRef: (node) => {
|
|
996
|
+
const ref = node.$ref;
|
|
997
|
+
if (typeof ref !== "string") return void 0;
|
|
998
|
+
return ref.startsWith("#/parameters/") ? ref : void 0;
|
|
999
|
+
},
|
|
1000
|
+
lookup: (ref) => {
|
|
1001
|
+
const name = ref.slice(13);
|
|
1002
|
+
const globalParams = doc.parameters;
|
|
1003
|
+
if (!isObject(globalParams)) return void 0;
|
|
1004
|
+
const resolved = globalParams[name];
|
|
1005
|
+
return isObject(resolved) ? resolved : void 0;
|
|
1006
|
+
},
|
|
1007
|
+
onCycle: (ref) => {
|
|
1008
|
+
cyclicRef = ref;
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
if (cyclicRef !== void 0) return {
|
|
906
1012
|
kind: "cycle",
|
|
907
|
-
ref
|
|
1013
|
+
ref: cyclicRef
|
|
908
1014
|
};
|
|
909
|
-
const nextVisited = new Set(visited);
|
|
910
|
-
nextVisited.add(ref);
|
|
911
|
-
const name = ref.slice(13);
|
|
912
|
-
const globalParams = doc.parameters;
|
|
913
|
-
if (isObject(globalParams)) {
|
|
914
|
-
const resolved = globalParams[name];
|
|
915
|
-
if (isObject(resolved)) {
|
|
916
|
-
if (typeof resolved.$ref === "string") return resolveSwaggerParameter(resolved, doc, nextVisited);
|
|
917
|
-
return {
|
|
918
|
-
kind: "ok",
|
|
919
|
-
param: resolved
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
1015
|
return {
|
|
924
1016
|
kind: "ok",
|
|
925
|
-
param
|
|
1017
|
+
param: finalNode ?? param
|
|
926
1018
|
};
|
|
927
1019
|
}
|
|
928
1020
|
/**
|
|
@@ -966,7 +1058,7 @@ function normaliseSwaggerParameter(param, doc, diagnostics, pointer = "") {
|
|
|
966
1058
|
const cf = param.collectionFormat;
|
|
967
1059
|
if (typeof cf === "string") switch (cf) {
|
|
968
1060
|
case "csv":
|
|
969
|
-
result.style = "form";
|
|
1061
|
+
result.style = param.in === "path" || param.in === "header" ? "simple" : "form";
|
|
970
1062
|
result.explode = false;
|
|
971
1063
|
break;
|
|
972
1064
|
case "ssv":
|
|
@@ -974,8 +1066,15 @@ function normaliseSwaggerParameter(param, doc, diagnostics, pointer = "") {
|
|
|
974
1066
|
result.explode = false;
|
|
975
1067
|
break;
|
|
976
1068
|
case "tsv":
|
|
977
|
-
|
|
978
|
-
|
|
1069
|
+
emitDiagnostic(diagnostics, {
|
|
1070
|
+
code: "swagger-collection-format-dropped",
|
|
1071
|
+
message: "Swagger 2.0 collectionFormat: \"tsv\" has no OpenAPI 3.x equivalent; dropping the keyword",
|
|
1072
|
+
pointer,
|
|
1073
|
+
detail: {
|
|
1074
|
+
feature: "collectionFormat:tsv",
|
|
1075
|
+
location: "parameter"
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
979
1078
|
break;
|
|
980
1079
|
case "pipes":
|
|
981
1080
|
result.style = "pipeDelimited";
|
|
@@ -1022,12 +1121,18 @@ function buildFormDataBody(param, allParams) {
|
|
|
1022
1121
|
}
|
|
1023
1122
|
/**
|
|
1024
1123
|
* Build an OpenAPI 3.x request body from a Swagger 2.0 body parameter.
|
|
1124
|
+
*
|
|
1125
|
+
* `consumes` is taken at face value — the caller is responsible for
|
|
1126
|
+
* deciding whether an absent value should fall back to a default
|
|
1127
|
+
* (and emitting `swagger-missing-consumes`) or be preserved as an
|
|
1128
|
+
* empty content map (an explicit clear). Inventing a default here
|
|
1129
|
+
* would mask the difference and silently override the upstream
|
|
1130
|
+
* resolution.
|
|
1025
1131
|
*/
|
|
1026
1132
|
function buildRequestBody(bodyParam, consumes) {
|
|
1027
1133
|
const schema = bodyParam.schema;
|
|
1028
1134
|
const content = {};
|
|
1029
|
-
const
|
|
1030
|
-
for (const ct of contentTypes) if (typeof ct === "string") content[ct] = isObject(schema) ? { schema } : {};
|
|
1135
|
+
for (const ct of consumes) if (typeof ct === "string") content[ct] = isObject(schema) ? { schema } : {};
|
|
1031
1136
|
const result = { content };
|
|
1032
1137
|
if (bodyParam.required === true) result.required = true;
|
|
1033
1138
|
if (typeof bodyParam.description === "string") result.description = bodyParam.description;
|
|
@@ -1035,23 +1140,28 @@ function buildRequestBody(bodyParam, consumes) {
|
|
|
1035
1140
|
}
|
|
1036
1141
|
/**
|
|
1037
1142
|
* Resolve a Swagger 2.0 response `$ref` (e.g. `#/responses/NotFound`).
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1143
|
+
*
|
|
1144
|
+
* Uses the shared {@link resolveRefChain} helper so cycle detection
|
|
1145
|
+
* and hop accounting stay consistent with the other OpenAPI $ref
|
|
1146
|
+
* walkers. On cycle the original wrapper is returned (legacy
|
|
1147
|
+
* behaviour), preserving the existing response-resolution contract.
|
|
1148
|
+
*/
|
|
1149
|
+
function resolveSwaggerResponse(response, doc) {
|
|
1150
|
+
return resolveRefChain(response, {
|
|
1151
|
+
extractRef: (node) => {
|
|
1152
|
+
const ref = node.$ref;
|
|
1153
|
+
if (typeof ref !== "string") return void 0;
|
|
1154
|
+
return ref.startsWith("#/responses/") ? ref : void 0;
|
|
1155
|
+
},
|
|
1156
|
+
lookup: (ref) => {
|
|
1157
|
+
const name = ref.slice(12);
|
|
1158
|
+
const globalResponses = doc.responses;
|
|
1159
|
+
if (!isObject(globalResponses)) return void 0;
|
|
1160
|
+
const resolved = globalResponses[name];
|
|
1161
|
+
return isObject(resolved) ? resolved : void 0;
|
|
1162
|
+
},
|
|
1163
|
+
onCycle: () => response
|
|
1164
|
+
}) ?? response;
|
|
1055
1165
|
}
|
|
1056
1166
|
function normaliseSwaggerResponses(responses, doc, produces, producesSource, diagnostics, path, method) {
|
|
1057
1167
|
const result = {};
|
|
@@ -1096,7 +1206,11 @@ function normaliseSwaggerSingleResponse(response, doc, produces, producesSource
|
|
|
1096
1206
|
const headers = resolved.headers;
|
|
1097
1207
|
if (isObject(headers)) {
|
|
1098
1208
|
const convertedHeaders = {};
|
|
1099
|
-
|
|
1209
|
+
const headersPointer = appendPointer(path !== void 0 && method !== void 0 && statusCode !== void 0 ? appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "responses"), statusCode) : "", "headers");
|
|
1210
|
+
for (const [name, header] of Object.entries(headers)) {
|
|
1211
|
+
const headerPointer = appendPointer(headersPointer, name);
|
|
1212
|
+
convertedHeaders[name] = isObject(header) ? normaliseSwaggerHeader(header, diagnostics, headerPointer) : header;
|
|
1213
|
+
}
|
|
1100
1214
|
normalised.headers = convertedHeaders;
|
|
1101
1215
|
}
|
|
1102
1216
|
return normalised;
|
|
@@ -1114,7 +1228,7 @@ function normaliseSwaggerSingleResponse(response, doc, produces, producesSource
|
|
|
1114
1228
|
* `simple`/`explode: false` rather than the `form` style used for query
|
|
1115
1229
|
* parameters.
|
|
1116
1230
|
*/
|
|
1117
|
-
function normaliseSwaggerHeader(header) {
|
|
1231
|
+
function normaliseSwaggerHeader(header, diagnostics, pointer = "") {
|
|
1118
1232
|
const result = {};
|
|
1119
1233
|
for (const [key, value] of Object.entries(header)) {
|
|
1120
1234
|
if (PARAM_KEYWORDS_LIFTED_INTO_SCHEMA.has(key)) continue;
|
|
@@ -1132,8 +1246,15 @@ function normaliseSwaggerHeader(header) {
|
|
|
1132
1246
|
result.explode = false;
|
|
1133
1247
|
break;
|
|
1134
1248
|
case "tsv":
|
|
1135
|
-
|
|
1136
|
-
|
|
1249
|
+
emitDiagnostic(diagnostics, {
|
|
1250
|
+
code: "swagger-collection-format-dropped",
|
|
1251
|
+
message: "Swagger 2.0 collectionFormat: \"tsv\" has no OpenAPI 3.x equivalent; dropping the keyword",
|
|
1252
|
+
pointer,
|
|
1253
|
+
detail: {
|
|
1254
|
+
feature: "collectionFormat:tsv",
|
|
1255
|
+
location: "header"
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1137
1258
|
break;
|
|
1138
1259
|
case "pipes":
|
|
1139
1260
|
result.style = "pipeDelimited";
|
|
@@ -1143,29 +1264,15 @@ function normaliseSwaggerHeader(header) {
|
|
|
1143
1264
|
return result;
|
|
1144
1265
|
}
|
|
1145
1266
|
/**
|
|
1146
|
-
* Mapping of Swagger 2.0 $ref prefixes to OpenAPI 3.x equivalents.
|
|
1147
|
-
* Applied after document restructuring so all $ref strings point
|
|
1148
|
-
* to the correct locations in the normalised document.
|
|
1149
|
-
*/
|
|
1150
|
-
const REF_REWRITES = [
|
|
1151
|
-
["#/definitions/", "#/components/schemas/"],
|
|
1152
|
-
["#/parameters/", "#/components/parameters/"],
|
|
1153
|
-
["#/responses/", "#/components/responses/"]
|
|
1154
|
-
];
|
|
1155
|
-
/**
|
|
1156
1267
|
* Deep-rewrite $ref strings in a normalised Swagger 2.0 document
|
|
1157
|
-
* from Swagger 2.0 locations to OpenAPI 3.x locations
|
|
1158
|
-
*
|
|
1159
|
-
* produced by
|
|
1268
|
+
* from Swagger 2.0 locations to OpenAPI 3.x locations using the
|
|
1269
|
+
* shared {@link rewriteSwaggerRefPrefix} mapping. Mutates the object
|
|
1270
|
+
* in place \u2014 called only on the fresh clone produced by
|
|
1271
|
+
* normaliseSwagger2Document.
|
|
1160
1272
|
*/
|
|
1161
1273
|
function rewriteSwaggerRefs(node) {
|
|
1162
1274
|
if (!isObject(node)) return;
|
|
1163
|
-
if (typeof node.$ref === "string")
|
|
1164
|
-
for (const [from, to] of REF_REWRITES) if (node.$ref.startsWith(from)) {
|
|
1165
|
-
node.$ref = to + node.$ref.slice(from.length);
|
|
1166
|
-
break;
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1275
|
+
if (typeof node.$ref === "string") node.$ref = rewriteSwaggerRefPrefix(node.$ref);
|
|
1169
1276
|
for (const value of Object.values(node)) if (isObject(value)) rewriteSwaggerRefs(value);
|
|
1170
1277
|
else if (Array.isArray(value)) for (const item of value) rewriteSwaggerRefs(item);
|
|
1171
1278
|
}
|
|
@@ -1195,7 +1302,7 @@ const SWAGGER_OAUTH_FLOW_RENAME = {
|
|
|
1195
1302
|
* (`unknown-security-scheme-type` diagnostic in the parser) handles
|
|
1196
1303
|
* those cases.
|
|
1197
1304
|
*/
|
|
1198
|
-
function translateSwaggerSecurityScheme(scheme) {
|
|
1305
|
+
function translateSwaggerSecurityScheme(scheme, diagnostics, pointer = "", name) {
|
|
1199
1306
|
const type = scheme.type;
|
|
1200
1307
|
if (type === "basic") {
|
|
1201
1308
|
const result = {
|
|
@@ -1207,10 +1314,21 @@ function translateSwaggerSecurityScheme(scheme) {
|
|
|
1207
1314
|
}
|
|
1208
1315
|
if (type === "oauth2") {
|
|
1209
1316
|
const flowName = scheme.flow;
|
|
1210
|
-
if (typeof flowName !== "string")
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1317
|
+
if (typeof flowName !== "string") {
|
|
1318
|
+
emitDiagnostic(diagnostics, {
|
|
1319
|
+
code: "swagger-malformed-oauth-flow",
|
|
1320
|
+
message: `Swagger 2.0 oauth2 security scheme${name !== void 0 ? ` "${name}"` : ""} is missing the required \`flow\` field; preserving the original shape verbatim`,
|
|
1321
|
+
pointer,
|
|
1322
|
+
detail: {
|
|
1323
|
+
name,
|
|
1324
|
+
flow: flowName
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
return {
|
|
1328
|
+
...scheme,
|
|
1329
|
+
type: "oauth2"
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1214
1332
|
const renamedFlow = SWAGGER_OAUTH_FLOW_RENAME[flowName] ?? flowName;
|
|
1215
1333
|
const flowBody = {};
|
|
1216
1334
|
if (typeof scheme.authorizationUrl === "string") flowBody.authorizationUrl = scheme.authorizationUrl;
|
|
@@ -1227,24 +1345,6 @@ function translateSwaggerSecurityScheme(scheme) {
|
|
|
1227
1345
|
}
|
|
1228
1346
|
return { ...scheme };
|
|
1229
1347
|
}
|
|
1230
|
-
/**
|
|
1231
|
-
* Recursively check whether any node in the supplied subtree carries an
|
|
1232
|
-
* `xml` annotation. Walks both objects and arrays so the check works for
|
|
1233
|
-
* schemas (definitions, parameter schemas, response schemas, request body
|
|
1234
|
-
* schemas) as well as operations and parameters that may carry `xml`
|
|
1235
|
-
* metadata at any depth.
|
|
1236
|
-
*/
|
|
1237
|
-
function hasXmlAnywhere(node) {
|
|
1238
|
-
if (!isObject(node)) {
|
|
1239
|
-
if (Array.isArray(node)) {
|
|
1240
|
-
for (const item of node) if (hasXmlAnywhere(item)) return true;
|
|
1241
|
-
}
|
|
1242
|
-
return false;
|
|
1243
|
-
}
|
|
1244
|
-
if ("xml" in node) return true;
|
|
1245
|
-
for (const value of Object.values(node)) if (hasXmlAnywhere(value)) return true;
|
|
1246
|
-
return false;
|
|
1247
|
-
}
|
|
1248
1348
|
//#endregion
|
|
1249
1349
|
//#region src/core/normalise.ts
|
|
1250
1350
|
/**
|
|
@@ -1432,17 +1532,24 @@ function collectDependencyStrings(items, property, keyword, ctx) {
|
|
|
1432
1532
|
* After splitting, `dependencies` is removed from the node.
|
|
1433
1533
|
*
|
|
1434
1534
|
* When `ctx` is supplied, diagnostics are emitted for:
|
|
1435
|
-
* - `
|
|
1436
|
-
* deprecated keyword
|
|
1437
|
-
* the
|
|
1535
|
+
* - The supplied `legacyDiagnostic` code (if any) once per node that
|
|
1536
|
+
* contained the deprecated keyword. Callers pass
|
|
1537
|
+
* `legacy-dependencies-split-2019` on the 2019-09 path (the draft
|
|
1538
|
+
* that introduced the split — authors should already have migrated)
|
|
1539
|
+
* and `legacy-dependencies-split` on the defensive 2020-12 path.
|
|
1438
1540
|
* - `dependent-required-invalid` for each array entry whose element is
|
|
1439
1541
|
* not a string.
|
|
1542
|
+
* - `dependencies-conflict` when both the legacy `dependencies` keyword
|
|
1543
|
+
* and a pre-existing `dependentRequired`/`dependentSchemas` declare
|
|
1544
|
+
* the same key with different values. The pre-existing modern
|
|
1545
|
+
* keyword wins and the legacy value is discarded — silently
|
|
1546
|
+
* overwriting would mask the author's bug.
|
|
1440
1547
|
*/
|
|
1441
|
-
function splitDependencies(node, ctx,
|
|
1548
|
+
function splitDependencies(node, ctx, legacyDiagnostic) {
|
|
1442
1549
|
const deps = node.dependencies;
|
|
1443
1550
|
if (!isObject(deps)) return;
|
|
1444
|
-
if (
|
|
1445
|
-
code:
|
|
1551
|
+
if (legacyDiagnostic !== void 0 && ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
|
|
1552
|
+
code: legacyDiagnostic,
|
|
1446
1553
|
message: "Legacy `dependencies` keyword was split into `dependentRequired`/`dependentSchemas`; `dependencies` was deprecated in Draft 2019-09",
|
|
1447
1554
|
pointer: appendPointer(ctx.pointer, "dependencies"),
|
|
1448
1555
|
detail: { keys: Object.keys(deps) }
|
|
@@ -1455,17 +1562,61 @@ function splitDependencies(node, ctx, emitLegacyDiagnostic) {
|
|
|
1455
1562
|
} else if (isObject(value)) schemaEntries[key] = value;
|
|
1456
1563
|
if (Object.keys(requiredEntries).length > 0) {
|
|
1457
1564
|
const existing = node.dependentRequired;
|
|
1458
|
-
if (isObject(existing)) for (const [k, v] of Object.entries(requiredEntries))
|
|
1565
|
+
if (isObject(existing)) for (const [k, v] of Object.entries(requiredEntries)) {
|
|
1566
|
+
if (k in existing) {
|
|
1567
|
+
if (ctx !== void 0 && !arraysEqual(existing[k], v)) emitDiagnostic(ctx.diagnostics, {
|
|
1568
|
+
code: "dependencies-conflict",
|
|
1569
|
+
message: `Legacy \`dependencies.${k}\` conflicts with the pre-existing \`dependentRequired.${k}\`; keeping the modern keyword and discarding the legacy value`,
|
|
1570
|
+
pointer: appendPointer(ctx.pointer, "dependencies"),
|
|
1571
|
+
detail: {
|
|
1572
|
+
key: k,
|
|
1573
|
+
kept: existing[k],
|
|
1574
|
+
discarded: v,
|
|
1575
|
+
keyword: "dependentRequired"
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
existing[k] = v;
|
|
1581
|
+
}
|
|
1459
1582
|
else node.dependentRequired = requiredEntries;
|
|
1460
1583
|
}
|
|
1461
1584
|
if (Object.keys(schemaEntries).length > 0) {
|
|
1462
1585
|
const existing = node.dependentSchemas;
|
|
1463
|
-
if (isObject(existing)) for (const [k, v] of Object.entries(schemaEntries))
|
|
1586
|
+
if (isObject(existing)) for (const [k, v] of Object.entries(schemaEntries)) {
|
|
1587
|
+
if (k in existing) {
|
|
1588
|
+
if (ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
|
|
1589
|
+
code: "dependencies-conflict",
|
|
1590
|
+
message: `Legacy \`dependencies.${k}\` conflicts with the pre-existing \`dependentSchemas.${k}\`; keeping the modern keyword and discarding the legacy value`,
|
|
1591
|
+
pointer: appendPointer(ctx.pointer, "dependencies"),
|
|
1592
|
+
detail: {
|
|
1593
|
+
key: k,
|
|
1594
|
+
kept: existing[k],
|
|
1595
|
+
discarded: v,
|
|
1596
|
+
keyword: "dependentSchemas"
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
existing[k] = v;
|
|
1602
|
+
}
|
|
1464
1603
|
else node.dependentSchemas = schemaEntries;
|
|
1465
1604
|
}
|
|
1466
1605
|
delete node.dependencies;
|
|
1467
1606
|
}
|
|
1468
1607
|
/**
|
|
1608
|
+
* Compare two values that should both be string arrays. Returns true
|
|
1609
|
+
* when both are arrays of equal length holding the same strings in
|
|
1610
|
+
* the same order. Used to skip the `dependencies-conflict` diagnostic
|
|
1611
|
+
* when the legacy and modern keywords agree.
|
|
1612
|
+
*/
|
|
1613
|
+
function arraysEqual(a, b) {
|
|
1614
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
1615
|
+
if (a.length !== b.length) return false;
|
|
1616
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
1617
|
+
return true;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1469
1620
|
* Emit diagnostics for any non-string entries inside a pre-existing
|
|
1470
1621
|
* `dependentRequired` keyword. Used on draft paths where the author may
|
|
1471
1622
|
* have already migrated to the Draft 2019-09 form but still produced
|
|
@@ -1563,7 +1714,7 @@ function applyDraft04Translations(node, ctx) {
|
|
|
1563
1714
|
delete node.id;
|
|
1564
1715
|
}
|
|
1565
1716
|
translateTupleItems(node);
|
|
1566
|
-
splitDependencies(node, ctx,
|
|
1717
|
+
splitDependencies(node, ctx, void 0);
|
|
1567
1718
|
validateDependentRequired(node, ctx);
|
|
1568
1719
|
}
|
|
1569
1720
|
/**
|
|
@@ -1616,7 +1767,7 @@ function normaliseDraft04NodeWithContext(node, ctx) {
|
|
|
1616
1767
|
*/
|
|
1617
1768
|
function normaliseDraft06Or07NodeWithContext(node, ctx) {
|
|
1618
1769
|
translateTupleItems(node);
|
|
1619
|
-
splitDependencies(node, ctx,
|
|
1770
|
+
splitDependencies(node, ctx, void 0);
|
|
1620
1771
|
validateDependentRequired(node, ctx);
|
|
1621
1772
|
return node;
|
|
1622
1773
|
}
|
|
@@ -1635,6 +1786,12 @@ function normaliseDraft06Or07NodeWithContext(node, ctx) {
|
|
|
1635
1786
|
* `$recursiveAnchor` names are likewise preserved as `$anchor`.
|
|
1636
1787
|
*/
|
|
1637
1788
|
function normaliseDraft201909NodeWithContext(node, ctx) {
|
|
1789
|
+
if (node.$anchor === "__recursive__" && node.$recursiveAnchor !== true) emitDiagnostic(ctx.diagnostics, {
|
|
1790
|
+
code: "recursive-anchor-collision",
|
|
1791
|
+
message: `User-supplied \`$anchor: "${RECURSIVE_ANCHOR_SENTINEL}"\` collides with the canonical sentinel synthesised by the Draft 2019-09 \`$recursiveAnchor: true\` rewrite; lookups for this anchor may resolve to either declaration`,
|
|
1792
|
+
pointer: appendPointer(ctx.pointer, "$anchor"),
|
|
1793
|
+
detail: { anchor: RECURSIVE_ANCHOR_SENTINEL }
|
|
1794
|
+
});
|
|
1638
1795
|
const recursiveRef = node.$recursiveRef;
|
|
1639
1796
|
if (typeof recursiveRef === "string") {
|
|
1640
1797
|
if (!recursiveRef.startsWith("#")) emitDiagnostic(ctx.diagnostics, {
|
|
@@ -1650,13 +1807,13 @@ function normaliseDraft201909NodeWithContext(node, ctx) {
|
|
|
1650
1807
|
delete node.$recursiveRef;
|
|
1651
1808
|
}
|
|
1652
1809
|
if (node.$recursiveAnchor === true) {
|
|
1653
|
-
if (typeof node.$anchor !== "string") node.$anchor =
|
|
1810
|
+
if (typeof node.$anchor !== "string") node.$anchor = RECURSIVE_ANCHOR_SENTINEL;
|
|
1654
1811
|
delete node.$recursiveAnchor;
|
|
1655
1812
|
} else if (typeof node.$recursiveAnchor === "string") {
|
|
1656
1813
|
if (typeof node.$anchor !== "string") node.$anchor = node.$recursiveAnchor;
|
|
1657
1814
|
delete node.$recursiveAnchor;
|
|
1658
1815
|
}
|
|
1659
|
-
splitDependencies(node, ctx,
|
|
1816
|
+
splitDependencies(node, ctx, "legacy-dependencies-split-2019");
|
|
1660
1817
|
validateDependentRequired(node, ctx);
|
|
1661
1818
|
return node;
|
|
1662
1819
|
}
|
|
@@ -1680,7 +1837,8 @@ function normaliseDynamicRefNodeWithContext(node, ctx) {
|
|
|
1680
1837
|
if (typeof node.$anchor !== "string") node.$anchor = node.$dynamicAnchor;
|
|
1681
1838
|
delete node.$dynamicAnchor;
|
|
1682
1839
|
}
|
|
1683
|
-
|
|
1840
|
+
translateTupleItems(node);
|
|
1841
|
+
splitDependencies(node, ctx, "legacy-dependencies-split");
|
|
1684
1842
|
validateDependentRequired(node, ctx);
|
|
1685
1843
|
return node;
|
|
1686
1844
|
}
|
|
@@ -1948,9 +2106,41 @@ function normaliseOpenApiSchemas(doc, version, diagnostics) {
|
|
|
1948
2106
|
}
|
|
1949
2107
|
return deepNormaliseOpenApiDoc(applyDiscriminatorAllOfPrepass(doc), (schema) => {
|
|
1950
2108
|
let intermediate = schema;
|
|
1951
|
-
|
|
2109
|
+
const effectiveDraft = resolveSchemaLocalDialect(intermediate, diagnostics) ?? dialectDraft;
|
|
2110
|
+
if (effectiveDraft !== void 0 && effectiveDraft !== "draft-2020-12") intermediate = deepNormaliseWithContext(intermediate, selectDraftTransform(effectiveDraft), buildRootContext(intermediate, diagnostics, effectiveDraft));
|
|
1952
2111
|
return resolveRelativeRefs(deepNormalise(intermediate, normaliseOpenApi30Discriminator), diagnostics);
|
|
1953
2112
|
});
|
|
1954
2113
|
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Resolve a Schema-Object-level `$schema` declaration to a
|
|
2116
|
+
* `JsonSchemaDraft`. Returns `undefined` when the keyword is absent or
|
|
2117
|
+
* its value is not a string; emits `unknown-json-schema-dialect` and
|
|
2118
|
+
* returns `undefined` when the value is a string that does not match
|
|
2119
|
+
* any supported draft URI. The caller falls back to the document-level
|
|
2120
|
+
* dialect (or 2020-12) in that case.
|
|
2121
|
+
*
|
|
2122
|
+
* Per OpenAPI 3.1 §4.7.5, the override applies to the Schema Object
|
|
2123
|
+
* and its subschemas. Nested overrides inside individual sub-schemas
|
|
2124
|
+
* are not currently honoured — the dispatch happens once at the top
|
|
2125
|
+
* of each Schema Object surfaced by `deepNormaliseOpenApiDoc`.
|
|
2126
|
+
*/
|
|
2127
|
+
function resolveSchemaLocalDialect(schema, diagnostics) {
|
|
2128
|
+
const value = schema.$schema;
|
|
2129
|
+
if (typeof value !== "string") return void 0;
|
|
2130
|
+
const draft = matchJsonSchemaDraftUri(value);
|
|
2131
|
+
if (draft === void 0) {
|
|
2132
|
+
emitDiagnostic(diagnostics, {
|
|
2133
|
+
code: "unknown-json-schema-dialect",
|
|
2134
|
+
message: `Schema Object-level \`$schema\` URI "${value}" does not match a supported JSON Schema draft; falling back to the document dialect`,
|
|
2135
|
+
pointer: "/$schema",
|
|
2136
|
+
detail: {
|
|
2137
|
+
uri: value,
|
|
2138
|
+
level: "schema-object"
|
|
2139
|
+
}
|
|
2140
|
+
});
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
return draft;
|
|
2144
|
+
}
|
|
1955
2145
|
//#endregion
|
|
1956
|
-
export {
|
|
2146
|
+
export { normaliseJsonSchema as a, normaliseSwagger2Document as c, deepNormaliseOpenApiDoc as d, liftExampleToExamples as f, normaliseOpenApi30Node as h, normaliseDraft04Node as i, applyDiscriminatorAllOfPrepass as l, normaliseOpenApi30Discriminator as m, deepNormaliseWithContext as n, normaliseOpenApiSchemas as o, normaliseOpenApi30Combined as p, documentContainsKeyword as r, selectDraftTransform as s, deepNormalise as t, deepNormaliseOpenApi30Doc as u };
|