schema-components 1.21.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 +115 -4
- package/dist/core/adapter.mjs +405 -75
- 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 +30 -2
- package/dist/core/formats.mjs +33 -1
- package/dist/core/idPath.d.mts +54 -0
- package/dist/core/idPath.mjs +66 -0
- package/dist/core/limits.d.mts +2 -0
- package/dist/core/limits.mjs +23 -0
- package/dist/core/merge.d.mts +10 -1
- package/dist/core/merge.mjs +49 -10
- package/dist/core/normalise.d.mts +40 -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 +85 -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/renderer.mjs +0 -2
- 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 +2 -2
- package/dist/core/types.mjs +1 -4
- 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 +123 -47
- package/dist/{diagnostics-CbBPsxSt.d.mts → diagnostics-BS2kaUyE.d.mts} +1 -1
- package/dist/{errors-QEwOtQAA.d.mts → errors-g_MCTQel.d.mts} +10 -16
- package/dist/html/a11y.d.mts +9 -4
- package/dist/html/a11y.mjs +10 -12
- package/dist/html/renderToHtml.d.mts +10 -3
- package/dist/html/renderToHtml.mjs +13 -3
- 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 -116
- package/dist/html/streamRenderers.d.mts +6 -6
- package/dist/html/streamRenderers.mjs +129 -89
- package/dist/limits-Cw5QZND8.d.mts +29 -0
- package/dist/{normalise-DaSrnr8g.mjs → normalise-DCYp06Sr.mjs} +770 -227
- 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/ApiSecurity.mjs +16 -2
- package/dist/openapi/components.d.mts +234 -23
- package/dist/openapi/components.mjs +183 -52
- package/dist/openapi/parser.d.mts +9 -8
- package/dist/openapi/parser.mjs +252 -70
- package/dist/openapi/resolve.d.mts +31 -15
- package/dist/openapi/resolve.mjs +260 -40
- package/dist/react/SchemaComponent.d.mts +126 -36
- package/dist/react/SchemaComponent.mjs +95 -57
- package/dist/react/SchemaView.d.mts +30 -10
- package/dist/react/SchemaView.mjs +2 -2
- 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/headless.mjs +1 -2
- package/dist/react/headlessRenderers.d.mts +9 -11
- package/dist/react/headlessRenderers.mjs +51 -102
- package/dist/{ref-si8ViYun.d.mts → ref-DjLEKa_E.d.mts} +38 -3
- package/dist/{renderer-DI6ZYf7a.d.mts → renderer-CXJ8y0qw.d.mts} +2 -2
- 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-BnxPEElk.d.mts → types-BTB73MB8.d.mts} +35 -14
- package/dist/{version-D-u7aMfy.d.mts → version-BFTVLsdb.d.mts} +7 -1
- package/package.json +1 -3
- package/dist/typeInference-Bxw3NOG1.d.mts +0 -647
|
@@ -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,22 +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" };
|
|
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
|
+
}
|
|
32
66
|
if (Array.isArray(node.anyOf)) {
|
|
33
|
-
|
|
67
|
+
const existing = node.anyOf;
|
|
68
|
+
node.anyOf = compositeAlreadyAllowsNull(existing) ? existing : [...existing, nullOption];
|
|
34
69
|
delete node.nullable;
|
|
35
70
|
return node;
|
|
36
71
|
}
|
|
37
72
|
if (Array.isArray(node.oneOf)) {
|
|
38
|
-
|
|
73
|
+
const existing = node.oneOf;
|
|
74
|
+
node.anyOf = compositeAlreadyAllowsNull(existing) ? existing : [...existing, nullOption];
|
|
39
75
|
delete node.oneOf;
|
|
40
76
|
delete node.nullable;
|
|
41
77
|
return node;
|
|
@@ -51,6 +87,39 @@ function normaliseOpenApi30Node(node) {
|
|
|
51
87
|
return { anyOf: [wrapper, nullOption] };
|
|
52
88
|
}
|
|
53
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
|
+
/**
|
|
54
123
|
* Normalise OpenAPI 3.0.x `discriminator` keyword by injecting `const`
|
|
55
124
|
* values into each `oneOf`/`anyOf` option's discriminator property.
|
|
56
125
|
*
|
|
@@ -78,7 +147,7 @@ function normaliseOpenApi30Discriminator(node) {
|
|
|
78
147
|
normalisedComposite.push(option);
|
|
79
148
|
continue;
|
|
80
149
|
}
|
|
81
|
-
const props = isObject(option.properties) ?
|
|
150
|
+
const props = isObject(option.properties) ? option.properties : void 0;
|
|
82
151
|
const discProp = props?.[propertyName];
|
|
83
152
|
if (isObject(discProp) && "const" in discProp) {
|
|
84
153
|
normalisedComposite.push(option);
|
|
@@ -100,7 +169,7 @@ function normaliseOpenApi30Discriminator(node) {
|
|
|
100
169
|
if (entry !== void 0) constValue = entry[0];
|
|
101
170
|
}
|
|
102
171
|
if (constValue !== void 0) {
|
|
103
|
-
const normalisedProps = props
|
|
172
|
+
const normalisedProps = { ...props };
|
|
104
173
|
normalisedProps[propertyName] = {
|
|
105
174
|
...isObject(discProp) ? discProp : {},
|
|
106
175
|
const: constValue
|
|
@@ -113,7 +182,13 @@ function normaliseOpenApi30Discriminator(node) {
|
|
|
113
182
|
}
|
|
114
183
|
if ("oneOf" in node) node.oneOf = normalisedComposite;
|
|
115
184
|
else if ("anyOf" in node) node.anyOf = normalisedComposite;
|
|
116
|
-
|
|
185
|
+
const extensions = {};
|
|
186
|
+
for (const [key, value] of Object.entries(discriminator)) if (key.startsWith("x-")) extensions[key] = value;
|
|
187
|
+
if (Object.keys(extensions).length > 0) node.discriminator = {
|
|
188
|
+
propertyName,
|
|
189
|
+
...extensions
|
|
190
|
+
};
|
|
191
|
+
else delete node.discriminator;
|
|
117
192
|
return node;
|
|
118
193
|
}
|
|
119
194
|
/**
|
|
@@ -479,10 +554,7 @@ function normaliseParameter(param, normaliseSchema) {
|
|
|
479
554
|
if (isObject(schema)) result.schema = normaliseSchema(schema);
|
|
480
555
|
const content = param.content;
|
|
481
556
|
if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
|
|
482
|
-
|
|
483
|
-
result.examples = [result.example];
|
|
484
|
-
delete result.example;
|
|
485
|
-
} else if ("example" in result) delete result.example;
|
|
557
|
+
liftExampleToExamples(result, "map");
|
|
486
558
|
return result;
|
|
487
559
|
}
|
|
488
560
|
function normaliseRequestBody(requestBody, normaliseSchema) {
|
|
@@ -505,10 +577,7 @@ function normaliseHeader(header, normaliseSchema) {
|
|
|
505
577
|
if (isObject(schema)) result.schema = normaliseSchema(schema);
|
|
506
578
|
const content = header.content;
|
|
507
579
|
if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
|
|
508
|
-
|
|
509
|
-
result.examples = [result.example];
|
|
510
|
-
delete result.example;
|
|
511
|
-
} else if ("example" in result) delete result.example;
|
|
580
|
+
liftExampleToExamples(result, "map");
|
|
512
581
|
return result;
|
|
513
582
|
}
|
|
514
583
|
/**
|
|
@@ -531,10 +600,7 @@ function normaliseContentMap(content, normaliseSchema) {
|
|
|
531
600
|
if (isObject(schema)) normalised.schema = normaliseSchema(schema);
|
|
532
601
|
const encoding = mediaObj.encoding;
|
|
533
602
|
if (isObject(encoding)) normalised.encoding = mapObjectValues(encoding, (enc) => isObject(enc) ? normaliseEncoding(enc, normaliseSchema) : enc);
|
|
534
|
-
|
|
535
|
-
normalised.examples = { default: { value: normalised.example } };
|
|
536
|
-
delete normalised.example;
|
|
537
|
-
} else if ("example" in normalised) delete normalised.example;
|
|
603
|
+
liftExampleToExamples(normalised, "map");
|
|
538
604
|
result[mediaType] = normalised;
|
|
539
605
|
}
|
|
540
606
|
return result;
|
|
@@ -579,9 +645,19 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
|
|
|
579
645
|
version: "0.0.0"
|
|
580
646
|
}
|
|
581
647
|
};
|
|
582
|
-
if (typeof doc.host
|
|
583
|
-
|
|
584
|
-
|
|
648
|
+
if (typeof doc.host !== "string") {
|
|
649
|
+
if (Array.isArray(doc.schemes) || typeof doc.basePath === "string") emitDiagnostic(diagnostics, {
|
|
650
|
+
code: "swagger-missing-host",
|
|
651
|
+
message: "Swagger 2.0 document declares schemes or basePath without host; skipping server URL synthesis",
|
|
652
|
+
pointer: "",
|
|
653
|
+
detail: {
|
|
654
|
+
hasSchemes: Array.isArray(doc.schemes),
|
|
655
|
+
hasBasePath: typeof doc.basePath === "string"
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
} else {
|
|
659
|
+
const host = doc.host;
|
|
660
|
+
const basePath = typeof doc.basePath === "string" ? doc.basePath : "";
|
|
585
661
|
const schemes = Array.isArray(doc.schemes) ? doc.schemes : ["https"];
|
|
586
662
|
result.servers = [{ url: `${typeof schemes[0] === "string" ? schemes[0] : "https"}://${host}${basePath}` }];
|
|
587
663
|
}
|
|
@@ -597,37 +673,90 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
|
|
|
597
673
|
const parameters = doc.parameters;
|
|
598
674
|
const requestBodies = {};
|
|
599
675
|
if (isObject(parameters)) {
|
|
600
|
-
const
|
|
676
|
+
const consumesResolution = resolveSwaggerContentTypes(void 0, doc.consumes);
|
|
677
|
+
const globalConsumes = consumesResolution.types;
|
|
601
678
|
const convertedParameters = {};
|
|
602
679
|
for (const [name, param] of Object.entries(parameters)) {
|
|
603
680
|
if (!isObject(param)) {
|
|
604
681
|
convertedParameters[name] = param;
|
|
605
682
|
continue;
|
|
606
683
|
}
|
|
607
|
-
const
|
|
684
|
+
const resolution = resolveSwaggerParameter(param, doc);
|
|
685
|
+
if (resolution.kind === "cycle") {
|
|
686
|
+
emitDiagnostic(diagnostics, {
|
|
687
|
+
code: "swagger-cyclic-parameter-ref",
|
|
688
|
+
message: `Cyclic Swagger 2.0 parameter $ref "${resolution.ref}"; skipping entry`,
|
|
689
|
+
pointer: appendPointer(appendPointer("", "parameters"), name),
|
|
690
|
+
detail: {
|
|
691
|
+
ref: resolution.ref,
|
|
692
|
+
name
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const resolved = resolution.param;
|
|
608
698
|
const location = resolved.in;
|
|
609
|
-
if (location === "body")
|
|
610
|
-
|
|
611
|
-
|
|
699
|
+
if (location === "body") {
|
|
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);
|
|
728
|
+
} else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]), formDataContentTypes(globalConsumes));
|
|
729
|
+
else {
|
|
730
|
+
const normalised = normaliseSwaggerParameter(resolved, doc, diagnostics, appendPointer(appendPointer("", "parameters"), name));
|
|
731
|
+
if (normalised !== void 0) convertedParameters[name] = normalised;
|
|
732
|
+
}
|
|
612
733
|
}
|
|
613
734
|
if (Object.keys(convertedParameters).length > 0) components.parameters = convertedParameters;
|
|
614
735
|
}
|
|
615
736
|
const responses = doc.responses;
|
|
616
737
|
if (isObject(responses)) {
|
|
617
|
-
const
|
|
738
|
+
const producesResolution = resolveSwaggerContentTypes(void 0, doc.produces);
|
|
618
739
|
const convertedResponses = {};
|
|
619
|
-
for (const [name, response] of Object.entries(responses)) convertedResponses[name] = isObject(response) ? normaliseSwaggerSingleResponse(response, doc,
|
|
740
|
+
for (const [name, response] of Object.entries(responses)) convertedResponses[name] = isObject(response) ? normaliseSwaggerSingleResponse(response, doc, producesResolution.types, producesResolution.source, diagnostics, void 0, void 0, name) : response;
|
|
620
741
|
components.responses = convertedResponses;
|
|
621
742
|
}
|
|
622
743
|
if (Object.keys(requestBodies).length > 0) components.requestBodies = requestBodies;
|
|
623
744
|
const securityDefinitions = doc.securityDefinitions;
|
|
624
|
-
if (isObject(securityDefinitions))
|
|
745
|
+
if (isObject(securityDefinitions)) {
|
|
746
|
+
const translated = {};
|
|
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
|
+
}
|
|
752
|
+
components.securitySchemes = translated;
|
|
753
|
+
}
|
|
625
754
|
if (Object.keys(components).length > 0) result.components = components;
|
|
626
755
|
if (Array.isArray(doc.tags)) result.tags = doc.tags;
|
|
627
756
|
if (isObject(doc.externalDocs)) result.externalDocs = doc.externalDocs;
|
|
628
757
|
if (Array.isArray(doc.security)) result.security = doc.security;
|
|
629
758
|
rewriteSwaggerRefs(result);
|
|
630
|
-
if (
|
|
759
|
+
if (documentContainsKeyword(doc.definitions, "xml") || documentContainsKeyword(doc.paths, "xml") || documentContainsKeyword(doc.parameters, "xml") || documentContainsKeyword(doc.responses, "xml")) emitDiagnostic(diagnostics, {
|
|
631
760
|
code: "dropped-swagger-feature",
|
|
632
761
|
message: "Swagger 2.0 xml markup is not supported and will be dropped",
|
|
633
762
|
pointer: "",
|
|
@@ -637,38 +766,41 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
|
|
|
637
766
|
}
|
|
638
767
|
function normaliseSwaggerPaths(paths, doc, diagnostics) {
|
|
639
768
|
const result = {};
|
|
640
|
-
const METHODS = [
|
|
641
|
-
"get",
|
|
642
|
-
"post",
|
|
643
|
-
"put",
|
|
644
|
-
"patch",
|
|
645
|
-
"delete",
|
|
646
|
-
"head",
|
|
647
|
-
"options"
|
|
648
|
-
];
|
|
649
769
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
650
770
|
if (!isObject(pathItem)) {
|
|
651
771
|
result[path] = pathItem;
|
|
652
772
|
continue;
|
|
653
773
|
}
|
|
654
774
|
const normalisedPath = {};
|
|
655
|
-
for (const method of
|
|
775
|
+
for (const method of SWAGGER_2_METHODS) {
|
|
656
776
|
const operation = pathItem[method];
|
|
657
777
|
if (!isObject(operation)) continue;
|
|
658
778
|
normalisedPath[method] = normaliseSwaggerOperation(operation, doc, path, method, diagnostics);
|
|
659
779
|
}
|
|
660
780
|
const pathParams = pathItem.parameters;
|
|
661
|
-
if (Array.isArray(pathParams))
|
|
781
|
+
if (Array.isArray(pathParams)) {
|
|
782
|
+
const paramsPointer = appendPointer(appendPointer(appendPointer("", "paths"), path), "parameters");
|
|
783
|
+
const out = [];
|
|
784
|
+
for (const [index, p] of pathParams.entries()) {
|
|
785
|
+
if (!isObject(p)) {
|
|
786
|
+
out.push(p);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const normalised = normaliseSwaggerParameter(p, doc, diagnostics, appendPointer(paramsPointer, String(index)));
|
|
790
|
+
if (normalised !== void 0) out.push(normalised);
|
|
791
|
+
}
|
|
792
|
+
normalisedPath.parameters = out;
|
|
793
|
+
}
|
|
662
794
|
result[path] = normalisedPath;
|
|
663
795
|
}
|
|
664
796
|
return result;
|
|
665
797
|
}
|
|
666
798
|
function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
|
|
667
799
|
const result = {};
|
|
668
|
-
const
|
|
669
|
-
const
|
|
670
|
-
const produces =
|
|
671
|
-
const consumes =
|
|
800
|
+
const consumesResolution = resolveSwaggerContentTypes(operation.consumes, doc.consumes);
|
|
801
|
+
const producesResolution = resolveSwaggerContentTypes(operation.produces, doc.produces);
|
|
802
|
+
const produces = producesResolution.types;
|
|
803
|
+
const consumes = consumesResolution.types;
|
|
672
804
|
for (const [key, value] of Object.entries(operation)) if (key !== "parameters" && key !== "responses" && key !== "produces" && key !== "consumes") result[key] = value;
|
|
673
805
|
const params = operation.parameters;
|
|
674
806
|
if (Array.isArray(params)) {
|
|
@@ -681,7 +813,17 @@ function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
|
|
|
681
813
|
nonBodyParams.push(param);
|
|
682
814
|
continue;
|
|
683
815
|
}
|
|
684
|
-
const
|
|
816
|
+
const paramResolution = resolveSwaggerParameter(param, doc);
|
|
817
|
+
if (paramResolution.kind === "cycle") {
|
|
818
|
+
emitDiagnostic(diagnostics, {
|
|
819
|
+
code: "swagger-cyclic-parameter-ref",
|
|
820
|
+
message: `Cyclic Swagger 2.0 parameter $ref "${paramResolution.ref}"; skipping entry`,
|
|
821
|
+
pointer: appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "parameters"), String(index)),
|
|
822
|
+
detail: { ref: paramResolution.ref }
|
|
823
|
+
});
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
const resolvedParam = paramResolution.param;
|
|
685
827
|
const location = resolvedParam.in;
|
|
686
828
|
if (location === "body") {
|
|
687
829
|
if (bodyParam !== void 0) {
|
|
@@ -705,16 +847,69 @@ function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
|
|
|
705
847
|
bodyParam = buildFormDataBody(resolvedParam, params);
|
|
706
848
|
usesFormData = true;
|
|
707
849
|
}
|
|
708
|
-
} else
|
|
850
|
+
} else {
|
|
851
|
+
const normalised = normaliseSwaggerParameter(resolvedParam, doc, diagnostics, appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "parameters"), String(index)));
|
|
852
|
+
if (normalised !== void 0) nonBodyParams.push(normalised);
|
|
853
|
+
}
|
|
709
854
|
}
|
|
710
855
|
if (nonBodyParams.length > 0) result.parameters = nonBodyParams;
|
|
711
|
-
if (bodyParam !== void 0)
|
|
856
|
+
if (bodyParam !== void 0) {
|
|
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;
|
|
885
|
+
result.requestBody = buildRequestBody(bodyParam, bodyContentTypes);
|
|
886
|
+
}
|
|
712
887
|
}
|
|
713
888
|
const responses = operation.responses;
|
|
714
|
-
if (isObject(responses)) result.responses = normaliseSwaggerResponses(responses, doc, produces);
|
|
889
|
+
if (isObject(responses)) result.responses = normaliseSwaggerResponses(responses, doc, produces, producesResolution.source, diagnostics, path, method);
|
|
715
890
|
return result;
|
|
716
891
|
}
|
|
717
892
|
/**
|
|
893
|
+
* Resolve a Swagger 2.0 `consumes` or `produces` array, recording
|
|
894
|
+
* where the value came from so callers can decide whether to emit a
|
|
895
|
+
* "missing content type" diagnostic. Per the Swagger 2.0 spec, absence
|
|
896
|
+
* at BOTH levels means no body — not an implicit `application/json`.
|
|
897
|
+
*/
|
|
898
|
+
function resolveSwaggerContentTypes(operationLevel, documentLevel) {
|
|
899
|
+
if (Array.isArray(operationLevel)) return {
|
|
900
|
+
types: operationLevel,
|
|
901
|
+
source: "operation"
|
|
902
|
+
};
|
|
903
|
+
if (Array.isArray(documentLevel)) return {
|
|
904
|
+
types: documentLevel,
|
|
905
|
+
source: "document"
|
|
906
|
+
};
|
|
907
|
+
return {
|
|
908
|
+
types: ["application/json"],
|
|
909
|
+
source: "synthesised"
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
718
913
|
* Determine the request body media type for a Swagger 2.0 formData operation.
|
|
719
914
|
*
|
|
720
915
|
* Per the OAS 3 conversion rules, `application/x-www-form-urlencoded` is
|
|
@@ -729,51 +924,141 @@ function formDataContentTypes(consumes) {
|
|
|
729
924
|
return ["multipart/form-data"];
|
|
730
925
|
}
|
|
731
926
|
/**
|
|
732
|
-
*
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
927
|
+
* Every JSON-Schema-compatible constraint keyword Swagger 2.0 allows on
|
|
928
|
+
* a Parameter Object or Header Object alongside `type`/`format`. These
|
|
929
|
+
* lift into the synthesised `schema` so consumers see the original
|
|
930
|
+
* validation semantics under OAS 3.x's parameter shape.
|
|
931
|
+
*
|
|
932
|
+
* `allowEmptyValue` is included even though it is a Swagger 2.0
|
|
933
|
+
* parameter-level keyword in the source (not a schema keyword) — OAS
|
|
934
|
+
* 3.x defines it at the Parameter Object root, so the calling function
|
|
935
|
+
* keeps it at the parameter root rather than copying it into `schema`.
|
|
936
|
+
*/
|
|
937
|
+
const SWAGGER_PARAM_SCHEMA_KEYWORDS = [
|
|
938
|
+
"enum",
|
|
939
|
+
"default",
|
|
940
|
+
"minimum",
|
|
941
|
+
"maximum",
|
|
942
|
+
"exclusiveMinimum",
|
|
943
|
+
"exclusiveMaximum",
|
|
944
|
+
"multipleOf",
|
|
945
|
+
"minLength",
|
|
946
|
+
"maxLength",
|
|
947
|
+
"pattern",
|
|
948
|
+
"minItems",
|
|
949
|
+
"maxItems",
|
|
950
|
+
"uniqueItems"
|
|
951
|
+
];
|
|
952
|
+
/**
|
|
953
|
+
* Set of every Swagger 2.0 parameter-root keyword that must be lifted
|
|
954
|
+
* into the synthesised `schema` rather than copied onto the OAS 3.x
|
|
955
|
+
* parameter root. Includes `type`, `format`, `items` (Swagger 2.0
|
|
956
|
+
* parameter-shaped array element descriptor), `collectionFormat`
|
|
957
|
+
* (handled separately by the caller as `style`/`explode`), and every
|
|
958
|
+
* entry from {@link SWAGGER_PARAM_SCHEMA_KEYWORDS}.
|
|
959
|
+
*/
|
|
960
|
+
const PARAM_KEYWORDS_LIFTED_INTO_SCHEMA = new Set([
|
|
961
|
+
"type",
|
|
962
|
+
"format",
|
|
963
|
+
"items",
|
|
964
|
+
"collectionFormat",
|
|
965
|
+
...SWAGGER_PARAM_SCHEMA_KEYWORDS
|
|
966
|
+
]);
|
|
967
|
+
/**
|
|
968
|
+
* Synthesise an OpenAPI 3.x `schema` object from a Swagger 2.0
|
|
969
|
+
* parameter-shaped node (parameter or header). Copies `type`,
|
|
970
|
+
* `format`, and every JSON-Schema-compatible constraint that Swagger
|
|
971
|
+
* 2.0 places at the parameter root. Nested `items` is recursively
|
|
972
|
+
* synthesised the same way so array element constraints survive.
|
|
973
|
+
*/
|
|
974
|
+
function buildSchemaFromSwaggerParameterShape(node) {
|
|
975
|
+
const schema = { type: node.type };
|
|
976
|
+
if (typeof node.format === "string") schema.format = node.format;
|
|
977
|
+
for (const keyword of SWAGGER_PARAM_SCHEMA_KEYWORDS) if (node[keyword] !== void 0) schema[keyword] = node[keyword];
|
|
978
|
+
if (isObject(node.items)) schema.items = buildSchemaFromSwaggerParameterShape(node.items);
|
|
979
|
+
return schema;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Resolve a Swagger parameter that may be a `$ref`. Returns the
|
|
983
|
+
* resolved parameter object, or a cycle marker so the caller can
|
|
984
|
+
* decide how to surface the failure. Non-ref parameters resolve to
|
|
985
|
+
* themselves; ref targets that don't exist also resolve to the input
|
|
986
|
+
* (the caller treats unknown refs the same as bare parameters).
|
|
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;
|
|
747
1009
|
}
|
|
748
|
-
}
|
|
749
|
-
return
|
|
1010
|
+
});
|
|
1011
|
+
if (cyclicRef !== void 0) return {
|
|
1012
|
+
kind: "cycle",
|
|
1013
|
+
ref: cyclicRef
|
|
1014
|
+
};
|
|
1015
|
+
return {
|
|
1016
|
+
kind: "ok",
|
|
1017
|
+
param: finalNode ?? param
|
|
1018
|
+
};
|
|
750
1019
|
}
|
|
751
1020
|
/**
|
|
752
1021
|
* Normalise a single Swagger parameter to OpenAPI 3.x form.
|
|
753
1022
|
*/
|
|
754
|
-
function normaliseSwaggerParameter(param, doc) {
|
|
1023
|
+
function normaliseSwaggerParameter(param, doc, diagnostics, pointer = "") {
|
|
755
1024
|
if (typeof param.$ref === "string") {
|
|
756
|
-
const
|
|
757
|
-
if (
|
|
1025
|
+
const resolution = resolveSwaggerParameter(param, doc);
|
|
1026
|
+
if (resolution.kind === "cycle") {
|
|
1027
|
+
emitDiagnostic(diagnostics, {
|
|
1028
|
+
code: "swagger-cyclic-parameter-ref",
|
|
1029
|
+
message: `Cyclic Swagger 2.0 parameter $ref "${resolution.ref}"; skipping entry`,
|
|
1030
|
+
pointer,
|
|
1031
|
+
detail: { ref: resolution.ref }
|
|
1032
|
+
});
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const resolved = resolution.param;
|
|
1036
|
+
if (resolved !== param) return normaliseSwaggerParameter(resolved, doc, diagnostics, pointer);
|
|
758
1037
|
}
|
|
759
1038
|
const result = {};
|
|
760
1039
|
for (const [key, value] of Object.entries(param)) {
|
|
761
|
-
if (key
|
|
1040
|
+
if (PARAM_KEYWORDS_LIFTED_INTO_SCHEMA.has(key)) continue;
|
|
762
1041
|
result[key] = value;
|
|
763
1042
|
}
|
|
764
|
-
if (typeof param.type === "string") {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1043
|
+
if (typeof param.type === "string") if (param.type === "file" && param.in !== "formData") {
|
|
1044
|
+
emitDiagnostic(diagnostics, {
|
|
1045
|
+
code: "swagger-invalid-file-parameter",
|
|
1046
|
+
message: `Swagger 2.0 type: "file" is only valid under in: formData; converting to { type: "string", format: "binary" }`,
|
|
1047
|
+
pointer,
|
|
1048
|
+
detail: {
|
|
1049
|
+
name: param.name,
|
|
1050
|
+
in: param.in
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
result.schema = {
|
|
1054
|
+
type: "string",
|
|
1055
|
+
format: "binary"
|
|
1056
|
+
};
|
|
1057
|
+
} else result.schema = buildSchemaFromSwaggerParameterShape(param);
|
|
773
1058
|
const cf = param.collectionFormat;
|
|
774
1059
|
if (typeof cf === "string") switch (cf) {
|
|
775
1060
|
case "csv":
|
|
776
|
-
result.style = "form";
|
|
1061
|
+
result.style = param.in === "path" || param.in === "header" ? "simple" : "form";
|
|
777
1062
|
result.explode = false;
|
|
778
1063
|
break;
|
|
779
1064
|
case "ssv":
|
|
@@ -781,8 +1066,15 @@ function normaliseSwaggerParameter(param, doc) {
|
|
|
781
1066
|
result.explode = false;
|
|
782
1067
|
break;
|
|
783
1068
|
case "tsv":
|
|
784
|
-
|
|
785
|
-
|
|
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
|
+
});
|
|
786
1078
|
break;
|
|
787
1079
|
case "pipes":
|
|
788
1080
|
result.style = "pipeDelimited";
|
|
@@ -829,12 +1121,18 @@ function buildFormDataBody(param, allParams) {
|
|
|
829
1121
|
}
|
|
830
1122
|
/**
|
|
831
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.
|
|
832
1131
|
*/
|
|
833
1132
|
function buildRequestBody(bodyParam, consumes) {
|
|
834
1133
|
const schema = bodyParam.schema;
|
|
835
1134
|
const content = {};
|
|
836
|
-
const
|
|
837
|
-
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 } : {};
|
|
838
1136
|
const result = { content };
|
|
839
1137
|
if (bodyParam.required === true) result.required = true;
|
|
840
1138
|
if (typeof bodyParam.description === "string") result.description = bodyParam.description;
|
|
@@ -842,32 +1140,37 @@ function buildRequestBody(bodyParam, consumes) {
|
|
|
842
1140
|
}
|
|
843
1141
|
/**
|
|
844
1142
|
* Resolve a Swagger 2.0 response `$ref` (e.g. `#/responses/NotFound`).
|
|
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.
|
|
845
1148
|
*/
|
|
846
|
-
function resolveSwaggerResponse(response, doc
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
return resolved;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
}
|
|
863
|
-
function normaliseSwaggerResponses(responses, doc, produces) {
|
|
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;
|
|
1165
|
+
}
|
|
1166
|
+
function normaliseSwaggerResponses(responses, doc, produces, producesSource, diagnostics, path, method) {
|
|
864
1167
|
const result = {};
|
|
865
1168
|
for (const [code, response] of Object.entries(responses)) {
|
|
866
1169
|
if (!isObject(response)) {
|
|
867
1170
|
result[code] = response;
|
|
868
1171
|
continue;
|
|
869
1172
|
}
|
|
870
|
-
result[code] = normaliseSwaggerSingleResponse(response, doc, produces);
|
|
1173
|
+
result[code] = normaliseSwaggerSingleResponse(response, doc, produces, producesSource, diagnostics, path, method, code);
|
|
871
1174
|
}
|
|
872
1175
|
return result;
|
|
873
1176
|
}
|
|
@@ -880,7 +1183,7 @@ function normaliseSwaggerResponses(responses, doc, produces) {
|
|
|
880
1183
|
* operation’s `responses` map or under document-level `responses`
|
|
881
1184
|
* (now `components.responses`).
|
|
882
1185
|
*/
|
|
883
|
-
function normaliseSwaggerSingleResponse(response, doc, produces) {
|
|
1186
|
+
function normaliseSwaggerSingleResponse(response, doc, produces, producesSource = "synthesised", diagnostics, path, method, statusCode) {
|
|
884
1187
|
const resolved = resolveSwaggerResponse(response, doc);
|
|
885
1188
|
const normalised = {};
|
|
886
1189
|
for (const [key, value] of Object.entries(resolved)) if (key !== "schema" && key !== "headers") normalised[key] = value;
|
|
@@ -890,11 +1193,24 @@ function normaliseSwaggerSingleResponse(response, doc, produces) {
|
|
|
890
1193
|
const contentTypes = produces.length > 0 ? produces : ["application/json"];
|
|
891
1194
|
for (const ct of contentTypes) if (typeof ct === "string") content[ct] = { schema };
|
|
892
1195
|
normalised.content = content;
|
|
1196
|
+
if (producesSource === "synthesised") emitDiagnostic(diagnostics, {
|
|
1197
|
+
code: "swagger-missing-consumes",
|
|
1198
|
+
message: "Response declares a schema but neither operation-level nor document-level `produces` is set; defaulting to application/json",
|
|
1199
|
+
pointer: path !== void 0 && method !== void 0 && statusCode !== void 0 ? appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "responses"), statusCode) : "",
|
|
1200
|
+
detail: {
|
|
1201
|
+
level: "response",
|
|
1202
|
+
statusCode
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
893
1205
|
}
|
|
894
1206
|
const headers = resolved.headers;
|
|
895
1207
|
if (isObject(headers)) {
|
|
896
1208
|
const convertedHeaders = {};
|
|
897
|
-
|
|
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
|
+
}
|
|
898
1214
|
normalised.headers = convertedHeaders;
|
|
899
1215
|
}
|
|
900
1216
|
return normalised;
|
|
@@ -912,21 +1228,13 @@ function normaliseSwaggerSingleResponse(response, doc, produces) {
|
|
|
912
1228
|
* `simple`/`explode: false` rather than the `form` style used for query
|
|
913
1229
|
* parameters.
|
|
914
1230
|
*/
|
|
915
|
-
function normaliseSwaggerHeader(header) {
|
|
1231
|
+
function normaliseSwaggerHeader(header, diagnostics, pointer = "") {
|
|
916
1232
|
const result = {};
|
|
917
1233
|
for (const [key, value] of Object.entries(header)) {
|
|
918
|
-
if (key
|
|
1234
|
+
if (PARAM_KEYWORDS_LIFTED_INTO_SCHEMA.has(key)) continue;
|
|
919
1235
|
result[key] = value;
|
|
920
1236
|
}
|
|
921
|
-
if (typeof header.type === "string")
|
|
922
|
-
const schema = { type: header.type };
|
|
923
|
-
if (typeof header.format === "string") schema.format = header.format;
|
|
924
|
-
if (header.enum !== void 0) schema.enum = header.enum;
|
|
925
|
-
if (header.default !== void 0) schema.default = header.default;
|
|
926
|
-
if (header.minimum !== void 0) schema.minimum = header.minimum;
|
|
927
|
-
if (header.maximum !== void 0) schema.maximum = header.maximum;
|
|
928
|
-
result.schema = schema;
|
|
929
|
-
}
|
|
1237
|
+
if (typeof header.type === "string") result.schema = buildSchemaFromSwaggerParameterShape(header);
|
|
930
1238
|
const cf = header.collectionFormat;
|
|
931
1239
|
if (typeof cf === "string") switch (cf) {
|
|
932
1240
|
case "csv":
|
|
@@ -938,8 +1246,15 @@ function normaliseSwaggerHeader(header) {
|
|
|
938
1246
|
result.explode = false;
|
|
939
1247
|
break;
|
|
940
1248
|
case "tsv":
|
|
941
|
-
|
|
942
|
-
|
|
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
|
+
});
|
|
943
1258
|
break;
|
|
944
1259
|
case "pipes":
|
|
945
1260
|
result.style = "pipeDelimited";
|
|
@@ -949,49 +1264,86 @@ function normaliseSwaggerHeader(header) {
|
|
|
949
1264
|
return result;
|
|
950
1265
|
}
|
|
951
1266
|
/**
|
|
952
|
-
* Mapping of Swagger 2.0 $ref prefixes to OpenAPI 3.x equivalents.
|
|
953
|
-
* Applied after document restructuring so all $ref strings point
|
|
954
|
-
* to the correct locations in the normalised document.
|
|
955
|
-
*/
|
|
956
|
-
const REF_REWRITES = [
|
|
957
|
-
["#/definitions/", "#/components/schemas/"],
|
|
958
|
-
["#/parameters/", "#/components/parameters/"],
|
|
959
|
-
["#/responses/", "#/components/responses/"]
|
|
960
|
-
];
|
|
961
|
-
/**
|
|
962
1267
|
* Deep-rewrite $ref strings in a normalised Swagger 2.0 document
|
|
963
|
-
* from Swagger 2.0 locations to OpenAPI 3.x locations
|
|
964
|
-
*
|
|
965
|
-
* 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.
|
|
966
1272
|
*/
|
|
967
1273
|
function rewriteSwaggerRefs(node) {
|
|
968
1274
|
if (!isObject(node)) return;
|
|
969
|
-
if (typeof node.$ref === "string")
|
|
970
|
-
for (const [from, to] of REF_REWRITES) if (node.$ref.startsWith(from)) {
|
|
971
|
-
node.$ref = to + node.$ref.slice(from.length);
|
|
972
|
-
break;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
1275
|
+
if (typeof node.$ref === "string") node.$ref = rewriteSwaggerRefPrefix(node.$ref);
|
|
975
1276
|
for (const value of Object.values(node)) if (isObject(value)) rewriteSwaggerRefs(value);
|
|
976
1277
|
else if (Array.isArray(value)) for (const item of value) rewriteSwaggerRefs(item);
|
|
977
1278
|
}
|
|
978
1279
|
/**
|
|
979
|
-
*
|
|
980
|
-
* `
|
|
981
|
-
*
|
|
982
|
-
|
|
983
|
-
|
|
1280
|
+
* Map from Swagger 2.0 `oauth2.flow` (singular) to the OAS 3.x flow key
|
|
1281
|
+
* under `flows.<key>`. `application` and `accessCode` were renamed in
|
|
1282
|
+
* OAS 3.x to align with RFC 6749 grant-type names.
|
|
1283
|
+
*/
|
|
1284
|
+
const SWAGGER_OAUTH_FLOW_RENAME = {
|
|
1285
|
+
implicit: "implicit",
|
|
1286
|
+
password: "password",
|
|
1287
|
+
application: "clientCredentials",
|
|
1288
|
+
accessCode: "authorizationCode"
|
|
1289
|
+
};
|
|
1290
|
+
/**
|
|
1291
|
+
* Translate a Swagger 2.0 Security Scheme Object into an OpenAPI 3.x
|
|
1292
|
+
* Security Scheme Object. The Swagger 2.0 spec defines three types:
|
|
1293
|
+
*
|
|
1294
|
+
* - `basic` — has no other fields; OAS 3.x represents this as
|
|
1295
|
+
* `{ type: "http", scheme: "basic" }`.
|
|
1296
|
+
* - `apiKey` — carries `name`/`in`; OAS 3.x uses the same shape.
|
|
1297
|
+
* - `oauth2` — carries `flow`/`authorizationUrl`/`tokenUrl`/`scopes` at
|
|
1298
|
+
* the root. OAS 3.x nests these under `flows.<name>` where the flow
|
|
1299
|
+
* name maps via {@link SWAGGER_OAUTH_FLOW_RENAME}.
|
|
1300
|
+
*
|
|
1301
|
+
* Unknown `type` values pass through verbatim — downstream validation
|
|
1302
|
+
* (`unknown-security-scheme-type` diagnostic in the parser) handles
|
|
1303
|
+
* those cases.
|
|
984
1304
|
*/
|
|
985
|
-
function
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1305
|
+
function translateSwaggerSecurityScheme(scheme, diagnostics, pointer = "", name) {
|
|
1306
|
+
const type = scheme.type;
|
|
1307
|
+
if (type === "basic") {
|
|
1308
|
+
const result = {
|
|
1309
|
+
type: "http",
|
|
1310
|
+
scheme: "basic"
|
|
1311
|
+
};
|
|
1312
|
+
if (typeof scheme.description === "string") result.description = scheme.description;
|
|
1313
|
+
return result;
|
|
1314
|
+
}
|
|
1315
|
+
if (type === "oauth2") {
|
|
1316
|
+
const flowName = scheme.flow;
|
|
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
|
+
};
|
|
989
1331
|
}
|
|
990
|
-
|
|
1332
|
+
const renamedFlow = SWAGGER_OAUTH_FLOW_RENAME[flowName] ?? flowName;
|
|
1333
|
+
const flowBody = {};
|
|
1334
|
+
if (typeof scheme.authorizationUrl === "string") flowBody.authorizationUrl = scheme.authorizationUrl;
|
|
1335
|
+
if (typeof scheme.tokenUrl === "string") flowBody.tokenUrl = scheme.tokenUrl;
|
|
1336
|
+
if (typeof scheme.refreshUrl === "string") flowBody.refreshUrl = scheme.refreshUrl;
|
|
1337
|
+
const scopes = scheme.scopes;
|
|
1338
|
+
flowBody.scopes = isObject(scopes) ? { ...scopes } : {};
|
|
1339
|
+
const result = {
|
|
1340
|
+
type: "oauth2",
|
|
1341
|
+
flows: { [renamedFlow]: flowBody }
|
|
1342
|
+
};
|
|
1343
|
+
if (typeof scheme.description === "string") result.description = scheme.description;
|
|
1344
|
+
return result;
|
|
991
1345
|
}
|
|
992
|
-
|
|
993
|
-
for (const value of Object.values(node)) if (hasXmlAnywhere(value)) return true;
|
|
994
|
-
return false;
|
|
1346
|
+
return { ...scheme };
|
|
995
1347
|
}
|
|
996
1348
|
//#endregion
|
|
997
1349
|
//#region src/core/normalise.ts
|
|
@@ -1075,24 +1427,34 @@ function deepNormalise(schema, transform, visited = /* @__PURE__ */ new WeakSet(
|
|
|
1075
1427
|
} else result[key] = value;
|
|
1076
1428
|
return result;
|
|
1077
1429
|
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Construct a child {@link NodeContext} that descends to `segment`,
|
|
1432
|
+
* preserving document-level flags (`documentHasDynamicAnchor`,
|
|
1433
|
+
* `documentHasRecursiveAnchor`, `declaredDraft`). Centralising the copy
|
|
1434
|
+
* keeps the recursion in `deepNormaliseWithContext` from drifting from
|
|
1435
|
+
* the {@link NodeContext} shape.
|
|
1436
|
+
*/
|
|
1437
|
+
function childContext(ctx, segment) {
|
|
1438
|
+
return {
|
|
1439
|
+
diagnostics: ctx.diagnostics,
|
|
1440
|
+
pointer: appendPointer(ctx.pointer, segment),
|
|
1441
|
+
documentHasDynamicAnchor: ctx.documentHasDynamicAnchor,
|
|
1442
|
+
documentHasRecursiveAnchor: ctx.documentHasRecursiveAnchor,
|
|
1443
|
+
declaredDraft: ctx.declaredDraft
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1078
1446
|
function normaliseArrayWithContext(items, transform, ctx) {
|
|
1079
1447
|
const result = [];
|
|
1080
1448
|
for (let i = 0; i < items.length; i++) {
|
|
1081
1449
|
const item = items[i];
|
|
1082
|
-
if (isObject(item)) result.push(deepNormaliseWithContext(item, transform,
|
|
1083
|
-
diagnostics: ctx.diagnostics,
|
|
1084
|
-
pointer: appendPointer(ctx.pointer, String(i))
|
|
1085
|
-
}));
|
|
1450
|
+
if (isObject(item)) result.push(deepNormaliseWithContext(item, transform, childContext(ctx, String(i))));
|
|
1086
1451
|
else result.push(item);
|
|
1087
1452
|
}
|
|
1088
1453
|
return result;
|
|
1089
1454
|
}
|
|
1090
1455
|
function normaliseSubSchemaMapWithContext(map, transform, ctx) {
|
|
1091
1456
|
const result = {};
|
|
1092
|
-
for (const [k, v] of Object.entries(map)) if (isObject(v)) result[k] = deepNormaliseWithContext(v, transform,
|
|
1093
|
-
diagnostics: ctx.diagnostics,
|
|
1094
|
-
pointer: appendPointer(ctx.pointer, k)
|
|
1095
|
-
});
|
|
1457
|
+
for (const [k, v] of Object.entries(map)) if (isObject(v)) result[k] = deepNormaliseWithContext(v, transform, childContext(ctx, k));
|
|
1096
1458
|
else result[k] = v;
|
|
1097
1459
|
return result;
|
|
1098
1460
|
}
|
|
@@ -1108,34 +1470,16 @@ function normaliseSubSchemaMapWithContext(map, transform, ctx) {
|
|
|
1108
1470
|
function deepNormaliseWithContext(schema, transform, ctx) {
|
|
1109
1471
|
const node = transform({ ...schema }, ctx);
|
|
1110
1472
|
const result = {};
|
|
1111
|
-
for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMapWithContext(value, transform,
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
else if (
|
|
1116
|
-
diagnostics: ctx.diagnostics,
|
|
1117
|
-
pointer: appendPointer(ctx.pointer, key)
|
|
1118
|
-
});
|
|
1119
|
-
else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormaliseWithContext(value, transform, {
|
|
1120
|
-
diagnostics: ctx.diagnostics,
|
|
1121
|
-
pointer: appendPointer(ctx.pointer, key)
|
|
1122
|
-
});
|
|
1123
|
-
else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArrayWithContext(value, transform, {
|
|
1124
|
-
diagnostics: ctx.diagnostics,
|
|
1125
|
-
pointer: appendPointer(ctx.pointer, key)
|
|
1126
|
-
});
|
|
1127
|
-
else if (isObject(value)) result[key] = deepNormaliseWithContext(value, transform, {
|
|
1128
|
-
diagnostics: ctx.diagnostics,
|
|
1129
|
-
pointer: appendPointer(ctx.pointer, key)
|
|
1130
|
-
});
|
|
1473
|
+
for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMapWithContext(value, transform, childContext(ctx, key));
|
|
1474
|
+
else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArrayWithContext(value, transform, childContext(ctx, key));
|
|
1475
|
+
else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormaliseWithContext(value, transform, childContext(ctx, key));
|
|
1476
|
+
else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArrayWithContext(value, transform, childContext(ctx, key));
|
|
1477
|
+
else if (isObject(value)) result[key] = deepNormaliseWithContext(value, transform, childContext(ctx, key));
|
|
1131
1478
|
else result[key] = value;
|
|
1132
1479
|
else if (key === "dependencies" && isObject(value)) {
|
|
1133
1480
|
const normalised = {};
|
|
1134
|
-
const
|
|
1135
|
-
for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormaliseWithContext(dv, transform,
|
|
1136
|
-
diagnostics: ctx.diagnostics,
|
|
1137
|
-
pointer: appendPointer(depsPointer, dk)
|
|
1138
|
-
});
|
|
1481
|
+
const depsContext = childContext(ctx, key);
|
|
1482
|
+
for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormaliseWithContext(dv, transform, childContext(depsContext, dk));
|
|
1139
1483
|
else normalised[dk] = dv;
|
|
1140
1484
|
result[key] = normalised;
|
|
1141
1485
|
} else result[key] = value;
|
|
@@ -1188,17 +1532,24 @@ function collectDependencyStrings(items, property, keyword, ctx) {
|
|
|
1188
1532
|
* After splitting, `dependencies` is removed from the node.
|
|
1189
1533
|
*
|
|
1190
1534
|
* When `ctx` is supplied, diagnostics are emitted for:
|
|
1191
|
-
* - `
|
|
1192
|
-
* deprecated keyword
|
|
1193
|
-
* 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.
|
|
1194
1540
|
* - `dependent-required-invalid` for each array entry whose element is
|
|
1195
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.
|
|
1196
1547
|
*/
|
|
1197
|
-
function splitDependencies(node, ctx,
|
|
1548
|
+
function splitDependencies(node, ctx, legacyDiagnostic) {
|
|
1198
1549
|
const deps = node.dependencies;
|
|
1199
1550
|
if (!isObject(deps)) return;
|
|
1200
|
-
if (
|
|
1201
|
-
code:
|
|
1551
|
+
if (legacyDiagnostic !== void 0 && ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
|
|
1552
|
+
code: legacyDiagnostic,
|
|
1202
1553
|
message: "Legacy `dependencies` keyword was split into `dependentRequired`/`dependentSchemas`; `dependencies` was deprecated in Draft 2019-09",
|
|
1203
1554
|
pointer: appendPointer(ctx.pointer, "dependencies"),
|
|
1204
1555
|
detail: { keys: Object.keys(deps) }
|
|
@@ -1211,17 +1562,61 @@ function splitDependencies(node, ctx, emitLegacyDiagnostic) {
|
|
|
1211
1562
|
} else if (isObject(value)) schemaEntries[key] = value;
|
|
1212
1563
|
if (Object.keys(requiredEntries).length > 0) {
|
|
1213
1564
|
const existing = node.dependentRequired;
|
|
1214
|
-
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
|
+
}
|
|
1215
1582
|
else node.dependentRequired = requiredEntries;
|
|
1216
1583
|
}
|
|
1217
1584
|
if (Object.keys(schemaEntries).length > 0) {
|
|
1218
1585
|
const existing = node.dependentSchemas;
|
|
1219
|
-
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
|
+
}
|
|
1220
1603
|
else node.dependentSchemas = schemaEntries;
|
|
1221
1604
|
}
|
|
1222
1605
|
delete node.dependencies;
|
|
1223
1606
|
}
|
|
1224
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
|
+
/**
|
|
1225
1620
|
* Emit diagnostics for any non-string entries inside a pre-existing
|
|
1226
1621
|
* `dependentRequired` keyword. Used on draft paths where the author may
|
|
1227
1622
|
* have already migrated to the Draft 2019-09 form but still produced
|
|
@@ -1319,7 +1714,7 @@ function applyDraft04Translations(node, ctx) {
|
|
|
1319
1714
|
delete node.id;
|
|
1320
1715
|
}
|
|
1321
1716
|
translateTupleItems(node);
|
|
1322
|
-
splitDependencies(node, ctx,
|
|
1717
|
+
splitDependencies(node, ctx, void 0);
|
|
1323
1718
|
validateDependentRequired(node, ctx);
|
|
1324
1719
|
}
|
|
1325
1720
|
/**
|
|
@@ -1372,7 +1767,7 @@ function normaliseDraft04NodeWithContext(node, ctx) {
|
|
|
1372
1767
|
*/
|
|
1373
1768
|
function normaliseDraft06Or07NodeWithContext(node, ctx) {
|
|
1374
1769
|
translateTupleItems(node);
|
|
1375
|
-
splitDependencies(node, ctx,
|
|
1770
|
+
splitDependencies(node, ctx, void 0);
|
|
1376
1771
|
validateDependentRequired(node, ctx);
|
|
1377
1772
|
return node;
|
|
1378
1773
|
}
|
|
@@ -1391,34 +1786,120 @@ function normaliseDraft06Or07NodeWithContext(node, ctx) {
|
|
|
1391
1786
|
* `$recursiveAnchor` names are likewise preserved as `$anchor`.
|
|
1392
1787
|
*/
|
|
1393
1788
|
function normaliseDraft201909NodeWithContext(node, ctx) {
|
|
1394
|
-
if (
|
|
1395
|
-
|
|
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
|
+
});
|
|
1795
|
+
const recursiveRef = node.$recursiveRef;
|
|
1796
|
+
if (typeof recursiveRef === "string") {
|
|
1797
|
+
if (!recursiveRef.startsWith("#")) emitDiagnostic(ctx.diagnostics, {
|
|
1798
|
+
code: "dynamic-ref-degraded",
|
|
1799
|
+
message: `Cross-document \`$recursiveRef\` "${recursiveRef}" rewritten to a static \`$ref\`; dynamic-scope resolution is not preserved`,
|
|
1800
|
+
pointer: appendPointer(ctx.pointer, "$recursiveRef"),
|
|
1801
|
+
detail: {
|
|
1802
|
+
keyword: "$recursiveRef",
|
|
1803
|
+
ref: recursiveRef
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
node.$ref = recursiveRef;
|
|
1396
1807
|
delete node.$recursiveRef;
|
|
1397
1808
|
}
|
|
1398
1809
|
if (node.$recursiveAnchor === true) {
|
|
1399
|
-
if (typeof node.$anchor !== "string") node.$anchor =
|
|
1810
|
+
if (typeof node.$anchor !== "string") node.$anchor = RECURSIVE_ANCHOR_SENTINEL;
|
|
1400
1811
|
delete node.$recursiveAnchor;
|
|
1401
1812
|
} else if (typeof node.$recursiveAnchor === "string") {
|
|
1402
1813
|
if (typeof node.$anchor !== "string") node.$anchor = node.$recursiveAnchor;
|
|
1403
1814
|
delete node.$recursiveAnchor;
|
|
1404
1815
|
}
|
|
1816
|
+
splitDependencies(node, ctx, "legacy-dependencies-split-2019");
|
|
1405
1817
|
validateDependentRequired(node, ctx);
|
|
1406
1818
|
return node;
|
|
1407
1819
|
}
|
|
1408
1820
|
function normaliseDynamicRefNodeWithContext(node, ctx) {
|
|
1409
|
-
|
|
1410
|
-
|
|
1821
|
+
const dynamicRef = node.$dynamicRef;
|
|
1822
|
+
if (typeof dynamicRef === "string") {
|
|
1823
|
+
const crossDocument = !dynamicRef.startsWith("#");
|
|
1824
|
+
if (crossDocument || ctx.documentHasDynamicAnchor) emitDiagnostic(ctx.diagnostics, {
|
|
1825
|
+
code: "dynamic-ref-degraded",
|
|
1826
|
+
message: crossDocument ? `Cross-document \`$dynamicRef\` "${dynamicRef}" rewritten to a static \`$ref\`; dynamic-scope resolution is not preserved` : `\`$dynamicRef\` "${dynamicRef}" rewritten to a static \`$ref\` in a document declaring \`$dynamicAnchor\`; dynamic-scope resolution is not preserved`,
|
|
1827
|
+
pointer: appendPointer(ctx.pointer, "$dynamicRef"),
|
|
1828
|
+
detail: {
|
|
1829
|
+
keyword: "$dynamicRef",
|
|
1830
|
+
ref: dynamicRef
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
node.$ref = dynamicRef;
|
|
1411
1834
|
delete node.$dynamicRef;
|
|
1412
1835
|
}
|
|
1413
1836
|
if (typeof node.$dynamicAnchor === "string") {
|
|
1414
1837
|
if (typeof node.$anchor !== "string") node.$anchor = node.$dynamicAnchor;
|
|
1415
1838
|
delete node.$dynamicAnchor;
|
|
1416
1839
|
}
|
|
1417
|
-
|
|
1840
|
+
translateTupleItems(node);
|
|
1841
|
+
splitDependencies(node, ctx, "legacy-dependencies-split");
|
|
1418
1842
|
validateDependentRequired(node, ctx);
|
|
1419
1843
|
return node;
|
|
1420
1844
|
}
|
|
1421
1845
|
/**
|
|
1846
|
+
* Pick the per-node transform that normalises a single Schema Object to
|
|
1847
|
+
* canonical Draft 2020-12 form for the supplied draft. Exposed so the
|
|
1848
|
+
* OpenAPI 3.1 path can honour a non-default `jsonSchemaDialect`
|
|
1849
|
+
* declaration by routing each Schema Object through the matching
|
|
1850
|
+
* transform without re-implementing the dispatch.
|
|
1851
|
+
*/
|
|
1852
|
+
function selectDraftTransform(draft) {
|
|
1853
|
+
switch (draft) {
|
|
1854
|
+
case "draft-04": return normaliseDraft04NodeWithContext;
|
|
1855
|
+
case "draft-06":
|
|
1856
|
+
case "draft-07": return normaliseDraft06Or07NodeWithContext;
|
|
1857
|
+
case "draft-2019-09": return normaliseDraft201909NodeWithContext;
|
|
1858
|
+
case "draft-2020-12": return normaliseDynamicRefNodeWithContext;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Scan a JSON document body for the presence of a named keyword
|
|
1863
|
+
* anywhere in the structure. Walks both arrays and objects without
|
|
1864
|
+
* regard to schema-vs-data position — the caller is responsible for
|
|
1865
|
+
* passing a keyword whose presence is meaningful at any depth.
|
|
1866
|
+
*
|
|
1867
|
+
* Cycle-safe: cyclic references introduced by the OpenAPI bundler's
|
|
1868
|
+
* `structuredClone` of external refs are short-circuited via the
|
|
1869
|
+
* `visited` set so the scan terminates.
|
|
1870
|
+
*/
|
|
1871
|
+
function documentContainsKeyword(value, keyword, visited = /* @__PURE__ */ new WeakSet()) {
|
|
1872
|
+
if (Array.isArray(value)) {
|
|
1873
|
+
if (visited.has(value)) return false;
|
|
1874
|
+
visited.add(value);
|
|
1875
|
+
for (const item of value) if (documentContainsKeyword(item, keyword, visited)) return true;
|
|
1876
|
+
return false;
|
|
1877
|
+
}
|
|
1878
|
+
if (isObject(value)) {
|
|
1879
|
+
if (visited.has(value)) return false;
|
|
1880
|
+
visited.add(value);
|
|
1881
|
+
if (keyword in value) return true;
|
|
1882
|
+
for (const v of Object.values(value)) if (documentContainsKeyword(v, keyword, visited)) return true;
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Build a root {@link NodeContext} for a document being normalised.
|
|
1889
|
+
* Pre-scans for `$dynamicAnchor` and `$recursiveAnchor` so per-node
|
|
1890
|
+
* transforms can decide whether a `$dynamicRef`/`$recursiveRef`
|
|
1891
|
+
* rewrite needs a `dynamic-ref-degraded` diagnostic.
|
|
1892
|
+
*/
|
|
1893
|
+
function buildRootContext(schema, diagnostics, declaredDraft) {
|
|
1894
|
+
return {
|
|
1895
|
+
diagnostics,
|
|
1896
|
+
pointer: "",
|
|
1897
|
+
documentHasDynamicAnchor: documentContainsKeyword(schema, "$dynamicAnchor"),
|
|
1898
|
+
documentHasRecursiveAnchor: documentContainsKeyword(schema, "$recursiveAnchor"),
|
|
1899
|
+
declaredDraft
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1422
1903
|
* Normalise a JSON Schema to canonical Draft 2020-12 form.
|
|
1423
1904
|
* Deep-clones the input — the original is never mutated.
|
|
1424
1905
|
*
|
|
@@ -1428,27 +1909,8 @@ function normaliseDynamicRefNodeWithContext(node, ctx) {
|
|
|
1428
1909
|
* `dependencies` reaching the 2020-12 path).
|
|
1429
1910
|
*/
|
|
1430
1911
|
function normaliseJsonSchema(schema, draft, diagnostics) {
|
|
1431
|
-
const ctx =
|
|
1432
|
-
|
|
1433
|
-
pointer: ""
|
|
1434
|
-
};
|
|
1435
|
-
let normalised;
|
|
1436
|
-
switch (draft) {
|
|
1437
|
-
case "draft-04":
|
|
1438
|
-
normalised = deepNormaliseWithContext(schema, normaliseDraft04NodeWithContext, ctx);
|
|
1439
|
-
break;
|
|
1440
|
-
case "draft-2019-09":
|
|
1441
|
-
normalised = deepNormaliseWithContext(schema, normaliseDraft201909NodeWithContext, ctx);
|
|
1442
|
-
break;
|
|
1443
|
-
case "draft-2020-12":
|
|
1444
|
-
normalised = deepNormaliseWithContext(schema, normaliseDynamicRefNodeWithContext, ctx);
|
|
1445
|
-
break;
|
|
1446
|
-
case "draft-06":
|
|
1447
|
-
case "draft-07":
|
|
1448
|
-
normalised = deepNormaliseWithContext(schema, normaliseDraft06Or07NodeWithContext, ctx);
|
|
1449
|
-
break;
|
|
1450
|
-
}
|
|
1451
|
-
return resolveRelativeRefs(normalised, diagnostics);
|
|
1912
|
+
const ctx = buildRootContext(schema, diagnostics, draft);
|
|
1913
|
+
return resolveRelativeRefs(deepNormaliseWithContext(schema, selectDraftTransform(draft), ctx), diagnostics);
|
|
1452
1914
|
}
|
|
1453
1915
|
/**
|
|
1454
1916
|
* Parse a string as an absolute URI, returning `undefined` when it has
|
|
@@ -1486,6 +1948,29 @@ function stripFragment(url) {
|
|
|
1486
1948
|
return clone.toString();
|
|
1487
1949
|
}
|
|
1488
1950
|
/**
|
|
1951
|
+
* Emit an `invalid-id-fragment` diagnostic when an `$id` value carries
|
|
1952
|
+
* a fragment that will be stripped during base-URI resolution. Per
|
|
1953
|
+
* JSON Schema 2020-12 §8.2.1, the URI in `$id` MUST NOT contain a
|
|
1954
|
+
* non-empty fragment (an empty `#` fragment is permitted for historical
|
|
1955
|
+
* reasons but conveys nothing). Stripping it silently loses authoring
|
|
1956
|
+
* intent — the caller almost certainly meant to declare an `$anchor`
|
|
1957
|
+
* or sibling identifier instead.
|
|
1958
|
+
*/
|
|
1959
|
+
function reportFragmentInId(value, url, pointer, diagnostics) {
|
|
1960
|
+
if (diagnostics === void 0) return;
|
|
1961
|
+
if (typeof value !== "string") return;
|
|
1962
|
+
if (url.hash.length === 0) return;
|
|
1963
|
+
emitDiagnostic(diagnostics, {
|
|
1964
|
+
code: "invalid-id-fragment",
|
|
1965
|
+
message: `\`$id\` URI "${value}" includes the fragment "${url.hash}", which is not permitted by JSON Schema §8.2.1; the fragment is stripped before use`,
|
|
1966
|
+
pointer: appendPointer(pointer, "$id"),
|
|
1967
|
+
detail: {
|
|
1968
|
+
id: value,
|
|
1969
|
+
fragment: url.hash
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1489
1974
|
* Recursively rewrite relative `$ref`s in a schema so they resolve
|
|
1490
1975
|
* correctly under the JSON Schema base-URI rules (RFC 3986 + JSON
|
|
1491
1976
|
* Schema §8.2). Refs that resolve to the document's own `$id` are
|
|
@@ -1519,7 +2004,10 @@ function rewriteRelativeRefsNode(node, currentBase, docBase, pointer, diagnostic
|
|
|
1519
2004
|
const nodeId = node.$id;
|
|
1520
2005
|
if (typeof nodeId === "string" && nodeId.length > 0) {
|
|
1521
2006
|
const resolved = resolveAgainst(nodeId, currentBase);
|
|
1522
|
-
if (resolved !== void 0)
|
|
2007
|
+
if (resolved !== void 0) {
|
|
2008
|
+
reportFragmentInId(nodeId, resolved, pointer, diagnostics);
|
|
2009
|
+
nextBase = stripFragment(resolved);
|
|
2010
|
+
}
|
|
1523
2011
|
}
|
|
1524
2012
|
const result = {};
|
|
1525
2013
|
for (const [key, value] of Object.entries(node)) {
|
|
@@ -1588,6 +2076,24 @@ function rewriteRef(ref, currentBase, docBase, pointer, diagnostics) {
|
|
|
1588
2076
|
function normaliseOpenApiSchemas(doc, version, diagnostics) {
|
|
1589
2077
|
if (isSwagger2(version)) return normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, diagnostics);
|
|
1590
2078
|
if (isOpenApi30(version)) return deepNormaliseOpenApi30Doc(applyDiscriminatorAllOfPrepass(doc), deepNormalise);
|
|
2079
|
+
if (!isOpenApi31(version)) {
|
|
2080
|
+
const rawOpenApi = typeof doc.openapi === "string" ? doc.openapi : void 0;
|
|
2081
|
+
const rawSwagger = typeof doc.swagger === "string" ? doc.swagger : void 0;
|
|
2082
|
+
const versionLabel = rawOpenApi ?? rawSwagger ?? `${String(version.major)}.${String(version.minor)}.${String(version.patch)}`;
|
|
2083
|
+
const pointer = rawOpenApi !== void 0 ? "/openapi" : "/swagger";
|
|
2084
|
+
emitDiagnostic(diagnostics, {
|
|
2085
|
+
code: "unknown-openapi-version",
|
|
2086
|
+
message: `Unsupported OpenAPI/Swagger version "${versionLabel}"; falling back to the OpenAPI 3.1 pipeline`,
|
|
2087
|
+
pointer,
|
|
2088
|
+
detail: {
|
|
2089
|
+
version: versionLabel,
|
|
2090
|
+
major: version.major,
|
|
2091
|
+
minor: version.minor,
|
|
2092
|
+
patch: version.patch
|
|
2093
|
+
}
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
let dialectDraft;
|
|
1591
2097
|
if (isOpenApi31(version)) {
|
|
1592
2098
|
const dialect = readJsonSchemaDialect(doc);
|
|
1593
2099
|
if (dialect.kind === "unknown") emitDiagnostic(diagnostics, {
|
|
@@ -1596,8 +2102,45 @@ function normaliseOpenApiSchemas(doc, version, diagnostics) {
|
|
|
1596
2102
|
pointer: "/jsonSchemaDialect",
|
|
1597
2103
|
detail: { uri: dialect.uri }
|
|
1598
2104
|
});
|
|
2105
|
+
else if (dialect.kind === "known") dialectDraft = dialect.draft;
|
|
2106
|
+
}
|
|
2107
|
+
return deepNormaliseOpenApiDoc(applyDiscriminatorAllOfPrepass(doc), (schema) => {
|
|
2108
|
+
let intermediate = schema;
|
|
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));
|
|
2111
|
+
return resolveRelativeRefs(deepNormalise(intermediate, normaliseOpenApi30Discriminator), diagnostics);
|
|
2112
|
+
});
|
|
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;
|
|
1599
2142
|
}
|
|
1600
|
-
return
|
|
2143
|
+
return draft;
|
|
1601
2144
|
}
|
|
1602
2145
|
//#endregion
|
|
1603
|
-
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 };
|