@jetio/validator 1.0.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/LICENSE +21 -0
- package/README.md +1362 -0
- package/dist/cli.js +219 -0
- package/dist/compileSchema.d.ts +148 -0
- package/dist/compileSchema.js +2199 -0
- package/dist/compileSchema.js.map +1 -0
- package/dist/formats.d.ts +41 -0
- package/dist/formats.js +166 -0
- package/dist/formats.js.map +1 -0
- package/dist/index.cjs.js +6167 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.esm.js +6148 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/jet-validator.d.ts +88 -0
- package/dist/jet-validator.js +983 -0
- package/dist/jet-validator.js.map +1 -0
- package/dist/resolver.d.ts +348 -0
- package/dist/resolver.js +2459 -0
- package/dist/resolver.js.map +1 -0
- package/dist/scripts/load-metaschemas.d.ts +1 -0
- package/dist/scripts/metaschema-loader.d.ts +2 -0
- package/dist/src/compileSchema.d.ts +148 -0
- package/dist/src/formats.d.ts +41 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/jet-validator.d.ts +88 -0
- package/dist/src/resolver.d.ts +348 -0
- package/dist/src/types/format.d.ts +7 -0
- package/dist/src/types/keywords.d.ts +78 -0
- package/dist/src/types/schema.d.ts +123 -0
- package/dist/src/types/standalone.d.ts +4 -0
- package/dist/src/types/validation.d.ts +49 -0
- package/dist/src/utilities/index.d.ts +11 -0
- package/dist/src/utilities/schema.d.ts +10 -0
- package/dist/types/format.d.ts +7 -0
- package/dist/types/format.js +3 -0
- package/dist/types/format.js.map +1 -0
- package/dist/types/keywords.d.ts +78 -0
- package/dist/types/keywords.js +4 -0
- package/dist/types/keywords.js.map +1 -0
- package/dist/types/schema.d.ts +123 -0
- package/dist/types/schema.js +3 -0
- package/dist/types/schema.js.map +1 -0
- package/dist/types/standalone.d.ts +4 -0
- package/dist/types/standalone.js +3 -0
- package/dist/types/standalone.js.map +1 -0
- package/dist/types/validation.d.ts +49 -0
- package/dist/types/validation.js +3 -0
- package/dist/types/validation.js.map +1 -0
- package/dist/utilities/index.d.ts +11 -0
- package/dist/utilities/index.js +146 -0
- package/dist/utilities/index.js.map +1 -0
- package/dist/utilities/schema.d.ts +10 -0
- package/dist/utilities/schema.js +232 -0
- package/dist/utilities/schema.js.map +1 -0
- package/dist/validator.umd.js +6196 -0
- package/package.json +79 -0
package/dist/resolver.js
ADDED
|
@@ -0,0 +1,2459 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SchemaResolver = void 0;
|
|
4
|
+
const utilities_1 = require("./utilities");
|
|
5
|
+
const schema_1 = require("./utilities/schema");
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// UTILITY FUNCTIONS
|
|
8
|
+
// ============================================================================
|
|
9
|
+
/**
|
|
10
|
+
* Sanitizes a reference name by replacing all non-alphanumeric characters with underscores.
|
|
11
|
+
* Used to create valid JavaScript function names from schema references.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* sanitizeRefName("https://example.com/schema#/defs/user")
|
|
15
|
+
* // Returns: "https___example_com_schema__defs_user"
|
|
16
|
+
*/
|
|
17
|
+
function sanitizeRefName(ref) {
|
|
18
|
+
return ref.replace(/[^a-zA-Z0-9]/g, "_");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Splits a URL-like path into its base path and fragment (hash) components.
|
|
22
|
+
* Handles edge cases like trailing hashes and missing fragments.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* getPathAndHash("https://example.com/schema#/definitions/user")
|
|
26
|
+
* // Returns: { path: "https://example.com/schema", hash: "#/definitions/user" }
|
|
27
|
+
*
|
|
28
|
+
* getPathAndHash("https://example.com/schema")
|
|
29
|
+
* // Returns: { path: "https://example.com/schema", hash: undefined }
|
|
30
|
+
*/
|
|
31
|
+
function splitUrlIntoPathAndFragment(pathUrl) {
|
|
32
|
+
const [basePath, fragment] = pathUrl.split("#");
|
|
33
|
+
let hash;
|
|
34
|
+
if (fragment !== undefined) {
|
|
35
|
+
// Handle edge case where path ends with "#" (empty fragment)
|
|
36
|
+
hash = pathUrl.endsWith("#") ? "#" : "#" + fragment;
|
|
37
|
+
}
|
|
38
|
+
return { path: basePath, hash };
|
|
39
|
+
}
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// SCHEMA IDENTIFIER HANDLERS
|
|
42
|
+
// ============================================================================
|
|
43
|
+
/**
|
|
44
|
+
* Resolves a schema's $id to an absolute URI and registers it.
|
|
45
|
+
* Handles relative $id values by resolving them against the current context's $id.
|
|
46
|
+
*
|
|
47
|
+
* @returns The resolved absolute $id value
|
|
48
|
+
*/
|
|
49
|
+
function resolveAndRegisterSchemaId(schema, currentContextId, currentPath, identifierRegistry) {
|
|
50
|
+
let resolvedId;
|
|
51
|
+
if (schema.$id.startsWith("http")) {
|
|
52
|
+
// Already absolute URL
|
|
53
|
+
resolvedId = schema.$id;
|
|
54
|
+
}
|
|
55
|
+
else if (currentContextId?.startsWith("http")) {
|
|
56
|
+
// Resolve relative URL against current context
|
|
57
|
+
resolvedId = new URL(schema.$id, currentContextId).href;
|
|
58
|
+
schema.$id = resolvedId; // Update schema with resolved value
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Keep as-is (local identifier)
|
|
62
|
+
resolvedId = schema.$id;
|
|
63
|
+
}
|
|
64
|
+
identifierRegistry.push({
|
|
65
|
+
schemaPath: currentPath,
|
|
66
|
+
identifier: resolvedId,
|
|
67
|
+
});
|
|
68
|
+
return resolvedId;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Registers a $anchor and its various reference forms.
|
|
72
|
+
* Anchors can be referenced directly or combined with the schema's $id.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* For schema: { "$id": "https://example.com/schema", "$anchor": "user" }
|
|
76
|
+
* Registers:
|
|
77
|
+
* - "user:ANCHOR" -> currentPath
|
|
78
|
+
* - "https://example.com/schema#user:ANCHOR" -> currentPath
|
|
79
|
+
*/
|
|
80
|
+
function registerAnchor(schema, currentPath, currentContextId, anchorToPathMap, identifierRegistry) {
|
|
81
|
+
const anchorName = schema.$anchor;
|
|
82
|
+
// Map anchor name to its definition path (for local resolution)
|
|
83
|
+
anchorToPathMap[anchorName] = currentPath;
|
|
84
|
+
if (schema.$id) {
|
|
85
|
+
// Anchor is defined alongside an $id - register both forms
|
|
86
|
+
identifierRegistry.push({
|
|
87
|
+
schemaPath: currentPath,
|
|
88
|
+
identifier: anchorName + ":ANCHOR",
|
|
89
|
+
parentSchemaId: schema.$id,
|
|
90
|
+
}, {
|
|
91
|
+
schemaPath: currentPath,
|
|
92
|
+
identifier: schema.$id + "#" + anchorName + ":ANCHOR",
|
|
93
|
+
parentSchemaId: schema.$id,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Anchor without $id - register with current context
|
|
98
|
+
identifierRegistry.push({
|
|
99
|
+
schemaPath: currentPath,
|
|
100
|
+
identifier: anchorName + ":ANCHOR",
|
|
101
|
+
});
|
|
102
|
+
if (currentContextId) {
|
|
103
|
+
identifierRegistry.push({
|
|
104
|
+
schemaPath: currentPath,
|
|
105
|
+
identifier: currentContextId + "#" + anchorName + ":ANCHOR",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Registers a $dynamicAnchor and its various reference forms.
|
|
112
|
+
* Dynamic anchors enable recursive schema extension patterns.
|
|
113
|
+
* They are only registered once (first definition wins).
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* For schema: { "$id": "https://example.com/schema", "$dynamicAnchor": "meta" }
|
|
117
|
+
* Registers:
|
|
118
|
+
* - "meta:DYNAMIC" -> currentPath
|
|
119
|
+
* - "https://example.com/schema#meta:DYNAMIC" -> currentPath
|
|
120
|
+
*/
|
|
121
|
+
function registerDynamicAnchor(schema, currentPath, basePath, currentContextId, dynamicAnchorToPathMap, identifierRegistry, alreadyRegisteredAnchors) {
|
|
122
|
+
const dynamicAnchorName = schema.$dynamicAnchor;
|
|
123
|
+
const dynamicAnchorKey = dynamicAnchorName + ":DYNAMIC";
|
|
124
|
+
// Dynamic anchors are only registered once (first definition wins)
|
|
125
|
+
if (alreadyRegisteredAnchors.includes(dynamicAnchorKey)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
dynamicAnchorToPathMap[dynamicAnchorName] = currentPath;
|
|
129
|
+
if (schema.$id) {
|
|
130
|
+
alreadyRegisteredAnchors.push(dynamicAnchorKey);
|
|
131
|
+
// Determine if this is the root schema (for parentSchemaId tracking)
|
|
132
|
+
const isRootSchema = basePath === "#";
|
|
133
|
+
identifierRegistry.push({
|
|
134
|
+
schemaPath: currentPath,
|
|
135
|
+
identifier: schema.$id + "#" + dynamicAnchorKey,
|
|
136
|
+
parentSchemaId: isRootSchema ? schema.$id : undefined,
|
|
137
|
+
}, {
|
|
138
|
+
schemaPath: currentPath,
|
|
139
|
+
identifier: dynamicAnchorKey,
|
|
140
|
+
parentSchemaId: isRootSchema ? schema.$id : undefined,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
identifierRegistry.push({
|
|
145
|
+
schemaPath: currentPath,
|
|
146
|
+
identifier: dynamicAnchorKey,
|
|
147
|
+
}, {
|
|
148
|
+
schemaPath: currentPath,
|
|
149
|
+
identifier: currentContextId + "#" + dynamicAnchorKey,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// REFERENCE HANDLERS
|
|
155
|
+
// ============================================================================
|
|
156
|
+
/**
|
|
157
|
+
* Processes a $ref and resolves it to its canonical form.
|
|
158
|
+
* Handles local refs (#/...), anchor refs (#name), and external refs (http://...).
|
|
159
|
+
*
|
|
160
|
+
* Resolution rules:
|
|
161
|
+
* - "#/definitions/x" -> Resolve relative to basePath
|
|
162
|
+
* - "#anchor" -> Look up in anchorToPathMap or mark as :ANCHOR
|
|
163
|
+
* - "http://..." -> Keep as absolute URL, mark anchors appropriately
|
|
164
|
+
* - "relative/path" -> Resolve against currentContextId
|
|
165
|
+
*/
|
|
166
|
+
function processReference(schema, basePath, anchorToPathMap, currentContextId, collectedRefs, currentPath, refPaths, inline) {
|
|
167
|
+
const rawRef = schema.$ref;
|
|
168
|
+
let resolvedRef;
|
|
169
|
+
if (rawRef.startsWith("#/")) {
|
|
170
|
+
// JSON Pointer reference - resolve relative to base path
|
|
171
|
+
resolvedRef = basePath ? basePath + rawRef.slice(1) : rawRef;
|
|
172
|
+
}
|
|
173
|
+
else if (rawRef.startsWith("#")) {
|
|
174
|
+
if (rawRef === "#") {
|
|
175
|
+
// Self-reference to root
|
|
176
|
+
resolvedRef = rawRef;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// Anchor reference - look up or mark for later resolution
|
|
180
|
+
const anchorName = rawRef.slice(1);
|
|
181
|
+
resolvedRef = anchorToPathMap[anchorName] || rawRef + ":ANCHOR";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// External reference - resolve to absolute URL
|
|
186
|
+
let absoluteUrl;
|
|
187
|
+
if (rawRef.startsWith("http")) {
|
|
188
|
+
absoluteUrl = rawRef;
|
|
189
|
+
}
|
|
190
|
+
else if (currentContextId?.startsWith("http")) {
|
|
191
|
+
absoluteUrl = new URL(rawRef, currentContextId).href;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
absoluteUrl = rawRef;
|
|
195
|
+
}
|
|
196
|
+
// Check if URL has a fragment that's an anchor (not a JSON pointer)
|
|
197
|
+
if (absoluteUrl.includes("#")) {
|
|
198
|
+
const urlParts = splitUrlIntoPathAndFragment(absoluteUrl);
|
|
199
|
+
const isAnchorFragment = urlParts.hash &&
|
|
200
|
+
urlParts.hash !== "#" &&
|
|
201
|
+
!urlParts.hash.startsWith("#/");
|
|
202
|
+
resolvedRef = isAnchorFragment ? absoluteUrl + ":ANCHOR" : absoluteUrl;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
resolvedRef = absoluteUrl;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Track paths that contain external (non-local) references
|
|
209
|
+
if (!inline) {
|
|
210
|
+
if (resolvedRef.startsWith("#/")) {
|
|
211
|
+
refPaths.push(resolvedRef);
|
|
212
|
+
}
|
|
213
|
+
refPaths.push(currentPath);
|
|
214
|
+
}
|
|
215
|
+
// Update schema with resolved reference and add to collection
|
|
216
|
+
schema.$ref = resolvedRef;
|
|
217
|
+
collectedRefs.push(resolvedRef);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Processes a $dynamicRef and resolves it to its canonical form.
|
|
221
|
+
* Dynamic references enable runtime resolution based on the call stack.
|
|
222
|
+
*/
|
|
223
|
+
function processDynamicReference(schema, basePath, currentPath, currentContextId, collectedRefs, refPaths, inline) {
|
|
224
|
+
const rawDynamicRef = schema.$dynamicRef;
|
|
225
|
+
let resolvedDynamicRef;
|
|
226
|
+
if (rawDynamicRef.startsWith("#/")) {
|
|
227
|
+
// JSON Pointer - resolve relative to base path
|
|
228
|
+
resolvedDynamicRef = basePath + rawDynamicRef.slice(1);
|
|
229
|
+
}
|
|
230
|
+
else if (rawDynamicRef.startsWith("#")) {
|
|
231
|
+
if (rawDynamicRef === "#") {
|
|
232
|
+
// Self-reference to root
|
|
233
|
+
resolvedDynamicRef = rawDynamicRef;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Dynamic anchor reference
|
|
237
|
+
resolvedDynamicRef = currentContextId + rawDynamicRef + ":DYNAMIC";
|
|
238
|
+
collectedRefs.push(rawDynamicRef + ":DYNAMIC");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// External dynamic reference
|
|
243
|
+
let absoluteUrl;
|
|
244
|
+
if (rawDynamicRef.startsWith("http")) {
|
|
245
|
+
absoluteUrl = rawDynamicRef;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
absoluteUrl = new URL(rawDynamicRef, currentContextId).href;
|
|
249
|
+
}
|
|
250
|
+
// Dynamic refs with anchors (not JSON pointers) get :DYNAMIC suffix
|
|
251
|
+
if (absoluteUrl.includes("#")) {
|
|
252
|
+
const urlParts = splitUrlIntoPathAndFragment(absoluteUrl);
|
|
253
|
+
const hasAnchorFragment = urlParts.hash &&
|
|
254
|
+
urlParts.hash !== "#" &&
|
|
255
|
+
!urlParts.hash.startsWith("#/");
|
|
256
|
+
resolvedDynamicRef = hasAnchorFragment
|
|
257
|
+
? absoluteUrl + ":DYNAMIC"
|
|
258
|
+
: absoluteUrl;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
resolvedDynamicRef = absoluteUrl;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Track paths with external references
|
|
265
|
+
if (!inline) {
|
|
266
|
+
if (resolvedDynamicRef.startsWith("#/")) {
|
|
267
|
+
refPaths.push(resolvedDynamicRef);
|
|
268
|
+
}
|
|
269
|
+
refPaths.push(currentPath);
|
|
270
|
+
}
|
|
271
|
+
collectedRefs.push(resolvedDynamicRef);
|
|
272
|
+
schema.$dynamicRef = resolvedDynamicRef;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Marks a path and all its parent paths as "containing references".
|
|
276
|
+
* This is used for inlining optimization to know which schemas can't be inlined
|
|
277
|
+
* because they or their parents contain references that need to be resolved.
|
|
278
|
+
*
|
|
279
|
+
* Stops at $defs/definitions boundaries since those are definition containers,
|
|
280
|
+
* not validation schemas.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* markPathsContainingRefs("#/properties/user/items", pathsWithRefs)
|
|
284
|
+
* // Marks: "#/properties/user/items", "#/properties/user", "#/properties", "#"
|
|
285
|
+
*/
|
|
286
|
+
function markPathsContainingRefs(currentPath, pathsContainingRefs) {
|
|
287
|
+
const DEFINITION_KEYWORDS = new Set(["$defs", "definitions"]);
|
|
288
|
+
// Always mark the current path
|
|
289
|
+
pathsContainingRefs.add(currentPath);
|
|
290
|
+
// Split path into segments (remove leading '#' and empty strings)
|
|
291
|
+
const pathSegments = currentPath
|
|
292
|
+
.slice(1)
|
|
293
|
+
.split("/")
|
|
294
|
+
.filter((segment) => segment);
|
|
295
|
+
// Trace upwards through parent paths
|
|
296
|
+
for (let i = pathSegments.length - 1; i > 0; i--) {
|
|
297
|
+
// Stop if we're about to cross into a definitions container
|
|
298
|
+
if (DEFINITION_KEYWORDS.has(pathSegments[i - 1])) {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
const parentPath = "#/" + pathSegments.slice(0, i).join("/");
|
|
302
|
+
pathsContainingRefs.add(parentPath);
|
|
303
|
+
}
|
|
304
|
+
// Mark root if the first segment isn't a definitions container
|
|
305
|
+
if (pathSegments.length > 0 && !DEFINITION_KEYWORDS.has(pathSegments[0])) {
|
|
306
|
+
pathsContainingRefs.add("#");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// ============================================================================
|
|
310
|
+
// MAIN SCHEMA RESOLVER CLASS
|
|
311
|
+
// ============================================================================
|
|
312
|
+
/**
|
|
313
|
+
* SchemaResolver handles the complex task of resolving JSON Schema references.
|
|
314
|
+
*
|
|
315
|
+
* It performs several key functions:
|
|
316
|
+
* 1. Collects all $id, $anchor, $dynamicAnchor declarations
|
|
317
|
+
* 2. Resolves all $ref and $dynamicRef to their target schemas
|
|
318
|
+
* 3. Generates unique function names for each referenceable schema
|
|
319
|
+
* 4. Optionally inlines references that don't form cycles
|
|
320
|
+
* 5. Loads external schemas (sync or async)
|
|
321
|
+
*
|
|
322
|
+
* The resolver supports JSON Schema drafts 6, 7, 2019-09, and 2020-12.
|
|
323
|
+
*/
|
|
324
|
+
class SchemaResolver {
|
|
325
|
+
constructor(jetValidator, options) {
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// INSTANCE STATE
|
|
328
|
+
// ============================================================================
|
|
329
|
+
/**
|
|
330
|
+
* Maps external schema URLs to their internal reference maps.
|
|
331
|
+
* Structure: externalSchemaUrl -> (refIdentifier -> functionName)
|
|
332
|
+
*/
|
|
333
|
+
this.externalSchemaRefMaps = new Map();
|
|
334
|
+
/**
|
|
335
|
+
* Collection of all schemas that need to be compiled into validator functions.
|
|
336
|
+
* Each entry contains the schema, its path, and the generated function name.
|
|
337
|
+
*/
|
|
338
|
+
this.schemasToCompile = [];
|
|
339
|
+
this.rootFunctionName = "validate";
|
|
340
|
+
/**
|
|
341
|
+
* Tracks which schemas have already been added to schemasToCompile.
|
|
342
|
+
* Structure: schemaUrl -> Set of paths already processed
|
|
343
|
+
* Prevents duplicate compilation of the same schema.
|
|
344
|
+
*/
|
|
345
|
+
this.compiledSchemaPaths = new Map();
|
|
346
|
+
/**
|
|
347
|
+
* Cache of fully processed external schemas.
|
|
348
|
+
* Avoids re-processing the same external schema multiple times.
|
|
349
|
+
*/
|
|
350
|
+
this.processedExternalSchemas = new Map();
|
|
351
|
+
/**
|
|
352
|
+
* Whether the root schema has been set (used for function naming).
|
|
353
|
+
* The root schema always gets the function name "validate".
|
|
354
|
+
*/
|
|
355
|
+
this.hasSetRootSchema = false;
|
|
356
|
+
/**
|
|
357
|
+
* All format strings encountered in the schema.
|
|
358
|
+
* Used to validate that required format validators are available.
|
|
359
|
+
*/
|
|
360
|
+
this.discoveredFormats = new Set();
|
|
361
|
+
/**
|
|
362
|
+
* All custom keywords encountered in the schema.
|
|
363
|
+
* Used to validate that required keyword handlers are registered.
|
|
364
|
+
*/
|
|
365
|
+
this.discoveredCustomKeywords = new Set();
|
|
366
|
+
/**
|
|
367
|
+
* Counter for generating unique function names.
|
|
368
|
+
* Incremented each time a new function name is generated.
|
|
369
|
+
*/
|
|
370
|
+
this.functionNameCounter = 0;
|
|
371
|
+
/**
|
|
372
|
+
* Maps schema IDs to their paths containing refs (for inlining decisions).
|
|
373
|
+
* Used to determine which external refs can be safely inlined.
|
|
374
|
+
*/
|
|
375
|
+
this.schemaIdToRefPaths = new Map();
|
|
376
|
+
/**
|
|
377
|
+
* Tracks schemas currently being resolved to detect circular references.
|
|
378
|
+
* Prevents infinite loops when schemas reference each other.
|
|
379
|
+
*/
|
|
380
|
+
this.currentlyResolvingSchemas = new Set();
|
|
381
|
+
/**
|
|
382
|
+
* Context information needed during compilation.
|
|
383
|
+
* Accumulated during resolution and passed to the compiler.
|
|
384
|
+
*/
|
|
385
|
+
this.compilationContext = {
|
|
386
|
+
hasUnevaluatedProperties: false,
|
|
387
|
+
hasUnevaluatedItems: false,
|
|
388
|
+
hasRootReference: false,
|
|
389
|
+
referencedFunctions: [],
|
|
390
|
+
uses$Data: false,
|
|
391
|
+
inliningStats: {
|
|
392
|
+
totalRefs: 0,
|
|
393
|
+
inlinedRefs: 0,
|
|
394
|
+
functionRefs: 0,
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
this.jetValidator = jetValidator;
|
|
398
|
+
this.options = options;
|
|
399
|
+
}
|
|
400
|
+
// ============================================================================
|
|
401
|
+
// CLEANUP METHODS
|
|
402
|
+
// ============================================================================
|
|
403
|
+
/**
|
|
404
|
+
* Clears all resolution state.
|
|
405
|
+
* Called after resolution is complete to free memory.
|
|
406
|
+
*/
|
|
407
|
+
clearResolutionState() {
|
|
408
|
+
this.compiledSchemaPaths.forEach((set) => set.clear());
|
|
409
|
+
this.compiledSchemaPaths.clear();
|
|
410
|
+
this.externalSchemaRefMaps.forEach((map) => map.clear());
|
|
411
|
+
this.externalSchemaRefMaps.clear();
|
|
412
|
+
this.processedExternalSchemas.clear();
|
|
413
|
+
this.schemaIdToRefPaths.forEach((set) => set.clear());
|
|
414
|
+
this.schemaIdToRefPaths.clear();
|
|
415
|
+
}
|
|
416
|
+
// ============================================================================
|
|
417
|
+
// REFERENCE MAP MANAGEMENT
|
|
418
|
+
// ============================================================================
|
|
419
|
+
/**
|
|
420
|
+
* Gets or creates a reference map for storing function names for a schema.
|
|
421
|
+
* The map key is determined by the schema's identity (URL, external ID, or context).
|
|
422
|
+
*/
|
|
423
|
+
getOrCreateRefMapForSchema(entry, context) {
|
|
424
|
+
const identifier = entry.identifier;
|
|
425
|
+
let mapKey;
|
|
426
|
+
if (identifier.startsWith("http")) {
|
|
427
|
+
mapKey = identifier;
|
|
428
|
+
}
|
|
429
|
+
else if (entry.parentSchemaId) {
|
|
430
|
+
mapKey = entry.parentSchemaId;
|
|
431
|
+
}
|
|
432
|
+
else if (context.schemaId?.startsWith("http")) {
|
|
433
|
+
mapKey = context.schemaId;
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
mapKey = identifier;
|
|
437
|
+
}
|
|
438
|
+
let refMap = this.externalSchemaRefMaps.get(mapKey);
|
|
439
|
+
if (!refMap) {
|
|
440
|
+
refMap = new Map();
|
|
441
|
+
this.externalSchemaRefMaps.set(mapKey, refMap);
|
|
442
|
+
}
|
|
443
|
+
return refMap;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Gets an existing reference map for a schema identifier.
|
|
447
|
+
* Returns undefined if no map exists.
|
|
448
|
+
*/
|
|
449
|
+
getRefMapForIdentifier(entry, context) {
|
|
450
|
+
const identifier = entry.identifier;
|
|
451
|
+
if (identifier.startsWith("http")) {
|
|
452
|
+
return this.externalSchemaRefMaps.get(identifier);
|
|
453
|
+
}
|
|
454
|
+
else if (entry.parentSchemaId) {
|
|
455
|
+
return this.externalSchemaRefMaps.get(entry.parentSchemaId);
|
|
456
|
+
}
|
|
457
|
+
else if (context.schemaId?.startsWith("http")) {
|
|
458
|
+
return this.externalSchemaRefMaps.get(context.schemaId);
|
|
459
|
+
}
|
|
460
|
+
return this.externalSchemaRefMaps.get(identifier);
|
|
461
|
+
}
|
|
462
|
+
// ============================================================================
|
|
463
|
+
// SCHEMA PATH TRACKING
|
|
464
|
+
// ============================================================================
|
|
465
|
+
/**
|
|
466
|
+
* Updates the tracking of which schema paths have been processed.
|
|
467
|
+
* Returns whether this is a new path (true) or already processed (false).
|
|
468
|
+
*/
|
|
469
|
+
trackSchemaPath(path, schemaUrl, contextId, additionalPaths = []) {
|
|
470
|
+
const existingUrlPaths = this.compiledSchemaPaths.get(schemaUrl);
|
|
471
|
+
const existingContextPaths = this.compiledSchemaPaths.get(contextId);
|
|
472
|
+
// Check if already processed
|
|
473
|
+
if (existingUrlPaths?.has(path) || existingContextPaths?.has(path)) {
|
|
474
|
+
return { isNewPath: false, existingUrlPaths, existingContextPaths };
|
|
475
|
+
}
|
|
476
|
+
// Add to URL-based tracking
|
|
477
|
+
if (existingUrlPaths) {
|
|
478
|
+
existingUrlPaths.add(path);
|
|
479
|
+
additionalPaths.forEach((p) => existingUrlPaths.add(p));
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
const newSet = new Set([path, ...additionalPaths]);
|
|
483
|
+
this.compiledSchemaPaths.set(schemaUrl, newSet);
|
|
484
|
+
}
|
|
485
|
+
// Add to context-based tracking (for cross-schema references)
|
|
486
|
+
if (path.startsWith("http") || schemaUrl !== contextId) {
|
|
487
|
+
if (existingContextPaths) {
|
|
488
|
+
existingContextPaths.add(path);
|
|
489
|
+
additionalPaths.forEach((p) => existingContextPaths.add(p));
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
const newSet = new Set([path, ...additionalPaths]);
|
|
493
|
+
this.compiledSchemaPaths.set(contextId, newSet);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return { isNewPath: true, existingUrlPaths, existingContextPaths };
|
|
497
|
+
}
|
|
498
|
+
// ============================================================================
|
|
499
|
+
// FUNCTION NAME GENERATION
|
|
500
|
+
// ============================================================================
|
|
501
|
+
/**
|
|
502
|
+
* Generates a unique function name for a schema.
|
|
503
|
+
* Format: validate_{sanitized_identifier}_{counter}
|
|
504
|
+
*/
|
|
505
|
+
generateFunctionName(identifier) {
|
|
506
|
+
const sanitized = sanitizeRefName(identifier);
|
|
507
|
+
return `validate_${sanitized}_${this.functionNameCounter++}`;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Assigns function names to all collected schema identifiers ($id, $anchor, $dynamicAnchor).
|
|
511
|
+
*/
|
|
512
|
+
assignFunctionNamesToIdentifiers(identifiers, context) {
|
|
513
|
+
for (const entry of identifiers) {
|
|
514
|
+
const identifier = entry.identifier;
|
|
515
|
+
// Skip if already assigned
|
|
516
|
+
if (context.refToFunctionName.has(identifier))
|
|
517
|
+
continue;
|
|
518
|
+
if (entry.schemaPath === "#" && !this.hasSetRootSchema) {
|
|
519
|
+
this.assignRootSchemaFunctionName(entry, context);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
this.assignNonRootSchemaFunctionName(entry, context);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Assigns function name for the root schema (always "validate" for main root schema).
|
|
528
|
+
*/
|
|
529
|
+
assignRootSchemaFunctionName(entry, context) {
|
|
530
|
+
const existingRefMap = this.getRefMapForIdentifier(entry, context);
|
|
531
|
+
const functionName = existingRefMap?.get(entry.schemaPath) ??
|
|
532
|
+
existingRefMap?.get(entry.identifier) ??
|
|
533
|
+
this.rootFunctionName;
|
|
534
|
+
context.refToFunctionName.set(entry.identifier, functionName);
|
|
535
|
+
context.refToFunctionName.set(entry.schemaPath, functionName);
|
|
536
|
+
const refMap = existingRefMap || this.getOrCreateRefMapForSchema(entry, context);
|
|
537
|
+
refMap.set(entry.identifier, functionName);
|
|
538
|
+
refMap.set(entry.schemaPath, functionName);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Assigns function name for non-root schemas.
|
|
542
|
+
*/
|
|
543
|
+
assignNonRootSchemaFunctionName(entry, context) {
|
|
544
|
+
const identifier = entry.identifier;
|
|
545
|
+
let primaryRefMap;
|
|
546
|
+
let secondaryRefMap;
|
|
547
|
+
// Determine which ref maps to check based on identifier type
|
|
548
|
+
if (identifier.startsWith("http")) {
|
|
549
|
+
primaryRefMap = this.externalSchemaRefMaps.get(identifier.split("#")[0]);
|
|
550
|
+
secondaryRefMap = this.externalSchemaRefMaps.get(context.schemaId);
|
|
551
|
+
}
|
|
552
|
+
else if (entry.parentSchemaId) {
|
|
553
|
+
primaryRefMap = this.externalSchemaRefMaps.get(entry.parentSchemaId);
|
|
554
|
+
if (entry.parentSchemaId.startsWith("https")) {
|
|
555
|
+
secondaryRefMap = this.externalSchemaRefMaps.get(context.schemaId);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
primaryRefMap = this.externalSchemaRefMaps.get(context.schemaId);
|
|
560
|
+
}
|
|
561
|
+
// Look for existing function name
|
|
562
|
+
let functionName = primaryRefMap?.get(entry.schemaPath) ??
|
|
563
|
+
primaryRefMap?.get(identifier) ??
|
|
564
|
+
secondaryRefMap?.get(entry.schemaPath) ??
|
|
565
|
+
secondaryRefMap?.get(identifier);
|
|
566
|
+
if (functionName) {
|
|
567
|
+
context.refToFunctionName.set(identifier, functionName);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
// Generate new function name
|
|
571
|
+
functionName = this.generateFunctionName(identifier);
|
|
572
|
+
context.refToFunctionName.set(identifier, functionName);
|
|
573
|
+
context.refToFunctionName.set(entry.schemaPath, functionName);
|
|
574
|
+
const refMap = primaryRefMap || this.getOrCreateRefMapForSchema(entry, context);
|
|
575
|
+
refMap.set(identifier, functionName);
|
|
576
|
+
refMap.set(entry.schemaPath, functionName);
|
|
577
|
+
// Update secondary ref map for cross-schema references
|
|
578
|
+
const needsSecondaryUpdate = identifier.startsWith("http") ||
|
|
579
|
+
entry.parentSchemaId?.startsWith("https");
|
|
580
|
+
if (needsSecondaryUpdate) {
|
|
581
|
+
if (secondaryRefMap) {
|
|
582
|
+
secondaryRefMap.set(identifier, functionName);
|
|
583
|
+
secondaryRefMap.set(entry.schemaPath, functionName);
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
const newMap = new Map();
|
|
587
|
+
this.externalSchemaRefMaps.set(context.schemaId, newMap);
|
|
588
|
+
newMap.set(identifier, functionName);
|
|
589
|
+
newMap.set(entry.schemaPath, functionName);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Assigns function names to all collected references ($ref, $dynamicRef).
|
|
596
|
+
*/
|
|
597
|
+
assignFunctionNamesToReferences(references, context, identifierToPath) {
|
|
598
|
+
for (const ref of references) {
|
|
599
|
+
// Normalize the reference key
|
|
600
|
+
const refKey = ref.startsWith("#/")
|
|
601
|
+
? ref
|
|
602
|
+
: ref.startsWith("#") && ref !== "#"
|
|
603
|
+
? ref.slice(1)
|
|
604
|
+
: ref;
|
|
605
|
+
if (context.refToFunctionName.has(refKey))
|
|
606
|
+
continue;
|
|
607
|
+
if (ref.startsWith("#")) {
|
|
608
|
+
this.assignHashRefFunctionName(ref, context);
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
this.assignExternalRefFunctionName(ref, context, identifierToPath);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Assigns function name for a local hash reference (#, #/, #anchor).
|
|
617
|
+
*/
|
|
618
|
+
assignHashRefFunctionName(ref, context) {
|
|
619
|
+
if (ref === "#" && !this.hasSetRootSchema) {
|
|
620
|
+
context.refToFunctionName.set(ref, this.rootFunctionName);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (!context.schemaId) {
|
|
624
|
+
const functionName = this.generateFunctionName(ref);
|
|
625
|
+
context.refToFunctionName.set(ref, functionName);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const urlParts = splitUrlIntoPathAndFragment(context.schemaId);
|
|
629
|
+
const existingRefMap = this.externalSchemaRefMaps.get(urlParts.path);
|
|
630
|
+
if (existingRefMap) {
|
|
631
|
+
const existingFunction = existingRefMap.get(ref);
|
|
632
|
+
if (existingFunction) {
|
|
633
|
+
context.refToFunctionName.set(ref, existingFunction);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
const functionName = this.generateFunctionName(ref);
|
|
637
|
+
existingRefMap.set(ref, functionName);
|
|
638
|
+
context.refToFunctionName.set(ref, functionName);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
const newMap = new Map();
|
|
643
|
+
const functionName = this.generateFunctionName(ref);
|
|
644
|
+
newMap.set(ref, functionName);
|
|
645
|
+
context.refToFunctionName.set(ref, functionName);
|
|
646
|
+
this.externalSchemaRefMaps.set(urlParts.path, newMap);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Assigns function name for an external reference (http://...).
|
|
651
|
+
*/
|
|
652
|
+
assignExternalRefFunctionName(ref, context, identifierToPath) {
|
|
653
|
+
const urlParts = splitUrlIntoPathAndFragment(ref);
|
|
654
|
+
const baseUrl = urlParts.path;
|
|
655
|
+
// Check if this external URL maps to a local path via $id
|
|
656
|
+
let localPath;
|
|
657
|
+
if (identifierToPath[baseUrl]) {
|
|
658
|
+
const fragment = urlParts.hash ?? "";
|
|
659
|
+
localPath =
|
|
660
|
+
identifierToPath[baseUrl] +
|
|
661
|
+
(fragment.startsWith("#/") ? fragment.slice(1) : "");
|
|
662
|
+
}
|
|
663
|
+
if (localPath === undefined) {
|
|
664
|
+
this.assignHttpRefFunctionName(ref, urlParts, context);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
this.assignIdentifierPathRefFunctionName(ref, baseUrl, localPath, context, identifierToPath);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Assigns function name for an HTTP URL reference.
|
|
672
|
+
*/
|
|
673
|
+
assignHttpRefFunctionName(ref, urlParts, context) {
|
|
674
|
+
if (!ref.startsWith("http"))
|
|
675
|
+
return;
|
|
676
|
+
const baseUrl = urlParts.path;
|
|
677
|
+
const fragment = urlParts.hash;
|
|
678
|
+
const existingRefMap = this.externalSchemaRefMaps.get(baseUrl);
|
|
679
|
+
if (existingRefMap) {
|
|
680
|
+
// Handle fragment if present
|
|
681
|
+
if (fragment) {
|
|
682
|
+
const existingFragmentFunction = existingRefMap.get(fragment);
|
|
683
|
+
if (existingFragmentFunction) {
|
|
684
|
+
context.refToFunctionName.set(ref, existingFragmentFunction);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
const functionName = this.generateFunctionName(fragment);
|
|
688
|
+
context.refToFunctionName.set(ref, functionName);
|
|
689
|
+
if (fragment.startsWith("#/")) {
|
|
690
|
+
existingRefMap.set(fragment, functionName);
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
existingRefMap.set(fragment.slice(1), functionName);
|
|
694
|
+
}
|
|
695
|
+
existingRefMap.set(ref, functionName);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Handle base URL
|
|
699
|
+
if (existingRefMap.has(baseUrl)) {
|
|
700
|
+
context.refToFunctionName.set(ref, existingRefMap.get(baseUrl));
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
const functionName = this.generateFunctionName(baseUrl);
|
|
704
|
+
context.refToFunctionName.set(ref, functionName);
|
|
705
|
+
existingRefMap.set(baseUrl, functionName);
|
|
706
|
+
existingRefMap.set("#", functionName);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
// Create new ref map for this URL
|
|
711
|
+
const newMap = new Map();
|
|
712
|
+
this.externalSchemaRefMaps.set(baseUrl, newMap);
|
|
713
|
+
if (fragment) {
|
|
714
|
+
const functionName = this.generateFunctionName(fragment);
|
|
715
|
+
context.refToFunctionName.set(ref, functionName);
|
|
716
|
+
if (fragment.startsWith("#/")) {
|
|
717
|
+
newMap.set(fragment, functionName);
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
newMap.set(fragment.slice(1), functionName);
|
|
721
|
+
}
|
|
722
|
+
newMap.set(ref, functionName);
|
|
723
|
+
}
|
|
724
|
+
const baseFunctionName = this.generateFunctionName(baseUrl);
|
|
725
|
+
context.refToFunctionName.set(baseUrl, baseFunctionName);
|
|
726
|
+
newMap.set(baseUrl, baseFunctionName);
|
|
727
|
+
newMap.set("#", baseFunctionName);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Assigns function name for a reference that maps to a local $id path.
|
|
732
|
+
*/
|
|
733
|
+
assignIdentifierPathRefFunctionName(ref, baseUrl, localPath, context, identifierToPath) {
|
|
734
|
+
const fragment = splitUrlIntoPathAndFragment(ref).hash ?? "";
|
|
735
|
+
// Skip anchor references that aren't in the identifier map
|
|
736
|
+
if (fragment && !fragment.startsWith("#/")) {
|
|
737
|
+
if (!identifierToPath[ref]) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
const functionName = context.refToFunctionName.get(ref);
|
|
742
|
+
context.refToFunctionName.set(ref, functionName);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const existingRefMap = this.externalSchemaRefMaps.get(baseUrl);
|
|
746
|
+
if (existingRefMap) {
|
|
747
|
+
const existingFunction = existingRefMap.get(localPath);
|
|
748
|
+
if (existingFunction) {
|
|
749
|
+
context.refToFunctionName.set(ref, existingFunction);
|
|
750
|
+
context.refToFunctionName.set(localPath, existingFunction);
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
const functionName = this.generateFunctionName(localPath);
|
|
754
|
+
context.refToFunctionName.set(ref, functionName);
|
|
755
|
+
context.refToFunctionName.set(localPath, functionName);
|
|
756
|
+
existingRefMap.set(localPath, functionName);
|
|
757
|
+
existingRefMap.set(ref, functionName);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
const newMap = new Map();
|
|
762
|
+
this.externalSchemaRefMaps.set(baseUrl, newMap);
|
|
763
|
+
const functionName = this.generateFunctionName(localPath);
|
|
764
|
+
context.refToFunctionName.set(ref, functionName);
|
|
765
|
+
newMap.set(ref, functionName);
|
|
766
|
+
newMap.set(localPath, functionName);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// ============================================================================
|
|
770
|
+
// SCHEMA PREPROCESSING
|
|
771
|
+
// ============================================================================
|
|
772
|
+
/**
|
|
773
|
+
* Pre-processes a schema to collect all identifiers, references, and paths.
|
|
774
|
+
* This is the first pass that gathers information needed for resolution.
|
|
775
|
+
*/
|
|
776
|
+
preprocessSchema(rootSchema, context) {
|
|
777
|
+
const { refs: collectedRefs, ids: identifiers, pathsWithRefs: pathsContainingRefs, refPaths: pathsOfRefs, } = this.collectSchemaMetadata(rootSchema, Array.from(context.refToFunctionName.keys()));
|
|
778
|
+
// Assign function names to all identifiers
|
|
779
|
+
this.assignFunctionNamesToIdentifiers(identifiers, context);
|
|
780
|
+
// Build identifier -> path mapping
|
|
781
|
+
const identifierToPath = identifiers.reduce((map, entry) => {
|
|
782
|
+
if (map[entry.identifier] === undefined) {
|
|
783
|
+
map[entry.identifier] = entry.schemaPath;
|
|
784
|
+
}
|
|
785
|
+
return map;
|
|
786
|
+
}, {});
|
|
787
|
+
// Assign function names to all references
|
|
788
|
+
this.assignFunctionNamesToReferences(collectedRefs, context, identifierToPath);
|
|
789
|
+
this.hasSetRootSchema = true;
|
|
790
|
+
// Store local identifiers for later reference
|
|
791
|
+
const localIdentifiers = identifiers.map((entry) => entry.identifier);
|
|
792
|
+
context.localSchemaIds = localIdentifiers;
|
|
793
|
+
// Initialize schemas that have identifiers
|
|
794
|
+
this.initializeIdentifiedSchemas(rootSchema, identifiers, context, collectedRefs);
|
|
795
|
+
return {
|
|
796
|
+
collectedRefs,
|
|
797
|
+
localIdentifiers,
|
|
798
|
+
identifiers,
|
|
799
|
+
identifierToPath,
|
|
800
|
+
pathsContainingRefs,
|
|
801
|
+
pathsOfRefs,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
// ============================================================================
|
|
805
|
+
// MACRO EXPANSION
|
|
806
|
+
// ============================================================================
|
|
807
|
+
/**
|
|
808
|
+
* Expands macro keywords in a schema.
|
|
809
|
+
* Macros are custom keywords that transform into standard JSON Schema.
|
|
810
|
+
*/
|
|
811
|
+
expandMacros(schema, macroContext) {
|
|
812
|
+
if (typeof schema !== "object" || schema === null) {
|
|
813
|
+
return schema;
|
|
814
|
+
}
|
|
815
|
+
let expandedSchema = schema;
|
|
816
|
+
const implementedKeywords = new Set();
|
|
817
|
+
for (const [keyword, value] of Object.entries(schema)) {
|
|
818
|
+
const keywordDef = this.jetValidator.getKeyword(keyword);
|
|
819
|
+
if (!keywordDef?.macro)
|
|
820
|
+
continue;
|
|
821
|
+
if (!(0, utilities_1.shouldApplyKeyword)(keywordDef, value))
|
|
822
|
+
continue;
|
|
823
|
+
// Validate macro value if meta-schema is defined
|
|
824
|
+
if (keywordDef.metaSchema) {
|
|
825
|
+
(0, utilities_1.validateKeywordValue)(keyword, value, keywordDef.metaSchema, this.jetValidator);
|
|
826
|
+
}
|
|
827
|
+
// Execute the macro transformation
|
|
828
|
+
const macroResult = keywordDef.macro(value, schema, {
|
|
829
|
+
schemaPath: `${macroContext.schemaPath}/${keyword}`,
|
|
830
|
+
rootSchema: macroContext.rootSchema,
|
|
831
|
+
opts: { ...this.options },
|
|
832
|
+
});
|
|
833
|
+
if (typeof macroResult === "object" && macroResult !== null) {
|
|
834
|
+
Object.assign(expandedSchema, macroResult);
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
expandedSchema = macroResult;
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
delete expandedSchema[keyword];
|
|
841
|
+
// Track keywords that this macro implements
|
|
842
|
+
if (keywordDef.implements) {
|
|
843
|
+
const implemented = Array.isArray(keywordDef.implements)
|
|
844
|
+
? keywordDef.implements
|
|
845
|
+
: [keywordDef.implements];
|
|
846
|
+
implemented.forEach((k) => implementedKeywords.add(k));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// Remove implemented keywords
|
|
850
|
+
for (const implKeyword of Array.from(implementedKeywords)) {
|
|
851
|
+
delete expandedSchema[implKeyword];
|
|
852
|
+
}
|
|
853
|
+
// Recursively expand nested schemas
|
|
854
|
+
expandedSchema = this.expandMacrosRecursively(expandedSchema, macroContext);
|
|
855
|
+
return expandedSchema;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Recursively expands macros in nested schema locations.
|
|
859
|
+
*/
|
|
860
|
+
expandMacrosRecursively(schema, macroContext) {
|
|
861
|
+
if (typeof schema !== "object" || schema === null) {
|
|
862
|
+
return schema;
|
|
863
|
+
}
|
|
864
|
+
// Helper to expand a single nested schema
|
|
865
|
+
const expandNestedSchema = (key, pathSegment) => {
|
|
866
|
+
if (schema[key] &&
|
|
867
|
+
typeof schema[key] === "object" &&
|
|
868
|
+
!Array.isArray(schema[key])) {
|
|
869
|
+
schema[key] = this.expandMacros(schema[key], {
|
|
870
|
+
schemaPath: `${macroContext.schemaPath}/${pathSegment}`,
|
|
871
|
+
rootSchema: macroContext.rootSchema,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
// Helper to expand a map of schemas (properties, $defs, etc.)
|
|
876
|
+
const expandSchemaMap = (key) => {
|
|
877
|
+
if (schema[key]) {
|
|
878
|
+
for (const [propKey, propSchema] of Object.entries(schema[key])) {
|
|
879
|
+
if (typeof propSchema === "object" && propSchema !== null) {
|
|
880
|
+
schema[key][propKey] = this.expandMacros(propSchema, {
|
|
881
|
+
schemaPath: `${macroContext.schemaPath}/${key}/${propKey}`,
|
|
882
|
+
rootSchema: macroContext.rootSchema,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
// Helper to expand an array of schemas
|
|
889
|
+
const expandSchemaArray = (key, pathSegment) => {
|
|
890
|
+
if (schema[key] && Array.isArray(schema[key])) {
|
|
891
|
+
schema[key] = schema[key].map((subSchema, i) => typeof subSchema === "object" && subSchema !== null
|
|
892
|
+
? this.expandMacros(subSchema, {
|
|
893
|
+
schemaPath: `${macroContext.schemaPath}/${pathSegment ?? key}/${i}`,
|
|
894
|
+
rootSchema: macroContext.rootSchema,
|
|
895
|
+
})
|
|
896
|
+
: subSchema);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
// Expand all schema map locations
|
|
900
|
+
expandSchemaMap("properties");
|
|
901
|
+
expandSchemaMap("patternProperties");
|
|
902
|
+
expandSchemaMap("dependentSchemas");
|
|
903
|
+
expandSchemaMap("$defs");
|
|
904
|
+
expandSchemaMap("definitions");
|
|
905
|
+
// Handle items (can be object or array)
|
|
906
|
+
if (schema.items) {
|
|
907
|
+
if (Array.isArray(schema.items)) {
|
|
908
|
+
expandSchemaArray("items");
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
expandNestedSchema("items", "items");
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
// Expand array schema locations
|
|
915
|
+
expandSchemaArray("prefixItems");
|
|
916
|
+
for (const combiner of ["allOf", "anyOf", "oneOf"]) {
|
|
917
|
+
expandSchemaArray(combiner);
|
|
918
|
+
}
|
|
919
|
+
// Expand single schema locations
|
|
920
|
+
expandNestedSchema("contains", "contains");
|
|
921
|
+
expandNestedSchema("not", "not");
|
|
922
|
+
expandNestedSchema("if", "if");
|
|
923
|
+
expandNestedSchema("then", "then");
|
|
924
|
+
expandNestedSchema("additionalProperties", "additionalProperties");
|
|
925
|
+
expandNestedSchema("unevaluatedProperties", "unevaluatedProperties");
|
|
926
|
+
expandNestedSchema("propertyNames", "propertyNames");
|
|
927
|
+
expandNestedSchema("additionalItems", "additionalItems");
|
|
928
|
+
expandNestedSchema("unevaluatedItems", "unevaluatedItems");
|
|
929
|
+
// Handle elseIf array (custom extension)
|
|
930
|
+
if (schema.elseIf && Array.isArray(schema.elseIf)) {
|
|
931
|
+
schema.elseIf = schema.elseIf.map((elseIfItem, i) => {
|
|
932
|
+
const expandedElseIf = {};
|
|
933
|
+
if (elseIfItem.if && typeof elseIfItem.if === "object") {
|
|
934
|
+
expandedElseIf.if = this.expandMacros(elseIfItem.if, {
|
|
935
|
+
schemaPath: `${macroContext.schemaPath}/elseIf/${i}/if`,
|
|
936
|
+
rootSchema: macroContext.rootSchema,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
if (elseIfItem.then && typeof elseIfItem.then === "object") {
|
|
940
|
+
expandedElseIf.then = this.expandMacros(elseIfItem.then, {
|
|
941
|
+
schemaPath: `${macroContext.schemaPath}/elseIf/${i}/then`,
|
|
942
|
+
rootSchema: macroContext.rootSchema,
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
return expandedElseIf;
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
expandNestedSchema("else", "else");
|
|
949
|
+
return schema;
|
|
950
|
+
}
|
|
951
|
+
logInliningSummary() {
|
|
952
|
+
const total = this.compilationContext.inliningStats.totalRefs;
|
|
953
|
+
const inlined = this.compilationContext.inliningStats.inlinedRefs;
|
|
954
|
+
const skipped = this.compilationContext.inliningStats.totalRefs -
|
|
955
|
+
this.compilationContext.inliningStats.inlinedRefs;
|
|
956
|
+
const rate = ((inlined / total) * 100).toFixed(1);
|
|
957
|
+
console.log(`\n[Resolver] Inlining Summary:`);
|
|
958
|
+
console.log(` Total references: ${total}`);
|
|
959
|
+
console.log(` Inlined: ${inlined} (${rate}%)`);
|
|
960
|
+
console.log(` Skipped: ${skipped} (contain circular)`);
|
|
961
|
+
console.log(` Function calls saved: ~${inlined}`);
|
|
962
|
+
}
|
|
963
|
+
// ============================================================================
|
|
964
|
+
// PUBLIC RESOLVER METHODS
|
|
965
|
+
// ============================================================================
|
|
966
|
+
/**
|
|
967
|
+
* Asynchronously resolves a schema, loading external schemas as needed.
|
|
968
|
+
* Use this when external schemas need to be fetched over the network.
|
|
969
|
+
*/
|
|
970
|
+
async resolveAsync(schema, loadSchema) {
|
|
971
|
+
if (typeof schema === "boolean") {
|
|
972
|
+
return {
|
|
973
|
+
schema,
|
|
974
|
+
refables: this.schemasToCompile,
|
|
975
|
+
allFormats: this.discoveredFormats,
|
|
976
|
+
keywords: this.discoveredCustomKeywords,
|
|
977
|
+
compileContext: this.compilationContext,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
let processedSchema = schema;
|
|
981
|
+
// Expand macros if any are registered
|
|
982
|
+
if (this.jetValidator.hasMacroKeywords()) {
|
|
983
|
+
processedSchema = this.expandMacros(schema, {
|
|
984
|
+
schemaPath: "#",
|
|
985
|
+
rootSchema: schema,
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
const result = await this.resolveSchemaAsync(processedSchema, {
|
|
989
|
+
isRootResolution: true,
|
|
990
|
+
currentSchemaPath: "#",
|
|
991
|
+
refToFunctionName: new Map(),
|
|
992
|
+
}, loadSchema);
|
|
993
|
+
if (this.options.debug &&
|
|
994
|
+
this.compilationContext.inliningStats.totalRefs > 0)
|
|
995
|
+
this.logInliningSummary();
|
|
996
|
+
this.clearResolutionState();
|
|
997
|
+
return {
|
|
998
|
+
schema: result.schema,
|
|
999
|
+
refables: this.schemasToCompile,
|
|
1000
|
+
allFormats: this.discoveredFormats,
|
|
1001
|
+
keywords: this.discoveredCustomKeywords,
|
|
1002
|
+
compileContext: this.compilationContext,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Synchronously resolves a schema.
|
|
1007
|
+
* External schemas must already be registered with the JetValidator instance.
|
|
1008
|
+
*/
|
|
1009
|
+
resolveSync(schema) {
|
|
1010
|
+
if (typeof schema === "boolean") {
|
|
1011
|
+
return {
|
|
1012
|
+
schema,
|
|
1013
|
+
refables: this.schemasToCompile,
|
|
1014
|
+
allFormats: this.discoveredFormats,
|
|
1015
|
+
keywords: this.discoveredCustomKeywords,
|
|
1016
|
+
compileContext: this.compilationContext,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
let processedSchema = schema;
|
|
1020
|
+
// Expand macros if any are registered
|
|
1021
|
+
if (this.jetValidator.hasMacroKeywords()) {
|
|
1022
|
+
processedSchema = this.expandMacros(schema, {
|
|
1023
|
+
schemaPath: "#",
|
|
1024
|
+
rootSchema: schema,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
const result = this.resolveSchemaSynchronously(processedSchema, {
|
|
1028
|
+
isRootResolution: true,
|
|
1029
|
+
currentSchemaPath: "#",
|
|
1030
|
+
refToFunctionName: new Map(),
|
|
1031
|
+
}).schema;
|
|
1032
|
+
if (this.options.debug &&
|
|
1033
|
+
this.compilationContext.inliningStats.totalRefs > 0)
|
|
1034
|
+
this.logInliningSummary();
|
|
1035
|
+
this.clearResolutionState();
|
|
1036
|
+
return {
|
|
1037
|
+
schema: result,
|
|
1038
|
+
refables: this.schemasToCompile,
|
|
1039
|
+
allFormats: this.discoveredFormats,
|
|
1040
|
+
keywords: this.discoveredCustomKeywords,
|
|
1041
|
+
compileContext: this.compilationContext,
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
// ============================================================================
|
|
1045
|
+
// ASYNC SCHEMA RESOLUTION
|
|
1046
|
+
// ============================================================================
|
|
1047
|
+
/**
|
|
1048
|
+
* Resolves a schema asynchronously, handling external references.
|
|
1049
|
+
*/
|
|
1050
|
+
async resolveSchemaAsync(rootSchema, context = {
|
|
1051
|
+
isRootResolution: false,
|
|
1052
|
+
refToFunctionName: new Map(),
|
|
1053
|
+
currentSchemaPath: "#",
|
|
1054
|
+
}, loadSchema) {
|
|
1055
|
+
if (rootSchema === true || rootSchema === false) {
|
|
1056
|
+
return { schema: rootSchema, idPaths: {}, refs: [] };
|
|
1057
|
+
}
|
|
1058
|
+
// Clone schema on first call to avoid mutating the original
|
|
1059
|
+
let schema = (context.isRootResolution ? structuredClone(rootSchema) : rootSchema);
|
|
1060
|
+
this.initializeResolutionContext(schema, context);
|
|
1061
|
+
let identifierToPath = {};
|
|
1062
|
+
const collectedRefs = [];
|
|
1063
|
+
let pathsContainingRefs;
|
|
1064
|
+
let pathsOfRefs = [];
|
|
1065
|
+
if (context.isRootResolution) {
|
|
1066
|
+
const preprocessResult = this.preprocessSchema(schema, context);
|
|
1067
|
+
pathsContainingRefs = preprocessResult.pathsContainingRefs;
|
|
1068
|
+
pathsOfRefs = preprocessResult.pathsOfRefs;
|
|
1069
|
+
identifierToPath = preprocessResult.identifierToPath;
|
|
1070
|
+
collectedRefs.push(...preprocessResult.collectedRefs);
|
|
1071
|
+
for (const ref of preprocessResult.collectedRefs) {
|
|
1072
|
+
if (ref === "#")
|
|
1073
|
+
continue;
|
|
1074
|
+
if (preprocessResult.localIdentifiers.includes(ref))
|
|
1075
|
+
continue;
|
|
1076
|
+
const shouldSkip = this.shouldSkipReference(ref, context, identifierToPath);
|
|
1077
|
+
if (shouldSkip)
|
|
1078
|
+
continue;
|
|
1079
|
+
const urlParts = splitUrlIntoPathAndFragment(ref);
|
|
1080
|
+
const isExternalRef = !ref.startsWith("#") &&
|
|
1081
|
+
!preprocessResult.localIdentifiers.includes(urlParts.path);
|
|
1082
|
+
if (isExternalRef) {
|
|
1083
|
+
await this.resolveExternalSchemaAsync(ref, preprocessResult.identifiers, context, loadSchema);
|
|
1084
|
+
}
|
|
1085
|
+
else if (ref.startsWith("#/") || !ref.startsWith("#")) {
|
|
1086
|
+
this.resolveLocalReference(schema, ref, identifierToPath, context);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
// Handle inlining if enabled
|
|
1091
|
+
if (this.options.inlineRefs) {
|
|
1092
|
+
this.compilationContext.inliningStats.totalRefs += pathsOfRefs.length;
|
|
1093
|
+
this.processInlining(schema, context, identifierToPath, pathsOfRefs, pathsContainingRefs);
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
for (const path of pathsOfRefs) {
|
|
1097
|
+
this.resolveReferenceAtPath((0, utilities_1.getSchemaAtPath)(schema, path), schema, context.refToFunctionName, path, pathsOfRefs, identifierToPath, context.localSchemaIds, false);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
schema,
|
|
1102
|
+
idPaths: identifierToPath,
|
|
1103
|
+
refs: collectedRefs,
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
// ============================================================================
|
|
1107
|
+
// SYNC SCHEMA RESOLUTION
|
|
1108
|
+
// ============================================================================
|
|
1109
|
+
/**
|
|
1110
|
+
* Resolves a schema synchronously.
|
|
1111
|
+
*/
|
|
1112
|
+
resolveSchemaSynchronously(rootSchema, context = {
|
|
1113
|
+
isRootResolution: false,
|
|
1114
|
+
refToFunctionName: new Map(),
|
|
1115
|
+
currentSchemaPath: "#",
|
|
1116
|
+
}) {
|
|
1117
|
+
if (rootSchema === true || rootSchema === false) {
|
|
1118
|
+
return { schema: rootSchema, idPaths: {}, refs: [] };
|
|
1119
|
+
}
|
|
1120
|
+
// Clone schema on first call to avoid mutating the original
|
|
1121
|
+
let schema = (context.isRootResolution ? structuredClone(rootSchema) : rootSchema);
|
|
1122
|
+
this.initializeResolutionContext(schema, context);
|
|
1123
|
+
let identifierToPath = {};
|
|
1124
|
+
const collectedRefs = [];
|
|
1125
|
+
let pathsContainingRefs;
|
|
1126
|
+
let pathsOfRefs = [];
|
|
1127
|
+
if (context.isRootResolution) {
|
|
1128
|
+
const preprocessResult = this.preprocessSchema(schema, context);
|
|
1129
|
+
pathsContainingRefs = preprocessResult.pathsContainingRefs;
|
|
1130
|
+
pathsOfRefs = preprocessResult.pathsOfRefs;
|
|
1131
|
+
identifierToPath = preprocessResult.identifierToPath;
|
|
1132
|
+
collectedRefs.push(...preprocessResult.collectedRefs);
|
|
1133
|
+
if (preprocessResult.collectedRefs.length > 0) {
|
|
1134
|
+
for (const ref of preprocessResult.collectedRefs) {
|
|
1135
|
+
if (ref === "#")
|
|
1136
|
+
continue;
|
|
1137
|
+
if (preprocessResult.localIdentifiers.includes(ref))
|
|
1138
|
+
continue;
|
|
1139
|
+
const shouldSkip = this.shouldSkipReference(ref, context, identifierToPath);
|
|
1140
|
+
if (shouldSkip)
|
|
1141
|
+
continue;
|
|
1142
|
+
const urlParts = splitUrlIntoPathAndFragment(ref);
|
|
1143
|
+
const isHttpRef = ref.startsWith("http") &&
|
|
1144
|
+
!preprocessResult.localIdentifiers.includes(urlParts.path);
|
|
1145
|
+
if (isHttpRef) {
|
|
1146
|
+
this.resolveExternalSchemaSync(ref, preprocessResult.identifiers, context);
|
|
1147
|
+
}
|
|
1148
|
+
else if (ref.startsWith("#/") || !ref.startsWith("#")) {
|
|
1149
|
+
this.resolveLocalReference(schema, ref, identifierToPath, context);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
// Handle inlining if enabled
|
|
1155
|
+
if (this.options.inlineRefs) {
|
|
1156
|
+
this.compilationContext.inliningStats.totalRefs += pathsOfRefs.length;
|
|
1157
|
+
this.processInlining(schema, context, identifierToPath, pathsOfRefs, pathsContainingRefs);
|
|
1158
|
+
}
|
|
1159
|
+
else {
|
|
1160
|
+
// Process all refs without inlining=
|
|
1161
|
+
for (const path of pathsOfRefs) {
|
|
1162
|
+
this.resolveReferenceAtPath((0, utilities_1.getSchemaAtPath)(schema, path), schema, context.refToFunctionName, path, pathsOfRefs, identifierToPath, context.localSchemaIds, false);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return {
|
|
1166
|
+
schema,
|
|
1167
|
+
idPaths: identifierToPath,
|
|
1168
|
+
refs: collectedRefs,
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
// ============================================================================
|
|
1172
|
+
// INLINING LOGIC
|
|
1173
|
+
// ============================================================================
|
|
1174
|
+
/**
|
|
1175
|
+
* Processes schema inlining for optimization.
|
|
1176
|
+
* Inlines references that don't form cycles to reduce function call overhead.
|
|
1177
|
+
*/
|
|
1178
|
+
processInlining(schema, context, identifierToPath, pathsOfRefs, pathsContainingRefs) {
|
|
1179
|
+
if (context.isRootResolution && context.schemaId) {
|
|
1180
|
+
if (!pathsContainingRefs)
|
|
1181
|
+
pathsContainingRefs = new Set();
|
|
1182
|
+
this.schemaIdToRefPaths.set(context.schemaId, pathsContainingRefs);
|
|
1183
|
+
}
|
|
1184
|
+
const processRefType = (refType, schemaAtPath, path) => {
|
|
1185
|
+
const refValue = schemaAtPath[refType];
|
|
1186
|
+
if (!refValue)
|
|
1187
|
+
return false;
|
|
1188
|
+
if (refValue.startsWith("#/")) {
|
|
1189
|
+
const skipInline = pathsContainingRefs?.has(refValue);
|
|
1190
|
+
if (skipInline) {
|
|
1191
|
+
if (this.options.debug) {
|
|
1192
|
+
console.log(`[Resolver - ${context.schemaId}] Skipping Inlining ${refType} at ${path} (${refValue} contains refs)`);
|
|
1193
|
+
}
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
delete schemaAtPath[refType];
|
|
1197
|
+
const objectKeys = Object.keys(schemaAtPath).length;
|
|
1198
|
+
const targetSchema = (0, utilities_1.getSchemaAtPath)(schema, refValue);
|
|
1199
|
+
if (objectKeys === 0) {
|
|
1200
|
+
if (typeof targetSchema === "object") {
|
|
1201
|
+
Object.assign(schemaAtPath, targetSchema);
|
|
1202
|
+
}
|
|
1203
|
+
else {
|
|
1204
|
+
schemaAtPath.__inlinedRef = targetSchema;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
else if (objectKeys === 1 && "$id" in schemaAtPath) {
|
|
1208
|
+
if (typeof targetSchema === "object") {
|
|
1209
|
+
const previousId = schemaAtPath.$id;
|
|
1210
|
+
Object.assign(schemaAtPath, targetSchema);
|
|
1211
|
+
schemaAtPath.$id = previousId;
|
|
1212
|
+
}
|
|
1213
|
+
else {
|
|
1214
|
+
schemaAtPath.__inlinedRef = targetSchema;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
else {
|
|
1218
|
+
schemaAtPath.__inlinedRef = targetSchema;
|
|
1219
|
+
}
|
|
1220
|
+
pathsContainingRefs?.delete(path);
|
|
1221
|
+
const pathParts = path.split("/");
|
|
1222
|
+
for (let j = pathParts.length - 1; j > 0; j--) {
|
|
1223
|
+
const currentPath = pathParts.slice(0, j).join("/");
|
|
1224
|
+
const childRefsCount = Array.from(pathsContainingRefs || []).filter((p) => p.startsWith(currentPath)).length;
|
|
1225
|
+
if (childRefsCount === 1) {
|
|
1226
|
+
pathsContainingRefs?.delete(currentPath);
|
|
1227
|
+
}
|
|
1228
|
+
else {
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (this.options.debug) {
|
|
1233
|
+
console.log(`[Resolver - ${context.schemaId}] Inlining ${refType} at ${path} -> ${refValue}`);
|
|
1234
|
+
}
|
|
1235
|
+
this.compilationContext.inliningStats.inlinedRefs++;
|
|
1236
|
+
return true;
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
let urlParts;
|
|
1240
|
+
if (refValue.startsWith("#")) {
|
|
1241
|
+
urlParts = { path: context.schemaId || "", hash: refValue };
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
urlParts = splitUrlIntoPathAndFragment(refValue);
|
|
1245
|
+
}
|
|
1246
|
+
let lookupKey = this.computeLookupKey(refValue, urlParts, refType, context);
|
|
1247
|
+
if (lookupKey && lookupKey !== "#") {
|
|
1248
|
+
if (lookupKey.startsWith("#") && !lookupKey.startsWith("#/")) {
|
|
1249
|
+
lookupKey = lookupKey.slice(1);
|
|
1250
|
+
}
|
|
1251
|
+
if (lookupKey.endsWith("#")) {
|
|
1252
|
+
lookupKey = lookupKey.slice(0, -1);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
// Try to find referenced path in current schema
|
|
1256
|
+
let referencedPath;
|
|
1257
|
+
if (lookupKey.startsWith("#/")) {
|
|
1258
|
+
if (identifierToPath[urlParts.path]) {
|
|
1259
|
+
referencedPath =
|
|
1260
|
+
identifierToPath[urlParts.path] + lookupKey.slice(1);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
referencedPath = identifierToPath[lookupKey];
|
|
1265
|
+
}
|
|
1266
|
+
// Inline if the referenced path doesn't contain refs
|
|
1267
|
+
if (referencedPath && !pathsContainingRefs?.has(referencedPath)) {
|
|
1268
|
+
if (referencedPath !== "#") {
|
|
1269
|
+
const targetSchema = (0, utilities_1.getSchemaAtPath)(schema, referencedPath);
|
|
1270
|
+
delete schemaAtPath[refType];
|
|
1271
|
+
const objectKeys = Object.keys(schemaAtPath).length;
|
|
1272
|
+
if (objectKeys === 0) {
|
|
1273
|
+
if (typeof targetSchema === "object") {
|
|
1274
|
+
Object.assign(schemaAtPath, targetSchema);
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
schemaAtPath.__inlinedRef = targetSchema;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
else if (objectKeys === 1 && "$id" in schemaAtPath) {
|
|
1281
|
+
if (typeof targetSchema === "object") {
|
|
1282
|
+
const previousId = schemaAtPath.$id;
|
|
1283
|
+
Object.assign(schemaAtPath, targetSchema);
|
|
1284
|
+
schemaAtPath.$id = previousId;
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
schemaAtPath.__inlinedRef = targetSchema;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
else {
|
|
1291
|
+
schemaAtPath.__inlinedRef = targetSchema;
|
|
1292
|
+
}
|
|
1293
|
+
pathsContainingRefs?.delete(path);
|
|
1294
|
+
const pathParts = path.split("/");
|
|
1295
|
+
for (let j = pathParts.length - 1; j > 0; j--) {
|
|
1296
|
+
const currentPath = pathParts.slice(0, j).join("/");
|
|
1297
|
+
const childRefsCount = Array.from(pathsContainingRefs || []).filter((p) => p.startsWith(currentPath)).length;
|
|
1298
|
+
if (childRefsCount === 1) {
|
|
1299
|
+
pathsContainingRefs?.delete(currentPath);
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (this.options.debug) {
|
|
1306
|
+
console.log(`[Resolver - ${context.schemaId}] Inlining ${refType} at ${path} -> ${referencedPath}`);
|
|
1307
|
+
}
|
|
1308
|
+
this.compilationContext.inliningStats.inlinedRefs++;
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
else if (referencedPath && this.options.debug) {
|
|
1313
|
+
console.log(`[Resolver - ${context.schemaId}] Skipping Inlining ${refType} at ${path} (${referencedPath} contains refs)`);
|
|
1314
|
+
}
|
|
1315
|
+
// Try external schema
|
|
1316
|
+
if (!referencedPath) {
|
|
1317
|
+
const externalSchema = this.processedExternalSchemas.get(urlParts.path);
|
|
1318
|
+
if (externalSchema) {
|
|
1319
|
+
if (lookupKey.startsWith("#/")) {
|
|
1320
|
+
if (externalSchema.idPaths[urlParts.path]) {
|
|
1321
|
+
referencedPath =
|
|
1322
|
+
externalSchema.idPaths[urlParts.path] + lookupKey.slice(1);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
else {
|
|
1326
|
+
referencedPath = externalSchema.idPaths[lookupKey];
|
|
1327
|
+
}
|
|
1328
|
+
if (referencedPath &&
|
|
1329
|
+
!this.schemaIdToRefPaths.get(urlParts.path)?.has(referencedPath)) {
|
|
1330
|
+
if (referencedPath !== "#") {
|
|
1331
|
+
const targetSchema = (0, utilities_1.getSchemaAtPath)(externalSchema, referencedPath);
|
|
1332
|
+
delete schemaAtPath[refType];
|
|
1333
|
+
const objectKeys = Object.keys(schemaAtPath).length;
|
|
1334
|
+
if (objectKeys === 0) {
|
|
1335
|
+
if (typeof targetSchema === "object") {
|
|
1336
|
+
Object.assign(schemaAtPath, targetSchema);
|
|
1337
|
+
}
|
|
1338
|
+
else {
|
|
1339
|
+
schemaAtPath.__inlinedRef = targetSchema;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
else if (objectKeys === 1 && "$id" in schemaAtPath) {
|
|
1343
|
+
if (typeof targetSchema === "object") {
|
|
1344
|
+
const previousId = schemaAtPath.$id;
|
|
1345
|
+
Object.assign(schemaAtPath, targetSchema);
|
|
1346
|
+
schemaAtPath.$id = previousId;
|
|
1347
|
+
}
|
|
1348
|
+
else {
|
|
1349
|
+
schemaAtPath.__inlinedRef = targetSchema;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
schemaAtPath.__inlinedRef = targetSchema;
|
|
1354
|
+
}
|
|
1355
|
+
pathsContainingRefs?.delete(path);
|
|
1356
|
+
const pathParts = path.split("/");
|
|
1357
|
+
for (let j = pathParts.length - 1; j > 0; j--) {
|
|
1358
|
+
const currentPath = pathParts.slice(0, j).join("/");
|
|
1359
|
+
const childRefsCount = Array.from(pathsContainingRefs || []).filter((p) => p.startsWith(currentPath)).length;
|
|
1360
|
+
if (childRefsCount === 1) {
|
|
1361
|
+
pathsContainingRefs?.delete(currentPath);
|
|
1362
|
+
}
|
|
1363
|
+
else {
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (this.options.debug) {
|
|
1368
|
+
console.log(`[Resolver] Inlining ${refType} at ${path} -> ${urlParts.path + referencedPath} - (external schema)`);
|
|
1369
|
+
}
|
|
1370
|
+
this.compilationContext.inliningStats.inlinedRefs++;
|
|
1371
|
+
return true;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
else if (referencedPath && this.options.debug) {
|
|
1375
|
+
console.log(`[Resolver - ${context.schemaId}] Skipping Inlining ${refType} at ${path} (${urlParts.path + referencedPath} contains refs) - (external schema)`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
// Can't inline - resolve normally
|
|
1380
|
+
this.resolveReferenceAtPath(schemaAtPath, schema, context.refToFunctionName, path, pathsOfRefs, identifierToPath, context.localSchemaIds, false);
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
if (pathsOfRefs.length > 0) {
|
|
1385
|
+
let changed = true;
|
|
1386
|
+
while (changed) {
|
|
1387
|
+
changed = false;
|
|
1388
|
+
for (let i = pathsOfRefs.length - 1; i >= 0; i--) {
|
|
1389
|
+
const path = pathsOfRefs[i];
|
|
1390
|
+
const schemaAtPath = (0, utilities_1.getSchemaAtPath)(schema, path);
|
|
1391
|
+
if (typeof schemaAtPath !== "object")
|
|
1392
|
+
continue;
|
|
1393
|
+
const hasRef = schemaAtPath.$ref !== undefined;
|
|
1394
|
+
const hasDynamicRef = schemaAtPath.$dynamicRef !== undefined;
|
|
1395
|
+
let refProcessed = false;
|
|
1396
|
+
if (hasRef && processRefType("$ref", schemaAtPath, path)) {
|
|
1397
|
+
refProcessed = true;
|
|
1398
|
+
}
|
|
1399
|
+
if (hasDynamicRef &&
|
|
1400
|
+
processRefType("$dynamicRef", schemaAtPath, path)) {
|
|
1401
|
+
refProcessed = true;
|
|
1402
|
+
}
|
|
1403
|
+
if (refProcessed) {
|
|
1404
|
+
pathsOfRefs.splice(i, 1);
|
|
1405
|
+
changed = true;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
for (const path of pathsOfRefs) {
|
|
1411
|
+
this.resolveReferenceAtPath((0, utilities_1.getSchemaAtPath)(schema, path), schema, context.refToFunctionName, path, pathsOfRefs, identifierToPath, context.localSchemaIds, false);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Computes the lookup key for a reference during inlining.
|
|
1416
|
+
*/
|
|
1417
|
+
computeLookupKey(refValue, urlParts, refType, context) {
|
|
1418
|
+
if (urlParts.hash?.startsWith("#/")) {
|
|
1419
|
+
return urlParts.hash;
|
|
1420
|
+
}
|
|
1421
|
+
if (refType === "$dynamicRef" && refValue.endsWith("DYNAMIC")) {
|
|
1422
|
+
if (!refValue.startsWith("#") && refValue.includes("#")) {
|
|
1423
|
+
const hasFunction = context.refToFunctionName.get(refValue);
|
|
1424
|
+
if (hasFunction) {
|
|
1425
|
+
let lookupKey = urlParts.hash?.slice(1);
|
|
1426
|
+
let functionName = context.refToFunctionName.get(lookupKey);
|
|
1427
|
+
if (functionName)
|
|
1428
|
+
return lookupKey;
|
|
1429
|
+
lookupKey = refValue;
|
|
1430
|
+
functionName = context.refToFunctionName.get(refValue);
|
|
1431
|
+
if (functionName)
|
|
1432
|
+
return lookupKey;
|
|
1433
|
+
lookupKey = refValue.slice(0, -7) + "ANCHOR";
|
|
1434
|
+
functionName = context.refToFunctionName.get(lookupKey);
|
|
1435
|
+
if (functionName)
|
|
1436
|
+
return lookupKey;
|
|
1437
|
+
}
|
|
1438
|
+
let lookupKey = urlParts.hash?.slice(1).slice(0, -7) + "ANCHOR";
|
|
1439
|
+
if (context.refToFunctionName.get(lookupKey))
|
|
1440
|
+
return lookupKey;
|
|
1441
|
+
const hashRef = urlParts.hash || "";
|
|
1442
|
+
if (context.refToFunctionName.get(hashRef))
|
|
1443
|
+
return hashRef;
|
|
1444
|
+
lookupKey = hashRef.slice(0, -7) + "ANCHOR";
|
|
1445
|
+
if (context.refToFunctionName.get(lookupKey))
|
|
1446
|
+
return lookupKey;
|
|
1447
|
+
return hashRef;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return refValue;
|
|
1451
|
+
}
|
|
1452
|
+
// ============================================================================
|
|
1453
|
+
// CONTEXT INITIALIZATION
|
|
1454
|
+
// ============================================================================
|
|
1455
|
+
/**
|
|
1456
|
+
* Initializes the resolution context with schema identity information.
|
|
1457
|
+
*/
|
|
1458
|
+
initializeResolutionContext(schema, context) {
|
|
1459
|
+
if (schema.$id) {
|
|
1460
|
+
context.schemaId = schema.$id;
|
|
1461
|
+
}
|
|
1462
|
+
else if (context.schemaId) {
|
|
1463
|
+
schema.$id = context.schemaId;
|
|
1464
|
+
}
|
|
1465
|
+
// Generate a random ID if none exists
|
|
1466
|
+
if (!context.schemaId) {
|
|
1467
|
+
const generatedId = Math.random().toString(36).substring(2, 8);
|
|
1468
|
+
context.schemaId = generatedId;
|
|
1469
|
+
schema.$id = generatedId;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Initializes schemas that have identifiers ($id, $anchor, etc.) by adding them to schemasToCompile.
|
|
1474
|
+
*/
|
|
1475
|
+
initializeIdentifiedSchemas(schema, identifiers, context, allRefs) {
|
|
1476
|
+
for (const entry of identifiers) {
|
|
1477
|
+
// Skip if this is the context's own ID
|
|
1478
|
+
if (context.schemaId === entry.identifier ||
|
|
1479
|
+
context.schemaId === entry.parentSchemaId) {
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
// Check if this identifier is actually referenced
|
|
1483
|
+
let isReferenced;
|
|
1484
|
+
if (entry.identifier.endsWith("ANCHOR")) {
|
|
1485
|
+
isReferenced =
|
|
1486
|
+
allRefs.includes(entry.identifier) ||
|
|
1487
|
+
allRefs.includes("#" + entry.identifier) ||
|
|
1488
|
+
allRefs.includes("#" + entry.identifier.slice(0, -6) + "DYNAMIC") ||
|
|
1489
|
+
allRefs.includes(entry.identifier.slice(0, -6) + "DYNAMIC") ||
|
|
1490
|
+
allRefs.includes(entry.schemaPath);
|
|
1491
|
+
}
|
|
1492
|
+
else if (entry.identifier.endsWith("DYNAMIC")) {
|
|
1493
|
+
isReferenced =
|
|
1494
|
+
allRefs.includes(entry.identifier) ||
|
|
1495
|
+
allRefs.includes("#" + entry.identifier) ||
|
|
1496
|
+
allRefs.includes("#" + entry.identifier.slice(0, -7) + "ANCHOR") ||
|
|
1497
|
+
allRefs.includes(entry.identifier.slice(0, -7) + "ANCHOR") ||
|
|
1498
|
+
allRefs.includes(entry.schemaPath);
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
isReferenced =
|
|
1502
|
+
allRefs.includes(entry.identifier) ||
|
|
1503
|
+
allRefs.includes(entry.schemaPath);
|
|
1504
|
+
}
|
|
1505
|
+
// Skip unreferenced external identifiers
|
|
1506
|
+
if (!isReferenced && entry.identifier.startsWith("http")) {
|
|
1507
|
+
if (entry.parentSchemaId) {
|
|
1508
|
+
if (!allRefs.includes(entry.parentSchemaId)) {
|
|
1509
|
+
context.refToFunctionName.delete(entry.schemaPath);
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
else {
|
|
1514
|
+
context.refToFunctionName.delete(entry.schemaPath);
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
const path = entry.schemaPath;
|
|
1519
|
+
let schemaUrl;
|
|
1520
|
+
if (entry.identifier.startsWith("http") || entry.parentSchemaId) {
|
|
1521
|
+
schemaUrl = entry.identifier.startsWith("http")
|
|
1522
|
+
? splitUrlIntoPathAndFragment(entry.identifier).path
|
|
1523
|
+
: entry.parentSchemaId;
|
|
1524
|
+
}
|
|
1525
|
+
else {
|
|
1526
|
+
schemaUrl = context.schemaId;
|
|
1527
|
+
}
|
|
1528
|
+
// Check if already processed
|
|
1529
|
+
const existingUrlPaths = this.compiledSchemaPaths.get(schemaUrl);
|
|
1530
|
+
const existingContextPaths = this.compiledSchemaPaths.get(context.schemaId);
|
|
1531
|
+
if (existingUrlPaths?.has(path) ||
|
|
1532
|
+
existingUrlPaths?.has(entry.identifier) ||
|
|
1533
|
+
existingContextPaths?.has(path) ||
|
|
1534
|
+
existingContextPaths?.has(entry.identifier)) {
|
|
1535
|
+
// Already processed - just update tracking
|
|
1536
|
+
const additionalPaths = [entry.identifier];
|
|
1537
|
+
if (entry.parentSchemaId)
|
|
1538
|
+
additionalPaths.push(entry.parentSchemaId);
|
|
1539
|
+
if (existingUrlPaths?.has(path)) {
|
|
1540
|
+
additionalPaths.forEach((p) => existingUrlPaths?.add(p));
|
|
1541
|
+
}
|
|
1542
|
+
if (existingContextPaths?.has(path)) {
|
|
1543
|
+
additionalPaths.forEach((p) => existingContextPaths?.add(p));
|
|
1544
|
+
}
|
|
1545
|
+
const functionName = context.refToFunctionName.get(path) ||
|
|
1546
|
+
context.refToFunctionName.get(entry.identifier);
|
|
1547
|
+
if (functionName) {
|
|
1548
|
+
context.refToFunctionName.set(path, functionName);
|
|
1549
|
+
if (!context.refToFunctionName.has(entry.identifier)) {
|
|
1550
|
+
context.refToFunctionName.set(entry.identifier, functionName);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
// Get the schema at this path
|
|
1556
|
+
let schemaAtPath;
|
|
1557
|
+
if (path.startsWith("#")) {
|
|
1558
|
+
schemaAtPath = (0, utilities_1.getSchemaAtPath)(schema, path);
|
|
1559
|
+
}
|
|
1560
|
+
if (schemaAtPath === undefined) {
|
|
1561
|
+
context.refToFunctionName.delete(entry.identifier);
|
|
1562
|
+
}
|
|
1563
|
+
else {
|
|
1564
|
+
const functionName = context.refToFunctionName.get(entry.schemaPath) ||
|
|
1565
|
+
context.refToFunctionName.get(entry.identifier);
|
|
1566
|
+
const pathsToTrack = [path, entry.identifier];
|
|
1567
|
+
if (entry.parentSchemaId)
|
|
1568
|
+
pathsToTrack.push(entry.parentSchemaId);
|
|
1569
|
+
// Update tracking sets
|
|
1570
|
+
if (existingUrlPaths) {
|
|
1571
|
+
if (existingUrlPaths.has(path))
|
|
1572
|
+
continue;
|
|
1573
|
+
pathsToTrack.forEach((p) => existingUrlPaths.add(p));
|
|
1574
|
+
}
|
|
1575
|
+
else {
|
|
1576
|
+
this.compiledSchemaPaths.set(schemaUrl, new Set(pathsToTrack));
|
|
1577
|
+
}
|
|
1578
|
+
if (entry.identifier.startsWith("http")) {
|
|
1579
|
+
if (existingContextPaths) {
|
|
1580
|
+
if (existingContextPaths.has(path))
|
|
1581
|
+
continue;
|
|
1582
|
+
pathsToTrack.forEach((p) => existingContextPaths.add(p));
|
|
1583
|
+
}
|
|
1584
|
+
else {
|
|
1585
|
+
this.compiledSchemaPaths.set(context.schemaId, new Set(pathsToTrack));
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
// Add to schemas to compile
|
|
1589
|
+
this.schemasToCompile.push({
|
|
1590
|
+
path: entry.schemaPath,
|
|
1591
|
+
schema: schemaAtPath,
|
|
1592
|
+
functionName,
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
// ============================================================================
|
|
1598
|
+
// REFERENCE SKIPPING LOGIC
|
|
1599
|
+
// ============================================================================
|
|
1600
|
+
/**
|
|
1601
|
+
* Determines if a reference should be skipped (already processed).
|
|
1602
|
+
*/
|
|
1603
|
+
shouldSkipReference(ref, context, identifierToPath) {
|
|
1604
|
+
let urlParts;
|
|
1605
|
+
let baseUrl;
|
|
1606
|
+
if (ref.startsWith("http")) {
|
|
1607
|
+
urlParts = splitUrlIntoPathAndFragment(ref);
|
|
1608
|
+
baseUrl = urlParts.path;
|
|
1609
|
+
}
|
|
1610
|
+
else {
|
|
1611
|
+
urlParts = splitUrlIntoPathAndFragment(context.schemaId);
|
|
1612
|
+
baseUrl = urlParts.path;
|
|
1613
|
+
const refHash = splitUrlIntoPathAndFragment(ref).hash;
|
|
1614
|
+
if (refHash) {
|
|
1615
|
+
urlParts.hash = refHash;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
const existingUrlPaths = this.compiledSchemaPaths.get(baseUrl);
|
|
1619
|
+
const existingContextPaths = this.compiledSchemaPaths.get(context.schemaId);
|
|
1620
|
+
if (!existingUrlPaths && !existingContextPaths)
|
|
1621
|
+
return false;
|
|
1622
|
+
if (identifierToPath[baseUrl]) {
|
|
1623
|
+
if (urlParts.hash) {
|
|
1624
|
+
if (urlParts.hash.startsWith("#/")) {
|
|
1625
|
+
const targetPath = identifierToPath[baseUrl] + urlParts.hash.slice(1);
|
|
1626
|
+
return (existingContextPaths?.has(targetPath) ||
|
|
1627
|
+
existingUrlPaths?.has(ref) ||
|
|
1628
|
+
existingUrlPaths?.has(targetPath) ||
|
|
1629
|
+
false);
|
|
1630
|
+
}
|
|
1631
|
+
else {
|
|
1632
|
+
return (existingUrlPaths?.has(ref) ||
|
|
1633
|
+
existingContextPaths?.has(ref) ||
|
|
1634
|
+
false);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
else {
|
|
1638
|
+
return (existingUrlPaths?.has(ref) || existingContextPaths?.has(ref) || false);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
else {
|
|
1642
|
+
if (existingUrlPaths) {
|
|
1643
|
+
if (urlParts.hash) {
|
|
1644
|
+
return (existingUrlPaths.has(urlParts.hash) || existingUrlPaths.has(ref));
|
|
1645
|
+
}
|
|
1646
|
+
else {
|
|
1647
|
+
return existingUrlPaths.has(baseUrl);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return false;
|
|
1652
|
+
}
|
|
1653
|
+
// ============================================================================
|
|
1654
|
+
// LOCAL REFERENCE RESOLUTION
|
|
1655
|
+
// ============================================================================
|
|
1656
|
+
/**
|
|
1657
|
+
* Resolves a local reference (within the same schema).
|
|
1658
|
+
*/
|
|
1659
|
+
resolveLocalReference(schema, ref, identifierToPath, context) {
|
|
1660
|
+
let schemaAtPath;
|
|
1661
|
+
if (ref.startsWith("#/")) {
|
|
1662
|
+
schemaAtPath = (0, utilities_1.getSchemaAtPath)(schema, ref);
|
|
1663
|
+
}
|
|
1664
|
+
// Handle external refs that map to local paths via $id
|
|
1665
|
+
if (!ref.startsWith("#") && schemaAtPath === undefined) {
|
|
1666
|
+
const urlParts = splitUrlIntoPathAndFragment(ref);
|
|
1667
|
+
const baseUrl = urlParts.path;
|
|
1668
|
+
const fragment = urlParts.hash?.startsWith("#/")
|
|
1669
|
+
? urlParts.hash
|
|
1670
|
+
: undefined;
|
|
1671
|
+
if (identifierToPath[baseUrl] && fragment) {
|
|
1672
|
+
schemaAtPath = (0, utilities_1.getSchemaAtPath)(schema, identifierToPath[baseUrl] + fragment.slice(1));
|
|
1673
|
+
}
|
|
1674
|
+
else {
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (schemaAtPath !== undefined) {
|
|
1679
|
+
this.addLocalRefToCompile(ref, schemaAtPath, context, identifierToPath);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Adds a locally resolved reference to the compilation queue.
|
|
1684
|
+
*/
|
|
1685
|
+
addLocalRefToCompile(ref, schemaAtPath, context, identifierToPath) {
|
|
1686
|
+
let urlParts;
|
|
1687
|
+
let baseUrl;
|
|
1688
|
+
if (ref.startsWith("http")) {
|
|
1689
|
+
urlParts = splitUrlIntoPathAndFragment(ref);
|
|
1690
|
+
baseUrl = urlParts.path;
|
|
1691
|
+
}
|
|
1692
|
+
else {
|
|
1693
|
+
urlParts = splitUrlIntoPathAndFragment(context.schemaId);
|
|
1694
|
+
baseUrl = urlParts.path;
|
|
1695
|
+
urlParts.hash = splitUrlIntoPathAndFragment(ref).hash;
|
|
1696
|
+
}
|
|
1697
|
+
let resolvedPath;
|
|
1698
|
+
const additionalPaths = [];
|
|
1699
|
+
if (urlParts.hash?.startsWith("#/")) {
|
|
1700
|
+
resolvedPath = identifierToPath[baseUrl] + urlParts.hash.slice(1);
|
|
1701
|
+
additionalPaths.push(resolvedPath);
|
|
1702
|
+
}
|
|
1703
|
+
const trackingResult = this.trackSchemaPath(ref, baseUrl, context.schemaId, additionalPaths);
|
|
1704
|
+
if (!trackingResult.isNewPath)
|
|
1705
|
+
return;
|
|
1706
|
+
this.schemasToCompile.push({
|
|
1707
|
+
path: resolvedPath ?? identifierToPath[ref],
|
|
1708
|
+
schema: schemaAtPath,
|
|
1709
|
+
functionName: context.refToFunctionName.get(ref),
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
// ============================================================================
|
|
1713
|
+
// EXTERNAL REFERENCE RESOLUTION
|
|
1714
|
+
// ============================================================================
|
|
1715
|
+
/**
|
|
1716
|
+
* Resolves an external reference asynchronously.
|
|
1717
|
+
*/
|
|
1718
|
+
async resolveExternalSchemaAsync(ref, identifiers, context, loadSchema) {
|
|
1719
|
+
const urlParts = splitUrlIntoPathAndFragment(ref);
|
|
1720
|
+
const baseUrl = urlParts.path;
|
|
1721
|
+
// Prevent circular resolution
|
|
1722
|
+
if (this.currentlyResolvingSchemas.has(baseUrl)) {
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
this.currentlyResolvingSchemas.add(baseUrl);
|
|
1726
|
+
let externalSchema;
|
|
1727
|
+
let wasAlreadyProcessed = false;
|
|
1728
|
+
if (baseUrl) {
|
|
1729
|
+
// Check cache first
|
|
1730
|
+
const cachedSchema = this.processedExternalSchemas.get(baseUrl);
|
|
1731
|
+
if (cachedSchema) {
|
|
1732
|
+
externalSchema = cachedSchema;
|
|
1733
|
+
wasAlreadyProcessed = true;
|
|
1734
|
+
}
|
|
1735
|
+
// Try to load from registered schemas
|
|
1736
|
+
if (!cachedSchema) {
|
|
1737
|
+
let storedSchema = this.jetValidator.getSchema(baseUrl);
|
|
1738
|
+
if (!storedSchema) {
|
|
1739
|
+
storedSchema = this.jetValidator.getMetaSchema(baseUrl).metaSchema;
|
|
1740
|
+
}
|
|
1741
|
+
if (storedSchema) {
|
|
1742
|
+
externalSchema = storedSchema;
|
|
1743
|
+
}
|
|
1744
|
+
else if (loadSchema) {
|
|
1745
|
+
try {
|
|
1746
|
+
externalSchema = await loadSchema(baseUrl);
|
|
1747
|
+
if (this.options.addUsedSchema) {
|
|
1748
|
+
this.jetValidator.addSchema(externalSchema, baseUrl);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
catch (e) {
|
|
1752
|
+
throw e;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (externalSchema !== undefined) {
|
|
1758
|
+
// Build the initial ref map from parent identifiers
|
|
1759
|
+
const newRefMap = new Map();
|
|
1760
|
+
for (const entry of identifiers) {
|
|
1761
|
+
let refMap = this.externalSchemaRefMaps.get(baseUrl) || new Map();
|
|
1762
|
+
if (!this.externalSchemaRefMaps.has(baseUrl)) {
|
|
1763
|
+
this.externalSchemaRefMaps.set(baseUrl, refMap);
|
|
1764
|
+
}
|
|
1765
|
+
if (!entry.identifier.startsWith("http")) {
|
|
1766
|
+
const functionName = context.refToFunctionName.get(entry.identifier ?? entry.schemaPath ?? entry.parentSchemaId);
|
|
1767
|
+
refMap.set(entry.identifier, functionName);
|
|
1768
|
+
newRefMap.set(entry.identifier, functionName);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
let resolvedExternalSchema;
|
|
1772
|
+
if (wasAlreadyProcessed) {
|
|
1773
|
+
resolvedExternalSchema = {
|
|
1774
|
+
schema: externalSchema,
|
|
1775
|
+
refs: [],
|
|
1776
|
+
idPaths: externalSchema.idPaths,
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
else {
|
|
1780
|
+
resolvedExternalSchema = await this.resolveSchemaAsync(externalSchema, {
|
|
1781
|
+
isRootResolution: true,
|
|
1782
|
+
refToFunctionName: newRefMap,
|
|
1783
|
+
currentSchemaPath: baseUrl,
|
|
1784
|
+
schemaId: baseUrl,
|
|
1785
|
+
rootHash: baseUrl,
|
|
1786
|
+
}, loadSchema);
|
|
1787
|
+
}
|
|
1788
|
+
this.addExternalSchemaToCompile(ref, resolvedExternalSchema, context);
|
|
1789
|
+
}
|
|
1790
|
+
this.currentlyResolvingSchemas.delete(baseUrl);
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Resolves an external reference synchronously.
|
|
1794
|
+
*/
|
|
1795
|
+
resolveExternalSchemaSync(ref, identifiers, context) {
|
|
1796
|
+
const urlParts = splitUrlIntoPathAndFragment(ref);
|
|
1797
|
+
const baseUrl = urlParts.path;
|
|
1798
|
+
// Prevent circular resolution
|
|
1799
|
+
if (this.currentlyResolvingSchemas.has(baseUrl)) {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
this.currentlyResolvingSchemas.add(baseUrl);
|
|
1803
|
+
let externalSchema;
|
|
1804
|
+
let wasAlreadyProcessed = false;
|
|
1805
|
+
if (baseUrl) {
|
|
1806
|
+
// Check cache first
|
|
1807
|
+
const cachedSchema = this.processedExternalSchemas.get(baseUrl);
|
|
1808
|
+
if (cachedSchema) {
|
|
1809
|
+
externalSchema = cachedSchema;
|
|
1810
|
+
wasAlreadyProcessed = true;
|
|
1811
|
+
}
|
|
1812
|
+
// Try to load from registered schemas
|
|
1813
|
+
if (!cachedSchema) {
|
|
1814
|
+
let storedSchema = this.jetValidator.getSchema(baseUrl);
|
|
1815
|
+
if (!storedSchema) {
|
|
1816
|
+
storedSchema = this.jetValidator.getMetaSchema(baseUrl).metaSchema;
|
|
1817
|
+
}
|
|
1818
|
+
if (storedSchema) {
|
|
1819
|
+
externalSchema = storedSchema;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
if (externalSchema !== undefined) {
|
|
1824
|
+
// Build the initial ref map from parent identifiers
|
|
1825
|
+
const newRefMap = new Map();
|
|
1826
|
+
for (const entry of identifiers) {
|
|
1827
|
+
let refMap = this.externalSchemaRefMaps.get(baseUrl) || new Map();
|
|
1828
|
+
if (!this.externalSchemaRefMaps.has(baseUrl)) {
|
|
1829
|
+
this.externalSchemaRefMaps.set(baseUrl, refMap);
|
|
1830
|
+
}
|
|
1831
|
+
if (!entry.identifier.startsWith("http")) {
|
|
1832
|
+
const functionName = context.refToFunctionName.get(entry.identifier ?? entry.schemaPath ?? entry.parentSchemaId);
|
|
1833
|
+
refMap.set(entry.identifier, functionName);
|
|
1834
|
+
newRefMap.set(entry.identifier, functionName);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
let resolvedExternalSchema;
|
|
1838
|
+
if (wasAlreadyProcessed) {
|
|
1839
|
+
resolvedExternalSchema = {
|
|
1840
|
+
schema: externalSchema,
|
|
1841
|
+
refs: [],
|
|
1842
|
+
idPaths: externalSchema.idPaths,
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
else {
|
|
1846
|
+
resolvedExternalSchema = this.resolveSchemaSynchronously(externalSchema, {
|
|
1847
|
+
isRootResolution: true,
|
|
1848
|
+
refToFunctionName: newRefMap,
|
|
1849
|
+
currentSchemaPath: baseUrl,
|
|
1850
|
+
schemaId: baseUrl,
|
|
1851
|
+
rootHash: baseUrl,
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
this.addExternalSchemaToCompile(ref, resolvedExternalSchema, context);
|
|
1855
|
+
}
|
|
1856
|
+
this.currentlyResolvingSchemas.delete(baseUrl);
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Adds an external schema to the compilation queue.
|
|
1860
|
+
*/
|
|
1861
|
+
addExternalSchemaToCompile(ref, resolvedSchema, context) {
|
|
1862
|
+
const urlParts = splitUrlIntoPathAndFragment(ref);
|
|
1863
|
+
const baseUrl = urlParts.path;
|
|
1864
|
+
const fragment = urlParts.hash;
|
|
1865
|
+
// Ensure ref map exists
|
|
1866
|
+
let refMap = this.externalSchemaRefMaps.get(baseUrl) || new Map();
|
|
1867
|
+
if (!this.externalSchemaRefMaps.has(baseUrl)) {
|
|
1868
|
+
this.externalSchemaRefMaps.set(baseUrl, refMap);
|
|
1869
|
+
}
|
|
1870
|
+
const existingPaths = this.compiledSchemaPaths.get(baseUrl);
|
|
1871
|
+
// Handle JSON pointer fragments
|
|
1872
|
+
if (fragment &&
|
|
1873
|
+
fragment !== "" &&
|
|
1874
|
+
fragment.startsWith("#/") &&
|
|
1875
|
+
typeof resolvedSchema.schema === "object") {
|
|
1876
|
+
if (existingPaths?.has(fragment) || existingPaths?.has(ref)) {
|
|
1877
|
+
existingPaths.add(fragment);
|
|
1878
|
+
existingPaths.add(ref);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
// Check if we need the root schema too
|
|
1882
|
+
if (resolvedSchema.refs.includes(baseUrl) ||
|
|
1883
|
+
resolvedSchema.refs.includes("#")) {
|
|
1884
|
+
if (!existingPaths?.has(baseUrl)) {
|
|
1885
|
+
const functionName = context.refToFunctionName.get(baseUrl);
|
|
1886
|
+
this.schemasToCompile.push({
|
|
1887
|
+
path: "#",
|
|
1888
|
+
schema: resolvedSchema.schema,
|
|
1889
|
+
functionName: functionName,
|
|
1890
|
+
});
|
|
1891
|
+
if (existingPaths) {
|
|
1892
|
+
existingPaths.add(baseUrl);
|
|
1893
|
+
}
|
|
1894
|
+
else {
|
|
1895
|
+
this.compiledSchemaPaths.set(baseUrl, new Set([baseUrl]));
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
// Add the fragment schema
|
|
1900
|
+
const fragmentSchema = (0, utilities_1.getSchemaAtPath)(resolvedSchema.schema, fragment);
|
|
1901
|
+
if (!existingPaths?.has(fragment) || !existingPaths?.has(ref)) {
|
|
1902
|
+
if (typeof fragmentSchema === "object") {
|
|
1903
|
+
const functionName = context.refToFunctionName.get(ref);
|
|
1904
|
+
this.schemasToCompile.push({
|
|
1905
|
+
path: fragment,
|
|
1906
|
+
schema: fragmentSchema,
|
|
1907
|
+
functionName: functionName,
|
|
1908
|
+
});
|
|
1909
|
+
const currentSet = this.compiledSchemaPaths.get(baseUrl) || new Set();
|
|
1910
|
+
currentSet.add(fragment);
|
|
1911
|
+
currentSet.add(ref);
|
|
1912
|
+
this.compiledSchemaPaths.set(baseUrl, currentSet);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
else if (baseUrl) {
|
|
1917
|
+
// Handle non-pointer fragments (anchors) or no fragment
|
|
1918
|
+
if (existingPaths?.has(baseUrl)) {
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const functionName = context.refToFunctionName.get(baseUrl);
|
|
1922
|
+
let finalPath;
|
|
1923
|
+
// Handle anchor fragments
|
|
1924
|
+
if (fragment && fragment !== "#") {
|
|
1925
|
+
const anchorName = fragment.slice(1);
|
|
1926
|
+
finalPath = resolvedSchema.idPaths[anchorName];
|
|
1927
|
+
if (!finalPath) {
|
|
1928
|
+
// Try alternate anchor forms
|
|
1929
|
+
finalPath = anchorName.endsWith("DYNAMIC")
|
|
1930
|
+
? resolvedSchema.idPaths[anchorName.slice(0, -7) + "ANCHOR"]
|
|
1931
|
+
: resolvedSchema.idPaths[anchorName.slice(0, -6) + "DYNAMIC"];
|
|
1932
|
+
}
|
|
1933
|
+
if (finalPath &&
|
|
1934
|
+
finalPath !== "#" &&
|
|
1935
|
+
typeof resolvedSchema.schema === "object") {
|
|
1936
|
+
const anchorSchema = (0, utilities_1.getSchemaAtPath)(resolvedSchema.schema, finalPath);
|
|
1937
|
+
if (existingPaths) {
|
|
1938
|
+
if (!existingPaths.has(finalPath) && !existingPaths.has(ref)) {
|
|
1939
|
+
if (typeof anchorSchema === "object") {
|
|
1940
|
+
const anchorFunctionName = context.refToFunctionName.get(ref);
|
|
1941
|
+
this.schemasToCompile.push({
|
|
1942
|
+
path: finalPath,
|
|
1943
|
+
schema: anchorSchema,
|
|
1944
|
+
functionName: anchorFunctionName,
|
|
1945
|
+
});
|
|
1946
|
+
const currentSet = this.compiledSchemaPaths.get(baseUrl) || new Set();
|
|
1947
|
+
currentSet.add(finalPath);
|
|
1948
|
+
currentSet.add(ref);
|
|
1949
|
+
this.compiledSchemaPaths.set(baseUrl, currentSet);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
else {
|
|
1953
|
+
if (existingPaths.has(finalPath))
|
|
1954
|
+
existingPaths.add(ref);
|
|
1955
|
+
if (existingPaths.has(ref))
|
|
1956
|
+
existingPaths.add(finalPath);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
const currentSet = existingPaths || new Set();
|
|
1962
|
+
currentSet.add(finalPath);
|
|
1963
|
+
currentSet.add(ref);
|
|
1964
|
+
// Add root schema if no fragment or fragment points to root
|
|
1965
|
+
if (!fragment || fragment === "#" || finalPath === "#") {
|
|
1966
|
+
this.schemasToCompile.push({
|
|
1967
|
+
path: "#",
|
|
1968
|
+
schema: resolvedSchema.schema,
|
|
1969
|
+
functionName: functionName,
|
|
1970
|
+
});
|
|
1971
|
+
currentSet.add(baseUrl);
|
|
1972
|
+
}
|
|
1973
|
+
this.compiledSchemaPaths.set(baseUrl, currentSet);
|
|
1974
|
+
}
|
|
1975
|
+
// Cache the processed external schema
|
|
1976
|
+
if (!this.processedExternalSchemas.has(baseUrl) &&
|
|
1977
|
+
typeof resolvedSchema.schema === "object") {
|
|
1978
|
+
resolvedSchema.schema["idPaths"] = resolvedSchema.idPaths;
|
|
1979
|
+
this.processedExternalSchemas.set(baseUrl, resolvedSchema.schema);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
// ============================================================================
|
|
1983
|
+
// REFERENCE RESOLVER (FINAL PASS)
|
|
1984
|
+
// ============================================================================
|
|
1985
|
+
/**
|
|
1986
|
+
* Resolves a reference at a specific path, updating the schema with function names.
|
|
1987
|
+
* This is called after all schemas have been collected to finalize references.
|
|
1988
|
+
*/
|
|
1989
|
+
resolveReferenceAtPath(targetSchema, rootSchema, refToFunctionName, currentPath, externalRefPaths, identifierToPath, localIdentifiers, isInlined = true) {
|
|
1990
|
+
if (targetSchema === true || targetSchema === false) {
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
const schema = targetSchema;
|
|
1994
|
+
if (!refToFunctionName) {
|
|
1995
|
+
throw new Error("refToFunctionName is required");
|
|
1996
|
+
}
|
|
1997
|
+
if (!schema || typeof schema !== "object") {
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
// Skip if already has a function name assigned
|
|
2001
|
+
if (schema.__functionName) {
|
|
2002
|
+
this.compilationContext.referencedFunctions.push(schema.__functionName);
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
// Assign function name if this path has one
|
|
2006
|
+
if (refToFunctionName.has(currentPath) && currentPath !== "#") {
|
|
2007
|
+
schema.__functionName = refToFunctionName.get(currentPath);
|
|
2008
|
+
}
|
|
2009
|
+
// Process $ref
|
|
2010
|
+
if (schema.$ref && !schema.$ref.startsWith("*")) {
|
|
2011
|
+
this.finalizeRef(schema, rootSchema, refToFunctionName, externalRefPaths, identifierToPath, localIdentifiers, isInlined);
|
|
2012
|
+
}
|
|
2013
|
+
// Process $dynamicRef
|
|
2014
|
+
if (schema.$dynamicRef && !schema.$dynamicRef.startsWith("*")) {
|
|
2015
|
+
this.finalizeDynamicRef(schema, rootSchema, refToFunctionName, externalRefPaths, identifierToPath, localIdentifiers, isInlined);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Finalizes a $ref by resolving it to a function name.
|
|
2020
|
+
*/
|
|
2021
|
+
finalizeRef(schema, rootSchema, refToFunctionName, externalRefPaths, identifierToPath, localIdentifiers, isInlined = true) {
|
|
2022
|
+
const rawRef = schema.$ref;
|
|
2023
|
+
let lookupKey;
|
|
2024
|
+
if (rawRef === "#") {
|
|
2025
|
+
lookupKey = rawRef;
|
|
2026
|
+
}
|
|
2027
|
+
else if (rawRef.startsWith("http") || rawRef.startsWith("#/")) {
|
|
2028
|
+
lookupKey = rawRef;
|
|
2029
|
+
}
|
|
2030
|
+
else if (rawRef.startsWith("#")) {
|
|
2031
|
+
lookupKey = rawRef.slice(1);
|
|
2032
|
+
}
|
|
2033
|
+
else {
|
|
2034
|
+
lookupKey = rawRef;
|
|
2035
|
+
}
|
|
2036
|
+
// Remove trailing hash
|
|
2037
|
+
if (lookupKey !== "#" && lookupKey.endsWith("#")) {
|
|
2038
|
+
lookupKey = lookupKey.slice(0, -1);
|
|
2039
|
+
}
|
|
2040
|
+
let functionName = refToFunctionName.get(lookupKey);
|
|
2041
|
+
// Try alternate anchor form
|
|
2042
|
+
if (!functionName && lookupKey.endsWith(":ANCHOR")) {
|
|
2043
|
+
functionName = refToFunctionName.get(lookupKey.slice(0, -6) + "DYNAMIC");
|
|
2044
|
+
}
|
|
2045
|
+
// Recursively resolve referenced schema if not inlined
|
|
2046
|
+
if (!isInlined && lookupKey && !lookupKey.startsWith("#/")) {
|
|
2047
|
+
const normalizedKey = lookupKey.startsWith("#")
|
|
2048
|
+
? lookupKey.slice(1)
|
|
2049
|
+
: lookupKey;
|
|
2050
|
+
const urlParts = splitUrlIntoPathAndFragment(normalizedKey);
|
|
2051
|
+
const identifier = urlParts.path +
|
|
2052
|
+
(urlParts.hash &&
|
|
2053
|
+
!urlParts.hash.startsWith("#/") &&
|
|
2054
|
+
urlParts.hash !== "#"
|
|
2055
|
+
? urlParts.hash
|
|
2056
|
+
: "");
|
|
2057
|
+
const targetPath = identifierToPath[identifier];
|
|
2058
|
+
if (targetPath !== undefined) {
|
|
2059
|
+
let schemaAtPath;
|
|
2060
|
+
let finalPath;
|
|
2061
|
+
if (urlParts.hash && urlParts.hash.startsWith("#/")) {
|
|
2062
|
+
finalPath = targetPath + urlParts.hash.slice(1);
|
|
2063
|
+
schemaAtPath = (0, utilities_1.getSchemaAtPath)(rootSchema, finalPath);
|
|
2064
|
+
}
|
|
2065
|
+
else {
|
|
2066
|
+
finalPath = targetPath;
|
|
2067
|
+
schemaAtPath = (0, utilities_1.getSchemaAtPath)(rootSchema, targetPath);
|
|
2068
|
+
}
|
|
2069
|
+
if (typeof schemaAtPath === "object") {
|
|
2070
|
+
this.resolveReferenceAtPath(schemaAtPath, rootSchema, refToFunctionName, finalPath, externalRefPaths, identifierToPath, localIdentifiers);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
// Update schema with resolved function name
|
|
2075
|
+
if (functionName) {
|
|
2076
|
+
schema.$ref = "*" + functionName;
|
|
2077
|
+
this.compilationContext.referencedFunctions.push(functionName);
|
|
2078
|
+
}
|
|
2079
|
+
// Add external reference marker
|
|
2080
|
+
if (lookupKey && !lookupKey.startsWith("#/")) {
|
|
2081
|
+
if (lookupKey.startsWith("http")) {
|
|
2082
|
+
schema.$ref = schema.$ref + "**" + lookupKey;
|
|
2083
|
+
}
|
|
2084
|
+
else {
|
|
2085
|
+
schema.$ref = schema.$ref + "**#" + lookupKey.split("#")[1];
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
if (functionName === this.rootFunctionName) {
|
|
2089
|
+
this.compilationContext.hasRootReference = true;
|
|
2090
|
+
}
|
|
2091
|
+
if (!functionName) {
|
|
2092
|
+
schema.$ref = "*unavailable";
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
|
|
2097
|
+
Finalizes a $dynamicRef by resolving it to a function name.
|
|
2098
|
+
*/
|
|
2099
|
+
finalizeDynamicRef(schema, rootSchema, refToFunctionName, externalRefPaths, identifierToPath, localIdentifiers, isInlined = true) {
|
|
2100
|
+
const rawDynamicRef = schema.$dynamicRef;
|
|
2101
|
+
let lookupKey;
|
|
2102
|
+
let functionName;
|
|
2103
|
+
if (rawDynamicRef === "#") {
|
|
2104
|
+
lookupKey = rawDynamicRef;
|
|
2105
|
+
}
|
|
2106
|
+
else if (rawDynamicRef.endsWith("DYNAMIC")) {
|
|
2107
|
+
if (!rawDynamicRef.startsWith("#") && rawDynamicRef.includes("#")) {
|
|
2108
|
+
lookupKey = rawDynamicRef;
|
|
2109
|
+
const hasDirectFunction = refToFunctionName.get(lookupKey);
|
|
2110
|
+
if (hasDirectFunction) {
|
|
2111
|
+
lookupKey = splitUrlIntoPathAndFragment(rawDynamicRef).hash.slice(1);
|
|
2112
|
+
functionName = refToFunctionName.get(lookupKey);
|
|
2113
|
+
if (!functionName) {
|
|
2114
|
+
functionName = refToFunctionName.get(rawDynamicRef);
|
|
2115
|
+
if (!functionName) {
|
|
2116
|
+
functionName = refToFunctionName.get(rawDynamicRef.slice(0, -7) + "ANCHOR");
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
else {
|
|
2120
|
+
lookupKey = "#" + lookupKey;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
if (!functionName) {
|
|
2124
|
+
functionName = refToFunctionName.get(lookupKey.slice(0, -7) + "ANCHOR");
|
|
2125
|
+
}
|
|
2126
|
+
if (!functionName) {
|
|
2127
|
+
lookupKey = splitUrlIntoPathAndFragment(rawDynamicRef).hash.slice(1);
|
|
2128
|
+
functionName = refToFunctionName.get(lookupKey);
|
|
2129
|
+
if (!functionName) {
|
|
2130
|
+
functionName = refToFunctionName.get(lookupKey.slice(0, -7) + "ANCHOR");
|
|
2131
|
+
}
|
|
2132
|
+
lookupKey = "#" + lookupKey;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
else {
|
|
2137
|
+
lookupKey = rawDynamicRef;
|
|
2138
|
+
functionName = refToFunctionName.get(lookupKey);
|
|
2139
|
+
if (!functionName) {
|
|
2140
|
+
functionName = refToFunctionName.get(lookupKey.slice(0, -7) + "ANCHOR");
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
// Recursively resolve referenced schema if not inlined
|
|
2144
|
+
if (!isInlined && lookupKey && !lookupKey.startsWith("#/")) {
|
|
2145
|
+
const normalizedKey = lookupKey.startsWith("#")
|
|
2146
|
+
? lookupKey.slice(1)
|
|
2147
|
+
: lookupKey;
|
|
2148
|
+
const urlParts = splitUrlIntoPathAndFragment(normalizedKey);
|
|
2149
|
+
const identifier = urlParts.path +
|
|
2150
|
+
(urlParts.hash &&
|
|
2151
|
+
!urlParts.hash.startsWith("#/") &&
|
|
2152
|
+
urlParts.hash !== "#"
|
|
2153
|
+
? urlParts.hash
|
|
2154
|
+
: "");
|
|
2155
|
+
const targetPath = identifierToPath[identifier];
|
|
2156
|
+
if (targetPath !== undefined) {
|
|
2157
|
+
let schemaAtPath;
|
|
2158
|
+
let finalPath;
|
|
2159
|
+
if (urlParts.hash && urlParts.hash.startsWith("#/")) {
|
|
2160
|
+
finalPath = targetPath + urlParts.hash.slice(1);
|
|
2161
|
+
schemaAtPath = (0, utilities_1.getSchemaAtPath)(rootSchema, finalPath);
|
|
2162
|
+
}
|
|
2163
|
+
else {
|
|
2164
|
+
finalPath = targetPath;
|
|
2165
|
+
schemaAtPath = (0, utilities_1.getSchemaAtPath)(rootSchema, targetPath);
|
|
2166
|
+
}
|
|
2167
|
+
if (typeof schemaAtPath === "object") {
|
|
2168
|
+
this.resolveReferenceAtPath(schemaAtPath, rootSchema, refToFunctionName, finalPath, externalRefPaths, identifierToPath, localIdentifiers);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
// Update schema with resolved function name
|
|
2173
|
+
if (functionName) {
|
|
2174
|
+
this.compilationContext.referencedFunctions.push(functionName);
|
|
2175
|
+
schema.$dynamicRef = "*" + functionName;
|
|
2176
|
+
}
|
|
2177
|
+
if (functionName === this.rootFunctionName) {
|
|
2178
|
+
this.compilationContext.hasRootReference = true;
|
|
2179
|
+
}
|
|
2180
|
+
// Add dynamic anchor reference marker
|
|
2181
|
+
if (lookupKey && !lookupKey.startsWith("#/")) {
|
|
2182
|
+
if (localIdentifiers?.includes(lookupKey) ||
|
|
2183
|
+
localIdentifiers?.includes(splitUrlIntoPathAndFragment(lookupKey).path)) {
|
|
2184
|
+
let finalLookupKey;
|
|
2185
|
+
if (lookupKey.startsWith("#")) {
|
|
2186
|
+
finalLookupKey = lookupKey;
|
|
2187
|
+
}
|
|
2188
|
+
else {
|
|
2189
|
+
finalLookupKey = lookupKey.split("#")[1];
|
|
2190
|
+
}
|
|
2191
|
+
schema.$dynamicRef =
|
|
2192
|
+
schema.$dynamicRef +
|
|
2193
|
+
"**" +
|
|
2194
|
+
(finalLookupKey.endsWith("ANCHOR")
|
|
2195
|
+
? finalLookupKey.slice(0, -7)
|
|
2196
|
+
: finalLookupKey.slice(0, -8));
|
|
2197
|
+
}
|
|
2198
|
+
else {
|
|
2199
|
+
schema.$dynamicRef =
|
|
2200
|
+
schema.$dynamicRef +
|
|
2201
|
+
"**" +
|
|
2202
|
+
(lookupKey.endsWith("ANCHOR")
|
|
2203
|
+
? lookupKey.slice(0, -7)
|
|
2204
|
+
: lookupKey.slice(0, -8));
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
if (!functionName) {
|
|
2208
|
+
schema.$dynamicRef = "*unavailable";
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
// ============================================================================
|
|
2212
|
+
// SCHEMA METADATA COLLECTION
|
|
2213
|
+
// ============================================================================
|
|
2214
|
+
/**
|
|
2215
|
+
Recursively collects all metadata from a schema:
|
|
2216
|
+
Identifiers ($id, $anchor, $dynamicAnchor)
|
|
2217
|
+
References ($ref, $dynamicRef)
|
|
2218
|
+
Paths containing references
|
|
2219
|
+
Formats and custom keywords
|
|
2220
|
+
*/
|
|
2221
|
+
collectSchemaMetadata(schema, existingAnchors, currentPath = "#", basePath = "#", anchorToPathMap = {}, dynamicAnchorToPathMap = {}, collectedRefs = [], identifiers = [], pathsContainingRefs = new Set(), refPaths = [], currentContextId) {
|
|
2222
|
+
// Handle boolean schemas and null/undefined
|
|
2223
|
+
if (typeof schema === "boolean" ||
|
|
2224
|
+
schema === null ||
|
|
2225
|
+
schema === undefined) {
|
|
2226
|
+
return {
|
|
2227
|
+
refs: collectedRefs,
|
|
2228
|
+
ids: identifiers,
|
|
2229
|
+
pathsWithRefs: pathsContainingRefs,
|
|
2230
|
+
refPaths,
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
// Validate strict mode requirements
|
|
2234
|
+
this.validateStrictModeRequirements(schema, currentPath);
|
|
2235
|
+
// Collect custom keywords
|
|
2236
|
+
this.collectCustomKeywords(schema);
|
|
2237
|
+
// Check for $data usage
|
|
2238
|
+
if (schema.format &&
|
|
2239
|
+
typeof schema.format === "object" &&
|
|
2240
|
+
"$data" in schema.format) {
|
|
2241
|
+
this.compilationContext.uses$Data = true;
|
|
2242
|
+
}
|
|
2243
|
+
// Handle draft 6/7 behavior: $ref removes all sibling keywords
|
|
2244
|
+
if (schema.$ref !== undefined &&
|
|
2245
|
+
(this.options.draft === "draft6" || this.options.draft === "draft7")) {
|
|
2246
|
+
Object.keys(schema).forEach((key) => {
|
|
2247
|
+
if (key !== "$ref") {
|
|
2248
|
+
delete schema[key];
|
|
2249
|
+
}
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
const result = {
|
|
2253
|
+
refs: collectedRefs,
|
|
2254
|
+
ids: identifiers,
|
|
2255
|
+
pathsWithRefs: pathsContainingRefs,
|
|
2256
|
+
refPaths,
|
|
2257
|
+
};
|
|
2258
|
+
// Track current context
|
|
2259
|
+
let contextId = currentContextId;
|
|
2260
|
+
let contextBasePath = basePath;
|
|
2261
|
+
let contextAnchorMap = anchorToPathMap;
|
|
2262
|
+
let contextDynamicAnchorMap = dynamicAnchorToPathMap;
|
|
2263
|
+
// Process $id
|
|
2264
|
+
if (schema.$id) {
|
|
2265
|
+
if (schema.$id.startsWith("#")) {
|
|
2266
|
+
// Convert hash-only $id to $anchor
|
|
2267
|
+
schema.$anchor = schema.$id.slice(1);
|
|
2268
|
+
schema.$id = undefined;
|
|
2269
|
+
}
|
|
2270
|
+
else {
|
|
2271
|
+
contextId = resolveAndRegisterSchemaId(schema, contextId, currentPath, identifiers);
|
|
2272
|
+
}
|
|
2273
|
+
contextBasePath = currentPath;
|
|
2274
|
+
contextAnchorMap = {};
|
|
2275
|
+
}
|
|
2276
|
+
// Process $anchor
|
|
2277
|
+
if (schema.$anchor) {
|
|
2278
|
+
registerAnchor(schema, currentPath, contextId, contextAnchorMap, identifiers);
|
|
2279
|
+
}
|
|
2280
|
+
// Process $dynamicAnchor
|
|
2281
|
+
if (schema.$dynamicAnchor) {
|
|
2282
|
+
registerDynamicAnchor(schema, currentPath, contextBasePath, contextId, contextDynamicAnchorMap, identifiers, existingAnchors);
|
|
2283
|
+
}
|
|
2284
|
+
// Process $ref
|
|
2285
|
+
if (schema.$ref) {
|
|
2286
|
+
if (this.options.inlineRefs) {
|
|
2287
|
+
markPathsContainingRefs(currentPath, pathsContainingRefs);
|
|
2288
|
+
refPaths.push(currentPath);
|
|
2289
|
+
}
|
|
2290
|
+
processReference(schema, contextBasePath, contextAnchorMap, contextId, collectedRefs, currentPath, refPaths, this.options.inlineRefs);
|
|
2291
|
+
}
|
|
2292
|
+
// Process $dynamicRef
|
|
2293
|
+
if (schema.$dynamicRef) {
|
|
2294
|
+
if (this.options.inlineRefs) {
|
|
2295
|
+
markPathsContainingRefs(currentPath, pathsContainingRefs);
|
|
2296
|
+
refPaths.push(currentPath);
|
|
2297
|
+
}
|
|
2298
|
+
processDynamicReference(schema, contextBasePath, currentPath, contextId, collectedRefs, refPaths, this.options.inlineRefs);
|
|
2299
|
+
}
|
|
2300
|
+
// Collect format strings
|
|
2301
|
+
if (schema.format && typeof schema.format === "string") {
|
|
2302
|
+
this.discoveredFormats.add(schema.format);
|
|
2303
|
+
}
|
|
2304
|
+
// Recursively process nested schemas
|
|
2305
|
+
this.collectNestedSchemaMetadata(schema, existingAnchors, currentPath, contextBasePath, contextAnchorMap, contextDynamicAnchorMap, collectedRefs, identifiers, pathsContainingRefs, refPaths, contextId);
|
|
2306
|
+
return result;
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
|
|
2310
|
+
Validates schema against strict mode requirements.
|
|
2311
|
+
*/
|
|
2312
|
+
validateStrictModeRequirements(schema, currentPath) {
|
|
2313
|
+
// Strict type checking
|
|
2314
|
+
const strictTypes = this.options.strictTypes;
|
|
2315
|
+
if ((strictTypes || this.options.strict) && !schema.type) {
|
|
2316
|
+
const mode = strictTypes ? "strictTypes" : "strict";
|
|
2317
|
+
if (this.options.strict === true || strictTypes) {
|
|
2318
|
+
throw new Error(`[${mode}] Schema path ${currentPath} is missing the type keyword`);
|
|
2319
|
+
}
|
|
2320
|
+
else {
|
|
2321
|
+
console.log(`[${mode}] Schema path ${currentPath} is missing the type keyword`);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
// Strict required checking
|
|
2325
|
+
if ((this.options.strictRequired || this.options.strict) &&
|
|
2326
|
+
Array.isArray(schema.required)) {
|
|
2327
|
+
const mode = this.options.strictRequired ? "strictRequired" : "strict";
|
|
2328
|
+
if (!schema.properties) {
|
|
2329
|
+
throw Error(`[${mode}] Missing properties for required fields`);
|
|
2330
|
+
}
|
|
2331
|
+
for (const requiredField of schema.required) {
|
|
2332
|
+
if (!(requiredField in schema.properties)) {
|
|
2333
|
+
throw Error(`[${mode}] Required field "${String(requiredField)}" is not defined in properties`);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
// Strict schema/type checking
|
|
2338
|
+
if (schema.type && (this.options.strictSchema || this.options.strict)) {
|
|
2339
|
+
const mode = this.options.strictSchema ? "strictSchema" : "strict";
|
|
2340
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
2341
|
+
const allPossibleIncompatible = new Set();
|
|
2342
|
+
for (const type of types) {
|
|
2343
|
+
const incompatible = schema_1.incompatibleKeywords[type];
|
|
2344
|
+
if (incompatible) {
|
|
2345
|
+
incompatible.forEach((kw) => allPossibleIncompatible.add(kw));
|
|
2346
|
+
}
|
|
2347
|
+
else {
|
|
2348
|
+
throw Error(`[${mode}] Unknown type ${type}`);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
for (const keyword of allPossibleIncompatible) {
|
|
2352
|
+
const incompatibleWithAll = types.every((type) => schema_1.incompatibleKeywords[type]?.includes(keyword));
|
|
2353
|
+
if (incompatibleWithAll && schema[keyword] !== undefined) {
|
|
2354
|
+
throw Error(`[${mode}] Keyword "${keyword}" is incompatible with ${types.length > 1 ? "all types" : "type"} "${types.join(", ")}"`);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
|
|
2361
|
+
Collects custom keywords from a schema.
|
|
2362
|
+
*/
|
|
2363
|
+
collectCustomKeywords(schema) {
|
|
2364
|
+
Object.keys(schema).forEach((keyword) => {
|
|
2365
|
+
if (!schema_1.baseSchemaKeys.has(keyword)) {
|
|
2366
|
+
if (this.jetValidator.getAllKeywords().has(keyword)) {
|
|
2367
|
+
this.discoveredCustomKeywords.add(keyword);
|
|
2368
|
+
}
|
|
2369
|
+
else if (this.options.strictSchema || this.options.strict) {
|
|
2370
|
+
const mode = this.options.strictSchema ? "strictSchema" : "strict";
|
|
2371
|
+
throw new Error(`[${mode}] Unknown keyword: ${keyword}`);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
/**
|
|
2377
|
+
|
|
2378
|
+
Recursively collects metadata from nested schema locations.
|
|
2379
|
+
*/
|
|
2380
|
+
collectNestedSchemaMetadata(schema, existingAnchors, currentPath, basePath, anchorToPathMap, dynamicAnchorToPathMap, collectedRefs, identifiers, pathsContainingRefs, refPaths, contextId) {
|
|
2381
|
+
const schemaMapLocations = [
|
|
2382
|
+
{ key: "$defs", pathSegment: "$defs" },
|
|
2383
|
+
{ key: "definitions", pathSegment: "definitions" },
|
|
2384
|
+
{ key: "properties", pathSegment: "properties" },
|
|
2385
|
+
{ key: "patternProperties", pathSegment: "patternProperties" },
|
|
2386
|
+
{ key: "dependentSchemas", pathSegment: "dependentSchemas" },
|
|
2387
|
+
];
|
|
2388
|
+
for (const location of schemaMapLocations) {
|
|
2389
|
+
if (schema[location.key]) {
|
|
2390
|
+
Object.entries(schema[location.key]).forEach(([key, subSchema]) => {
|
|
2391
|
+
const subPath = `${currentPath}/${location.pathSegment}/${key}`;
|
|
2392
|
+
this.collectSchemaMetadata(subSchema, existingAnchors, subPath, basePath, anchorToPathMap, dynamicAnchorToPathMap, collectedRefs, identifiers, pathsContainingRefs, refPaths, contextId);
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
// Track unevaluated keywords
|
|
2397
|
+
if (schema.unevaluatedProperties !== undefined &&
|
|
2398
|
+
schema.unevaluatedProperties !== true) {
|
|
2399
|
+
this.compilationContext.hasUnevaluatedProperties = true;
|
|
2400
|
+
}
|
|
2401
|
+
if (schema.unevaluatedItems !== undefined &&
|
|
2402
|
+
schema.unevaluatedItems !== true) {
|
|
2403
|
+
this.compilationContext.hasUnevaluatedItems = true;
|
|
2404
|
+
}
|
|
2405
|
+
// Single schema locations
|
|
2406
|
+
const singleSchemaLocations = [
|
|
2407
|
+
"additionalProperties",
|
|
2408
|
+
"unevaluatedProperties",
|
|
2409
|
+
"propertyNames",
|
|
2410
|
+
"items",
|
|
2411
|
+
"additionalItems",
|
|
2412
|
+
"unevaluatedItems",
|
|
2413
|
+
"contains",
|
|
2414
|
+
"not",
|
|
2415
|
+
"if",
|
|
2416
|
+
"then",
|
|
2417
|
+
"else",
|
|
2418
|
+
];
|
|
2419
|
+
for (const key of singleSchemaLocations) {
|
|
2420
|
+
if (schema[key] &&
|
|
2421
|
+
typeof schema[key] === "object" &&
|
|
2422
|
+
!Array.isArray(schema[key]) &&
|
|
2423
|
+
schema[key] !== null) {
|
|
2424
|
+
const subPath = `${currentPath}/${key}`;
|
|
2425
|
+
this.collectSchemaMetadata(schema[key], existingAnchors, subPath, basePath, anchorToPathMap, dynamicAnchorToPathMap, collectedRefs, identifiers, pathsContainingRefs, refPaths, contextId);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
// Array schema locations
|
|
2429
|
+
const arraySchemaLocations = ["allOf", "anyOf", "oneOf", "prefixItems"];
|
|
2430
|
+
for (const key of arraySchemaLocations) {
|
|
2431
|
+
if (Array.isArray(schema[key])) {
|
|
2432
|
+
schema[key].forEach((subSchema, index) => {
|
|
2433
|
+
const subPath = `${currentPath}/${key}/${index}`;
|
|
2434
|
+
this.collectSchemaMetadata(subSchema, existingAnchors, subPath, basePath, anchorToPathMap, dynamicAnchorToPathMap, collectedRefs, identifiers, pathsContainingRefs, refPaths, contextId);
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
// Handle items as array (legacy tuple validation)
|
|
2439
|
+
if (schema.items && Array.isArray(schema.items)) {
|
|
2440
|
+
schema.items.forEach((item, index) => {
|
|
2441
|
+
const subPath = `${currentPath}/items/${index}`;
|
|
2442
|
+
this.collectSchemaMetadata(item, existingAnchors, subPath, basePath, anchorToPathMap, dynamicAnchorToPathMap, collectedRefs, identifiers, pathsContainingRefs, refPaths, contextId);
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
// Handle elseIf extension
|
|
2446
|
+
if (schema.elseIf) {
|
|
2447
|
+
schema.elseIf.forEach((elseIfSchema, index) => {
|
|
2448
|
+
["if", "then"].forEach((condKey) => {
|
|
2449
|
+
if (elseIfSchema[condKey]) {
|
|
2450
|
+
const subPath = `${currentPath}/elseIf/${index}/${condKey}`;
|
|
2451
|
+
this.collectSchemaMetadata(elseIfSchema[condKey], existingAnchors, subPath, basePath, anchorToPathMap, dynamicAnchorToPathMap, collectedRefs, identifiers, pathsContainingRefs, refPaths, contextId);
|
|
2452
|
+
}
|
|
2453
|
+
});
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
exports.SchemaResolver = SchemaResolver;
|
|
2459
|
+
//# sourceMappingURL=resolver.js.map
|