ata-validator 0.7.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CMakeLists.txt +41 -23
- package/README.md +22 -9
- package/binding/ata_napi.cpp +206 -5
- package/include/ata.h +11 -3
- package/index.js +121 -27
- package/lib/js-compiler.js +692 -83
- package/package.json +3 -2
- package/prebuilds/ata-darwin-arm64/node-napi-v10.node +0 -0
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/src/ata.cpp +607 -154
- package/prebuilds/ata-linux-arm64/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-x64/node-napi-v10.node +0 -0
package/lib/js-compiler.js
CHANGED
|
@@ -39,7 +39,11 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
39
39
|
if (typeof schema !== 'object' || schema === null) return null
|
|
40
40
|
|
|
41
41
|
// Bail if schema has edge cases that JS fast path gets wrong
|
|
42
|
-
|
|
42
|
+
// Exception: $dynamicRef/$anchor are handled by the interpretive path even though codegen can't
|
|
43
|
+
if (!defs && !codegenSafe(schema, schemaMap)) {
|
|
44
|
+
const str = JSON.stringify(schema)
|
|
45
|
+
if (!str.includes('"$dynamicRef"') && !str.includes('"$dynamicAnchor"') && !str.includes('"$anchor"')) return null
|
|
46
|
+
}
|
|
43
47
|
|
|
44
48
|
// Collect $defs early so sub-schemas can resolve $ref
|
|
45
49
|
const rootDefs = defs || collectDefs(schema)
|
|
@@ -60,6 +64,23 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
60
64
|
checks.push(refFn)
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
// $dynamicRef — resolve via anchor defs or JSON pointer
|
|
68
|
+
if (schema.$dynamicRef) {
|
|
69
|
+
const ref = schema.$dynamicRef
|
|
70
|
+
const anchorName = ref.startsWith('#') ? ref : '#' + ref
|
|
71
|
+
if (rootDefs && rootDefs[anchorName]) {
|
|
72
|
+
const entry = rootDefs[anchorName]
|
|
73
|
+
checks.push((d) => { const fn = entry.fn; return fn ? fn(d) : true })
|
|
74
|
+
} else {
|
|
75
|
+
// JSON pointer: "#/$defs/foo" or "#/definitions/foo"
|
|
76
|
+
const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
77
|
+
if (m && rootDefs && rootDefs[m[1]]) {
|
|
78
|
+
const entry = rootDefs[m[1]]
|
|
79
|
+
checks.push((d) => { const fn = entry.fn; return fn ? fn(d) : true })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
// type
|
|
64
85
|
if (schema.type) {
|
|
65
86
|
const types = Array.isArray(schema.type) ? schema.type : [schema.type]
|
|
@@ -395,12 +416,75 @@ function collectDefs(schema) {
|
|
|
395
416
|
},
|
|
396
417
|
raw: def
|
|
397
418
|
}
|
|
419
|
+
// Register anchors
|
|
420
|
+
if (def && typeof def === 'object') {
|
|
421
|
+
if (def.$anchor) {
|
|
422
|
+
const anchorDef = def
|
|
423
|
+
let anchorCached = undefined
|
|
424
|
+
defs['#' + def.$anchor] = {
|
|
425
|
+
get fn() {
|
|
426
|
+
if (anchorCached === undefined) {
|
|
427
|
+
anchorCached = null
|
|
428
|
+
anchorCached = compileToJS(anchorDef, defs)
|
|
429
|
+
}
|
|
430
|
+
return anchorCached || (() => true)
|
|
431
|
+
},
|
|
432
|
+
raw: anchorDef
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (def.$dynamicAnchor) {
|
|
436
|
+
const daDef = def
|
|
437
|
+
let daCached = undefined
|
|
438
|
+
defs['#' + def.$dynamicAnchor] = {
|
|
439
|
+
get fn() {
|
|
440
|
+
if (daCached === undefined) {
|
|
441
|
+
daCached = null
|
|
442
|
+
daCached = compileToJS(daDef, defs)
|
|
443
|
+
}
|
|
444
|
+
return daCached || (() => true)
|
|
445
|
+
},
|
|
446
|
+
raw: daDef
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Register root-level $anchor/$dynamicAnchor (self-referencing schemas)
|
|
453
|
+
if (schema.$anchor && !defs['#' + schema.$anchor]) {
|
|
454
|
+
const rootAnchorSchema = schema
|
|
455
|
+
let rootACached = undefined
|
|
456
|
+
defs['#' + schema.$anchor] = {
|
|
457
|
+
get fn() {
|
|
458
|
+
if (rootACached === undefined) {
|
|
459
|
+
rootACached = null
|
|
460
|
+
rootACached = compileToJS(rootAnchorSchema, defs)
|
|
461
|
+
}
|
|
462
|
+
return rootACached || (() => true)
|
|
463
|
+
},
|
|
464
|
+
raw: rootAnchorSchema
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (schema.$dynamicAnchor && !defs['#' + schema.$dynamicAnchor]) {
|
|
468
|
+
const rootDASchema = schema
|
|
469
|
+
let rootDACached = undefined
|
|
470
|
+
defs['#' + schema.$dynamicAnchor] = {
|
|
471
|
+
get fn() {
|
|
472
|
+
if (rootDACached === undefined) {
|
|
473
|
+
rootDACached = null
|
|
474
|
+
rootDACached = compileToJS(rootDASchema, defs)
|
|
475
|
+
}
|
|
476
|
+
return rootDACached || (() => true)
|
|
477
|
+
},
|
|
478
|
+
raw: rootDASchema
|
|
398
479
|
}
|
|
399
480
|
}
|
|
400
481
|
return defs
|
|
401
482
|
}
|
|
402
483
|
|
|
403
484
|
function resolveRef(ref, defs, schemaMap) {
|
|
485
|
+
// Self-reference: "#" — treat as permissive to avoid infinite recursion
|
|
486
|
+
if (ref === '#') return () => true
|
|
487
|
+
|
|
404
488
|
// 1. Local ref
|
|
405
489
|
if (defs) {
|
|
406
490
|
const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
@@ -409,13 +493,28 @@ function resolveRef(ref, defs, schemaMap) {
|
|
|
409
493
|
const entry = defs[name]
|
|
410
494
|
if (entry) return (d) => { const fn = entry.fn; return fn ? fn(d) : true }
|
|
411
495
|
}
|
|
496
|
+
// Anchor ref: "#foo"
|
|
497
|
+
if (ref.startsWith('#') && !ref.startsWith('#/')) {
|
|
498
|
+
const entry = defs[ref]
|
|
499
|
+
if (entry) return (d) => { const fn = entry.fn; return fn ? fn(d) : true }
|
|
500
|
+
}
|
|
412
501
|
}
|
|
413
|
-
// 2. Cross-schema ref
|
|
502
|
+
// 2. Cross-schema ref (exact match)
|
|
414
503
|
if (schemaMap && schemaMap.has(ref)) {
|
|
415
504
|
const resolved = schemaMap.get(ref)
|
|
416
505
|
const fn = compileToJS(resolved, null, schemaMap)
|
|
417
506
|
return fn || (() => true)
|
|
418
507
|
}
|
|
508
|
+
// 3. Cross-schema ref (relative URI resolution)
|
|
509
|
+
if (schemaMap && !ref.includes('://') && !ref.startsWith('#')) {
|
|
510
|
+
for (const [id] of schemaMap) {
|
|
511
|
+
if (id.endsWith('/' + ref)) {
|
|
512
|
+
const resolved = schemaMap.get(id)
|
|
513
|
+
const fn = compileToJS(resolved, null, schemaMap)
|
|
514
|
+
return fn || (() => true)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
419
518
|
return null
|
|
420
519
|
}
|
|
421
520
|
|
|
@@ -444,7 +543,7 @@ const TYPE_CHECKS = {
|
|
|
444
543
|
|
|
445
544
|
const FORMAT_CHECKS = {
|
|
446
545
|
email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1 },
|
|
447
|
-
date: (s) => s.length
|
|
546
|
+
date: (s) => { if (s.length !== 10 || !/^\d{4}-\d{2}-\d{2}$/.test(s)) return false; const m = +s.slice(5, 7), d = +s.slice(8, 10); return m >= 1 && m <= 12 && d >= 1 && d <= 31 },
|
|
448
547
|
uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
|
|
449
548
|
ipv4: (s) => { const p = s.split('.'); return p.length === 4 && p.every(n => { const v = +n; return v >= 0 && v <= 255 && String(v) === n }) },
|
|
450
549
|
hostname: (s) => s.length > 0 && s.length <= 253 && /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i.test(s),
|
|
@@ -454,12 +553,49 @@ const FORMAT_CHECKS = {
|
|
|
454
553
|
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
|
|
455
554
|
'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString'])
|
|
456
555
|
|
|
556
|
+
// Check if all $dynamicRef in a target schema can be resolved via the calling schema's anchors.
|
|
557
|
+
function canResolveDynamicRefs(target, callingSchema, schemaMap) {
|
|
558
|
+
// Collect anchors from calling schema
|
|
559
|
+
const anchors = new Set()
|
|
560
|
+
if (callingSchema.$dynamicAnchor) anchors.add(callingSchema.$dynamicAnchor)
|
|
561
|
+
const defs = callingSchema.$defs || callingSchema.definitions
|
|
562
|
+
if (defs) {
|
|
563
|
+
for (const def of Object.values(defs)) {
|
|
564
|
+
if (def && typeof def === 'object' && def.$dynamicAnchor) anchors.add(def.$dynamicAnchor)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Also collect from schemaMap
|
|
568
|
+
if (schemaMap) {
|
|
569
|
+
for (const ext of schemaMap.values()) {
|
|
570
|
+
if (ext && typeof ext === 'object' && ext.$dynamicAnchor) anchors.add(ext.$dynamicAnchor)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// Find all $dynamicRef in target
|
|
574
|
+
const refs = []
|
|
575
|
+
const findDynRefs = (s) => {
|
|
576
|
+
if (typeof s !== 'object' || s === null) return
|
|
577
|
+
if (s.$dynamicRef) {
|
|
578
|
+
const name = s.$dynamicRef.startsWith('#') ? s.$dynamicRef.slice(1) : s.$dynamicRef
|
|
579
|
+
refs.push(name)
|
|
580
|
+
}
|
|
581
|
+
for (const v of Object.values(s)) {
|
|
582
|
+
if (Array.isArray(v)) v.forEach(findDynRefs)
|
|
583
|
+
else if (typeof v === 'object' && v !== null) findDynRefs(v)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
findDynRefs(target)
|
|
587
|
+
return refs.every(r => anchors.has(r))
|
|
588
|
+
}
|
|
589
|
+
|
|
457
590
|
// Recursively check if a schema can be safely compiled to JS codegen.
|
|
458
591
|
// Returns false if any sub-schema contains features codegen gets wrong.
|
|
459
592
|
function codegenSafe(schema, schemaMap) {
|
|
460
593
|
if (typeof schema === 'boolean') return true
|
|
461
594
|
if (typeof schema !== 'object' || schema === null) return true
|
|
462
595
|
|
|
596
|
+
// Only bail on $dynamicRef if it can't be resolved at compile time
|
|
597
|
+
if (schema.$dynamicRef && !schema.$dynamicRef.startsWith('#')) return false
|
|
598
|
+
|
|
463
599
|
// Boolean sub-schemas anywhere cause bail — codegen doesn't handle schema=false correctly
|
|
464
600
|
if (schema.items === false) return false
|
|
465
601
|
if (schema.items === true && !schema.unevaluatedItems) return false
|
|
@@ -490,12 +626,37 @@ function codegenSafe(schema, schemaMap) {
|
|
|
490
626
|
|
|
491
627
|
// $ref — allow local refs (#/$defs/Name) and non-local refs if in schemaMap
|
|
492
628
|
if (schema.$ref) {
|
|
629
|
+
// Self-reference "#" — treated as permissive (no-op) to avoid infinite recursion
|
|
630
|
+
if (schema.$ref === '#') return true
|
|
493
631
|
const isLocal = /^#\/(?:\$defs|definitions)\/[^/]+$/.test(schema.$ref)
|
|
494
|
-
|
|
495
|
-
if
|
|
632
|
+
let isResolvable = !isLocal && schemaMap && schemaMap.has(schema.$ref)
|
|
633
|
+
// Relative URI resolution: check if any schemaMap key ends with "/" + ref
|
|
634
|
+
let resolvedTarget = null
|
|
635
|
+
if (!isLocal && !isResolvable && schemaMap && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
|
|
636
|
+
for (const [id] of schemaMap) {
|
|
637
|
+
if (id.endsWith('/' + schema.$ref)) { isResolvable = true; resolvedTarget = schemaMap.get(id); break }
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// Anchor-style ref: #name (not #/path, not bare #) — resolvable at compile time via anchors map
|
|
641
|
+
const isAnchorRef = !isLocal && !isResolvable && schema.$ref.length > 1 && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')
|
|
642
|
+
if (!isLocal && !isResolvable && !isAnchorRef) return false
|
|
643
|
+
// If the resolved target contains $dynamicRef, allow codegen only when:
|
|
644
|
+
// 1. All $dynamicRefs can be resolved via the current schema's anchors
|
|
645
|
+
// 2. The resolved target itself is simple enough for codegen (no additionalProperties: false, etc.)
|
|
646
|
+
if (!resolvedTarget && isResolvable) resolvedTarget = schemaMap.get(schema.$ref)
|
|
647
|
+
if (resolvedTarget && JSON.stringify(resolvedTarget).includes('"$dynamicRef"')) {
|
|
648
|
+
const canResolve = canResolveDynamicRefs(resolvedTarget, schema, schemaMap)
|
|
649
|
+
// Also verify the resolved target doesn't have complex features that codegen can't inline
|
|
650
|
+
const targetSimple = canResolve && resolvedTarget.additionalProperties === undefined &&
|
|
651
|
+
!resolvedTarget.patternProperties && !resolvedTarget.dependentSchemas &&
|
|
652
|
+
!resolvedTarget.propertyNames
|
|
653
|
+
if (!targetSimple && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
|
|
654
|
+
}
|
|
496
655
|
// In Draft 2020-12, $ref with siblings is allowed. Only bail if no unevaluated* keyword
|
|
497
656
|
// (unevaluated schemas need $ref + siblings to work properly)
|
|
498
|
-
|
|
657
|
+
// Schema-organization keywords ($dynamicAnchor, $anchor) are not validation siblings
|
|
658
|
+
const SCHEMA_ORG_KEYS = new Set(['$ref', '$defs', 'definitions', '$schema', '$id', '$dynamicAnchor', '$anchor'])
|
|
659
|
+
const siblings = Object.keys(schema).filter(k => !SCHEMA_ORG_KEYS.has(k))
|
|
499
660
|
if (siblings.length > 0 && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
|
|
500
661
|
}
|
|
501
662
|
|
|
@@ -590,7 +751,31 @@ function compileToJSCodegen(schema, schemaMap) {
|
|
|
590
751
|
if (keys.some(k => !supported.includes(k))) return null
|
|
591
752
|
}
|
|
592
753
|
|
|
593
|
-
|
|
754
|
+
// Build anchors map for $ref/#anchor and $dynamicRef resolution
|
|
755
|
+
const anchors = {}
|
|
756
|
+
// Root schema's own $dynamicAnchor / $anchor
|
|
757
|
+
if (schema.$dynamicAnchor) anchors['#' + schema.$dynamicAnchor] = schema
|
|
758
|
+
if (schema.$anchor) anchors['#' + schema.$anchor] = schema
|
|
759
|
+
// Anchors from $defs
|
|
760
|
+
if (rootDefs) {
|
|
761
|
+
for (const def of Object.values(rootDefs)) {
|
|
762
|
+
if (def && typeof def === 'object') {
|
|
763
|
+
if (def.$dynamicAnchor) anchors['#' + def.$dynamicAnchor] = def
|
|
764
|
+
if (def.$anchor) anchors['#' + def.$anchor] = def
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// Anchors from external schemas in schemaMap
|
|
769
|
+
if (schemaMap) {
|
|
770
|
+
for (const ext of schemaMap.values()) {
|
|
771
|
+
if (ext && typeof ext === 'object') {
|
|
772
|
+
if (ext.$dynamicAnchor && !anchors['#' + ext.$dynamicAnchor]) anchors['#' + ext.$dynamicAnchor] = ext
|
|
773
|
+
if (ext.$anchor && !anchors['#' + ext.$anchor]) anchors['#' + ext.$anchor] = ext
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const ctx = { varCounter: 0, helpers: [], helperCode: [], closureVars: [], closureVals: [], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema }
|
|
594
779
|
const lines = []
|
|
595
780
|
genCode(schema, 'd', lines, ctx)
|
|
596
781
|
|
|
@@ -616,7 +801,16 @@ function compileToJSCodegen(schema, schemaMap) {
|
|
|
616
801
|
}
|
|
617
802
|
}
|
|
618
803
|
|
|
619
|
-
|
|
804
|
+
let body, hybridBody
|
|
805
|
+
if (ctx.usesRecursion) {
|
|
806
|
+
// Self-recursive: wrap in named function
|
|
807
|
+
body = `function _validate(d){\n ${checkStr}\n return true\n }\n return _validate(d)`
|
|
808
|
+
// Hybrid: keep _validate as boolean, wrap only the outer call
|
|
809
|
+
hybridBody = `function _validate(d){\n ${checkStr}\n return true\n }\n return _validate(d)?R:E(d)`
|
|
810
|
+
} else {
|
|
811
|
+
body = checkStr + '\n return true'
|
|
812
|
+
hybridBody = replaceTopLevel(checkStr + '\n return R')
|
|
813
|
+
}
|
|
620
814
|
|
|
621
815
|
try {
|
|
622
816
|
let boolFn
|
|
@@ -628,7 +822,6 @@ function compileToJSCodegen(schema, schemaMap) {
|
|
|
628
822
|
}
|
|
629
823
|
|
|
630
824
|
// Build hybrid: same body, return R instead of true, return E(d) instead of false.
|
|
631
|
-
const hybridBody = replaceTopLevel(checkStr + '\n return R')
|
|
632
825
|
try {
|
|
633
826
|
const hybridFactory = new Function(...closureNames, 'R', 'E', `return function(d){${hybridBody}}`)
|
|
634
827
|
boolFn._hybridFactory = (R, E) => hybridFactory(...closureValues, R, E)
|
|
@@ -679,6 +872,84 @@ function replaceTopLevel(code) {
|
|
|
679
872
|
return result
|
|
680
873
|
}
|
|
681
874
|
|
|
875
|
+
// Returns true if a property sub-schema will generate 2+ lines that each access v,
|
|
876
|
+
// meaning a local variable hoist is worthwhile.
|
|
877
|
+
function needsLocal(schema) {
|
|
878
|
+
if (typeof schema !== 'object' || schema === null) return false
|
|
879
|
+
// If it has $ref, allOf, anyOf etc., genCode handles it — don't hoist
|
|
880
|
+
if (schema.$ref || schema.allOf || schema.anyOf || schema.oneOf || schema.if) return false
|
|
881
|
+
if (schema.properties || schema.items || schema.prefixItems) return false
|
|
882
|
+
const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
|
|
883
|
+
if (!types || types.length !== 1) return false
|
|
884
|
+
const t = types[0]
|
|
885
|
+
let checkCount = 1 // type check itself
|
|
886
|
+
if (t === 'string') {
|
|
887
|
+
if (schema.minLength !== undefined) checkCount++
|
|
888
|
+
if (schema.maxLength !== undefined) checkCount++
|
|
889
|
+
if (schema.pattern) checkCount++
|
|
890
|
+
if (schema.format) checkCount++
|
|
891
|
+
} else if (t === 'integer' || t === 'number') {
|
|
892
|
+
if (schema.minimum !== undefined) checkCount++
|
|
893
|
+
if (schema.maximum !== undefined) checkCount++
|
|
894
|
+
if (schema.exclusiveMinimum !== undefined) checkCount++
|
|
895
|
+
if (schema.exclusiveMaximum !== undefined) checkCount++
|
|
896
|
+
if (schema.multipleOf !== undefined) checkCount++
|
|
897
|
+
}
|
|
898
|
+
return checkCount >= 2
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Try to generate a single combined check for simple leaf schemas.
|
|
902
|
+
// Returns a string like "{const _v=d["x"];if(typeof _v!=='string'||_v.length<1||_v.length>100)return false}"
|
|
903
|
+
// or null if the schema is too complex.
|
|
904
|
+
function tryGenCombined(schema, access, ctx) {
|
|
905
|
+
if (typeof schema !== 'object' || schema === null) return null
|
|
906
|
+
// Only handle simple leaf schemas with a single type and basic constraints
|
|
907
|
+
if (schema.$ref || schema.allOf || schema.anyOf || schema.oneOf || schema.if) return null
|
|
908
|
+
if (schema.properties || schema.items || schema.prefixItems || schema.patternProperties) return null
|
|
909
|
+
if (schema.enum || schema.const !== undefined) return null
|
|
910
|
+
if (schema.not || schema.dependentRequired || schema.dependentSchemas) return null
|
|
911
|
+
const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
|
|
912
|
+
if (!types || types.length !== 1) return null
|
|
913
|
+
const t = types[0]
|
|
914
|
+
|
|
915
|
+
if (t === 'string') {
|
|
916
|
+
const conds = [`typeof _v!=='string'`]
|
|
917
|
+
if (schema.minLength !== undefined) conds.push(`_v.length<${schema.minLength}`)
|
|
918
|
+
if (schema.maxLength !== undefined) conds.push(`_v.length>${schema.maxLength}`)
|
|
919
|
+
if (conds.length < 2 && !schema.pattern && !schema.format) return null // not worth combining
|
|
920
|
+
// pattern and format need separate statements, fall back if present
|
|
921
|
+
if (schema.pattern || schema.format) return null
|
|
922
|
+
const vi = ctx.varCounter++
|
|
923
|
+
return `{const _v=${access};if(${conds.join('||')})return false}`
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (t === 'integer') {
|
|
927
|
+
const conds = [`!Number.isInteger(_v)`]
|
|
928
|
+
if (schema.minimum !== undefined) conds.push(`_v<${schema.minimum}`)
|
|
929
|
+
if (schema.maximum !== undefined) conds.push(`_v>${schema.maximum}`)
|
|
930
|
+
if (schema.exclusiveMinimum !== undefined) conds.push(`_v<=${schema.exclusiveMinimum}`)
|
|
931
|
+
if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
|
|
932
|
+
if (schema.multipleOf !== undefined) conds.push(`_v%${schema.multipleOf}!==0`)
|
|
933
|
+
if (conds.length < 2) return null
|
|
934
|
+
const vi = ctx.varCounter++
|
|
935
|
+
return `{const _v=${access};if(${conds.join('||')})return false}`
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (t === 'number') {
|
|
939
|
+
const conds = [`typeof _v!=='number'||!isFinite(_v)`]
|
|
940
|
+
if (schema.minimum !== undefined) conds.push(`_v<${schema.minimum}`)
|
|
941
|
+
if (schema.maximum !== undefined) conds.push(`_v>${schema.maximum}`)
|
|
942
|
+
if (schema.exclusiveMinimum !== undefined) conds.push(`_v<=${schema.exclusiveMinimum}`)
|
|
943
|
+
if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
|
|
944
|
+
if (schema.multipleOf !== undefined) conds.push(`_v%${schema.multipleOf}!==0`)
|
|
945
|
+
if (conds.length < 2) return null
|
|
946
|
+
const vi = ctx.varCounter++
|
|
947
|
+
return `{const _v=${access};if(${conds.join('||')})return false}`
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return null
|
|
951
|
+
}
|
|
952
|
+
|
|
682
953
|
// knownType: if parent already verified the type, skip redundant guards.
|
|
683
954
|
// 'object' = we know v is a non-null non-array object
|
|
684
955
|
// 'array' = we know v is an array
|
|
@@ -691,8 +962,14 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
691
962
|
// Only when THIS schema has unevaluated keywords directly (not via $ref target)
|
|
692
963
|
const hasSiblings = schema.$ref && (schema.unevaluatedProperties !== undefined || schema.unevaluatedItems !== undefined)
|
|
693
964
|
if (schema.$ref) {
|
|
965
|
+
// Self-reference "#" — recursive call to root validator
|
|
966
|
+
if (schema.$ref === '#') {
|
|
967
|
+
ctx.usesRecursion = true
|
|
968
|
+
lines.push(`if(!_validate(${v}))return false`)
|
|
969
|
+
if (!hasSiblings) return
|
|
970
|
+
}
|
|
694
971
|
// 1. Local ref
|
|
695
|
-
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
972
|
+
const m = schema.$ref !== '#' && schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
696
973
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
697
974
|
if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
|
|
698
975
|
else {
|
|
@@ -701,13 +978,36 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
701
978
|
ctx.refStack.delete(schema.$ref)
|
|
702
979
|
if (!hasSiblings) return
|
|
703
980
|
}
|
|
704
|
-
} else if (
|
|
705
|
-
//
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
981
|
+
} else if (schema.$ref !== '#' && !m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
|
|
982
|
+
// Anchor ref: "#foo" — resolve via rootDefs or anchors map
|
|
983
|
+
const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
|
|
984
|
+
const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
|
|
985
|
+
if (anchorTarget) {
|
|
986
|
+
if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
|
|
987
|
+
else {
|
|
988
|
+
ctx.refStack.add(schema.$ref)
|
|
989
|
+
genCode(anchorTarget, v, lines, ctx, knownType)
|
|
990
|
+
ctx.refStack.delete(schema.$ref)
|
|
991
|
+
if (!hasSiblings) return
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
} else if (schema.$ref !== '#' && ctx.schemaMap) {
|
|
995
|
+
// 2. Cross-schema ref (exact match or relative URI)
|
|
996
|
+
let resolved = ctx.schemaMap.get(schema.$ref)
|
|
997
|
+
if (!resolved && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
|
|
998
|
+
for (const [id, s] of ctx.schemaMap) {
|
|
999
|
+
if (id.endsWith('/' + schema.$ref)) { resolved = s; break }
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (resolved) {
|
|
1003
|
+
if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
|
|
1004
|
+
else {
|
|
1005
|
+
ctx.refStack.add(schema.$ref)
|
|
1006
|
+
genCode(resolved, v, lines, ctx, knownType)
|
|
1007
|
+
ctx.refStack.delete(schema.$ref)
|
|
1008
|
+
if (!hasSiblings) return
|
|
1009
|
+
}
|
|
1010
|
+
} else {
|
|
711
1011
|
if (!hasSiblings) return
|
|
712
1012
|
}
|
|
713
1013
|
} else {
|
|
@@ -715,25 +1015,58 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
715
1015
|
}
|
|
716
1016
|
}
|
|
717
1017
|
|
|
1018
|
+
// $dynamicRef — resolve via anchors map
|
|
1019
|
+
if (schema.$dynamicRef) {
|
|
1020
|
+
const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
|
|
1021
|
+
if (ctx.anchors && ctx.anchors[anchorKey]) {
|
|
1022
|
+
const target = ctx.anchors[anchorKey]
|
|
1023
|
+
if (target === ctx.rootSchema) {
|
|
1024
|
+
// Self-recursive: generate _validate(v) call
|
|
1025
|
+
ctx.usesRecursion = true
|
|
1026
|
+
lines.push(`if(!_validate(${v}))return false`)
|
|
1027
|
+
} else {
|
|
1028
|
+
// Different schema: inline the target validation
|
|
1029
|
+
const refKey = '$dynamicRef:' + anchorKey
|
|
1030
|
+
if (!ctx.refStack.has(refKey)) {
|
|
1031
|
+
ctx.refStack.add(refKey)
|
|
1032
|
+
genCode(target, v, lines, ctx, knownType)
|
|
1033
|
+
ctx.refStack.delete(refKey)
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
718
1039
|
// Determine the single known type after this schema's type check
|
|
719
1040
|
const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
|
|
720
1041
|
let effectiveType = knownType
|
|
721
1042
|
if (types) {
|
|
722
1043
|
if (!knownType) {
|
|
723
|
-
// Emit the type check
|
|
724
|
-
|
|
725
|
-
switch (
|
|
726
|
-
case 'object':
|
|
727
|
-
case 'array':
|
|
728
|
-
case 'string':
|
|
729
|
-
case 'number':
|
|
730
|
-
case 'integer':
|
|
731
|
-
case 'boolean':
|
|
732
|
-
case 'null':
|
|
733
|
-
default: return 'true'
|
|
1044
|
+
// Emit the type check — use direct negation for single types (avoids !() wrapper)
|
|
1045
|
+
if (types.length === 1) {
|
|
1046
|
+
switch (types[0]) {
|
|
1047
|
+
case 'object': lines.push(`if(typeof ${v}!=='object'||${v}===null||Array.isArray(${v}))return false`); break
|
|
1048
|
+
case 'array': lines.push(`if(!Array.isArray(${v}))return false`); break
|
|
1049
|
+
case 'string': lines.push(`if(typeof ${v}!=='string')return false`); break
|
|
1050
|
+
case 'number': lines.push(`if(typeof ${v}!=='number'||!isFinite(${v}))return false`); break
|
|
1051
|
+
case 'integer': lines.push(`if(!Number.isInteger(${v}))return false`); break
|
|
1052
|
+
case 'boolean': lines.push(`if(typeof ${v}!=='boolean')return false`); break
|
|
1053
|
+
case 'null': lines.push(`if(${v}!==null)return false`); break
|
|
734
1054
|
}
|
|
735
|
-
}
|
|
736
|
-
|
|
1055
|
+
} else {
|
|
1056
|
+
const conds = types.map(t => {
|
|
1057
|
+
switch (t) {
|
|
1058
|
+
case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
1059
|
+
case 'array': return `Array.isArray(${v})`
|
|
1060
|
+
case 'string': return `typeof ${v}==='string'`
|
|
1061
|
+
case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
|
|
1062
|
+
case 'integer': return `Number.isInteger(${v})`
|
|
1063
|
+
case 'boolean': return `typeof ${v}==='boolean'`
|
|
1064
|
+
case 'null': return `${v}===null`
|
|
1065
|
+
default: return 'true'
|
|
1066
|
+
}
|
|
1067
|
+
})
|
|
1068
|
+
lines.push(`if(!(${conds.join('||')}))return false`)
|
|
1069
|
+
}
|
|
737
1070
|
}
|
|
738
1071
|
// If single type, downstream checks can skip guards
|
|
739
1072
|
if (types.length === 1) effectiveType = types[0]
|
|
@@ -1050,14 +1383,31 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1050
1383
|
if (schema.properties) {
|
|
1051
1384
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
1052
1385
|
if (requiredSet.has(key) && isObj) {
|
|
1053
|
-
// Required + type:object —
|
|
1054
|
-
|
|
1386
|
+
// Required + type:object — hoist to local to reduce repeated property lookups
|
|
1387
|
+
const access = hoisted[key] || `${v}[${JSON.stringify(key)}]`
|
|
1388
|
+
const combined = tryGenCombined(prop, access, ctx)
|
|
1389
|
+
if (combined) {
|
|
1390
|
+
lines.push(combined)
|
|
1391
|
+
} else if (needsLocal(prop)) {
|
|
1392
|
+
const oi = ctx.varCounter++
|
|
1393
|
+
const local = `_r${oi}`
|
|
1394
|
+
lines.push(`{const ${local}=${access}`)
|
|
1395
|
+
genCode(prop, local, lines, ctx)
|
|
1396
|
+
lines.push(`}`)
|
|
1397
|
+
} else {
|
|
1398
|
+
genCode(prop, access, lines, ctx)
|
|
1399
|
+
}
|
|
1055
1400
|
} else if (isObj) {
|
|
1056
1401
|
// Optional — hoist to local, check undefined
|
|
1057
1402
|
const oi = ctx.varCounter++
|
|
1058
1403
|
const local = `_o${oi}`
|
|
1059
1404
|
lines.push(`{const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined){`)
|
|
1060
|
-
|
|
1405
|
+
const combined = tryGenCombined(prop, local, ctx)
|
|
1406
|
+
if (combined) {
|
|
1407
|
+
lines.push(combined)
|
|
1408
|
+
} else {
|
|
1409
|
+
genCode(prop, local, lines, ctx)
|
|
1410
|
+
}
|
|
1061
1411
|
lines.push(`}}`)
|
|
1062
1412
|
} else {
|
|
1063
1413
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}){`)
|
|
@@ -1081,15 +1431,15 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1081
1431
|
|
|
1082
1432
|
// prefixItems
|
|
1083
1433
|
if (schema.prefixItems) {
|
|
1434
|
+
const pfxVar = ctx.varCounter++
|
|
1084
1435
|
for (let i = 0; i < schema.prefixItems.length; i++) {
|
|
1085
|
-
const elem = `_p${
|
|
1436
|
+
const elem = `_p${pfxVar}_${i}`
|
|
1086
1437
|
lines.push(isArr
|
|
1087
1438
|
? `if(${v}.length>${i}){const ${elem}=${v}[${i}]`
|
|
1088
1439
|
: `if(Array.isArray(${v})&&${v}.length>${i}){const ${elem}=${v}[${i}]`)
|
|
1089
1440
|
genCode(schema.prefixItems[i], elem, lines, ctx)
|
|
1090
1441
|
lines.push(`}`)
|
|
1091
1442
|
}
|
|
1092
|
-
ctx.varCounter++
|
|
1093
1443
|
}
|
|
1094
1444
|
|
|
1095
1445
|
// contains — use helper function to avoid try/catch overhead
|
|
@@ -1401,23 +1751,73 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1401
1751
|
const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
|
|
1402
1752
|
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1403
1753
|
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1404
|
-
} else
|
|
1405
|
-
//
|
|
1406
|
-
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1754
|
+
} else {
|
|
1755
|
+
// General fallback: collect all patternProperties from root + allOf sub-schemas + if
|
|
1756
|
+
// and use runtime regex matching
|
|
1757
|
+
const allPatterns = []
|
|
1758
|
+
if (schema.patternProperties) {
|
|
1759
|
+
allPatterns.push(...Object.keys(schema.patternProperties))
|
|
1760
|
+
}
|
|
1761
|
+
if (schema.allOf) {
|
|
1762
|
+
for (const sub of schema.allOf) {
|
|
1763
|
+
if (sub && sub.patternProperties) {
|
|
1764
|
+
allPatterns.push(...Object.keys(sub.patternProperties))
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
// lone if (no then/else) still contributes annotations when it passes
|
|
1769
|
+
if (schema.if && !schema.then && !schema.else && schema.if.patternProperties) {
|
|
1770
|
+
allPatterns.push(...Object.keys(schema.if.patternProperties))
|
|
1771
|
+
}
|
|
1772
|
+
if (allPatterns.length > 0) {
|
|
1773
|
+
const ei = ctx.varCounter++
|
|
1774
|
+
const evVar = `_ev${ei}`
|
|
1775
|
+
lines.push(`{const ${evVar}={}`)
|
|
1776
|
+
for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
|
|
1777
|
+
const reVars = []
|
|
1778
|
+
for (const pat of allPatterns) {
|
|
1779
|
+
const ri = ctx.varCounter++
|
|
1780
|
+
ctx.closureVars.push(`_ure${ri}`)
|
|
1781
|
+
ctx.closureVals.push(new RegExp(pat))
|
|
1782
|
+
reVars.push(`_ure${ri}`)
|
|
1783
|
+
}
|
|
1784
|
+
if (schema.if && !schema.then && !schema.else) {
|
|
1785
|
+
// Lone if: run the if check first; if it passes, its patternProperties contribute
|
|
1786
|
+
const ifLines2 = []
|
|
1787
|
+
genCode(schema.if, '_iv2', ifLines2, ctx)
|
|
1788
|
+
const ufi = ctx.varCounter++
|
|
1789
|
+
const ifFn = ifLines2.length === 0
|
|
1790
|
+
? `function(_iv2){return true}`
|
|
1791
|
+
: `function(_iv2){${ifLines2.join(';')};return true}`
|
|
1792
|
+
// Mark keys matching if's patterns as evaluated only when if passes
|
|
1793
|
+
const ifPatterns = schema.if.patternProperties ? Object.keys(schema.if.patternProperties) : []
|
|
1794
|
+
const ifReVars = []
|
|
1795
|
+
for (const pat of ifPatterns) {
|
|
1796
|
+
const ri = ctx.varCounter++
|
|
1797
|
+
ctx.closureVars.push(`_ure${ri}`)
|
|
1798
|
+
ctx.closureVals.push(new RegExp(pat))
|
|
1799
|
+
ifReVars.push(`_ure${ri}`)
|
|
1800
|
+
}
|
|
1801
|
+
const rootReVars = []
|
|
1802
|
+
if (schema.patternProperties) {
|
|
1803
|
+
for (const pat of Object.keys(schema.patternProperties)) {
|
|
1804
|
+
const ri = ctx.varCounter++
|
|
1805
|
+
ctx.closureVars.push(`_ure${ri}`)
|
|
1806
|
+
ctx.closureVals.push(new RegExp(pat))
|
|
1807
|
+
rootReVars.push(`_ure${ri}`)
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
const rootPatCheck = rootReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
|
|
1811
|
+
const ifPatCheck = ifReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
|
|
1812
|
+
const inner = `const _uif${ufi}=${ifFn};if(_uif${ufi}(${v})){for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}${ifPatCheck}return false}}else{for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}return false}}`
|
|
1813
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1814
|
+
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1815
|
+
} else {
|
|
1816
|
+
const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
|
|
1817
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1818
|
+
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1819
|
+
}
|
|
1417
1820
|
}
|
|
1418
|
-
const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
|
|
1419
|
-
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1420
|
-
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1421
1821
|
}
|
|
1422
1822
|
} else if (typeof schema.unevaluatedProperties === 'object') {
|
|
1423
1823
|
// Tier 3 with schema: validate unknown keys against sub-schema
|
|
@@ -1469,7 +1869,12 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1469
1869
|
if (schema.unevaluatedItems !== undefined) {
|
|
1470
1870
|
const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
1471
1871
|
|
|
1472
|
-
if
|
|
1872
|
+
// Check if allItems from anyOf/oneOf branches with `items` keyword needs dynamic tracking
|
|
1873
|
+
const branchKw = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
|
|
1874
|
+
const hasConditionalItems = evalResult.allItems && evalResult.dynamic && branchKw &&
|
|
1875
|
+
schema[branchKw].some(sub => sub && typeof sub === 'object' && ((sub.items && typeof sub.items === 'object') || sub.items === true))
|
|
1876
|
+
|
|
1877
|
+
if (schema.unevaluatedItems === true || (evalResult.allItems && !hasConditionalItems)) {
|
|
1473
1878
|
// All items evaluated or unevaluatedItems:true — no-op
|
|
1474
1879
|
} else if (!evalResult.dynamic) {
|
|
1475
1880
|
// Static: all evaluated items known at compile-time
|
|
@@ -1495,16 +1900,29 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1495
1900
|
}
|
|
1496
1901
|
} else {
|
|
1497
1902
|
// Dynamic: runtime tracking of max evaluated index
|
|
1498
|
-
|
|
1903
|
+
// Compute baseIdx from unconditional sources only (root prefixItems/items, allOf)
|
|
1904
|
+
let baseIdx = 0
|
|
1905
|
+
if (schema.prefixItems) baseIdx = Math.max(baseIdx, schema.prefixItems.length)
|
|
1906
|
+
if (schema.items && typeof schema.items === 'object') baseIdx = Infinity // items: schema → all evaluated
|
|
1907
|
+
if (schema.allOf) {
|
|
1908
|
+
for (const sub of schema.allOf) {
|
|
1909
|
+
const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
|
|
1910
|
+
if (subR.items !== null) baseIdx = Math.max(baseIdx, subR.items)
|
|
1911
|
+
if (subR.allItems) baseIdx = Infinity
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
if (baseIdx === Infinity) baseIdx = 0 // allItems already handled above
|
|
1499
1915
|
const branchKeyword = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
|
|
1500
1916
|
|
|
1501
1917
|
if (branchKeyword && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
|
|
1502
1918
|
// anyOf/oneOf: each branch may evaluate different number of items
|
|
1503
1919
|
const branches = schema[branchKeyword]
|
|
1504
1920
|
const branchMaxIdx = []
|
|
1921
|
+
const branchAllItems = []
|
|
1505
1922
|
for (const sub of branches) {
|
|
1506
1923
|
const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
|
|
1507
1924
|
branchMaxIdx.push(subR.items || 0)
|
|
1925
|
+
branchAllItems.push(subR.allItems)
|
|
1508
1926
|
}
|
|
1509
1927
|
// Runtime: find max evaluated index across all matching branches
|
|
1510
1928
|
const fns = []
|
|
@@ -1518,7 +1936,10 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1518
1936
|
const evVar = `_eidx${ei}`
|
|
1519
1937
|
lines.push(`{let ${evVar}=${baseIdx}`)
|
|
1520
1938
|
lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
|
|
1521
|
-
const maxExprs = branchMaxIdx.map((m, i) =>
|
|
1939
|
+
const maxExprs = branchMaxIdx.map((m, i) => {
|
|
1940
|
+
if (branchAllItems[i]) return `_bi===${i}?${v}.length`
|
|
1941
|
+
return `_bi===${i}?${Math.max(m, baseIdx)}`
|
|
1942
|
+
}).join(':') + `:${baseIdx}`
|
|
1522
1943
|
if (branchKeyword === 'oneOf') {
|
|
1523
1944
|
lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){${evVar}=${maxExprs};break}}`)
|
|
1524
1945
|
} else {
|
|
@@ -1543,8 +1964,8 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1543
1964
|
lines.push('}')
|
|
1544
1965
|
}
|
|
1545
1966
|
}
|
|
1546
|
-
} else if (schema.if && (schema.
|
|
1547
|
-
// if/then/else: branch-specific max index
|
|
1967
|
+
} else if (schema.if && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
|
|
1968
|
+
// if/then/else (or lone if): branch-specific max index
|
|
1548
1969
|
const ifEval = collectEvaluated(schema.if, ctx.schemaMap, ctx.rootDefs)
|
|
1549
1970
|
const thenEval = schema.then ? collectEvaluated(schema.then, ctx.schemaMap, ctx.rootDefs) : { items: null }
|
|
1550
1971
|
const elseEval = schema.else ? collectEvaluated(schema.else, ctx.schemaMap, ctx.rootDefs) : { items: null }
|
|
@@ -1563,6 +1984,54 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1563
1984
|
const guard = isArr ? '' : `if(Array.isArray(${v}))`
|
|
1564
1985
|
lines.push(`${guard}{const _uif${ufi}=${ifFn3};if(_uif${ufi}(${v})){if(${v}.length>${thenIdx})return false}else{if(${v}.length>${elseIdx})return false}}`)
|
|
1565
1986
|
}
|
|
1987
|
+
} else if ((schema.contains || (schema.allOf && schema.allOf.some(s => s && s.contains))) && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
|
|
1988
|
+
// contains + unevaluatedItems: per-item tracking of which items are matched by contains
|
|
1989
|
+
// Collect contains from root and allOf sub-schemas
|
|
1990
|
+
const allContains = []
|
|
1991
|
+
if (schema.contains) allContains.push(schema.contains)
|
|
1992
|
+
if (schema.allOf) {
|
|
1993
|
+
for (const sub of schema.allOf) {
|
|
1994
|
+
if (sub && sub.contains) allContains.push(sub.contains)
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
const ci = ctx.varCounter++
|
|
1998
|
+
const evArr = `_cev${ci}`
|
|
1999
|
+
const containsFns = []
|
|
2000
|
+
for (const c of allContains) {
|
|
2001
|
+
const cLines = []
|
|
2002
|
+
genCode(c, '_cv', cLines, ctx)
|
|
2003
|
+
containsFns.push(cLines.length === 0
|
|
2004
|
+
? `function(_cv){return true}`
|
|
2005
|
+
: `function(_cv){${cLines.join(';')};return true}`)
|
|
2006
|
+
}
|
|
2007
|
+
const cfnArr = `_cfn${ci}`
|
|
2008
|
+
lines.push(`{const ${cfnArr}=[${containsFns.join(',')}]`)
|
|
2009
|
+
// Mark items evaluated by prefixItems
|
|
2010
|
+
lines.push(`const ${evArr}=[]`)
|
|
2011
|
+
if (baseIdx > 0) {
|
|
2012
|
+
lines.push(`for(let _i=0;_i<${Math.min(baseIdx, 1000)};_i++)${evArr}[_i]=true`)
|
|
2013
|
+
}
|
|
2014
|
+
// Mark items matched by each contains function
|
|
2015
|
+
lines.push(`if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){for(let _cj=0;_cj<${cfnArr}.length;_cj++){if(${cfnArr}[_cj](${v}[_ci])){${evArr}[_ci]=true;break}}}}`)
|
|
2016
|
+
if (schema.unevaluatedItems === false) {
|
|
2017
|
+
const inner = `if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){if(!${evArr}[_ci])return false}}`
|
|
2018
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
2019
|
+
ctx.deferredChecks.push(inner + '}')
|
|
2020
|
+
} else {
|
|
2021
|
+
// unevaluatedItems: {schema}
|
|
2022
|
+
const ui = ctx.varCounter++
|
|
2023
|
+
const elemVar = `_ue${ui}`
|
|
2024
|
+
const subLines = []
|
|
2025
|
+
genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
|
|
2026
|
+
if (subLines.length > 0) {
|
|
2027
|
+
const check = subLines.join(';')
|
|
2028
|
+
const inner = `if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){if(!${evArr}[_ci]){const ${elemVar}=${v}[_ci];${check}}}}`
|
|
2029
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
2030
|
+
ctx.deferredChecks.push(inner + '}')
|
|
2031
|
+
} else {
|
|
2032
|
+
lines.push('}')
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
1566
2035
|
} else if (schema.unevaluatedItems === false) {
|
|
1567
2036
|
// Fallback: use static base index (may not be fully correct for all dynamic cases)
|
|
1568
2037
|
const maxIdx = evalResult.items || 0
|
|
@@ -1582,8 +2051,8 @@ const FORMAT_CODEGEN = {
|
|
|
1582
2051
|
: `if(typeof ${v}==='string'){const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
|
|
1583
2052
|
},
|
|
1584
2053
|
date: (v, isStr) => isStr
|
|
1585
|
-
? `if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false`
|
|
1586
|
-
: `if(typeof ${v}==='string'
|
|
2054
|
+
? `{if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false;const _dm=+${v}.slice(5,7),_dd=+${v}.slice(8,10);if(_dm<1||_dm>12||_dd<1||_dd>31)return false}`
|
|
2055
|
+
: `if(typeof ${v}==='string'){if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false;const _dm=+${v}.slice(5,7),_dd=+${v}.slice(8,10);if(_dm<1||_dm>12||_dd<1||_dd>31)return false}`,
|
|
1587
2056
|
uuid: (v, isStr) => isStr
|
|
1588
2057
|
? `if(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v}))return false`
|
|
1589
2058
|
: `if(typeof ${v}==='string'&&(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v})))return false`,
|
|
@@ -1741,6 +2210,8 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
|
|
|
1741
2210
|
if (typeof schema === 'object' && schema !== null) {
|
|
1742
2211
|
const s = JSON.stringify(schema)
|
|
1743
2212
|
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
2213
|
+
// Bail on self-referencing schemas — error codegen doesn't support recursion
|
|
2214
|
+
if (s.includes('"$ref":"#"')) return null
|
|
1744
2215
|
}
|
|
1745
2216
|
if (typeof schema === 'boolean') {
|
|
1746
2217
|
return schema
|
|
@@ -1770,15 +2241,46 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
|
|
|
1770
2241
|
if (keys.some(k => !supported.includes(k))) return null
|
|
1771
2242
|
}
|
|
1772
2243
|
|
|
1773
|
-
|
|
2244
|
+
// Build anchors map for $ref/#anchor and $dynamicRef resolution
|
|
2245
|
+
const eRootDefs = schema.$defs || schema.definitions || null
|
|
2246
|
+
const eAnchors = {}
|
|
2247
|
+
if (schema.$dynamicAnchor) eAnchors['#' + schema.$dynamicAnchor] = schema
|
|
2248
|
+
if (schema.$anchor) eAnchors['#' + schema.$anchor] = schema
|
|
2249
|
+
if (eRootDefs) {
|
|
2250
|
+
for (const def of Object.values(eRootDefs)) {
|
|
2251
|
+
if (def && typeof def === 'object') {
|
|
2252
|
+
if (def.$dynamicAnchor) eAnchors['#' + def.$dynamicAnchor] = def
|
|
2253
|
+
if (def.$anchor) eAnchors['#' + def.$anchor] = def
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
if (schemaMap) {
|
|
2258
|
+
for (const ext of schemaMap.values()) {
|
|
2259
|
+
if (ext && typeof ext === 'object') {
|
|
2260
|
+
if (ext.$dynamicAnchor && !eAnchors['#' + ext.$dynamicAnchor]) eAnchors['#' + ext.$dynamicAnchor] = ext
|
|
2261
|
+
if (ext.$anchor && !eAnchors['#' + ext.$anchor]) eAnchors['#' + ext.$anchor] = ext
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema }
|
|
1774
2267
|
const lines = []
|
|
1775
2268
|
genCodeE(schema, 'd', '', lines, ctx, '#')
|
|
1776
2269
|
if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
|
|
1777
2270
|
|
|
1778
|
-
const
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
2271
|
+
const checkStr = lines.join('\n ')
|
|
2272
|
+
let body
|
|
2273
|
+
if (ctx.usesRecursion) {
|
|
2274
|
+
body = `const _e=[];\n ` +
|
|
2275
|
+
(ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
|
|
2276
|
+
`function _validateE(d,_all,_e){\n ${checkStr}\n }\n _validateE(d,_all,_e);\n ` +
|
|
2277
|
+
`return{valid:_e.length===0,errors:_e}`
|
|
2278
|
+
} else {
|
|
2279
|
+
body = `const _e=[];\n ` +
|
|
2280
|
+
(ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
|
|
2281
|
+
checkStr +
|
|
2282
|
+
`\n return{valid:_e.length===0,errors:_e}`
|
|
2283
|
+
}
|
|
1782
2284
|
try {
|
|
1783
2285
|
const fn = new Function('d', '_all', body)
|
|
1784
2286
|
fn._errSource = body
|
|
@@ -1797,6 +2299,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1797
2299
|
|
|
1798
2300
|
// $ref — resolve local and cross-schema refs
|
|
1799
2301
|
if (schema.$ref) {
|
|
2302
|
+
// Self-reference "#" — no-op (permissive) to avoid infinite recursion
|
|
2303
|
+
if (schema.$ref === '#') return
|
|
1800
2304
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
1801
2305
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
1802
2306
|
if (ctx.refStack.has(schema.$ref)) return
|
|
@@ -1805,6 +2309,18 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1805
2309
|
ctx.refStack.delete(schema.$ref)
|
|
1806
2310
|
return
|
|
1807
2311
|
}
|
|
2312
|
+
// Anchor ref: "#foo" — resolve via rootDefs or anchors map
|
|
2313
|
+
if (!m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
|
|
2314
|
+
const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
|
|
2315
|
+
const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
|
|
2316
|
+
if (anchorTarget) {
|
|
2317
|
+
if (ctx.refStack.has(schema.$ref)) return
|
|
2318
|
+
ctx.refStack.add(schema.$ref)
|
|
2319
|
+
genCodeE(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
|
|
2320
|
+
ctx.refStack.delete(schema.$ref)
|
|
2321
|
+
return
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
1808
2324
|
if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
1809
2325
|
if (ctx.refStack.has(schema.$ref)) return
|
|
1810
2326
|
ctx.refStack.add(schema.$ref)
|
|
@@ -1814,6 +2330,26 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1814
2330
|
}
|
|
1815
2331
|
}
|
|
1816
2332
|
|
|
2333
|
+
// $dynamicRef — resolve via anchors map
|
|
2334
|
+
if (schema.$dynamicRef) {
|
|
2335
|
+
const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
|
|
2336
|
+
if (ctx.anchors && ctx.anchors[anchorKey]) {
|
|
2337
|
+
const target = ctx.anchors[anchorKey]
|
|
2338
|
+
if (target === ctx.rootSchema) {
|
|
2339
|
+
// Self-recursive: generate _validateE call
|
|
2340
|
+
ctx.usesRecursion = true
|
|
2341
|
+
lines.push(`_validateE(${v},_all,_e)`)
|
|
2342
|
+
} else {
|
|
2343
|
+
const refKey = '$dynamicRef:' + anchorKey
|
|
2344
|
+
if (!ctx.refStack.has(refKey)) {
|
|
2345
|
+
ctx.refStack.add(refKey)
|
|
2346
|
+
genCodeE(target, v, pathExpr, lines, ctx, schemaPrefix)
|
|
2347
|
+
ctx.refStack.delete(refKey)
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
|
|
1817
2353
|
const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
|
|
1818
2354
|
if (types) {
|
|
1819
2355
|
const conds = types.map(t => {
|
|
@@ -1823,7 +2359,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1823
2359
|
case 'string': return `typeof ${v}==='string'`
|
|
1824
2360
|
case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
|
|
1825
2361
|
case 'integer': return `Number.isInteger(${v})`
|
|
1826
|
-
case 'boolean': return `
|
|
2362
|
+
case 'boolean': return `typeof ${v}==='boolean'`
|
|
1827
2363
|
case 'null': return `${v}===null`
|
|
1828
2364
|
default: return 'true'
|
|
1829
2365
|
}
|
|
@@ -1864,8 +2400,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1864
2400
|
const ci = ctx.varCounter++
|
|
1865
2401
|
const canonFn = `_cnE${ci}`
|
|
1866
2402
|
ctx.helperCode.push(`const ${canonFn}=function(x){if(x===null||typeof x!=='object')return JSON.stringify(x);if(Array.isArray(x))return'['+x.map(${canonFn}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+${canonFn}(x[k])}).join(',')+'}'};`)
|
|
1867
|
-
const expected = canonFn + '(' + JSON.stringify(cv) + ')'
|
|
1868
|
-
lines.push(`if(${canonFn}(${v})!==${expected}){${fail('const', 'const', `{allowedValue
|
|
2403
|
+
const expected = canonFn + '(JSON.parse(' + JSON.stringify(JSON.stringify(cv)) + '))'
|
|
2404
|
+
lines.push(`if(${canonFn}(${v})!==${expected}){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
|
|
1869
2405
|
}
|
|
1870
2406
|
}
|
|
1871
2407
|
|
|
@@ -2157,6 +2693,8 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
|
2157
2693
|
if (typeof schema === 'object' && schema !== null) {
|
|
2158
2694
|
const s = JSON.stringify(schema)
|
|
2159
2695
|
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
2696
|
+
// Bail on self-referencing schemas — combined codegen doesn't support recursion
|
|
2697
|
+
if (s.includes('"$ref":"#"')) return null
|
|
2160
2698
|
}
|
|
2161
2699
|
if (typeof schema === 'boolean') {
|
|
2162
2700
|
return schema
|
|
@@ -2186,8 +2724,30 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
|
2186
2724
|
if (keys.some(k => !supported.includes(k))) return null
|
|
2187
2725
|
}
|
|
2188
2726
|
|
|
2727
|
+
// Build anchors map for $ref/#anchor and $dynamicRef resolution
|
|
2728
|
+
const cRootDefs = schema.$defs || schema.definitions || null
|
|
2729
|
+
const cAnchors = {}
|
|
2730
|
+
if (schema.$dynamicAnchor) cAnchors['#' + schema.$dynamicAnchor] = schema
|
|
2731
|
+
if (schema.$anchor) cAnchors['#' + schema.$anchor] = schema
|
|
2732
|
+
if (cRootDefs) {
|
|
2733
|
+
for (const def of Object.values(cRootDefs)) {
|
|
2734
|
+
if (def && typeof def === 'object') {
|
|
2735
|
+
if (def.$dynamicAnchor) cAnchors['#' + def.$dynamicAnchor] = def
|
|
2736
|
+
if (def.$anchor) cAnchors['#' + def.$anchor] = def
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
if (schemaMap) {
|
|
2741
|
+
for (const ext of schemaMap.values()) {
|
|
2742
|
+
if (ext && typeof ext === 'object') {
|
|
2743
|
+
if (ext.$dynamicAnchor && !cAnchors['#' + ext.$dynamicAnchor]) cAnchors['#' + ext.$dynamicAnchor] = ext
|
|
2744
|
+
if (ext.$anchor && !cAnchors['#' + ext.$anchor]) cAnchors['#' + ext.$anchor] = ext
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2189
2749
|
const ctx = { varCounter: 0, helperCode: [], closureVars: [], closureVals: [],
|
|
2190
|
-
rootDefs:
|
|
2750
|
+
rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: schema }
|
|
2191
2751
|
const lines = []
|
|
2192
2752
|
genCodeC(schema, 'd', '', lines, ctx, '#')
|
|
2193
2753
|
if (lines.length === 0) return () => VALID_RESULT
|
|
@@ -2218,8 +2778,10 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2218
2778
|
if (!schemaPrefix) schemaPrefix = '#'
|
|
2219
2779
|
if (typeof schema !== 'object' || schema === null) return
|
|
2220
2780
|
|
|
2221
|
-
// $ref — resolve local and cross-schema refs
|
|
2781
|
+
// $ref — resolve local, anchor, and cross-schema refs
|
|
2222
2782
|
if (schema.$ref) {
|
|
2783
|
+
// Self-reference "#" — no-op (permissive) to avoid infinite recursion
|
|
2784
|
+
if (schema.$ref === '#') return
|
|
2223
2785
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
2224
2786
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
2225
2787
|
if (ctx.refStack.has(schema.$ref)) return
|
|
@@ -2228,6 +2790,18 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2228
2790
|
ctx.refStack.delete(schema.$ref)
|
|
2229
2791
|
return
|
|
2230
2792
|
}
|
|
2793
|
+
// Anchor ref: "#foo" — resolve via rootDefs or anchors map
|
|
2794
|
+
if (!m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
|
|
2795
|
+
const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
|
|
2796
|
+
const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
|
|
2797
|
+
if (anchorTarget) {
|
|
2798
|
+
if (ctx.refStack.has(schema.$ref)) return
|
|
2799
|
+
ctx.refStack.add(schema.$ref)
|
|
2800
|
+
genCodeC(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
|
|
2801
|
+
ctx.refStack.delete(schema.$ref)
|
|
2802
|
+
return
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2231
2805
|
if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
2232
2806
|
if (ctx.refStack.has(schema.$ref)) return
|
|
2233
2807
|
ctx.refStack.add(schema.$ref)
|
|
@@ -2237,6 +2811,25 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2237
2811
|
}
|
|
2238
2812
|
}
|
|
2239
2813
|
|
|
2814
|
+
// $dynamicRef — resolve via anchors map
|
|
2815
|
+
if (schema.$dynamicRef) {
|
|
2816
|
+
const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
|
|
2817
|
+
if (ctx.anchors && ctx.anchors[anchorKey]) {
|
|
2818
|
+
const target = ctx.anchors[anchorKey]
|
|
2819
|
+
if (target === ctx.rootSchema) {
|
|
2820
|
+
// Self-recursive: bail to non-combined path (combined doesn't support named recursion)
|
|
2821
|
+
// Just skip — the hybrid path will handle this via jsFn + errFn
|
|
2822
|
+
} else {
|
|
2823
|
+
const refKey = '$dynamicRef:' + anchorKey
|
|
2824
|
+
if (!ctx.refStack.has(refKey)) {
|
|
2825
|
+
ctx.refStack.add(refKey)
|
|
2826
|
+
genCodeC(target, v, pathExpr, lines, ctx, schemaPrefix)
|
|
2827
|
+
ctx.refStack.delete(refKey)
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2240
2833
|
const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
|
|
2241
2834
|
let isObj = false, isArr = false, isStr = false, isNum = false
|
|
2242
2835
|
|
|
@@ -2272,7 +2865,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2272
2865
|
case 'string': return `typeof ${v}==='string'`
|
|
2273
2866
|
case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
|
|
2274
2867
|
case 'integer': return `Number.isInteger(${v})`
|
|
2275
|
-
case 'boolean': return `
|
|
2868
|
+
case 'boolean': return `typeof ${v}==='boolean'`
|
|
2276
2869
|
case 'null': return `${v}===null`
|
|
2277
2870
|
default: return 'true'
|
|
2278
2871
|
}
|
|
@@ -2312,7 +2905,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2312
2905
|
const ci = ctx.varCounter++
|
|
2313
2906
|
const canonFn = `_cn${ci}`
|
|
2314
2907
|
ctx.helperCode.push(`const ${canonFn}=function(x){if(x===null||typeof x!=='object')return JSON.stringify(x);if(Array.isArray(x))return'['+x.map(${canonFn}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+${canonFn}(x[k])}).join(',')+'}'};`)
|
|
2315
|
-
lines.push(`if(${canonFn}(${v})!==${canonFn}(${JSON.stringify(cv)})){${fail('const', 'const', `{allowedValue
|
|
2908
|
+
lines.push(`if(${canonFn}(${v})!==${canonFn}(JSON.parse(${JSON.stringify(JSON.stringify(cv))}))){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
|
|
2316
2909
|
}
|
|
2317
2910
|
}
|
|
2318
2911
|
|
|
@@ -2729,11 +3322,20 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
|
|
|
2729
3322
|
refStack.add(schema.$ref)
|
|
2730
3323
|
_collectEval(defs[m[1]], result, defs, schemaMap, refStack)
|
|
2731
3324
|
refStack.delete(schema.$ref)
|
|
2732
|
-
} else if (schemaMap && typeof schemaMap.get === 'function'
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
3325
|
+
} else if (schemaMap && typeof schemaMap.get === 'function') {
|
|
3326
|
+
let resolved = schemaMap.has(schema.$ref) ? schemaMap.get(schema.$ref) : null
|
|
3327
|
+
// Relative URI resolution
|
|
3328
|
+
if (!resolved && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
|
|
3329
|
+
for (const [id, s] of schemaMap) {
|
|
3330
|
+
if (id.endsWith('/' + schema.$ref)) { resolved = s; break }
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
if (resolved) {
|
|
3334
|
+
if (refStack.has(schema.$ref)) { result.dynamic = true; return }
|
|
3335
|
+
refStack.add(schema.$ref)
|
|
3336
|
+
_collectEval(resolved, result, defs, schemaMap, refStack)
|
|
3337
|
+
refStack.delete(schema.$ref)
|
|
3338
|
+
}
|
|
2737
3339
|
}
|
|
2738
3340
|
// In 2020-12, $ref can coexist with siblings — don't return early if there are other keywords
|
|
2739
3341
|
const hasOtherKeywords = Object.keys(schema).some(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema' && k !== '$id')
|
|
@@ -2771,15 +3373,10 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
|
|
|
2771
3373
|
result.allItems = true
|
|
2772
3374
|
}
|
|
2773
3375
|
|
|
2774
|
-
// contains
|
|
2775
|
-
//
|
|
2776
|
-
// In nested schemas: contains marks all items as evaluated
|
|
3376
|
+
// contains: marks matching items as evaluated (not ALL items)
|
|
3377
|
+
// Always set dynamic since which items match depends on the data
|
|
2777
3378
|
if (schema.contains) {
|
|
2778
|
-
|
|
2779
|
-
result.dynamic = true
|
|
2780
|
-
} else {
|
|
2781
|
-
result.allItems = true
|
|
2782
|
-
}
|
|
3379
|
+
result.dynamic = true
|
|
2783
3380
|
}
|
|
2784
3381
|
|
|
2785
3382
|
// unevaluatedProperties: true/schema → all props evaluated (for nested schemas only)
|
|
@@ -2814,6 +3411,18 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
|
|
|
2814
3411
|
_collectEval(schema.if, result, defs, schemaMap, refStack)
|
|
2815
3412
|
if (schema.then) _collectEval(schema.then, result, defs, schemaMap, refStack)
|
|
2816
3413
|
if (schema.else) _collectEval(schema.else, result, defs, schemaMap, refStack)
|
|
3414
|
+
} else if (schema.if) {
|
|
3415
|
+
// Standalone if (no then/else) still produces annotations per spec
|
|
3416
|
+
// Only collect properties and patterns, not deep items (contains etc.)
|
|
3417
|
+
result.dynamic = true
|
|
3418
|
+
if (schema.if.properties) {
|
|
3419
|
+
for (const k of Object.keys(schema.if.properties)) {
|
|
3420
|
+
if (!result.props.includes(k)) result.props.push(k)
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
if (schema.if.patternProperties) {
|
|
3424
|
+
// patternProperties contribute to dynamic evaluation
|
|
3425
|
+
}
|
|
2817
3426
|
}
|
|
2818
3427
|
|
|
2819
3428
|
// dependentSchemas → dynamic
|