@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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1362 -0
  3. package/dist/cli.js +219 -0
  4. package/dist/compileSchema.d.ts +148 -0
  5. package/dist/compileSchema.js +2199 -0
  6. package/dist/compileSchema.js.map +1 -0
  7. package/dist/formats.d.ts +41 -0
  8. package/dist/formats.js +166 -0
  9. package/dist/formats.js.map +1 -0
  10. package/dist/index.cjs.js +6167 -0
  11. package/dist/index.d.ts +9 -0
  12. package/dist/index.esm.js +6148 -0
  13. package/dist/index.js +28 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/jet-validator.d.ts +88 -0
  16. package/dist/jet-validator.js +983 -0
  17. package/dist/jet-validator.js.map +1 -0
  18. package/dist/resolver.d.ts +348 -0
  19. package/dist/resolver.js +2459 -0
  20. package/dist/resolver.js.map +1 -0
  21. package/dist/scripts/load-metaschemas.d.ts +1 -0
  22. package/dist/scripts/metaschema-loader.d.ts +2 -0
  23. package/dist/src/compileSchema.d.ts +148 -0
  24. package/dist/src/formats.d.ts +41 -0
  25. package/dist/src/index.d.ts +9 -0
  26. package/dist/src/jet-validator.d.ts +88 -0
  27. package/dist/src/resolver.d.ts +348 -0
  28. package/dist/src/types/format.d.ts +7 -0
  29. package/dist/src/types/keywords.d.ts +78 -0
  30. package/dist/src/types/schema.d.ts +123 -0
  31. package/dist/src/types/standalone.d.ts +4 -0
  32. package/dist/src/types/validation.d.ts +49 -0
  33. package/dist/src/utilities/index.d.ts +11 -0
  34. package/dist/src/utilities/schema.d.ts +10 -0
  35. package/dist/types/format.d.ts +7 -0
  36. package/dist/types/format.js +3 -0
  37. package/dist/types/format.js.map +1 -0
  38. package/dist/types/keywords.d.ts +78 -0
  39. package/dist/types/keywords.js +4 -0
  40. package/dist/types/keywords.js.map +1 -0
  41. package/dist/types/schema.d.ts +123 -0
  42. package/dist/types/schema.js +3 -0
  43. package/dist/types/schema.js.map +1 -0
  44. package/dist/types/standalone.d.ts +4 -0
  45. package/dist/types/standalone.js +3 -0
  46. package/dist/types/standalone.js.map +1 -0
  47. package/dist/types/validation.d.ts +49 -0
  48. package/dist/types/validation.js +3 -0
  49. package/dist/types/validation.js.map +1 -0
  50. package/dist/utilities/index.d.ts +11 -0
  51. package/dist/utilities/index.js +146 -0
  52. package/dist/utilities/index.js.map +1 -0
  53. package/dist/utilities/schema.d.ts +10 -0
  54. package/dist/utilities/schema.js +232 -0
  55. package/dist/utilities/schema.js.map +1 -0
  56. package/dist/validator.umd.js +6196 -0
  57. package/package.json +79 -0
@@ -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