schema-components 1.18.1 → 1.20.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/dist/core/adapter.d.mts +2 -2
- package/dist/core/adapter.mjs +128 -15
- package/dist/core/constraints.d.mts +2 -2
- package/dist/core/diagnostics.d.mts +1 -1
- package/dist/core/errors.d.mts +1 -1
- package/dist/core/errors.mjs +15 -1
- package/dist/core/fieldOrder.d.mts +1 -1
- package/dist/core/formats.d.mts +21 -14
- package/dist/core/formats.mjs +96 -4
- package/dist/core/merge.d.mts +1 -1
- package/dist/core/normalise.d.mts +38 -5
- package/dist/core/normalise.mjs +2 -2
- package/dist/core/openapi30.d.mts +33 -4
- package/dist/core/openapi30.mjs +2 -2
- package/dist/core/ref.d.mts +1 -1
- package/dist/core/renderer.d.mts +1 -1
- package/dist/core/renderer.mjs +7 -2
- package/dist/core/swagger2.d.mts +1 -1
- package/dist/core/swagger2.mjs +1 -1
- package/dist/core/typeInference.d.mts +2 -2
- package/dist/core/types.d.mts +1 -1
- package/dist/core/uri.d.mts +41 -0
- package/dist/core/uri.mjs +76 -0
- package/dist/core/version.d.mts +2 -2
- package/dist/core/version.mjs +43 -9
- package/dist/core/walkBuilders.d.mts +3 -3
- package/dist/core/walker.d.mts +1 -1
- package/dist/core/walker.mjs +50 -3
- package/dist/{diagnostics-BYk63jsC.d.mts → diagnostics-CbBPsxSt.d.mts} +1 -1
- package/dist/{errors-C5zRC2PU.d.mts → errors-C2iABcn9.d.mts} +14 -2
- package/dist/html/a11y.d.mts +2 -2
- package/dist/html/renderToHtml.d.mts +2 -2
- package/dist/html/renderToHtmlStream.d.mts +2 -2
- package/dist/html/renderers.d.mts +2 -2
- package/dist/html/renderers.mjs +37 -2
- package/dist/html/streamRenderers.d.mts +2 -2
- package/dist/normalise-CMMEl4cd.mjs +1306 -0
- 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/bundle.mjs +2 -0
- package/dist/openapi/components.d.mts +2 -2
- package/dist/openapi/components.mjs +20 -5
- package/dist/openapi/parser.d.mts +1 -1
- package/dist/openapi/parser.mjs +6 -1
- package/dist/openapi/resolve.d.mts +17 -6
- package/dist/openapi/resolve.mjs +45 -7
- package/dist/react/SchemaComponent.d.mts +21 -9
- package/dist/react/SchemaComponent.mjs +3 -13
- package/dist/react/SchemaView.d.mts +3 -3
- package/dist/react/SchemaView.mjs +1 -0
- package/dist/react/fieldPath.d.mts +1 -1
- package/dist/react/headless.d.mts +7 -1
- package/dist/react/headless.mjs +13 -1
- package/dist/react/headlessRenderers.d.mts +54 -3
- package/dist/react/headlessRenderers.mjs +153 -3
- package/dist/{ref-Ckt5liZs.d.mts → ref-C8JbwfiS.d.mts} +1 -1
- package/dist/{renderer-BAGoX4AK.d.mts → renderer-SOIbJBtk.d.mts} +9 -3
- 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/{typeInference-5JiqIZ8t.d.mts → typeInference-CDoD_LZ_.d.mts} +187 -42
- package/dist/{types-D_5ST7SS.d.mts → types-C9zw9wbX.d.mts} +6 -0
- package/dist/{version-B5NV-35j.d.mts → version-D-u7aMfy.d.mts} +43 -1
- package/package.json +1 -1
- package/dist/normalise-tL9FckAk.mjs +0 -748
|
@@ -0,0 +1,1306 @@
|
|
|
1
|
+
import { isObject } from "./core/guards.mjs";
|
|
2
|
+
import { appendPointer, emitDiagnostic } from "./core/diagnostics.mjs";
|
|
3
|
+
import { isOpenApi30, isOpenApi31, isSwagger2, readJsonSchemaDialect } from "./core/version.mjs";
|
|
4
|
+
//#region src/core/openapi30.ts
|
|
5
|
+
/**
|
|
6
|
+
* OpenAPI 3.0.x schema normalisation.
|
|
7
|
+
*
|
|
8
|
+
* Transforms `nullable`, `discriminator`, `example` keywords, and walks
|
|
9
|
+
* all schema locations (components, paths, parameters, request bodies,
|
|
10
|
+
* responses, headers, callbacks, links, examples) to apply normalisation.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Normalise OpenAPI 3.0.x `nullable` keyword to `anyOf [T, null]`.
|
|
14
|
+
*
|
|
15
|
+
* OpenAPI 3.0 uses `nullable: true` instead of the JSON Schema standard
|
|
16
|
+
* `anyOf: [T, { type: "null" }]`. The walker understands the latter form
|
|
17
|
+
* natively, so this normaliser converts `nullable` to `anyOf`.
|
|
18
|
+
*
|
|
19
|
+
* Only applied when `nullable` is explicitly `true`. `nullable: false` or
|
|
20
|
+
* absent is the default and requires no transformation.
|
|
21
|
+
*/
|
|
22
|
+
function normaliseOpenApi30Node(node) {
|
|
23
|
+
if ("example" in node && !("examples" in node)) {
|
|
24
|
+
node.examples = [node.example];
|
|
25
|
+
delete node.example;
|
|
26
|
+
} else if ("example" in node) delete node.example;
|
|
27
|
+
if (node.nullable !== true) {
|
|
28
|
+
if ("nullable" in node) delete node.nullable;
|
|
29
|
+
return node;
|
|
30
|
+
}
|
|
31
|
+
const nullOption = { type: "null" };
|
|
32
|
+
if (Array.isArray(node.anyOf)) {
|
|
33
|
+
node.anyOf = [...node.anyOf, nullOption];
|
|
34
|
+
delete node.nullable;
|
|
35
|
+
return node;
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(node.oneOf)) {
|
|
38
|
+
node.anyOf = [...node.oneOf, nullOption];
|
|
39
|
+
delete node.oneOf;
|
|
40
|
+
delete node.nullable;
|
|
41
|
+
return node;
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(node.allOf)) {
|
|
44
|
+
node.anyOf = [{ allOf: node.allOf }, nullOption];
|
|
45
|
+
delete node.allOf;
|
|
46
|
+
delete node.nullable;
|
|
47
|
+
return node;
|
|
48
|
+
}
|
|
49
|
+
const wrapper = {};
|
|
50
|
+
for (const [key, value] of Object.entries(node)) if (key !== "nullable") wrapper[key] = value;
|
|
51
|
+
return { anyOf: [wrapper, nullOption] };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Normalise OpenAPI 3.0.x `discriminator` keyword by injecting `const`
|
|
55
|
+
* values into each `oneOf`/`anyOf` option's discriminator property.
|
|
56
|
+
*
|
|
57
|
+
* In OpenAPI 3.0, `discriminator` is a sibling of `oneOf`/`anyOf`:
|
|
58
|
+
* discriminator: { propertyName: "type" }
|
|
59
|
+
* The walker detects discriminated unions from `oneOf` + `const` on a
|
|
60
|
+
* property, so this normaliser injects the `const` values from the
|
|
61
|
+
* `mapping` or infers them from `$ref` fragment names.
|
|
62
|
+
*/
|
|
63
|
+
function normaliseOpenApi30Discriminator(node) {
|
|
64
|
+
const discriminator = node.discriminator;
|
|
65
|
+
if (!isObject(discriminator)) return node;
|
|
66
|
+
const propertyName = discriminator.propertyName;
|
|
67
|
+
if (typeof propertyName !== "string") return node;
|
|
68
|
+
const mapping = isObject(discriminator.mapping) ? discriminator.mapping : void 0;
|
|
69
|
+
const composite = node.oneOf ?? node.anyOf;
|
|
70
|
+
if (!Array.isArray(composite)) return node;
|
|
71
|
+
const refToValue = /* @__PURE__ */ new Map();
|
|
72
|
+
if (mapping !== void 0) {
|
|
73
|
+
for (const [value, ref] of Object.entries(mapping)) if (typeof ref === "string") refToValue.set(ref, value);
|
|
74
|
+
}
|
|
75
|
+
const normalisedComposite = [];
|
|
76
|
+
for (const option of composite) {
|
|
77
|
+
if (!isObject(option)) {
|
|
78
|
+
normalisedComposite.push(option);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const props = isObject(option.properties) ? { ...option.properties } : void 0;
|
|
82
|
+
const discProp = props?.[propertyName];
|
|
83
|
+
if (isObject(discProp) && "const" in discProp) {
|
|
84
|
+
normalisedComposite.push(option);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
let constValue;
|
|
88
|
+
if (isObject(discProp) && typeof discProp.$ref === "string") constValue = refToValue.get(discProp.$ref);
|
|
89
|
+
if (constValue === void 0 && typeof option.$ref === "string") {
|
|
90
|
+
constValue = refToValue.get(option.$ref);
|
|
91
|
+
if (constValue === void 0) {
|
|
92
|
+
const fragment = option.$ref.split("/").pop();
|
|
93
|
+
if (fragment !== void 0) constValue = fragment;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (constValue === void 0 && mapping !== void 0) {
|
|
97
|
+
const optionIndex = composite.indexOf(option);
|
|
98
|
+
const mappingEntries = Object.entries(mapping);
|
|
99
|
+
const entry = optionIndex >= 0 && optionIndex < mappingEntries.length ? mappingEntries[optionIndex] : void 0;
|
|
100
|
+
if (entry !== void 0) constValue = entry[0];
|
|
101
|
+
}
|
|
102
|
+
if (constValue !== void 0) {
|
|
103
|
+
const normalisedProps = props ?? {};
|
|
104
|
+
normalisedProps[propertyName] = {
|
|
105
|
+
...isObject(discProp) ? discProp : {},
|
|
106
|
+
const: constValue
|
|
107
|
+
};
|
|
108
|
+
normalisedComposite.push({
|
|
109
|
+
...option,
|
|
110
|
+
properties: normalisedProps
|
|
111
|
+
});
|
|
112
|
+
} else normalisedComposite.push(option);
|
|
113
|
+
}
|
|
114
|
+
if ("oneOf" in node) node.oneOf = normalisedComposite;
|
|
115
|
+
else if ("anyOf" in node) node.anyOf = normalisedComposite;
|
|
116
|
+
delete node.discriminator;
|
|
117
|
+
return node;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Combined OpenAPI 3.0.x node transform: Draft 04 + nullable + discriminator.
|
|
121
|
+
* Applied to every schema node in an OpenAPI 3.0 document.
|
|
122
|
+
*
|
|
123
|
+
* Draft 04 normalisation is included because OpenAPI 3.0 inherits
|
|
124
|
+
* Draft 04/05 schema semantics including `exclusiveMinimum: boolean`.
|
|
125
|
+
*/
|
|
126
|
+
function normaliseOpenApi30Combined(node) {
|
|
127
|
+
return normaliseOpenApi30Discriminator(normaliseOpenApi30Node(normaliseDraft04Node(node)));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Deep-clone the parent first, then patch back any keys whose values were
|
|
131
|
+
* rewritten by the visitor. This preserves immutability of the original
|
|
132
|
+
* document while keeping the visitor straightforward to write.
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* Deep-normalise every Schema Object in an OpenAPI document.
|
|
136
|
+
*
|
|
137
|
+
* Walks: `paths.*` (operations + path-level parameters), `webhooks.*`
|
|
138
|
+
* (3.1), `components.schemas`, `components.parameters`,
|
|
139
|
+
* `components.responses`, `components.requestBodies`,
|
|
140
|
+
* `components.headers`, `components.callbacks`, `components.pathItems`
|
|
141
|
+
* (3.1). For each Schema-bearing location, applies the supplied
|
|
142
|
+
* `normaliseSchema` function.
|
|
143
|
+
*
|
|
144
|
+
* The walker is structural (it understands OAS document shapes) and
|
|
145
|
+
* delegates the per-schema transformation. For OAS 3.0 the caller
|
|
146
|
+
* passes a full Draft 04 + nullable + discriminator + example
|
|
147
|
+
* normaliser; for OAS 3.1 the caller passes a discriminator-only
|
|
148
|
+
* normaliser so the walker's discriminated-union detection sees the
|
|
149
|
+
* injected `const`s regardless of OAS minor version.
|
|
150
|
+
*/
|
|
151
|
+
function deepNormaliseOpenApiDoc(doc, normaliseSchema) {
|
|
152
|
+
const result = { ...doc };
|
|
153
|
+
const components = doc.components;
|
|
154
|
+
if (isObject(components)) result.components = normaliseComponents(components, normaliseSchema);
|
|
155
|
+
const paths = doc.paths;
|
|
156
|
+
if (isObject(paths)) result.paths = normalisePathMap(paths, normaliseSchema);
|
|
157
|
+
const webhooks = doc.webhooks;
|
|
158
|
+
if (isObject(webhooks)) result.webhooks = normalisePathMap(webhooks, normaliseSchema);
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Backwards-compatible wrapper retaining the historic `deepNormalise`
|
|
163
|
+
* signature used by callers in `normalise.ts`. Always applies the full
|
|
164
|
+
* 3.0 combined transform via `deepNormalise(schema, normaliseOpenApi30Combined)`.
|
|
165
|
+
*/
|
|
166
|
+
function deepNormaliseOpenApi30Doc(doc, deepNormalise) {
|
|
167
|
+
return deepNormaliseOpenApiDoc(doc, (schema) => deepNormalise(schema, normaliseOpenApi30Combined));
|
|
168
|
+
}
|
|
169
|
+
function normaliseComponents(components, normaliseSchema) {
|
|
170
|
+
const result = { ...components };
|
|
171
|
+
const schemas = components.schemas;
|
|
172
|
+
if (isObject(schemas)) result.schemas = mapObjectValues(schemas, (schema) => isObject(schema) ? normaliseSchema(schema) : schema);
|
|
173
|
+
const parameters = components.parameters;
|
|
174
|
+
if (isObject(parameters)) result.parameters = mapObjectValues(parameters, (param) => isObject(param) ? normaliseParameter(param, normaliseSchema) : param);
|
|
175
|
+
const responses = components.responses;
|
|
176
|
+
if (isObject(responses)) result.responses = mapObjectValues(responses, (response) => isObject(response) ? normaliseResponse(response, normaliseSchema) : response);
|
|
177
|
+
const requestBodies = components.requestBodies;
|
|
178
|
+
if (isObject(requestBodies)) result.requestBodies = mapObjectValues(requestBodies, (body) => isObject(body) ? normaliseRequestBody(body, normaliseSchema) : body);
|
|
179
|
+
const headers = components.headers;
|
|
180
|
+
if (isObject(headers)) result.headers = mapObjectValues(headers, (header) => isObject(header) ? normaliseHeader(header, normaliseSchema) : header);
|
|
181
|
+
const callbacks = components.callbacks;
|
|
182
|
+
if (isObject(callbacks)) result.callbacks = mapObjectValues(callbacks, (callback) => isObject(callback) ? normaliseCallback(callback, normaliseSchema) : callback);
|
|
183
|
+
const pathItems = components.pathItems;
|
|
184
|
+
if (isObject(pathItems)) result.pathItems = mapObjectValues(pathItems, (pathItem) => isObject(pathItem) ? normalisePathItem(pathItem, normaliseSchema) : pathItem);
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
function normalisePathMap(paths, normaliseSchema) {
|
|
188
|
+
return mapObjectValues(paths, (pathItem) => isObject(pathItem) ? normalisePathItem(pathItem, normaliseSchema) : pathItem);
|
|
189
|
+
}
|
|
190
|
+
const HTTP_METHODS = [
|
|
191
|
+
"get",
|
|
192
|
+
"put",
|
|
193
|
+
"post",
|
|
194
|
+
"delete",
|
|
195
|
+
"options",
|
|
196
|
+
"head",
|
|
197
|
+
"patch",
|
|
198
|
+
"trace"
|
|
199
|
+
];
|
|
200
|
+
function normalisePathItem(pathItem, normaliseSchema) {
|
|
201
|
+
const result = { ...pathItem };
|
|
202
|
+
for (const method of HTTP_METHODS) {
|
|
203
|
+
const operation = pathItem[method];
|
|
204
|
+
if (isObject(operation)) result[method] = normaliseOperation(operation, normaliseSchema);
|
|
205
|
+
}
|
|
206
|
+
const parameters = pathItem.parameters;
|
|
207
|
+
if (Array.isArray(parameters)) result.parameters = parameters.map((param) => isObject(param) ? normaliseParameter(param, normaliseSchema) : param);
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
function normaliseOperation(operation, normaliseSchema) {
|
|
211
|
+
const result = { ...operation };
|
|
212
|
+
const parameters = operation.parameters;
|
|
213
|
+
if (Array.isArray(parameters)) result.parameters = parameters.map((param) => isObject(param) ? normaliseParameter(param, normaliseSchema) : param);
|
|
214
|
+
const requestBody = operation.requestBody;
|
|
215
|
+
if (isObject(requestBody)) result.requestBody = normaliseRequestBody(requestBody, normaliseSchema);
|
|
216
|
+
const responses = operation.responses;
|
|
217
|
+
if (isObject(responses)) result.responses = mapObjectValues(responses, (response) => isObject(response) ? normaliseResponse(response, normaliseSchema) : response);
|
|
218
|
+
const callbacks = operation.callbacks;
|
|
219
|
+
if (isObject(callbacks)) result.callbacks = mapObjectValues(callbacks, (callback) => isObject(callback) ? normaliseCallback(callback, normaliseSchema) : callback);
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
function normaliseParameter(param, normaliseSchema) {
|
|
223
|
+
const result = { ...param };
|
|
224
|
+
const schema = param.schema;
|
|
225
|
+
if (isObject(schema)) result.schema = normaliseSchema(schema);
|
|
226
|
+
const content = param.content;
|
|
227
|
+
if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
|
|
228
|
+
if ("example" in result && !("examples" in result)) {
|
|
229
|
+
result.examples = [result.example];
|
|
230
|
+
delete result.example;
|
|
231
|
+
} else if ("example" in result) delete result.example;
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
function normaliseRequestBody(requestBody, normaliseSchema) {
|
|
235
|
+
const result = { ...requestBody };
|
|
236
|
+
const content = requestBody.content;
|
|
237
|
+
if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
function normaliseResponse(response, normaliseSchema) {
|
|
241
|
+
const result = { ...response };
|
|
242
|
+
const content = response.content;
|
|
243
|
+
if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
|
|
244
|
+
const headers = response.headers;
|
|
245
|
+
if (isObject(headers)) result.headers = mapObjectValues(headers, (header) => isObject(header) ? normaliseHeader(header, normaliseSchema) : header);
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
function normaliseHeader(header, normaliseSchema) {
|
|
249
|
+
const result = { ...header };
|
|
250
|
+
const schema = header.schema;
|
|
251
|
+
if (isObject(schema)) result.schema = normaliseSchema(schema);
|
|
252
|
+
const content = header.content;
|
|
253
|
+
if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
|
|
254
|
+
if ("example" in result && !("examples" in result)) {
|
|
255
|
+
result.examples = [result.example];
|
|
256
|
+
delete result.example;
|
|
257
|
+
} else if ("example" in result) delete result.example;
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* A Callback Object is a map of runtime-expression keys → Path Item
|
|
262
|
+
* Objects. Each Path Item carries operations whose responses, request
|
|
263
|
+
* bodies, parameters, and headers may all contain Schema Objects.
|
|
264
|
+
*/
|
|
265
|
+
function normaliseCallback(callback, normaliseSchema) {
|
|
266
|
+
return mapObjectValues(callback, (pathItem) => isObject(pathItem) ? normalisePathItem(pathItem, normaliseSchema) : pathItem);
|
|
267
|
+
}
|
|
268
|
+
function normaliseContentMap(content, normaliseSchema) {
|
|
269
|
+
const result = {};
|
|
270
|
+
for (const [mediaType, mediaObj] of Object.entries(content)) {
|
|
271
|
+
if (!isObject(mediaObj)) {
|
|
272
|
+
result[mediaType] = mediaObj;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const normalised = { ...mediaObj };
|
|
276
|
+
const schema = mediaObj.schema;
|
|
277
|
+
if (isObject(schema)) normalised.schema = normaliseSchema(schema);
|
|
278
|
+
const encoding = mediaObj.encoding;
|
|
279
|
+
if (isObject(encoding)) normalised.encoding = mapObjectValues(encoding, (enc) => isObject(enc) ? normaliseEncoding(enc, normaliseSchema) : enc);
|
|
280
|
+
if ("example" in normalised && !("examples" in normalised)) {
|
|
281
|
+
normalised.examples = { default: { value: normalised.example } };
|
|
282
|
+
delete normalised.example;
|
|
283
|
+
} else if ("example" in normalised) delete normalised.example;
|
|
284
|
+
result[mediaType] = normalised;
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
function normaliseEncoding(encoding, normaliseSchema) {
|
|
289
|
+
const result = { ...encoding };
|
|
290
|
+
const headers = encoding.headers;
|
|
291
|
+
if (isObject(headers)) result.headers = mapObjectValues(headers, (header) => isObject(header) ? normaliseHeader(header, normaliseSchema) : header);
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Apply `transform` to each value of a `Record<string, unknown>` and
|
|
296
|
+
* return a new record. Non-object values pass through transform unchanged
|
|
297
|
+
* — callers add their own `isObject` guard inside `transform`.
|
|
298
|
+
*/
|
|
299
|
+
function mapObjectValues(source, transform) {
|
|
300
|
+
const result = {};
|
|
301
|
+
for (const [key, value] of Object.entries(source)) result[key] = transform(value);
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
//#endregion
|
|
305
|
+
//#region src/core/swagger2.ts
|
|
306
|
+
/**
|
|
307
|
+
* Swagger 2.0 → OpenAPI 3.1 document normalisation.
|
|
308
|
+
*
|
|
309
|
+
* Transforms a Swagger 2.0 document into an OpenAPI 3.1-compatible
|
|
310
|
+
* structure: host/basePath/schemes → servers, definitions → components,
|
|
311
|
+
* body/formData params → requestBody, response schemas → content.
|
|
312
|
+
*
|
|
313
|
+
* Individual schemas within the document are also normalised for
|
|
314
|
+
* Draft 04 semantics (exclusiveMinimum/exclusiveMaximum booleans).
|
|
315
|
+
*/
|
|
316
|
+
/**
|
|
317
|
+
* Transform a Swagger 2.0 document into an OpenAPI 3.1-compatible
|
|
318
|
+
* structure.
|
|
319
|
+
*/
|
|
320
|
+
function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, diagnostics) {
|
|
321
|
+
const result = {
|
|
322
|
+
openapi: "3.1.0",
|
|
323
|
+
info: isObject(doc.info) ? { ...doc.info } : {
|
|
324
|
+
title: "API",
|
|
325
|
+
version: "0.0.0"
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
if (typeof doc.host === "string" || typeof doc.basePath === "string" || Array.isArray(doc.schemes)) {
|
|
329
|
+
const host = typeof doc.host === "string" ? doc.host : "localhost";
|
|
330
|
+
const basePath = typeof doc.basePath === "string" ? doc.basePath : "/";
|
|
331
|
+
const schemes = Array.isArray(doc.schemes) ? doc.schemes : ["https"];
|
|
332
|
+
result.servers = [{ url: `${typeof schemes[0] === "string" ? schemes[0] : "https"}://${host}${basePath}` }];
|
|
333
|
+
}
|
|
334
|
+
const paths = doc.paths;
|
|
335
|
+
if (isObject(paths)) result.paths = normaliseSwaggerPaths(paths, doc, diagnostics);
|
|
336
|
+
const components = {};
|
|
337
|
+
const definitions = doc.definitions;
|
|
338
|
+
if (isObject(definitions)) {
|
|
339
|
+
const schemas = {};
|
|
340
|
+
for (const [name, schema] of Object.entries(definitions)) schemas[name] = isObject(schema) ? deepNormalise(schema, (node) => normaliseOpenApi30Combined(normaliseDraft04Node(node))) : schema;
|
|
341
|
+
components.schemas = schemas;
|
|
342
|
+
}
|
|
343
|
+
const parameters = doc.parameters;
|
|
344
|
+
const requestBodies = {};
|
|
345
|
+
if (isObject(parameters)) {
|
|
346
|
+
const globalConsumes = Array.isArray(doc.consumes) ? doc.consumes : ["application/json"];
|
|
347
|
+
const convertedParameters = {};
|
|
348
|
+
for (const [name, param] of Object.entries(parameters)) {
|
|
349
|
+
if (!isObject(param)) {
|
|
350
|
+
convertedParameters[name] = param;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const resolved = resolveSwaggerParameter(param, doc);
|
|
354
|
+
const location = resolved.in;
|
|
355
|
+
if (location === "body") requestBodies[name] = buildRequestBody(resolved, globalConsumes);
|
|
356
|
+
else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]), formDataContentTypes(globalConsumes));
|
|
357
|
+
else convertedParameters[name] = normaliseSwaggerParameter(resolved, doc);
|
|
358
|
+
}
|
|
359
|
+
if (Object.keys(convertedParameters).length > 0) components.parameters = convertedParameters;
|
|
360
|
+
}
|
|
361
|
+
const responses = doc.responses;
|
|
362
|
+
if (isObject(responses)) {
|
|
363
|
+
const globalProduces = Array.isArray(doc.produces) ? doc.produces : ["application/json"];
|
|
364
|
+
const convertedResponses = {};
|
|
365
|
+
for (const [name, response] of Object.entries(responses)) convertedResponses[name] = isObject(response) ? normaliseSwaggerSingleResponse(response, doc, globalProduces) : response;
|
|
366
|
+
components.responses = convertedResponses;
|
|
367
|
+
}
|
|
368
|
+
if (Object.keys(requestBodies).length > 0) components.requestBodies = requestBodies;
|
|
369
|
+
const securityDefinitions = doc.securityDefinitions;
|
|
370
|
+
if (isObject(securityDefinitions)) components.securitySchemes = { ...securityDefinitions };
|
|
371
|
+
if (Object.keys(components).length > 0) result.components = components;
|
|
372
|
+
if (Array.isArray(doc.tags)) result.tags = doc.tags;
|
|
373
|
+
if (isObject(doc.externalDocs)) result.externalDocs = doc.externalDocs;
|
|
374
|
+
if (Array.isArray(doc.security)) result.security = doc.security;
|
|
375
|
+
rewriteSwaggerRefs(result);
|
|
376
|
+
if (hasXmlAnywhere(doc.definitions) || hasXmlAnywhere(doc.paths) || hasXmlAnywhere(doc.parameters) || hasXmlAnywhere(doc.responses)) emitDiagnostic(diagnostics, {
|
|
377
|
+
code: "dropped-swagger-feature",
|
|
378
|
+
message: "Swagger 2.0 xml markup is not supported and will be dropped",
|
|
379
|
+
pointer: "",
|
|
380
|
+
detail: { feature: "xml" }
|
|
381
|
+
});
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
function normaliseSwaggerPaths(paths, doc, diagnostics) {
|
|
385
|
+
const result = {};
|
|
386
|
+
const METHODS = [
|
|
387
|
+
"get",
|
|
388
|
+
"post",
|
|
389
|
+
"put",
|
|
390
|
+
"patch",
|
|
391
|
+
"delete",
|
|
392
|
+
"head",
|
|
393
|
+
"options"
|
|
394
|
+
];
|
|
395
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
396
|
+
if (!isObject(pathItem)) {
|
|
397
|
+
result[path] = pathItem;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const normalisedPath = {};
|
|
401
|
+
for (const method of METHODS) {
|
|
402
|
+
const operation = pathItem[method];
|
|
403
|
+
if (!isObject(operation)) continue;
|
|
404
|
+
normalisedPath[method] = normaliseSwaggerOperation(operation, doc, path, method, diagnostics);
|
|
405
|
+
}
|
|
406
|
+
const pathParams = pathItem.parameters;
|
|
407
|
+
if (Array.isArray(pathParams)) normalisedPath.parameters = pathParams.map((p) => isObject(p) ? normaliseSwaggerParameter(p, doc) : p);
|
|
408
|
+
result[path] = normalisedPath;
|
|
409
|
+
}
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
|
|
413
|
+
const result = {};
|
|
414
|
+
const globalProduces = Array.isArray(doc.produces) ? doc.produces : ["application/json"];
|
|
415
|
+
const globalConsumes = Array.isArray(doc.consumes) ? doc.consumes : ["application/json"];
|
|
416
|
+
const produces = Array.isArray(operation.produces) ? operation.produces : globalProduces;
|
|
417
|
+
const consumes = Array.isArray(operation.consumes) ? operation.consumes : globalConsumes;
|
|
418
|
+
for (const [key, value] of Object.entries(operation)) if (key !== "parameters" && key !== "responses" && key !== "produces" && key !== "consumes") result[key] = value;
|
|
419
|
+
const params = operation.parameters;
|
|
420
|
+
if (Array.isArray(params)) {
|
|
421
|
+
const nonBodyParams = [];
|
|
422
|
+
let bodyParam;
|
|
423
|
+
let firstBodyName;
|
|
424
|
+
let usesFormData = false;
|
|
425
|
+
for (const [index, param] of params.entries()) {
|
|
426
|
+
if (!isObject(param)) {
|
|
427
|
+
nonBodyParams.push(param);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const resolvedParam = resolveSwaggerParameter(param, doc);
|
|
431
|
+
const location = resolvedParam.in;
|
|
432
|
+
if (location === "body") {
|
|
433
|
+
if (bodyParam !== void 0) {
|
|
434
|
+
const duplicateName = typeof resolvedParam.name === "string" ? resolvedParam.name : `parameters[${String(index)}]`;
|
|
435
|
+
emitDiagnostic(diagnostics, {
|
|
436
|
+
code: "duplicate-body-parameter",
|
|
437
|
+
message: `Operation defines more than one "in: body" parameter; keeping the first ("${firstBodyName ?? "(unnamed)"}") and discarding "${duplicateName}"`,
|
|
438
|
+
pointer: appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "parameters"),
|
|
439
|
+
detail: {
|
|
440
|
+
kept: firstBodyName,
|
|
441
|
+
discarded: duplicateName,
|
|
442
|
+
location: "operation"
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
bodyParam = resolvedParam;
|
|
448
|
+
firstBodyName = typeof resolvedParam.name === "string" ? resolvedParam.name : void 0;
|
|
449
|
+
} else if (location === "formData") {
|
|
450
|
+
if (!usesFormData) {
|
|
451
|
+
bodyParam = buildFormDataBody(resolvedParam, params);
|
|
452
|
+
usesFormData = true;
|
|
453
|
+
}
|
|
454
|
+
} else nonBodyParams.push(normaliseSwaggerParameter(resolvedParam, doc));
|
|
455
|
+
}
|
|
456
|
+
if (nonBodyParams.length > 0) result.parameters = nonBodyParams;
|
|
457
|
+
if (bodyParam !== void 0) result.requestBody = buildRequestBody(bodyParam, usesFormData ? formDataContentTypes(consumes) : consumes);
|
|
458
|
+
}
|
|
459
|
+
const responses = operation.responses;
|
|
460
|
+
if (isObject(responses)) result.responses = normaliseSwaggerResponses(responses, doc, produces);
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Determine the request body media type for a Swagger 2.0 formData operation.
|
|
465
|
+
*
|
|
466
|
+
* Per the OAS 3 conversion rules, `application/x-www-form-urlencoded` is
|
|
467
|
+
* preferred when the operation- or document-level `consumes` includes it;
|
|
468
|
+
* otherwise `multipart/form-data` is the default. File uploads (Swagger 2.0
|
|
469
|
+
* `type: file`) still require `multipart/form-data`, but the formData body
|
|
470
|
+
* schema-builder normalises them to `string` + `format: binary` either way
|
|
471
|
+
* and the choice of media type is left to the source document.
|
|
472
|
+
*/
|
|
473
|
+
function formDataContentTypes(consumes) {
|
|
474
|
+
if (consumes.includes("application/x-www-form-urlencoded")) return ["application/x-www-form-urlencoded"];
|
|
475
|
+
return ["multipart/form-data"];
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Resolve a Swagger parameter that may be a `$ref`.
|
|
479
|
+
*/
|
|
480
|
+
function resolveSwaggerParameter(param, doc, visited = /* @__PURE__ */ new Set()) {
|
|
481
|
+
const ref = param.$ref;
|
|
482
|
+
if (typeof ref !== "string" || !ref.startsWith("#/parameters/")) return param;
|
|
483
|
+
if (visited.has(ref)) return param;
|
|
484
|
+
const nextVisited = new Set(visited);
|
|
485
|
+
nextVisited.add(ref);
|
|
486
|
+
const name = ref.slice(13);
|
|
487
|
+
const globalParams = doc.parameters;
|
|
488
|
+
if (isObject(globalParams)) {
|
|
489
|
+
const resolved = globalParams[name];
|
|
490
|
+
if (isObject(resolved)) {
|
|
491
|
+
if (typeof resolved.$ref === "string") return resolveSwaggerParameter(resolved, doc, nextVisited);
|
|
492
|
+
return resolved;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return param;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Normalise a single Swagger parameter to OpenAPI 3.x form.
|
|
499
|
+
*/
|
|
500
|
+
function normaliseSwaggerParameter(param, doc) {
|
|
501
|
+
if (typeof param.$ref === "string") {
|
|
502
|
+
const resolved = resolveSwaggerParameter(param, doc);
|
|
503
|
+
if (resolved !== param) return normaliseSwaggerParameter(resolved, doc);
|
|
504
|
+
}
|
|
505
|
+
const result = {};
|
|
506
|
+
for (const [key, value] of Object.entries(param)) {
|
|
507
|
+
if (key === "type" || key === "format" || key === "collectionFormat") continue;
|
|
508
|
+
result[key] = value;
|
|
509
|
+
}
|
|
510
|
+
if (typeof param.type === "string") {
|
|
511
|
+
const schema = { type: param.type };
|
|
512
|
+
if (typeof param.format === "string") schema.format = param.format;
|
|
513
|
+
if (param.enum !== void 0) schema.enum = param.enum;
|
|
514
|
+
if (param.default !== void 0) schema.default = param.default;
|
|
515
|
+
if (param.minimum !== void 0) schema.minimum = param.minimum;
|
|
516
|
+
if (param.maximum !== void 0) schema.maximum = param.maximum;
|
|
517
|
+
result.schema = schema;
|
|
518
|
+
}
|
|
519
|
+
const cf = param.collectionFormat;
|
|
520
|
+
if (typeof cf === "string") switch (cf) {
|
|
521
|
+
case "csv":
|
|
522
|
+
result.style = "form";
|
|
523
|
+
result.explode = false;
|
|
524
|
+
break;
|
|
525
|
+
case "ssv":
|
|
526
|
+
result.style = "spaceDelimited";
|
|
527
|
+
result.explode = false;
|
|
528
|
+
break;
|
|
529
|
+
case "tsv":
|
|
530
|
+
result.style = "tabDelimited";
|
|
531
|
+
result.explode = false;
|
|
532
|
+
break;
|
|
533
|
+
case "pipes":
|
|
534
|
+
result.style = "pipeDelimited";
|
|
535
|
+
result.explode = false;
|
|
536
|
+
break;
|
|
537
|
+
case "multi":
|
|
538
|
+
result.style = "form";
|
|
539
|
+
result.explode = true;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Build a request body from a `formData` parameter.
|
|
546
|
+
*/
|
|
547
|
+
function buildFormDataBody(param, allParams) {
|
|
548
|
+
const properties = {};
|
|
549
|
+
const required = [];
|
|
550
|
+
for (const p of allParams) {
|
|
551
|
+
if (!isObject(p) || p.in !== "formData") continue;
|
|
552
|
+
const name = p.name;
|
|
553
|
+
if (typeof name !== "string") continue;
|
|
554
|
+
const schema = {};
|
|
555
|
+
if (p.type === "file") {
|
|
556
|
+
schema.type = "string";
|
|
557
|
+
schema.format = "binary";
|
|
558
|
+
} else {
|
|
559
|
+
if (typeof p.type === "string") schema.type = p.type;
|
|
560
|
+
if (typeof p.format === "string") schema.format = p.format;
|
|
561
|
+
if (p.enum !== void 0) schema.enum = p.enum;
|
|
562
|
+
}
|
|
563
|
+
properties[name] = schema;
|
|
564
|
+
if (p.required === true) required.push(name);
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
name: param.name,
|
|
568
|
+
in: "body",
|
|
569
|
+
schema: {
|
|
570
|
+
type: "object",
|
|
571
|
+
properties,
|
|
572
|
+
...required.length > 0 ? { required } : {}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Build an OpenAPI 3.x request body from a Swagger 2.0 body parameter.
|
|
578
|
+
*/
|
|
579
|
+
function buildRequestBody(bodyParam, consumes) {
|
|
580
|
+
const schema = bodyParam.schema;
|
|
581
|
+
const content = {};
|
|
582
|
+
const contentTypes = consumes.length > 0 ? consumes : ["application/json"];
|
|
583
|
+
for (const ct of contentTypes) if (typeof ct === "string") content[ct] = isObject(schema) ? { schema } : {};
|
|
584
|
+
const result = { content };
|
|
585
|
+
if (bodyParam.required === true) result.required = true;
|
|
586
|
+
if (typeof bodyParam.description === "string") result.description = bodyParam.description;
|
|
587
|
+
return result;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Resolve a Swagger 2.0 response `$ref` (e.g. `#/responses/NotFound`).
|
|
591
|
+
*/
|
|
592
|
+
function resolveSwaggerResponse(response, doc, visited = /* @__PURE__ */ new Set()) {
|
|
593
|
+
const ref = response.$ref;
|
|
594
|
+
if (typeof ref !== "string" || !ref.startsWith("#/responses/")) return response;
|
|
595
|
+
if (visited.has(ref)) return response;
|
|
596
|
+
const nextVisited = new Set(visited);
|
|
597
|
+
nextVisited.add(ref);
|
|
598
|
+
const name = ref.slice(12);
|
|
599
|
+
const globalResponses = doc.responses;
|
|
600
|
+
if (isObject(globalResponses)) {
|
|
601
|
+
const resolved = globalResponses[name];
|
|
602
|
+
if (isObject(resolved)) {
|
|
603
|
+
if (typeof resolved.$ref === "string") return resolveSwaggerResponse(resolved, doc, nextVisited);
|
|
604
|
+
return resolved;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return response;
|
|
608
|
+
}
|
|
609
|
+
function normaliseSwaggerResponses(responses, doc, produces) {
|
|
610
|
+
const result = {};
|
|
611
|
+
for (const [code, response] of Object.entries(responses)) {
|
|
612
|
+
if (!isObject(response)) {
|
|
613
|
+
result[code] = response;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
result[code] = normaliseSwaggerSingleResponse(response, doc, produces);
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Normalise a single Swagger 2.0 response object — resolves any `$ref` to
|
|
622
|
+
* `#/responses/<Name>` and wraps a top-level `schema` in an OpenAPI 3.x
|
|
623
|
+
* `content` map keyed by the supplied media types.
|
|
624
|
+
*
|
|
625
|
+
* Extracted so the same logic applies whether the response sits inside an
|
|
626
|
+
* operation’s `responses` map or under document-level `responses`
|
|
627
|
+
* (now `components.responses`).
|
|
628
|
+
*/
|
|
629
|
+
function normaliseSwaggerSingleResponse(response, doc, produces) {
|
|
630
|
+
const resolved = resolveSwaggerResponse(response, doc);
|
|
631
|
+
const normalised = {};
|
|
632
|
+
for (const [key, value] of Object.entries(resolved)) if (key !== "schema" && key !== "headers") normalised[key] = value;
|
|
633
|
+
const schema = resolved.schema;
|
|
634
|
+
if (isObject(schema)) {
|
|
635
|
+
const content = {};
|
|
636
|
+
const contentTypes = produces.length > 0 ? produces : ["application/json"];
|
|
637
|
+
for (const ct of contentTypes) if (typeof ct === "string") content[ct] = { schema };
|
|
638
|
+
normalised.content = content;
|
|
639
|
+
}
|
|
640
|
+
const headers = resolved.headers;
|
|
641
|
+
if (isObject(headers)) {
|
|
642
|
+
const convertedHeaders = {};
|
|
643
|
+
for (const [name, header] of Object.entries(headers)) convertedHeaders[name] = isObject(header) ? normaliseSwaggerHeader(header) : header;
|
|
644
|
+
normalised.headers = convertedHeaders;
|
|
645
|
+
}
|
|
646
|
+
return normalised;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Normalise a single Swagger 2.0 response header to OpenAPI 3.x form.
|
|
650
|
+
*
|
|
651
|
+
* Swagger 2.0 headers mirror parameter shape: `type`/`format`/
|
|
652
|
+
* `collectionFormat` live at the root. OpenAPI 3.x requires the type
|
|
653
|
+
* descriptor under `schema`, with collection serialisation expressed via
|
|
654
|
+
* `style`/`explode`. Headers do not carry `name` or `in` — those are not
|
|
655
|
+
* part of either spec at this level — so this is a thin sibling to
|
|
656
|
+
* `normaliseSwaggerParameter` rather than a full reuse. The OpenAPI 3.x
|
|
657
|
+
* default header style is `simple`, so CSV-encoded headers map to
|
|
658
|
+
* `simple`/`explode: false` rather than the `form` style used for query
|
|
659
|
+
* parameters.
|
|
660
|
+
*/
|
|
661
|
+
function normaliseSwaggerHeader(header) {
|
|
662
|
+
const result = {};
|
|
663
|
+
for (const [key, value] of Object.entries(header)) {
|
|
664
|
+
if (key === "type" || key === "format" || key === "collectionFormat") continue;
|
|
665
|
+
result[key] = value;
|
|
666
|
+
}
|
|
667
|
+
if (typeof header.type === "string") {
|
|
668
|
+
const schema = { type: header.type };
|
|
669
|
+
if (typeof header.format === "string") schema.format = header.format;
|
|
670
|
+
if (header.enum !== void 0) schema.enum = header.enum;
|
|
671
|
+
if (header.default !== void 0) schema.default = header.default;
|
|
672
|
+
if (header.minimum !== void 0) schema.minimum = header.minimum;
|
|
673
|
+
if (header.maximum !== void 0) schema.maximum = header.maximum;
|
|
674
|
+
result.schema = schema;
|
|
675
|
+
}
|
|
676
|
+
const cf = header.collectionFormat;
|
|
677
|
+
if (typeof cf === "string") switch (cf) {
|
|
678
|
+
case "csv":
|
|
679
|
+
result.style = "simple";
|
|
680
|
+
result.explode = false;
|
|
681
|
+
break;
|
|
682
|
+
case "ssv":
|
|
683
|
+
result.style = "spaceDelimited";
|
|
684
|
+
result.explode = false;
|
|
685
|
+
break;
|
|
686
|
+
case "tsv":
|
|
687
|
+
result.style = "tabDelimited";
|
|
688
|
+
result.explode = false;
|
|
689
|
+
break;
|
|
690
|
+
case "pipes":
|
|
691
|
+
result.style = "pipeDelimited";
|
|
692
|
+
result.explode = false;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Mapping of Swagger 2.0 $ref prefixes to OpenAPI 3.x equivalents.
|
|
699
|
+
* Applied after document restructuring so all $ref strings point
|
|
700
|
+
* to the correct locations in the normalised document.
|
|
701
|
+
*/
|
|
702
|
+
const REF_REWRITES = [
|
|
703
|
+
["#/definitions/", "#/components/schemas/"],
|
|
704
|
+
["#/parameters/", "#/components/parameters/"],
|
|
705
|
+
["#/responses/", "#/components/responses/"]
|
|
706
|
+
];
|
|
707
|
+
/**
|
|
708
|
+
* Deep-rewrite $ref strings in a normalised Swagger 2.0 document
|
|
709
|
+
* from Swagger 2.0 locations to OpenAPI 3.x locations.
|
|
710
|
+
* Mutates the object in place \u2014 called only on the fresh clone
|
|
711
|
+
* produced by normaliseSwagger2Document.
|
|
712
|
+
*/
|
|
713
|
+
function rewriteSwaggerRefs(node) {
|
|
714
|
+
if (!isObject(node)) return;
|
|
715
|
+
if (typeof node.$ref === "string") {
|
|
716
|
+
for (const [from, to] of REF_REWRITES) if (node.$ref.startsWith(from)) {
|
|
717
|
+
node.$ref = to + node.$ref.slice(from.length);
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
for (const value of Object.values(node)) if (isObject(value)) rewriteSwaggerRefs(value);
|
|
722
|
+
else if (Array.isArray(value)) for (const item of value) rewriteSwaggerRefs(item);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Recursively check whether any node in the supplied subtree carries an
|
|
726
|
+
* `xml` annotation. Walks both objects and arrays so the check works for
|
|
727
|
+
* schemas (definitions, parameter schemas, response schemas, request body
|
|
728
|
+
* schemas) as well as operations and parameters that may carry `xml`
|
|
729
|
+
* metadata at any depth.
|
|
730
|
+
*/
|
|
731
|
+
function hasXmlAnywhere(node) {
|
|
732
|
+
if (!isObject(node)) {
|
|
733
|
+
if (Array.isArray(node)) {
|
|
734
|
+
for (const item of node) if (hasXmlAnywhere(item)) return true;
|
|
735
|
+
}
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
if ("xml" in node) return true;
|
|
739
|
+
for (const value of Object.values(node)) if (hasXmlAnywhere(value)) return true;
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
//#endregion
|
|
743
|
+
//#region src/core/normalise.ts
|
|
744
|
+
/**
|
|
745
|
+
* Keys whose values are `Record<string, SubSchema>` — objects where each
|
|
746
|
+
* property is a sub-schema.
|
|
747
|
+
*/
|
|
748
|
+
const OBJECT_SUBSCHEMA_KEYS = new Set([
|
|
749
|
+
"properties",
|
|
750
|
+
"patternProperties",
|
|
751
|
+
"$defs",
|
|
752
|
+
"definitions",
|
|
753
|
+
"dependentSchemas"
|
|
754
|
+
]);
|
|
755
|
+
/**
|
|
756
|
+
* Keys whose values are `SubSchema[]` — arrays of sub-schemas.
|
|
757
|
+
*/
|
|
758
|
+
const ARRAY_SUBSCHEMA_KEYS = new Set([
|
|
759
|
+
"allOf",
|
|
760
|
+
"anyOf",
|
|
761
|
+
"oneOf",
|
|
762
|
+
"prefixItems"
|
|
763
|
+
]);
|
|
764
|
+
/**
|
|
765
|
+
* Keys whose values are a single sub-schema object.
|
|
766
|
+
*/
|
|
767
|
+
const SINGLE_SUBSCHEMA_KEYS = new Set([
|
|
768
|
+
"additionalProperties",
|
|
769
|
+
"not",
|
|
770
|
+
"contains",
|
|
771
|
+
"propertyNames",
|
|
772
|
+
"if",
|
|
773
|
+
"then",
|
|
774
|
+
"else",
|
|
775
|
+
"unevaluatedProperties",
|
|
776
|
+
"unevaluatedItems"
|
|
777
|
+
]);
|
|
778
|
+
/**
|
|
779
|
+
* Normalise each element of an unknown array by applying deepNormalise
|
|
780
|
+
* to object elements and passing others through unchanged.
|
|
781
|
+
*/
|
|
782
|
+
function normaliseArray(items, transform) {
|
|
783
|
+
const result = [];
|
|
784
|
+
for (const item of items) result.push(isObject(item) ? deepNormalise(item, transform) : item);
|
|
785
|
+
return result;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Normalise each value of a sub-schema map (e.g. properties, $defs).
|
|
789
|
+
*/
|
|
790
|
+
function normaliseSubSchemaMap(map, transform) {
|
|
791
|
+
const result = {};
|
|
792
|
+
for (const [k, v] of Object.entries(map)) result[k] = isObject(v) ? deepNormalise(v, transform) : v;
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Deep-normalise a JSON Schema object by applying a per-node transform
|
|
797
|
+
* and recursing into every sub-schema location.
|
|
798
|
+
*/
|
|
799
|
+
function deepNormalise(schema, transform) {
|
|
800
|
+
const node = transform({ ...schema });
|
|
801
|
+
const result = {};
|
|
802
|
+
for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMap(value, transform);
|
|
803
|
+
else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArray(value, transform);
|
|
804
|
+
else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormalise(value, transform);
|
|
805
|
+
else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArray(value, transform);
|
|
806
|
+
else if (isObject(value)) result[key] = deepNormalise(value, transform);
|
|
807
|
+
else result[key] = value;
|
|
808
|
+
else if (key === "dependencies" && isObject(value)) {
|
|
809
|
+
const normalised = {};
|
|
810
|
+
for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormalise(dv, transform);
|
|
811
|
+
else normalised[dk] = dv;
|
|
812
|
+
result[key] = normalised;
|
|
813
|
+
} else result[key] = value;
|
|
814
|
+
return result;
|
|
815
|
+
}
|
|
816
|
+
function normaliseArrayWithContext(items, transform, ctx) {
|
|
817
|
+
const result = [];
|
|
818
|
+
for (let i = 0; i < items.length; i++) {
|
|
819
|
+
const item = items[i];
|
|
820
|
+
if (isObject(item)) result.push(deepNormaliseWithContext(item, transform, {
|
|
821
|
+
diagnostics: ctx.diagnostics,
|
|
822
|
+
pointer: appendPointer(ctx.pointer, String(i))
|
|
823
|
+
}));
|
|
824
|
+
else result.push(item);
|
|
825
|
+
}
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
function normaliseSubSchemaMapWithContext(map, transform, ctx) {
|
|
829
|
+
const result = {};
|
|
830
|
+
for (const [k, v] of Object.entries(map)) if (isObject(v)) result[k] = deepNormaliseWithContext(v, transform, {
|
|
831
|
+
diagnostics: ctx.diagnostics,
|
|
832
|
+
pointer: appendPointer(ctx.pointer, k)
|
|
833
|
+
});
|
|
834
|
+
else result[k] = v;
|
|
835
|
+
return result;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Deep-normalise a JSON Schema object, threading a context (diagnostics
|
|
839
|
+
* sink + JSON Pointer) through each recursive call. Used by the JSON
|
|
840
|
+
* Schema normalisation path so per-node transforms can emit diagnostics
|
|
841
|
+
* with accurate pointers.
|
|
842
|
+
*
|
|
843
|
+
* Mirrors `deepNormalise` structurally — keep the two in sync when
|
|
844
|
+
* adding new sub-schema locations.
|
|
845
|
+
*/
|
|
846
|
+
function deepNormaliseWithContext(schema, transform, ctx) {
|
|
847
|
+
const node = transform({ ...schema }, ctx);
|
|
848
|
+
const result = {};
|
|
849
|
+
for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMapWithContext(value, transform, {
|
|
850
|
+
diagnostics: ctx.diagnostics,
|
|
851
|
+
pointer: appendPointer(ctx.pointer, key)
|
|
852
|
+
});
|
|
853
|
+
else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArrayWithContext(value, transform, {
|
|
854
|
+
diagnostics: ctx.diagnostics,
|
|
855
|
+
pointer: appendPointer(ctx.pointer, key)
|
|
856
|
+
});
|
|
857
|
+
else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormaliseWithContext(value, transform, {
|
|
858
|
+
diagnostics: ctx.diagnostics,
|
|
859
|
+
pointer: appendPointer(ctx.pointer, key)
|
|
860
|
+
});
|
|
861
|
+
else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArrayWithContext(value, transform, {
|
|
862
|
+
diagnostics: ctx.diagnostics,
|
|
863
|
+
pointer: appendPointer(ctx.pointer, key)
|
|
864
|
+
});
|
|
865
|
+
else if (isObject(value)) result[key] = deepNormaliseWithContext(value, transform, {
|
|
866
|
+
diagnostics: ctx.diagnostics,
|
|
867
|
+
pointer: appendPointer(ctx.pointer, key)
|
|
868
|
+
});
|
|
869
|
+
else result[key] = value;
|
|
870
|
+
else if (key === "dependencies" && isObject(value)) {
|
|
871
|
+
const normalised = {};
|
|
872
|
+
const depsPointer = appendPointer(ctx.pointer, key);
|
|
873
|
+
for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormaliseWithContext(dv, transform, {
|
|
874
|
+
diagnostics: ctx.diagnostics,
|
|
875
|
+
pointer: appendPointer(depsPointer, dk)
|
|
876
|
+
});
|
|
877
|
+
else normalised[dk] = dv;
|
|
878
|
+
result[key] = normalised;
|
|
879
|
+
} else result[key] = value;
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Walk an array of supposed required-property names. Each non-string
|
|
884
|
+
* element triggers a `dependent-required-invalid` diagnostic against
|
|
885
|
+
* the supplied context. Returns the collected string entries when
|
|
886
|
+
* every element validates, or `undefined` when at least one entry
|
|
887
|
+
* was invalid (signalling the caller should drop the property
|
|
888
|
+
* entirely rather than emit a partial rewrite).
|
|
889
|
+
*
|
|
890
|
+
* `keyword` distinguishes diagnostics that originate from the legacy
|
|
891
|
+
* `dependencies` keyword versus the modern `dependentRequired`.
|
|
892
|
+
*/
|
|
893
|
+
function collectDependencyStrings(items, property, keyword, ctx) {
|
|
894
|
+
const strings = [];
|
|
895
|
+
let sawInvalid = false;
|
|
896
|
+
for (let i = 0; i < items.length; i++) {
|
|
897
|
+
const item = items[i];
|
|
898
|
+
if (typeof item === "string") {
|
|
899
|
+
strings.push(item);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
sawInvalid = true;
|
|
903
|
+
if (ctx === void 0) continue;
|
|
904
|
+
emitDiagnostic(ctx.diagnostics, {
|
|
905
|
+
code: "dependent-required-invalid",
|
|
906
|
+
message: `\`${keyword}.${property}[${String(i)}]\` is not a string; only string property names are valid in a required-dependency array`,
|
|
907
|
+
pointer: appendPointer(appendPointer(appendPointer(ctx.pointer, keyword), property), String(i)),
|
|
908
|
+
detail: {
|
|
909
|
+
property,
|
|
910
|
+
index: i,
|
|
911
|
+
value: item
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
return sawInvalid ? void 0 : strings;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Split the legacy `dependencies` keyword into `dependentRequired` and
|
|
919
|
+
* `dependentSchemas` per the Draft 2019-09+ replacement.
|
|
920
|
+
*
|
|
921
|
+
* Each key in `dependencies` maps to either:
|
|
922
|
+
* - `string[]` → `dependentRequired`
|
|
923
|
+
* - A schema object → `dependentSchemas`
|
|
924
|
+
*
|
|
925
|
+
* Both forms can coexist within the same `dependencies` object.
|
|
926
|
+
* After splitting, `dependencies` is removed from the node.
|
|
927
|
+
*
|
|
928
|
+
* When `ctx` is supplied, diagnostics are emitted for:
|
|
929
|
+
* - `legacy-dependencies-split` once per node that contained the
|
|
930
|
+
* deprecated keyword (callers pass this only on draft paths where
|
|
931
|
+
* the keyword is unexpected, e.g. 2020-12).
|
|
932
|
+
* - `dependent-required-invalid` for each array entry whose element is
|
|
933
|
+
* not a string.
|
|
934
|
+
*/
|
|
935
|
+
function splitDependencies(node, ctx, emitLegacyDiagnostic) {
|
|
936
|
+
const deps = node.dependencies;
|
|
937
|
+
if (!isObject(deps)) return;
|
|
938
|
+
if (emitLegacyDiagnostic && ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
|
|
939
|
+
code: "legacy-dependencies-split",
|
|
940
|
+
message: "Legacy `dependencies` keyword was split into `dependentRequired`/`dependentSchemas`; `dependencies` was deprecated in Draft 2019-09",
|
|
941
|
+
pointer: appendPointer(ctx.pointer, "dependencies"),
|
|
942
|
+
detail: { keys: Object.keys(deps) }
|
|
943
|
+
});
|
|
944
|
+
const requiredEntries = {};
|
|
945
|
+
const schemaEntries = {};
|
|
946
|
+
for (const [key, value] of Object.entries(deps)) if (Array.isArray(value)) {
|
|
947
|
+
const accepted = collectDependencyStrings(value, key, "dependencies", ctx);
|
|
948
|
+
if (accepted !== void 0) requiredEntries[key] = accepted;
|
|
949
|
+
} else if (isObject(value)) schemaEntries[key] = value;
|
|
950
|
+
if (Object.keys(requiredEntries).length > 0) {
|
|
951
|
+
const existing = node.dependentRequired;
|
|
952
|
+
if (isObject(existing)) for (const [k, v] of Object.entries(requiredEntries)) existing[k] = v;
|
|
953
|
+
else node.dependentRequired = requiredEntries;
|
|
954
|
+
}
|
|
955
|
+
if (Object.keys(schemaEntries).length > 0) {
|
|
956
|
+
const existing = node.dependentSchemas;
|
|
957
|
+
if (isObject(existing)) for (const [k, v] of Object.entries(schemaEntries)) existing[k] = v;
|
|
958
|
+
else node.dependentSchemas = schemaEntries;
|
|
959
|
+
}
|
|
960
|
+
delete node.dependencies;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Emit diagnostics for any non-string entries inside a pre-existing
|
|
964
|
+
* `dependentRequired` keyword. Used on draft paths where the author may
|
|
965
|
+
* have already migrated to the Draft 2019-09 form but still produced
|
|
966
|
+
* invalid array entries. The keyword value is not rewritten — the
|
|
967
|
+
* walker is responsible for honouring (or rejecting) the constraint.
|
|
968
|
+
*/
|
|
969
|
+
function validateDependentRequired(node, ctx) {
|
|
970
|
+
if (ctx === void 0) return;
|
|
971
|
+
const dr = node.dependentRequired;
|
|
972
|
+
if (!isObject(dr)) return;
|
|
973
|
+
for (const [key, value] of Object.entries(dr)) {
|
|
974
|
+
if (!Array.isArray(value)) continue;
|
|
975
|
+
collectDependencyStrings(value, key, "dependentRequired", ctx);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Apply the version-agnostic Draft 04 keyword translations to a single
|
|
980
|
+
* node: boolean exclusive-min/max → number form, bare `id` → `$id`, and
|
|
981
|
+
* tuple-form `items` → `prefixItems`.
|
|
982
|
+
*
|
|
983
|
+
* `divisibleBy` is also translated to `multipleOf` (a Draft 03 carryover
|
|
984
|
+
* that legitimately appears in legacy Draft 04 schemas). When `ctx` is
|
|
985
|
+
* supplied and both keywords are present with conflicting values, a
|
|
986
|
+
* `divisible-by-conflict` diagnostic is emitted.
|
|
987
|
+
*
|
|
988
|
+
* `dependencies` is split into `dependentRequired`/`dependentSchemas`
|
|
989
|
+
* via {@link splitDependencies}; passing `ctx` enables per-entry
|
|
990
|
+
* diagnostics for non-string array members.
|
|
991
|
+
*/
|
|
992
|
+
function applyDraft04Translations(node, ctx) {
|
|
993
|
+
if (node.exclusiveMinimum === true && typeof node.minimum === "number") {
|
|
994
|
+
node.exclusiveMinimum = node.minimum;
|
|
995
|
+
delete node.minimum;
|
|
996
|
+
} else if (node.exclusiveMinimum === false) delete node.exclusiveMinimum;
|
|
997
|
+
else if (node.exclusiveMinimum === true) {
|
|
998
|
+
if (ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
|
|
999
|
+
code: "bare-exclusive-bound",
|
|
1000
|
+
message: "`exclusiveMinimum: true` requires a sibling numeric `minimum` in Draft 04; dropping the keyword",
|
|
1001
|
+
pointer: appendPointer(ctx.pointer, "exclusiveMinimum"),
|
|
1002
|
+
detail: { keyword: "exclusiveMinimum" }
|
|
1003
|
+
});
|
|
1004
|
+
delete node.exclusiveMinimum;
|
|
1005
|
+
}
|
|
1006
|
+
if (node.exclusiveMaximum === true && typeof node.maximum === "number") {
|
|
1007
|
+
node.exclusiveMaximum = node.maximum;
|
|
1008
|
+
delete node.maximum;
|
|
1009
|
+
} else if (node.exclusiveMaximum === false) delete node.exclusiveMaximum;
|
|
1010
|
+
else if (node.exclusiveMaximum === true) {
|
|
1011
|
+
if (ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
|
|
1012
|
+
code: "bare-exclusive-bound",
|
|
1013
|
+
message: "`exclusiveMaximum: true` requires a sibling numeric `maximum` in Draft 04; dropping the keyword",
|
|
1014
|
+
pointer: appendPointer(ctx.pointer, "exclusiveMaximum"),
|
|
1015
|
+
detail: { keyword: "exclusiveMaximum" }
|
|
1016
|
+
});
|
|
1017
|
+
delete node.exclusiveMaximum;
|
|
1018
|
+
}
|
|
1019
|
+
const divisibleBy = node.divisibleBy;
|
|
1020
|
+
if (typeof divisibleBy === "number") {
|
|
1021
|
+
const multipleOf = node.multipleOf;
|
|
1022
|
+
if (typeof multipleOf === "number") {
|
|
1023
|
+
if (ctx !== void 0 && divisibleBy !== multipleOf) emitDiagnostic(ctx.diagnostics, {
|
|
1024
|
+
code: "divisible-by-conflict",
|
|
1025
|
+
message: `Legacy \`divisibleBy\` (${String(divisibleBy)}) conflicts with \`multipleOf\` (${String(multipleOf)}); keeping \`multipleOf\``,
|
|
1026
|
+
pointer: ctx.pointer,
|
|
1027
|
+
detail: {
|
|
1028
|
+
divisibleBy,
|
|
1029
|
+
multipleOf
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
} else node.multipleOf = divisibleBy;
|
|
1033
|
+
delete node.divisibleBy;
|
|
1034
|
+
}
|
|
1035
|
+
if (typeof node.id === "string" && !("$id" in node)) {
|
|
1036
|
+
node.$id = node.id;
|
|
1037
|
+
delete node.id;
|
|
1038
|
+
}
|
|
1039
|
+
if (Array.isArray(node.items) && !("prefixItems" in node)) {
|
|
1040
|
+
node.prefixItems = node.items;
|
|
1041
|
+
delete node.items;
|
|
1042
|
+
if ("additionalItems" in node) {
|
|
1043
|
+
node.items = node.additionalItems;
|
|
1044
|
+
delete node.additionalItems;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
splitDependencies(node, ctx, false);
|
|
1048
|
+
validateDependentRequired(node, ctx);
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Normalise Draft 04 `exclusiveMinimum`/`exclusiveMaximum` from boolean
|
|
1052
|
+
* to number form, plus the other Draft 04 translations applied to a
|
|
1053
|
+
* single node.
|
|
1054
|
+
*
|
|
1055
|
+
* In Draft 04:
|
|
1056
|
+
* - `exclusiveMinimum: true` + `minimum: 5` → value must be > 5
|
|
1057
|
+
* - `exclusiveMinimum: false` (or absent) + `minimum: 5` → value must be >= 5
|
|
1058
|
+
*
|
|
1059
|
+
* In Draft 06+:
|
|
1060
|
+
* - `exclusiveMinimum: 5` → value must be > 5 (no separate `minimum`)
|
|
1061
|
+
* - `minimum: 5` → value must be >= 5
|
|
1062
|
+
*
|
|
1063
|
+
* The transform converts boolean form to number form so the walker can
|
|
1064
|
+
* treat `exclusiveMinimum`/`exclusiveMaximum` uniformly as numbers.
|
|
1065
|
+
*
|
|
1066
|
+
* This function preserves the no-context signature for the OpenAPI 3.0
|
|
1067
|
+
* and Swagger 2.0 normalisers that compose it directly. The JSON Schema
|
|
1068
|
+
* normalisation path uses {@link normaliseDraft04NodeWithContext} via
|
|
1069
|
+
* {@link deepNormaliseWithContext} to thread diagnostics.
|
|
1070
|
+
*/
|
|
1071
|
+
function normaliseDraft04Node(node) {
|
|
1072
|
+
applyDraft04Translations(node, void 0);
|
|
1073
|
+
return node;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Context-aware Draft 04 per-node transform. Identical to
|
|
1077
|
+
* {@link normaliseDraft04Node} but threads a {@link NodeContext} so
|
|
1078
|
+
* `divisibleBy`/`multipleOf` conflicts and invalid dependency entries
|
|
1079
|
+
* can be surfaced as diagnostics with accurate pointers.
|
|
1080
|
+
*/
|
|
1081
|
+
function normaliseDraft04NodeWithContext(node, ctx) {
|
|
1082
|
+
applyDraft04Translations(node, ctx);
|
|
1083
|
+
return node;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Normalise Draft 06/07 nodes.
|
|
1087
|
+
*
|
|
1088
|
+
* These drafts introduced `exclusiveMinimum`/`exclusiveMaximum` as numbers
|
|
1089
|
+
* (already in final form) and `const`/`examples`, but still use the
|
|
1090
|
+
* legacy `dependencies` keyword. Split it into `dependentRequired` /
|
|
1091
|
+
* `dependentSchemas` so the walker can process them uniformly.
|
|
1092
|
+
*/
|
|
1093
|
+
function normaliseDraft06Or07NodeWithContext(node, ctx) {
|
|
1094
|
+
splitDependencies(node, ctx, false);
|
|
1095
|
+
validateDependentRequired(node, ctx);
|
|
1096
|
+
return node;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Normalise Draft 2019-09 `$recursiveRef` to `$ref`.
|
|
1100
|
+
*
|
|
1101
|
+
* `$recursiveRef` resolves to the nearest `$recursiveAnchor` in the
|
|
1102
|
+
* dynamic scope. For rendering, the common pattern is a root
|
|
1103
|
+
* `$recursiveAnchor: true` — the normaliser converts
|
|
1104
|
+
* `$recursiveRef: "#"` to `$ref: "#"` pointing to the root.
|
|
1105
|
+
*
|
|
1106
|
+
* The original `$recursiveRef` value is preserved (rather than
|
|
1107
|
+
* collapsed to `"#"`) so that anchored variants such as
|
|
1108
|
+
* `$recursiveRef: "#meta"` resolve correctly against the
|
|
1109
|
+
* corresponding `$recursiveAnchor` name. String-valued
|
|
1110
|
+
* `$recursiveAnchor` names are likewise preserved as `$anchor`.
|
|
1111
|
+
*/
|
|
1112
|
+
function normaliseDraft201909NodeWithContext(node, ctx) {
|
|
1113
|
+
if (typeof node.$recursiveRef === "string") {
|
|
1114
|
+
node.$ref = node.$recursiveRef;
|
|
1115
|
+
delete node.$recursiveRef;
|
|
1116
|
+
}
|
|
1117
|
+
if (node.$recursiveAnchor === true) {
|
|
1118
|
+
if (typeof node.$anchor !== "string") node.$anchor = "__recursive__";
|
|
1119
|
+
delete node.$recursiveAnchor;
|
|
1120
|
+
} else if (typeof node.$recursiveAnchor === "string") {
|
|
1121
|
+
if (typeof node.$anchor !== "string") node.$anchor = node.$recursiveAnchor;
|
|
1122
|
+
delete node.$recursiveAnchor;
|
|
1123
|
+
}
|
|
1124
|
+
validateDependentRequired(node, ctx);
|
|
1125
|
+
return node;
|
|
1126
|
+
}
|
|
1127
|
+
function normaliseDynamicRefNodeWithContext(node, ctx) {
|
|
1128
|
+
if (typeof node.$dynamicRef === "string") {
|
|
1129
|
+
node.$ref = node.$dynamicRef;
|
|
1130
|
+
delete node.$dynamicRef;
|
|
1131
|
+
}
|
|
1132
|
+
if (typeof node.$dynamicAnchor === "string") {
|
|
1133
|
+
if (typeof node.$anchor !== "string") node.$anchor = node.$dynamicAnchor;
|
|
1134
|
+
delete node.$dynamicAnchor;
|
|
1135
|
+
}
|
|
1136
|
+
splitDependencies(node, ctx, true);
|
|
1137
|
+
validateDependentRequired(node, ctx);
|
|
1138
|
+
return node;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Normalise a JSON Schema to canonical Draft 2020-12 form.
|
|
1142
|
+
* Deep-clones the input — the original is never mutated.
|
|
1143
|
+
*
|
|
1144
|
+
* When `diagnostics` is supplied, per-node transforms emit diagnostics
|
|
1145
|
+
* for legacy-keyword rewrites and invalid constructs (e.g. `divisibleBy`
|
|
1146
|
+
* conflicts, non-string entries in a `dependentRequired` array, legacy
|
|
1147
|
+
* `dependencies` reaching the 2020-12 path).
|
|
1148
|
+
*/
|
|
1149
|
+
function normaliseJsonSchema(schema, draft, diagnostics) {
|
|
1150
|
+
const ctx = {
|
|
1151
|
+
diagnostics,
|
|
1152
|
+
pointer: ""
|
|
1153
|
+
};
|
|
1154
|
+
let normalised;
|
|
1155
|
+
switch (draft) {
|
|
1156
|
+
case "draft-04":
|
|
1157
|
+
normalised = deepNormaliseWithContext(schema, normaliseDraft04NodeWithContext, ctx);
|
|
1158
|
+
break;
|
|
1159
|
+
case "draft-2019-09":
|
|
1160
|
+
normalised = deepNormaliseWithContext(schema, normaliseDraft201909NodeWithContext, ctx);
|
|
1161
|
+
break;
|
|
1162
|
+
case "draft-2020-12":
|
|
1163
|
+
normalised = deepNormaliseWithContext(schema, normaliseDynamicRefNodeWithContext, ctx);
|
|
1164
|
+
break;
|
|
1165
|
+
case "draft-06":
|
|
1166
|
+
case "draft-07":
|
|
1167
|
+
normalised = deepNormaliseWithContext(schema, normaliseDraft06Or07NodeWithContext, ctx);
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
1170
|
+
return resolveRelativeRefs(normalised, diagnostics);
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Parse a string as an absolute URI, returning `undefined` when it has
|
|
1174
|
+
* no scheme. Used to detect whether an `$id` value defines a base URI.
|
|
1175
|
+
*/
|
|
1176
|
+
function parseAbsoluteUri(value) {
|
|
1177
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
1178
|
+
try {
|
|
1179
|
+
const url = new URL(value);
|
|
1180
|
+
if (url.protocol.length === 0) return void 0;
|
|
1181
|
+
return url;
|
|
1182
|
+
} catch {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Resolve a relative reference against a base URI. Returns `undefined`
|
|
1188
|
+
* when the reference cannot be resolved (e.g. malformed input).
|
|
1189
|
+
*/
|
|
1190
|
+
function resolveAgainst(ref, base) {
|
|
1191
|
+
try {
|
|
1192
|
+
return new URL(ref, base);
|
|
1193
|
+
} catch {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Strip the fragment portion from a URL, returning the canonical
|
|
1199
|
+
* `scheme://authority/path?query` form. Used to compare a resolved
|
|
1200
|
+
* `$ref` URI against the document's `$id` base.
|
|
1201
|
+
*/
|
|
1202
|
+
function stripFragment(url) {
|
|
1203
|
+
const clone = new URL(url.toString());
|
|
1204
|
+
clone.hash = "";
|
|
1205
|
+
return clone.toString();
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Recursively rewrite relative `$ref`s in a schema so they resolve
|
|
1209
|
+
* correctly under the JSON Schema base-URI rules (RFC 3986 + JSON
|
|
1210
|
+
* Schema §8.2). Refs that resolve to the document's own `$id` are
|
|
1211
|
+
* rewritten to fragment-only form so the existing dereferencer can
|
|
1212
|
+
* handle them; refs that resolve outside the document are left as
|
|
1213
|
+
* absolute URIs (handled by the external resolver path).
|
|
1214
|
+
*
|
|
1215
|
+
* Returns the input unchanged when the document has no base URI or
|
|
1216
|
+
* no relative refs.
|
|
1217
|
+
*/
|
|
1218
|
+
function resolveRelativeRefs(schema, diagnostics) {
|
|
1219
|
+
const docBaseUrl = parseAbsoluteUri(schema.$id);
|
|
1220
|
+
if (docBaseUrl === void 0) return schema;
|
|
1221
|
+
const docBase = stripFragment(docBaseUrl);
|
|
1222
|
+
return rewriteRelativeRefsNode(schema, docBase, docBase, "", diagnostics);
|
|
1223
|
+
}
|
|
1224
|
+
function rewriteRelativeRefsNode(node, currentBase, docBase, pointer, diagnostics) {
|
|
1225
|
+
let nextBase = currentBase;
|
|
1226
|
+
const nodeId = node.$id;
|
|
1227
|
+
if (typeof nodeId === "string" && nodeId.length > 0) {
|
|
1228
|
+
const resolved = resolveAgainst(nodeId, currentBase);
|
|
1229
|
+
if (resolved !== void 0) nextBase = stripFragment(resolved);
|
|
1230
|
+
}
|
|
1231
|
+
const result = {};
|
|
1232
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1233
|
+
if (key === "$ref" && typeof value === "string") {
|
|
1234
|
+
result[key] = rewriteRef(value, nextBase, docBase, appendPointer(pointer, key), diagnostics);
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
result[key] = rewriteRelativeRefsValue(value, key, nextBase, docBase, appendPointer(pointer, key), diagnostics);
|
|
1238
|
+
}
|
|
1239
|
+
return result;
|
|
1240
|
+
}
|
|
1241
|
+
function rewriteRelativeRefsValue(value, parentKey, currentBase, docBase, pointer, diagnostics) {
|
|
1242
|
+
if (Array.isArray(value)) return value.map((item, i) => rewriteRelativeRefsValue(item, parentKey, currentBase, docBase, appendPointer(pointer, String(i)), diagnostics));
|
|
1243
|
+
if (isObject(value)) return rewriteRelativeRefsNode(value, currentBase, docBase, pointer, diagnostics);
|
|
1244
|
+
return value;
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Rewrite a single `$ref` string. Fragment-only refs and refs that
|
|
1248
|
+
* already include a scheme are returned unchanged. Relative refs are
|
|
1249
|
+
* resolved against `currentBase`; if the result lives in the same
|
|
1250
|
+
* document as `docBase`, the ref is rewritten to fragment form.
|
|
1251
|
+
*/
|
|
1252
|
+
function rewriteRef(ref, currentBase, docBase, pointer, diagnostics) {
|
|
1253
|
+
if (ref.startsWith("#")) return ref;
|
|
1254
|
+
if (/^[a-z][a-z0-9+\-.]*:/i.test(ref)) return ref;
|
|
1255
|
+
const resolved = resolveAgainst(ref, currentBase);
|
|
1256
|
+
if (resolved === void 0) return ref;
|
|
1257
|
+
if (stripFragment(resolved) === docBase) {
|
|
1258
|
+
const fragment = resolved.hash === "" ? "#" : resolved.hash;
|
|
1259
|
+
emitDiagnostic(diagnostics, {
|
|
1260
|
+
code: "relative-ref-resolved",
|
|
1261
|
+
message: `Relative $ref "${ref}" resolved to "${fragment}" against base "${currentBase}"`,
|
|
1262
|
+
pointer,
|
|
1263
|
+
detail: {
|
|
1264
|
+
ref,
|
|
1265
|
+
base: currentBase,
|
|
1266
|
+
resolved: fragment
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
return fragment;
|
|
1270
|
+
}
|
|
1271
|
+
const absolute = resolved.toString();
|
|
1272
|
+
emitDiagnostic(diagnostics, {
|
|
1273
|
+
code: "relative-ref-resolved",
|
|
1274
|
+
message: `Relative $ref "${ref}" resolved to "${absolute}" against base "${currentBase}"`,
|
|
1275
|
+
pointer,
|
|
1276
|
+
detail: {
|
|
1277
|
+
ref,
|
|
1278
|
+
base: currentBase,
|
|
1279
|
+
resolved: absolute
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
return absolute;
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Normalise an OpenAPI document's schemas for walker consumption.
|
|
1286
|
+
* Handles version-specific keyword transformations.
|
|
1287
|
+
*
|
|
1288
|
+
* Returns the same object reference if no normalisation is needed
|
|
1289
|
+
* (OpenAPI 3.1.x), or a deep-cloned normalised copy otherwise.
|
|
1290
|
+
*/
|
|
1291
|
+
function normaliseOpenApiSchemas(doc, version, diagnostics) {
|
|
1292
|
+
if (isSwagger2(version)) return normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, diagnostics);
|
|
1293
|
+
if (isOpenApi30(version)) return deepNormaliseOpenApi30Doc(doc, deepNormalise);
|
|
1294
|
+
if (isOpenApi31(version)) {
|
|
1295
|
+
const dialect = readJsonSchemaDialect(doc);
|
|
1296
|
+
if (dialect.kind === "unknown") emitDiagnostic(diagnostics, {
|
|
1297
|
+
code: "unknown-json-schema-dialect",
|
|
1298
|
+
message: `OpenAPI 3.1 \`jsonSchemaDialect\` URI "${dialect.uri}" does not match a supported JSON Schema draft; falling back to Draft 2020-12`,
|
|
1299
|
+
pointer: "/jsonSchemaDialect",
|
|
1300
|
+
detail: { uri: dialect.uri }
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
return deepNormaliseOpenApiDoc(doc, (schema) => deepNormalise(schema, normaliseOpenApi30Discriminator));
|
|
1304
|
+
}
|
|
1305
|
+
//#endregion
|
|
1306
|
+
export { normaliseOpenApiSchemas as a, deepNormaliseOpenApiDoc as c, normaliseOpenApi30Node as d, normaliseJsonSchema as i, normaliseOpenApi30Combined as l, deepNormaliseWithContext as n, normaliseSwagger2Document as o, normaliseDraft04Node as r, deepNormaliseOpenApi30Doc as s, deepNormalise as t, normaliseOpenApi30Discriminator as u };
|