schema-components 1.22.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/core/adapter.d.mts +97 -3
- package/dist/core/adapter.mjs +260 -111
- package/dist/core/constraints.d.mts +2 -2
- package/dist/core/constraints.mjs +0 -7
- package/dist/core/cssClasses.d.mts +52 -0
- package/dist/core/cssClasses.mjs +51 -0
- package/dist/core/diagnostics.d.mts +1 -1
- package/dist/core/errors.d.mts +1 -1
- package/dist/core/errors.mjs +5 -13
- package/dist/core/fieldOrder.d.mts +1 -1
- package/dist/core/formats.d.mts +9 -2
- package/dist/core/formats.mjs +12 -1
- package/dist/core/idPath.d.mts +54 -0
- package/dist/core/idPath.mjs +66 -0
- package/dist/core/merge.d.mts +10 -1
- package/dist/core/merge.mjs +49 -10
- package/dist/core/normalise.d.mts +14 -3
- package/dist/core/normalise.mjs +2 -2
- package/dist/core/openapi30.d.mts +15 -1
- package/dist/core/openapi30.mjs +2 -2
- package/dist/core/openapiConstants.d.mts +67 -0
- package/dist/core/openapiConstants.mjs +90 -0
- package/dist/core/ref.d.mts +2 -2
- package/dist/core/ref.mjs +83 -6
- package/dist/core/refChain.d.mts +70 -0
- package/dist/core/refChain.mjs +44 -0
- package/dist/core/renderer.d.mts +1 -1
- package/dist/core/swagger2.d.mts +1 -1
- package/dist/core/swagger2.mjs +1 -1
- package/dist/core/typeInference.d.mts +982 -2
- package/dist/core/types.d.mts +1 -1
- package/dist/core/unionMatch.d.mts +36 -0
- package/dist/core/unionMatch.mjs +53 -0
- package/dist/core/version.d.mts +1 -1
- package/dist/core/version.mjs +29 -17
- package/dist/core/walkBuilders.d.mts +23 -4
- package/dist/core/walkBuilders.mjs +27 -7
- package/dist/core/walker.d.mts +1 -1
- package/dist/core/walker.mjs +44 -45
- package/dist/{diagnostics-D0QCYGv0.d.mts → diagnostics-Cbwak-ZX.d.mts} +1 -1
- package/dist/{errors-DpFwqs5C.d.mts → errors-g_MCTQel.d.mts} +9 -15
- package/dist/html/a11y.d.mts +9 -4
- package/dist/html/a11y.mjs +10 -19
- package/dist/html/renderToHtml.d.mts +2 -2
- package/dist/html/renderToHtmlStream.d.mts +2 -2
- package/dist/html/renderToHtmlStream.mjs +12 -1
- package/dist/html/renderers.d.mts +32 -8
- package/dist/html/renderers.mjs +125 -111
- package/dist/html/streamRenderers.d.mts +4 -5
- package/dist/html/streamRenderers.mjs +40 -61
- package/dist/{normalise-DVEJQmF7.mjs → normalise-DCYp06Sr.mjs} +352 -162
- package/dist/openapi/ApiCallbacks.d.mts +1 -1
- package/dist/openapi/ApiLinks.d.mts +1 -1
- package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
- package/dist/openapi/ApiSecurity.d.mts +1 -1
- package/dist/openapi/components.d.mts +116 -37
- package/dist/openapi/components.mjs +54 -37
- package/dist/openapi/parser.d.mts +19 -9
- package/dist/openapi/parser.mjs +262 -86
- package/dist/openapi/resolve.d.mts +20 -11
- package/dist/openapi/resolve.mjs +135 -75
- package/dist/react/SchemaComponent.d.mts +32 -7
- package/dist/react/SchemaComponent.mjs +45 -21
- package/dist/react/SchemaView.d.mts +30 -10
- package/dist/react/a11y.d.mts +21 -0
- package/dist/react/a11y.mjs +24 -0
- package/dist/react/fieldPath.d.mts +1 -1
- package/dist/react/headless.d.mts +1 -1
- package/dist/react/headlessRenderers.d.mts +8 -9
- package/dist/react/headlessRenderers.mjs +41 -72
- package/dist/{ref-D-_JBZkF.d.mts → ref-DCDuswPe.d.mts} +38 -3
- package/dist/{renderer-BaRlQIuN.d.mts → renderer-CXJ8y0qw.d.mts} +1 -1
- package/dist/themes/mantine.d.mts +1 -1
- package/dist/themes/mui.d.mts +1 -1
- package/dist/themes/radix.d.mts +1 -1
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/themes/shadcn.mjs +2 -1
- package/dist/{types-BrRMV0en.d.mts → types-BTB73MB8.d.mts} +32 -4
- package/dist/{version-D2jfdX6E.d.mts → version-BFTVLsdb.d.mts} +7 -1
- package/package.json +1 -1
- package/dist/typeInference-DkcUHfaM.d.mts +0 -982
package/dist/openapi/parser.mjs
CHANGED
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
import { getProperty, isObject } from "../core/guards.mjs";
|
|
2
2
|
import "../core/limits.mjs";
|
|
3
|
+
import { emitDiagnostic } from "../core/diagnostics.mjs";
|
|
3
4
|
import { isPrototypePollutingKey } from "../core/uri.mjs";
|
|
5
|
+
import { detectOpenApiVersion } from "../core/version.mjs";
|
|
6
|
+
import { HTTP_METHODS } from "../core/openapiConstants.mjs";
|
|
7
|
+
import { resolveRefChain } from "../core/refChain.mjs";
|
|
4
8
|
//#region src/openapi/parser.ts
|
|
5
9
|
function getString(value, key) {
|
|
6
10
|
const result = isObject(value) ? value[key] : void 0;
|
|
7
11
|
return typeof result === "string" ? result : void 0;
|
|
8
12
|
}
|
|
9
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Narrow an OpenAPI parameter `in` value to the canonical four-value
|
|
15
|
+
* `ParameterLocation` union. Returns `undefined` for any other value
|
|
16
|
+
* (including the Swagger 2.0 `body`/`formData` locations, which the
|
|
17
|
+
* Swagger 2.0 → OpenAPI 3.x normaliser is expected to have already
|
|
18
|
+
* lifted out of the parameter array). When `diagnostics` is supplied,
|
|
19
|
+
* emits an `unknown-parameter-location` diagnostic so the caller can
|
|
20
|
+
* audit silent drops; without a sink the function still returns
|
|
21
|
+
* `undefined` rather than coercing the value, so the parameter is
|
|
22
|
+
* excluded from the operation's parameter list either way.
|
|
23
|
+
*/
|
|
24
|
+
function toParameterLocation(value, parameterName, pointer, diagnostics) {
|
|
10
25
|
if (value === "query" || value === "path" || value === "header" || value === "cookie") return value;
|
|
11
|
-
|
|
26
|
+
emitDiagnostic(diagnostics, {
|
|
27
|
+
code: "unknown-parameter-location",
|
|
28
|
+
message: parameterName !== void 0 ? `Parameter "${parameterName}" declares unknown \`in\` value ${JSON.stringify(value)}; expected one of query, path, header, cookie` : `Parameter declares unknown \`in\` value ${JSON.stringify(value)}; expected one of query, path, header, cookie`,
|
|
29
|
+
pointer,
|
|
30
|
+
detail: {
|
|
31
|
+
name: parameterName,
|
|
32
|
+
in: value
|
|
33
|
+
}
|
|
34
|
+
});
|
|
12
35
|
}
|
|
13
36
|
function parseOpenApiDocument(doc) {
|
|
14
37
|
const schemas = /* @__PURE__ */ new Map();
|
|
@@ -30,62 +53,94 @@ function getSchema(parsed, ref) {
|
|
|
30
53
|
return resolved;
|
|
31
54
|
}
|
|
32
55
|
}
|
|
33
|
-
const METHODS = [
|
|
34
|
-
"get",
|
|
35
|
-
"post",
|
|
36
|
-
"put",
|
|
37
|
-
"patch",
|
|
38
|
-
"delete",
|
|
39
|
-
"head",
|
|
40
|
-
"options",
|
|
41
|
-
"trace"
|
|
42
|
-
];
|
|
43
|
-
/**
|
|
44
|
-
* Resolve a path item, following a `$ref` to `components/pathItems/<Name>`
|
|
45
|
-
* (OpenAPI 3.1) if present. Returns `undefined` when the value is not a
|
|
46
|
-
* path item, the ref is malformed, or the target does not resolve.
|
|
47
|
-
*/
|
|
48
56
|
/**
|
|
49
57
|
* Follow Path Item Object `$ref` chains (up to MAX_PATH_ITEM_REF_HOPS).
|
|
50
58
|
* Returns the resolved Path Item, or `undefined` when the chain cycles,
|
|
51
|
-
* exceeds the cap, or any intermediate ref fails to resolve.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
59
|
+
* exceeds the cap, or any intermediate ref fails to resolve.
|
|
60
|
+
*
|
|
61
|
+
* When `diagnostics` is supplied, cycle and depth-cap conditions emit
|
|
62
|
+
* `cyclic-path-item-ref` and `path-item-ref-too-deep` respectively. When
|
|
63
|
+
* `diagnostics` is omitted, the resolver still rejects cycles and
|
|
64
|
+
* over-deep chains silently — callers that wire their own resolver
|
|
65
|
+
* (notably `resolve.ts:resolvePathItemNode`) supply diagnostics to
|
|
66
|
+
* mirror this behaviour with full pointer information.
|
|
55
67
|
*/
|
|
56
|
-
function resolvePathItem(parsed, pathItem) {
|
|
68
|
+
function resolvePathItem(parsed, pathItem, diagnostics) {
|
|
57
69
|
if (!isObject(pathItem)) return void 0;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
return resolveRefChain(pathItem, {
|
|
71
|
+
lookup: (ref) => ref.startsWith("#/") ? resolveRefInDoc(parsed.doc, ref) : void 0,
|
|
72
|
+
maxHops: 8,
|
|
73
|
+
onCycle: (ref) => {
|
|
74
|
+
emitDiagnostic(diagnostics, {
|
|
75
|
+
code: "cyclic-path-item-ref",
|
|
76
|
+
message: `Cyclic Path Item Object $ref "${ref}"`,
|
|
77
|
+
pointer: ref,
|
|
78
|
+
detail: { ref }
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
onDepthExceeded: (ref) => {
|
|
82
|
+
emitDiagnostic(diagnostics, {
|
|
83
|
+
code: "path-item-ref-too-deep",
|
|
84
|
+
message: `Path Item Object $ref chain exceeded ${String(8)} hops`,
|
|
85
|
+
pointer: ref,
|
|
86
|
+
detail: {
|
|
87
|
+
maxHops: 8,
|
|
88
|
+
ref
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
69
93
|
}
|
|
70
94
|
function lookupPathItem(parsed, path) {
|
|
71
95
|
const resolved = resolvePathItem(parsed, getProperty(getProperty(parsed.doc, "paths"), path));
|
|
72
96
|
if (resolved !== void 0) return resolved;
|
|
73
97
|
return resolvePathItem(parsed, getProperty(getProperty(parsed.doc, "webhooks"), path));
|
|
74
98
|
}
|
|
75
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Record an `operationId` against a shared `seenIds` map and emit a
|
|
101
|
+
* `duplicate-operation-id` diagnostic when a subsequent location reuses
|
|
102
|
+
* the same identifier. Returns the original `operationId` so the caller
|
|
103
|
+
* can pass the value straight onto its `OperationInfo`.
|
|
104
|
+
*
|
|
105
|
+
* When the same map is threaded through `listOperations` and
|
|
106
|
+
* `listWebhooks` (see `listAllOperations`), cross-list collisions
|
|
107
|
+
* between a path operation and a webhook operation surface as the same
|
|
108
|
+
* diagnostic class as same-list collisions.
|
|
109
|
+
*/
|
|
110
|
+
function recordOperationId(operationId, location, pointer, seenIds, diagnostics) {
|
|
111
|
+
if (operationId === void 0) return;
|
|
112
|
+
const firstSeenAt = seenIds.get(operationId);
|
|
113
|
+
if (firstSeenAt !== void 0) {
|
|
114
|
+
emitDiagnostic(diagnostics, {
|
|
115
|
+
code: "duplicate-operation-id",
|
|
116
|
+
message: `operationId "${operationId}" is declared more than once (first at ${firstSeenAt}, again at ${location})`,
|
|
117
|
+
pointer,
|
|
118
|
+
detail: {
|
|
119
|
+
operationId,
|
|
120
|
+
firstSeenAt,
|
|
121
|
+
duplicateAt: location
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
seenIds.set(operationId, location);
|
|
127
|
+
}
|
|
128
|
+
function listOperations(parsed, diagnostics, seenIds = /* @__PURE__ */ new Map()) {
|
|
76
129
|
const operations = [];
|
|
77
130
|
const paths = getProperty(parsed.doc, "paths");
|
|
78
131
|
if (!isObject(paths)) return operations;
|
|
79
132
|
for (const [path, rawPathItem] of Object.entries(paths)) {
|
|
80
|
-
const pathItem = resolvePathItem(parsed, rawPathItem);
|
|
133
|
+
const pathItem = resolvePathItem(parsed, rawPathItem, diagnostics);
|
|
81
134
|
if (pathItem === void 0) continue;
|
|
82
|
-
for (const method of
|
|
135
|
+
for (const method of HTTP_METHODS) {
|
|
83
136
|
const operation = getProperty(pathItem, method);
|
|
84
137
|
if (!isObject(operation)) continue;
|
|
138
|
+
const operationId = getString(operation, "operationId");
|
|
139
|
+
recordOperationId(operationId, `${method.toUpperCase()} ${path}`, `/paths/${jsonPointerEscape(path)}/${method}/operationId`, seenIds, diagnostics);
|
|
85
140
|
operations.push({
|
|
86
141
|
path,
|
|
87
142
|
method,
|
|
88
|
-
operationId
|
|
143
|
+
operationId,
|
|
89
144
|
summary: getString(operation, "summary"),
|
|
90
145
|
description: getString(operation, "description"),
|
|
91
146
|
deprecated: getProperty(operation, "deprecated") === true,
|
|
@@ -95,31 +150,35 @@ function listOperations(parsed) {
|
|
|
95
150
|
}
|
|
96
151
|
return operations;
|
|
97
152
|
}
|
|
98
|
-
function getParameters(parsed, path, method) {
|
|
153
|
+
function getParameters(parsed, path, method, diagnostics) {
|
|
99
154
|
const pathItem = lookupPathItem(parsed, path);
|
|
100
155
|
if (pathItem === void 0) return [];
|
|
101
156
|
const operation = getProperty(pathItem, method);
|
|
102
157
|
if (!isObject(operation)) return [];
|
|
103
|
-
const pathParams = extractParameterList(parsed.doc, getProperty(pathItem, "parameters"));
|
|
104
|
-
const opParams = extractParameterList(parsed.doc, getProperty(operation, "parameters"));
|
|
158
|
+
const pathParams = extractParameterList(parsed.doc, getProperty(pathItem, "parameters"), `/paths/${jsonPointerEscape(path)}/parameters`, diagnostics);
|
|
159
|
+
const opParams = extractParameterList(parsed.doc, getProperty(operation, "parameters"), `/paths/${jsonPointerEscape(path)}/${method}/parameters`, diagnostics);
|
|
105
160
|
const map = /* @__PURE__ */ new Map();
|
|
106
161
|
for (const param of pathParams) map.set(`${param.name}:${param.location}`, param);
|
|
107
162
|
for (const param of opParams) map.set(`${param.name}:${param.location}`, param);
|
|
108
163
|
return [...map.values()];
|
|
109
164
|
}
|
|
110
|
-
function extractParameterList(doc, parameters) {
|
|
165
|
+
function extractParameterList(doc, parameters, pointerBase, diagnostics) {
|
|
111
166
|
if (!Array.isArray(parameters)) return [];
|
|
112
167
|
const result = [];
|
|
113
|
-
for (const param of parameters) {
|
|
168
|
+
for (const [index, param] of parameters.entries()) {
|
|
114
169
|
if (!isObject(param)) continue;
|
|
115
|
-
const
|
|
170
|
+
const entryPointer = `${pointerBase}/${String(index)}`;
|
|
171
|
+
const resolved = resolveParam(doc, param, diagnostics);
|
|
172
|
+
if (resolved === void 0) continue;
|
|
116
173
|
const name = getProperty(resolved, "name");
|
|
117
|
-
const
|
|
118
|
-
if (typeof name !== "string" || typeof
|
|
174
|
+
const rawLocation = getProperty(resolved, "in");
|
|
175
|
+
if (typeof name !== "string" || typeof rawLocation !== "string") continue;
|
|
176
|
+
const location = toParameterLocation(rawLocation, name, `${entryPointer}/in`, diagnostics);
|
|
177
|
+
if (location === void 0) continue;
|
|
119
178
|
const schema = getProperty(resolved, "schema");
|
|
120
179
|
result.push({
|
|
121
180
|
name,
|
|
122
|
-
location
|
|
181
|
+
location,
|
|
123
182
|
required: getProperty(resolved, "required") === true,
|
|
124
183
|
deprecated: getProperty(resolved, "deprecated") === true,
|
|
125
184
|
description: getString(resolved, "description"),
|
|
@@ -128,13 +187,64 @@ function extractParameterList(doc, parameters) {
|
|
|
128
187
|
}
|
|
129
188
|
return result;
|
|
130
189
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Resolve a Reference Object chain on a non-Path-Item OpenAPI node
|
|
192
|
+
* (Parameter, Header, Link, etc.). Single-hop resolution is insufficient
|
|
193
|
+
* because OAS 3.x permits chains of Reference Objects of arbitrary
|
|
194
|
+
* length — a Reference Object whose target is itself a Reference Object
|
|
195
|
+
* is legal. `resolveRefChain` centralises cycle and depth-cap protection.
|
|
196
|
+
*
|
|
197
|
+
* Cycles and over-deep chains emit a dedicated diagnostic code per node
|
|
198
|
+
* kind (`cyclic-parameter-ref`, `parameter-ref-too-deep`, and the
|
|
199
|
+
* `header` / `link` equivalents), mirroring the existing
|
|
200
|
+
* `swagger-cyclic-parameter-ref` precedent so consumers can pattern-match
|
|
201
|
+
* directly on the code instead of filtering by `detail.kind`.
|
|
202
|
+
*/
|
|
203
|
+
function resolveReferenceObjectChain(doc, node, kind, diagnostics) {
|
|
204
|
+
const kindLabel = kind === "parameter" ? "Parameter Object" : kind === "header" ? "Header Object" : "Link Object";
|
|
205
|
+
const cyclicCode = kind === "parameter" ? "cyclic-parameter-ref" : kind === "header" ? "cyclic-header-ref" : "cyclic-link-ref";
|
|
206
|
+
const tooDeepCode = kind === "parameter" ? "parameter-ref-too-deep" : kind === "header" ? "header-ref-too-deep" : "link-ref-too-deep";
|
|
207
|
+
return resolveRefChain(node, {
|
|
208
|
+
lookup: (ref) => ref.startsWith("#/") ? resolveRefInDoc(doc, ref) : void 0,
|
|
209
|
+
onCycle: (ref) => {
|
|
210
|
+
emitDiagnostic(diagnostics, {
|
|
211
|
+
code: cyclicCode,
|
|
212
|
+
message: `Cyclic ${kindLabel} $ref "${ref}"`,
|
|
213
|
+
pointer: ref,
|
|
214
|
+
detail: {
|
|
215
|
+
ref,
|
|
216
|
+
kind
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
onDepthExceeded: (ref) => {
|
|
221
|
+
emitDiagnostic(diagnostics, {
|
|
222
|
+
code: tooDeepCode,
|
|
223
|
+
message: `${kindLabel} $ref chain exceeded the hop cap starting from "${ref}"`,
|
|
224
|
+
pointer: ref,
|
|
225
|
+
detail: {
|
|
226
|
+
ref,
|
|
227
|
+
kind
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Resolve a Parameter Object `$ref` chain. See
|
|
235
|
+
* `resolveReferenceObjectChain` for the resolution contract.
|
|
236
|
+
*/
|
|
237
|
+
function resolveParam(doc, param, diagnostics) {
|
|
238
|
+
return resolveReferenceObjectChain(doc, param, "parameter", diagnostics);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Encode a path segment for embedding in a JSON Pointer (RFC 6901).
|
|
242
|
+
* `~` → `~0`, `/` → `~1`. Used to build pointer fragments for diagnostics
|
|
243
|
+
* — the diagnostics layer does not currently re-encode segments inserted
|
|
244
|
+
* via template strings.
|
|
245
|
+
*/
|
|
246
|
+
function jsonPointerEscape(segment) {
|
|
247
|
+
return segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
138
248
|
}
|
|
139
249
|
function getRequestBody(parsed, path, method) {
|
|
140
250
|
const requestBodyRaw = getProperty(getProperty(lookupPathItem(parsed, path), method), "requestBody");
|
|
@@ -157,7 +267,7 @@ function getRequestBody(parsed, path, method) {
|
|
|
157
267
|
schema
|
|
158
268
|
};
|
|
159
269
|
}
|
|
160
|
-
function getResponses(parsed, path, method) {
|
|
270
|
+
function getResponses(parsed, path, method, diagnostics) {
|
|
161
271
|
const responses = getProperty(getProperty(lookupPathItem(parsed, path), method), "responses");
|
|
162
272
|
if (!isObject(responses)) return [];
|
|
163
273
|
const result = [];
|
|
@@ -168,7 +278,7 @@ function getResponses(parsed, path, method) {
|
|
|
168
278
|
const content = getProperty(response, "content");
|
|
169
279
|
const contentTypes = isObject(content) ? Object.keys(content) : [];
|
|
170
280
|
const schema = isObject(content) ? extractSchemaFromContent(content) : void 0;
|
|
171
|
-
const headers = getResponseHeaders(response, parsed.doc);
|
|
281
|
+
const headers = getResponseHeaders(response, parsed.doc, diagnostics);
|
|
172
282
|
result.push({
|
|
173
283
|
statusCode,
|
|
174
284
|
description: getString(response, "description"),
|
|
@@ -186,15 +296,60 @@ function getResponses(parsed, path, method) {
|
|
|
186
296
|
* it has no `$ref`, or `undefined` when the `$ref` is malformed or
|
|
187
297
|
* cannot be resolved (so the caller skips the entry rather than reading
|
|
188
298
|
* stale fields from the bare `{ $ref }` envelope).
|
|
299
|
+
*
|
|
300
|
+
* OpenAPI 3.1 Reference Object — sibling merge. OAS 3.1 explicitly
|
|
301
|
+
* permits `summary` and `description` siblings of `$ref`; the wrapper's
|
|
302
|
+
* siblings override the corresponding fields on the referenced node
|
|
303
|
+
* (spec: "If the property is present on both the Reference Object and
|
|
304
|
+
* the referenced node, the value on the Reference Object overrides the
|
|
305
|
+
* value of the referenced node"). OAS 3.0 forbids siblings, so the merge
|
|
306
|
+
* is gated on the document version. The gating is best-effort — if no
|
|
307
|
+
* recognisable `openapi`/`swagger` field is present we err on the side
|
|
308
|
+
* of NOT merging siblings to avoid changing behaviour for ambiguous or
|
|
309
|
+
* partially-built documents.
|
|
189
310
|
*/
|
|
190
311
|
function resolveWrapperRef(doc, wrapper) {
|
|
191
312
|
const ref = getString(wrapper, "$ref");
|
|
192
313
|
if (ref === void 0) return wrapper;
|
|
193
|
-
|
|
314
|
+
const target = resolveRefInDoc(doc, ref);
|
|
315
|
+
if (target === void 0) return void 0;
|
|
316
|
+
if (!documentAllowsReferenceSiblings(doc)) return target;
|
|
317
|
+
return mergeReferenceSiblings(wrapper, target);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* OAS 3.1 admits `summary` and `description` siblings on a Reference
|
|
321
|
+
* Object; OAS 3.0 does not. Detect the document version once per call
|
|
322
|
+
* — `detectOpenApiVersion` reads the top-level `openapi`/`swagger` field
|
|
323
|
+
* and is cheap enough to call on every resolution without caching.
|
|
324
|
+
*/
|
|
325
|
+
function documentAllowsReferenceSiblings(doc) {
|
|
326
|
+
const version = detectOpenApiVersion(doc);
|
|
327
|
+
if (version === void 0) return false;
|
|
328
|
+
return version.major === 3 && version.minor >= 1;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Per OAS 3.1, only `summary` and `description` siblings on a Reference
|
|
332
|
+
* Object are permitted and they override the referenced node. Any other
|
|
333
|
+
* sibling is ignored (spec: "Any properties of a Reference Object other
|
|
334
|
+
* than those described above SHALL be ignored"). The returned object is
|
|
335
|
+
* a fresh shallow merge — the input wrapper and target are not mutated.
|
|
336
|
+
*/
|
|
337
|
+
const REFERENCE_OBJECT_SIBLING_KEYS = ["summary", "description"];
|
|
338
|
+
function mergeReferenceSiblings(wrapper, target) {
|
|
339
|
+
const merged = { ...target };
|
|
340
|
+
for (const key of REFERENCE_OBJECT_SIBLING_KEYS) {
|
|
341
|
+
const siblingValue = wrapper[key];
|
|
342
|
+
if (typeof siblingValue === "string") merged[key] = siblingValue;
|
|
343
|
+
}
|
|
344
|
+
return merged;
|
|
194
345
|
}
|
|
195
346
|
function extractSchemaFromContent(content) {
|
|
196
|
-
const
|
|
197
|
-
|
|
347
|
+
for (const [mediaType, mediaObj] of Object.entries(content)) {
|
|
348
|
+
if (mediaTypeBase(mediaType) !== "application/json") continue;
|
|
349
|
+
if (!isObject(mediaObj)) continue;
|
|
350
|
+
const schema = getProperty(mediaObj, "schema");
|
|
351
|
+
if (isObject(schema)) return schema;
|
|
352
|
+
}
|
|
198
353
|
for (const [mediaType, mediaObj] of Object.entries(content)) {
|
|
199
354
|
if (!isJsonSuffixMediaType(mediaType)) continue;
|
|
200
355
|
if (!isObject(mediaObj)) continue;
|
|
@@ -208,16 +363,24 @@ function extractSchemaFromContent(content) {
|
|
|
208
363
|
}
|
|
209
364
|
}
|
|
210
365
|
/**
|
|
366
|
+
* Return the lowercased media-type base — the type/subtype with any
|
|
367
|
+
* RFC 7231 parameters (`; charset=...`, `; profile=...`, etc.) stripped
|
|
368
|
+
* and surrounding whitespace trimmed. Returns the empty string when the
|
|
369
|
+
* input has no recognisable base (defensive against malformed entries).
|
|
370
|
+
*/
|
|
371
|
+
function mediaTypeBase(mediaType) {
|
|
372
|
+
return mediaType.toLowerCase().split(";", 1)[0]?.trim() ?? "";
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
211
375
|
* Detect RFC 6839 structured-syntax-suffix media types that encode JSON.
|
|
212
376
|
* Matches `application/<anything>+json`, optionally with parameters
|
|
213
377
|
* (`; charset=utf-8`). Excludes the literal `application/json`, which
|
|
214
378
|
* the caller checks separately to preserve preference order.
|
|
215
379
|
*/
|
|
216
380
|
function isJsonSuffixMediaType(mediaType) {
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
return baseType.startsWith("application/") && baseType.endsWith("+json");
|
|
381
|
+
const base = mediaTypeBase(mediaType);
|
|
382
|
+
if (base === "application/json") return false;
|
|
383
|
+
return base.startsWith("application/") && base.endsWith("+json");
|
|
221
384
|
}
|
|
222
385
|
/**
|
|
223
386
|
* Resolve an in-document `$ref` against the supplied doc root.
|
|
@@ -280,14 +443,13 @@ function getSecuritySchemes(parsed) {
|
|
|
280
443
|
}
|
|
281
444
|
return result;
|
|
282
445
|
}
|
|
283
|
-
function getResponseHeaders(response, doc) {
|
|
446
|
+
function getResponseHeaders(response, doc, diagnostics) {
|
|
284
447
|
const result = /* @__PURE__ */ new Map();
|
|
285
448
|
const headers = getProperty(response, "headers");
|
|
286
449
|
if (!isObject(headers)) return result;
|
|
287
450
|
for (const [name, headerObj] of Object.entries(headers)) {
|
|
288
451
|
if (!isObject(headerObj)) continue;
|
|
289
|
-
const
|
|
290
|
-
const header = (ref !== void 0 && doc !== void 0 ? resolveRefInDoc(doc, ref) : void 0) ?? headerObj;
|
|
452
|
+
const header = doc !== void 0 ? resolveReferenceObjectChain(doc, headerObj, "header", diagnostics) ?? headerObj : headerObj;
|
|
291
453
|
const schemaProp = getProperty(header, "schema");
|
|
292
454
|
result.set(name, {
|
|
293
455
|
name,
|
|
@@ -299,20 +461,23 @@ function getResponseHeaders(response, doc) {
|
|
|
299
461
|
}
|
|
300
462
|
return result;
|
|
301
463
|
}
|
|
302
|
-
function listWebhooks(parsed) {
|
|
464
|
+
function listWebhooks(parsed, diagnostics, seenIds = /* @__PURE__ */ new Map()) {
|
|
303
465
|
const result = [];
|
|
304
466
|
const webhooks = getProperty(parsed.doc, "webhooks");
|
|
305
467
|
if (!isObject(webhooks)) return result;
|
|
306
|
-
for (const [name,
|
|
307
|
-
|
|
468
|
+
for (const [name, rawHookItem] of Object.entries(webhooks)) {
|
|
469
|
+
const hookItem = resolvePathItem(parsed, rawHookItem, diagnostics);
|
|
470
|
+
if (hookItem === void 0) continue;
|
|
308
471
|
const operations = [];
|
|
309
|
-
for (const method of
|
|
472
|
+
for (const method of HTTP_METHODS) {
|
|
310
473
|
const operation = getProperty(hookItem, method);
|
|
311
474
|
if (!isObject(operation)) continue;
|
|
475
|
+
const operationId = getString(operation, "operationId");
|
|
476
|
+
recordOperationId(operationId, `${method.toUpperCase()} webhook:${name}`, `/webhooks/${jsonPointerEscape(name)}/${method}/operationId`, seenIds, diagnostics);
|
|
312
477
|
operations.push({
|
|
313
478
|
path: name,
|
|
314
479
|
method,
|
|
315
|
-
operationId
|
|
480
|
+
operationId,
|
|
316
481
|
summary: getString(operation, "summary"),
|
|
317
482
|
description: getString(operation, "description"),
|
|
318
483
|
deprecated: getProperty(operation, "deprecated") === true,
|
|
@@ -326,6 +491,20 @@ function listWebhooks(parsed) {
|
|
|
326
491
|
}
|
|
327
492
|
return result;
|
|
328
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* Enumerate every operation in the document — both the `paths` map and
|
|
496
|
+
* the OpenAPI 3.1 `webhooks` map — sharing a single `seenIds` cache so
|
|
497
|
+
* cross-list `operationId` collisions surface the same way as same-list
|
|
498
|
+
* collisions. Returns the path-operation list followed by webhook
|
|
499
|
+
* operations (flattened); callers that need the structured webhook
|
|
500
|
+
* grouping should call `listWebhooks` directly.
|
|
501
|
+
*/
|
|
502
|
+
function listAllOperations(parsed, diagnostics) {
|
|
503
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
504
|
+
const pathOps = listOperations(parsed, diagnostics, seenIds);
|
|
505
|
+
const webhookOps = listWebhooks(parsed, diagnostics, seenIds).flatMap((w) => w.operations);
|
|
506
|
+
return [...pathOps, ...webhookOps];
|
|
507
|
+
}
|
|
329
508
|
function getExternalDocs(obj) {
|
|
330
509
|
const docs = getProperty(obj, "externalDocs");
|
|
331
510
|
if (!isObject(docs)) return void 0;
|
|
@@ -347,7 +526,7 @@ function getXmlInfo(schema) {
|
|
|
347
526
|
wrapped: getProperty(xml, "wrapped") === true
|
|
348
527
|
};
|
|
349
528
|
}
|
|
350
|
-
function listCallbacks(parsed, path, method) {
|
|
529
|
+
function listCallbacks(parsed, path, method, diagnostics) {
|
|
351
530
|
const operation = getProperty(lookupPathItem(parsed, path), method);
|
|
352
531
|
if (!isObject(operation)) return [];
|
|
353
532
|
const callbacks = getProperty(operation, "callbacks");
|
|
@@ -356,21 +535,20 @@ function listCallbacks(parsed, path, method) {
|
|
|
356
535
|
for (const [name, callbackItem] of Object.entries(callbacks)) {
|
|
357
536
|
if (!isObject(callbackItem)) continue;
|
|
358
537
|
const operations = [];
|
|
359
|
-
for (const [cbPath,
|
|
360
|
-
|
|
361
|
-
|
|
538
|
+
for (const [cbPath, rawCbPathItem] of Object.entries(callbackItem)) {
|
|
539
|
+
const cbPathItem = resolvePathItem(parsed, rawCbPathItem, diagnostics);
|
|
540
|
+
if (cbPathItem === void 0) continue;
|
|
541
|
+
for (const cbMethod of HTTP_METHODS) {
|
|
362
542
|
const cbOp = getProperty(cbPathItem, cbMethod);
|
|
363
543
|
if (!isObject(cbOp)) continue;
|
|
364
|
-
const ref = getString(cbOp, "$ref");
|
|
365
|
-
const resolved = ref !== void 0 ? resolveRefInDoc(parsed.doc, ref) ?? cbOp : cbOp;
|
|
366
544
|
operations.push({
|
|
367
545
|
path: `${name}/${cbPath}`,
|
|
368
546
|
method: cbMethod,
|
|
369
|
-
operationId: getString(
|
|
370
|
-
summary: getString(
|
|
371
|
-
description: getString(
|
|
372
|
-
deprecated: getProperty(
|
|
373
|
-
operation:
|
|
547
|
+
operationId: getString(cbOp, "operationId"),
|
|
548
|
+
summary: getString(cbOp, "summary"),
|
|
549
|
+
description: getString(cbOp, "description"),
|
|
550
|
+
deprecated: getProperty(cbOp, "deprecated") === true,
|
|
551
|
+
operation: cbOp
|
|
374
552
|
});
|
|
375
553
|
}
|
|
376
554
|
}
|
|
@@ -381,7 +559,7 @@ function listCallbacks(parsed, path, method) {
|
|
|
381
559
|
}
|
|
382
560
|
return result;
|
|
383
561
|
}
|
|
384
|
-
function getLinks(parsed, path, method, statusCode) {
|
|
562
|
+
function getLinks(parsed, path, method, statusCode, diagnostics) {
|
|
385
563
|
const response = getProperty(getProperty(getProperty(lookupPathItem(parsed, path), method), "responses"), statusCode);
|
|
386
564
|
if (!isObject(response)) return [];
|
|
387
565
|
const links = getProperty(response, "links");
|
|
@@ -389,9 +567,7 @@ function getLinks(parsed, path, method, statusCode) {
|
|
|
389
567
|
const result = [];
|
|
390
568
|
for (const [name, linkObj] of Object.entries(links)) {
|
|
391
569
|
if (!isObject(linkObj)) continue;
|
|
392
|
-
const
|
|
393
|
-
const resolved = ref !== void 0 ? resolveRefInDoc(parsed.doc, ref) ?? linkObj : linkObj;
|
|
394
|
-
const link = isObject(resolved) ? resolved : linkObj;
|
|
570
|
+
const link = resolveReferenceObjectChain(parsed.doc, linkObj, "link", diagnostics) ?? linkObj;
|
|
395
571
|
const params = getProperty(link, "parameters");
|
|
396
572
|
const paramMap = /* @__PURE__ */ new Map();
|
|
397
573
|
if (isObject(params)) {
|
|
@@ -409,4 +585,4 @@ function getLinks(parsed, path, method, statusCode) {
|
|
|
409
585
|
return result;
|
|
410
586
|
}
|
|
411
587
|
//#endregion
|
|
412
|
-
export { getExternalDocs, getLinks, getParameters, getRequestBody, getResponseHeaders, getResponses, getSchema, getSecurityRequirements, getSecuritySchemes, getXmlInfo, listCallbacks, listOperations, listWebhooks, parseOpenApiDocument };
|
|
588
|
+
export { getExternalDocs, getLinks, getParameters, getRequestBody, getResponseHeaders, getResponses, getSchema, getSecurityRequirements, getSecuritySchemes, getXmlInfo, listAllOperations, listCallbacks, listOperations, listWebhooks, parseOpenApiDocument };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as DiagnosticsOptions } from "../diagnostics-
|
|
1
|
+
import { i as DiagnosticsOptions } from "../diagnostics-Cbwak-ZX.mjs";
|
|
2
2
|
import { OpenApiDocument, OperationInfo, ParameterInfo, ResponseInfo, getRequestBody } from "./parser.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/openapi/resolve.d.ts
|
|
@@ -15,17 +15,26 @@ import { OpenApiDocument, OperationInfo, ParameterInfo, ResponseInfo, getRequest
|
|
|
15
15
|
* same form `<SchemaComponent>` does, keeping the OpenAPI components on
|
|
16
16
|
* the same pipeline as the top-level adapter.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
* (`duplicate-body-parameter`, `dropped-swagger-feature`,
|
|
20
|
-
* `unknown-json-schema-dialect`, `divisible-by-conflict`,
|
|
21
|
-
* `relative-ref-resolved`, etc.) are forwarded to the sink. Passing
|
|
22
|
-
* diagnostics also bypasses the cache so each call observes the
|
|
23
|
-
* normalisation pipeline running against the supplied sink — caching
|
|
24
|
-
* would silently swallow every emission after the first.
|
|
18
|
+
* ### Caching and diagnostics
|
|
25
19
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
20
|
+
* Normalisation runs at most once per document identity. The full set
|
|
21
|
+
* of doc-level diagnostics emitted during that single run is captured
|
|
22
|
+
* into the cache alongside the parsed result. Each caller-supplied
|
|
23
|
+
* sink receives the captured diagnostics exactly once per cached
|
|
24
|
+
* entry, no matter how many times `getParsed` is called with that
|
|
25
|
+
* `(doc, sink)` pair.
|
|
26
|
+
*
|
|
27
|
+
* The previous implementation bypassed the cache whenever
|
|
28
|
+
* `diagnostics` was supplied and re-ran the entire normalisation
|
|
29
|
+
* pipeline against the new sink. That fired every doc-level
|
|
30
|
+
* diagnostic once per call, so a parent like `ApiWebhooks` that
|
|
31
|
+
* renders `ApiWebhook` per webhook entry caused N-fold emission of a
|
|
32
|
+
* single real cause. With the new strategy, cardinality stays at one
|
|
33
|
+
* per real cause regardless of how many child renders share the
|
|
34
|
+
* sink.
|
|
35
|
+
*
|
|
36
|
+
* Strict mode is treated as a per-call invariant — see
|
|
37
|
+
* {@link replayCapturedDiagnostics} for the rationale.
|
|
29
38
|
*/
|
|
30
39
|
declare function getParsed(doc: Record<string, unknown>, diagnostics?: DiagnosticsOptions): OpenApiDocument;
|
|
31
40
|
/**
|