ata-validator 0.7.3 → 0.8.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 +1 -1
- package/binding/ata_napi.cpp +32 -4
- package/index.js +53 -23
- package/lib/js-compiler.js +404 -34
- 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 +536 -147
- 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
|
+
const hybridCheck = replaceTopLevel(checkStr + '\n return R')
|
|
809
|
+
hybridBody = `function _validate(d){\n ${hybridCheck}\n }\n return _validate(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)
|
|
@@ -691,8 +884,12 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
691
884
|
// Only when THIS schema has unevaluated keywords directly (not via $ref target)
|
|
692
885
|
const hasSiblings = schema.$ref && (schema.unevaluatedProperties !== undefined || schema.unevaluatedItems !== undefined)
|
|
693
886
|
if (schema.$ref) {
|
|
887
|
+
// Self-reference "#" — no-op (permissive) to avoid infinite recursion
|
|
888
|
+
if (schema.$ref === '#') {
|
|
889
|
+
if (!hasSiblings) return
|
|
890
|
+
}
|
|
694
891
|
// 1. Local ref
|
|
695
|
-
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
892
|
+
const m = schema.$ref !== '#' && schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
696
893
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
697
894
|
if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
|
|
698
895
|
else {
|
|
@@ -701,13 +898,36 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
701
898
|
ctx.refStack.delete(schema.$ref)
|
|
702
899
|
if (!hasSiblings) return
|
|
703
900
|
}
|
|
704
|
-
} else if (
|
|
705
|
-
//
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
901
|
+
} else if (schema.$ref !== '#' && !m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
|
|
902
|
+
// Anchor ref: "#foo" — resolve via rootDefs or anchors map
|
|
903
|
+
const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
|
|
904
|
+
const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
|
|
905
|
+
if (anchorTarget) {
|
|
906
|
+
if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
|
|
907
|
+
else {
|
|
908
|
+
ctx.refStack.add(schema.$ref)
|
|
909
|
+
genCode(anchorTarget, v, lines, ctx, knownType)
|
|
910
|
+
ctx.refStack.delete(schema.$ref)
|
|
911
|
+
if (!hasSiblings) return
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
} else if (schema.$ref !== '#' && ctx.schemaMap) {
|
|
915
|
+
// 2. Cross-schema ref (exact match or relative URI)
|
|
916
|
+
let resolved = ctx.schemaMap.get(schema.$ref)
|
|
917
|
+
if (!resolved && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
|
|
918
|
+
for (const [id, s] of ctx.schemaMap) {
|
|
919
|
+
if (id.endsWith('/' + schema.$ref)) { resolved = s; break }
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (resolved) {
|
|
923
|
+
if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
|
|
924
|
+
else {
|
|
925
|
+
ctx.refStack.add(schema.$ref)
|
|
926
|
+
genCode(resolved, v, lines, ctx, knownType)
|
|
927
|
+
ctx.refStack.delete(schema.$ref)
|
|
928
|
+
if (!hasSiblings) return
|
|
929
|
+
}
|
|
930
|
+
} else {
|
|
711
931
|
if (!hasSiblings) return
|
|
712
932
|
}
|
|
713
933
|
} else {
|
|
@@ -715,6 +935,27 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
715
935
|
}
|
|
716
936
|
}
|
|
717
937
|
|
|
938
|
+
// $dynamicRef — resolve via anchors map
|
|
939
|
+
if (schema.$dynamicRef) {
|
|
940
|
+
const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
|
|
941
|
+
if (ctx.anchors && ctx.anchors[anchorKey]) {
|
|
942
|
+
const target = ctx.anchors[anchorKey]
|
|
943
|
+
if (target === ctx.rootSchema) {
|
|
944
|
+
// Self-recursive: generate _validate(v) call
|
|
945
|
+
ctx.usesRecursion = true
|
|
946
|
+
lines.push(`if(!_validate(${v}))return false`)
|
|
947
|
+
} else {
|
|
948
|
+
// Different schema: inline the target validation
|
|
949
|
+
const refKey = '$dynamicRef:' + anchorKey
|
|
950
|
+
if (!ctx.refStack.has(refKey)) {
|
|
951
|
+
ctx.refStack.add(refKey)
|
|
952
|
+
genCode(target, v, lines, ctx, knownType)
|
|
953
|
+
ctx.refStack.delete(refKey)
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
718
959
|
// Determine the single known type after this schema's type check
|
|
719
960
|
const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
|
|
720
961
|
let effectiveType = knownType
|
|
@@ -1582,8 +1823,8 @@ const FORMAT_CODEGEN = {
|
|
|
1582
1823
|
: `if(typeof ${v}==='string'){const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
|
|
1583
1824
|
},
|
|
1584
1825
|
date: (v, isStr) => isStr
|
|
1585
|
-
? `if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false`
|
|
1586
|
-
: `if(typeof ${v}==='string'
|
|
1826
|
+
? `{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}`
|
|
1827
|
+
: `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
1828
|
uuid: (v, isStr) => isStr
|
|
1588
1829
|
? `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
1830
|
: `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`,
|
|
@@ -1770,15 +2011,46 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
|
|
|
1770
2011
|
if (keys.some(k => !supported.includes(k))) return null
|
|
1771
2012
|
}
|
|
1772
2013
|
|
|
1773
|
-
|
|
2014
|
+
// Build anchors map for $ref/#anchor and $dynamicRef resolution
|
|
2015
|
+
const eRootDefs = schema.$defs || schema.definitions || null
|
|
2016
|
+
const eAnchors = {}
|
|
2017
|
+
if (schema.$dynamicAnchor) eAnchors['#' + schema.$dynamicAnchor] = schema
|
|
2018
|
+
if (schema.$anchor) eAnchors['#' + schema.$anchor] = schema
|
|
2019
|
+
if (eRootDefs) {
|
|
2020
|
+
for (const def of Object.values(eRootDefs)) {
|
|
2021
|
+
if (def && typeof def === 'object') {
|
|
2022
|
+
if (def.$dynamicAnchor) eAnchors['#' + def.$dynamicAnchor] = def
|
|
2023
|
+
if (def.$anchor) eAnchors['#' + def.$anchor] = def
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
if (schemaMap) {
|
|
2028
|
+
for (const ext of schemaMap.values()) {
|
|
2029
|
+
if (ext && typeof ext === 'object') {
|
|
2030
|
+
if (ext.$dynamicAnchor && !eAnchors['#' + ext.$dynamicAnchor]) eAnchors['#' + ext.$dynamicAnchor] = ext
|
|
2031
|
+
if (ext.$anchor && !eAnchors['#' + ext.$anchor]) eAnchors['#' + ext.$anchor] = ext
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema }
|
|
1774
2037
|
const lines = []
|
|
1775
2038
|
genCodeE(schema, 'd', '', lines, ctx, '#')
|
|
1776
2039
|
if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
|
|
1777
2040
|
|
|
1778
|
-
const
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
2041
|
+
const checkStr = lines.join('\n ')
|
|
2042
|
+
let body
|
|
2043
|
+
if (ctx.usesRecursion) {
|
|
2044
|
+
body = `const _e=[];\n ` +
|
|
2045
|
+
(ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
|
|
2046
|
+
`function _validateE(d,_all,_e){\n ${checkStr}\n }\n _validateE(d,_all,_e);\n ` +
|
|
2047
|
+
`return{valid:_e.length===0,errors:_e}`
|
|
2048
|
+
} else {
|
|
2049
|
+
body = `const _e=[];\n ` +
|
|
2050
|
+
(ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
|
|
2051
|
+
checkStr +
|
|
2052
|
+
`\n return{valid:_e.length===0,errors:_e}`
|
|
2053
|
+
}
|
|
1782
2054
|
try {
|
|
1783
2055
|
const fn = new Function('d', '_all', body)
|
|
1784
2056
|
fn._errSource = body
|
|
@@ -1797,6 +2069,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1797
2069
|
|
|
1798
2070
|
// $ref — resolve local and cross-schema refs
|
|
1799
2071
|
if (schema.$ref) {
|
|
2072
|
+
// Self-reference "#" — no-op (permissive) to avoid infinite recursion
|
|
2073
|
+
if (schema.$ref === '#') return
|
|
1800
2074
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
1801
2075
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
1802
2076
|
if (ctx.refStack.has(schema.$ref)) return
|
|
@@ -1805,6 +2079,18 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1805
2079
|
ctx.refStack.delete(schema.$ref)
|
|
1806
2080
|
return
|
|
1807
2081
|
}
|
|
2082
|
+
// Anchor ref: "#foo" — resolve via rootDefs or anchors map
|
|
2083
|
+
if (!m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
|
|
2084
|
+
const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
|
|
2085
|
+
const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
|
|
2086
|
+
if (anchorTarget) {
|
|
2087
|
+
if (ctx.refStack.has(schema.$ref)) return
|
|
2088
|
+
ctx.refStack.add(schema.$ref)
|
|
2089
|
+
genCodeE(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
|
|
2090
|
+
ctx.refStack.delete(schema.$ref)
|
|
2091
|
+
return
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
1808
2094
|
if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
1809
2095
|
if (ctx.refStack.has(schema.$ref)) return
|
|
1810
2096
|
ctx.refStack.add(schema.$ref)
|
|
@@ -1814,6 +2100,26 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1814
2100
|
}
|
|
1815
2101
|
}
|
|
1816
2102
|
|
|
2103
|
+
// $dynamicRef — resolve via anchors map
|
|
2104
|
+
if (schema.$dynamicRef) {
|
|
2105
|
+
const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
|
|
2106
|
+
if (ctx.anchors && ctx.anchors[anchorKey]) {
|
|
2107
|
+
const target = ctx.anchors[anchorKey]
|
|
2108
|
+
if (target === ctx.rootSchema) {
|
|
2109
|
+
// Self-recursive: generate _validateE call
|
|
2110
|
+
ctx.usesRecursion = true
|
|
2111
|
+
lines.push(`_validateE(${v},_all,_e)`)
|
|
2112
|
+
} else {
|
|
2113
|
+
const refKey = '$dynamicRef:' + anchorKey
|
|
2114
|
+
if (!ctx.refStack.has(refKey)) {
|
|
2115
|
+
ctx.refStack.add(refKey)
|
|
2116
|
+
genCodeE(target, v, pathExpr, lines, ctx, schemaPrefix)
|
|
2117
|
+
ctx.refStack.delete(refKey)
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
1817
2123
|
const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
|
|
1818
2124
|
if (types) {
|
|
1819
2125
|
const conds = types.map(t => {
|
|
@@ -1864,8 +2170,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1864
2170
|
const ci = ctx.varCounter++
|
|
1865
2171
|
const canonFn = `_cnE${ci}`
|
|
1866
2172
|
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
|
|
2173
|
+
const expected = canonFn + '(JSON.parse(' + JSON.stringify(JSON.stringify(cv)) + '))'
|
|
2174
|
+
lines.push(`if(${canonFn}(${v})!==${expected}){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
|
|
1869
2175
|
}
|
|
1870
2176
|
}
|
|
1871
2177
|
|
|
@@ -2186,8 +2492,30 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
|
2186
2492
|
if (keys.some(k => !supported.includes(k))) return null
|
|
2187
2493
|
}
|
|
2188
2494
|
|
|
2495
|
+
// Build anchors map for $ref/#anchor and $dynamicRef resolution
|
|
2496
|
+
const cRootDefs = schema.$defs || schema.definitions || null
|
|
2497
|
+
const cAnchors = {}
|
|
2498
|
+
if (schema.$dynamicAnchor) cAnchors['#' + schema.$dynamicAnchor] = schema
|
|
2499
|
+
if (schema.$anchor) cAnchors['#' + schema.$anchor] = schema
|
|
2500
|
+
if (cRootDefs) {
|
|
2501
|
+
for (const def of Object.values(cRootDefs)) {
|
|
2502
|
+
if (def && typeof def === 'object') {
|
|
2503
|
+
if (def.$dynamicAnchor) cAnchors['#' + def.$dynamicAnchor] = def
|
|
2504
|
+
if (def.$anchor) cAnchors['#' + def.$anchor] = def
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
if (schemaMap) {
|
|
2509
|
+
for (const ext of schemaMap.values()) {
|
|
2510
|
+
if (ext && typeof ext === 'object') {
|
|
2511
|
+
if (ext.$dynamicAnchor && !cAnchors['#' + ext.$dynamicAnchor]) cAnchors['#' + ext.$dynamicAnchor] = ext
|
|
2512
|
+
if (ext.$anchor && !cAnchors['#' + ext.$anchor]) cAnchors['#' + ext.$anchor] = ext
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2189
2517
|
const ctx = { varCounter: 0, helperCode: [], closureVars: [], closureVals: [],
|
|
2190
|
-
rootDefs:
|
|
2518
|
+
rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: schema }
|
|
2191
2519
|
const lines = []
|
|
2192
2520
|
genCodeC(schema, 'd', '', lines, ctx, '#')
|
|
2193
2521
|
if (lines.length === 0) return () => VALID_RESULT
|
|
@@ -2218,8 +2546,10 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2218
2546
|
if (!schemaPrefix) schemaPrefix = '#'
|
|
2219
2547
|
if (typeof schema !== 'object' || schema === null) return
|
|
2220
2548
|
|
|
2221
|
-
// $ref — resolve local and cross-schema refs
|
|
2549
|
+
// $ref — resolve local, anchor, and cross-schema refs
|
|
2222
2550
|
if (schema.$ref) {
|
|
2551
|
+
// Self-reference "#" — no-op (permissive) to avoid infinite recursion
|
|
2552
|
+
if (schema.$ref === '#') return
|
|
2223
2553
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
2224
2554
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
2225
2555
|
if (ctx.refStack.has(schema.$ref)) return
|
|
@@ -2228,6 +2558,18 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2228
2558
|
ctx.refStack.delete(schema.$ref)
|
|
2229
2559
|
return
|
|
2230
2560
|
}
|
|
2561
|
+
// Anchor ref: "#foo" — resolve via rootDefs or anchors map
|
|
2562
|
+
if (!m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
|
|
2563
|
+
const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
|
|
2564
|
+
const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
|
|
2565
|
+
if (anchorTarget) {
|
|
2566
|
+
if (ctx.refStack.has(schema.$ref)) return
|
|
2567
|
+
ctx.refStack.add(schema.$ref)
|
|
2568
|
+
genCodeC(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
|
|
2569
|
+
ctx.refStack.delete(schema.$ref)
|
|
2570
|
+
return
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2231
2573
|
if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
2232
2574
|
if (ctx.refStack.has(schema.$ref)) return
|
|
2233
2575
|
ctx.refStack.add(schema.$ref)
|
|
@@ -2237,6 +2579,25 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2237
2579
|
}
|
|
2238
2580
|
}
|
|
2239
2581
|
|
|
2582
|
+
// $dynamicRef — resolve via anchors map
|
|
2583
|
+
if (schema.$dynamicRef) {
|
|
2584
|
+
const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
|
|
2585
|
+
if (ctx.anchors && ctx.anchors[anchorKey]) {
|
|
2586
|
+
const target = ctx.anchors[anchorKey]
|
|
2587
|
+
if (target === ctx.rootSchema) {
|
|
2588
|
+
// Self-recursive: bail to non-combined path (combined doesn't support named recursion)
|
|
2589
|
+
// Just skip — the hybrid path will handle this via jsFn + errFn
|
|
2590
|
+
} else {
|
|
2591
|
+
const refKey = '$dynamicRef:' + anchorKey
|
|
2592
|
+
if (!ctx.refStack.has(refKey)) {
|
|
2593
|
+
ctx.refStack.add(refKey)
|
|
2594
|
+
genCodeC(target, v, pathExpr, lines, ctx, schemaPrefix)
|
|
2595
|
+
ctx.refStack.delete(refKey)
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2240
2601
|
const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
|
|
2241
2602
|
let isObj = false, isArr = false, isStr = false, isNum = false
|
|
2242
2603
|
|
|
@@ -2312,7 +2673,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2312
2673
|
const ci = ctx.varCounter++
|
|
2313
2674
|
const canonFn = `_cn${ci}`
|
|
2314
2675
|
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
|
|
2676
|
+
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
2677
|
}
|
|
2317
2678
|
}
|
|
2318
2679
|
|
|
@@ -2729,11 +3090,20 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
|
|
|
2729
3090
|
refStack.add(schema.$ref)
|
|
2730
3091
|
_collectEval(defs[m[1]], result, defs, schemaMap, refStack)
|
|
2731
3092
|
refStack.delete(schema.$ref)
|
|
2732
|
-
} else if (schemaMap && typeof schemaMap.get === 'function'
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
3093
|
+
} else if (schemaMap && typeof schemaMap.get === 'function') {
|
|
3094
|
+
let resolved = schemaMap.has(schema.$ref) ? schemaMap.get(schema.$ref) : null
|
|
3095
|
+
// Relative URI resolution
|
|
3096
|
+
if (!resolved && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
|
|
3097
|
+
for (const [id, s] of schemaMap) {
|
|
3098
|
+
if (id.endsWith('/' + schema.$ref)) { resolved = s; break }
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
if (resolved) {
|
|
3102
|
+
if (refStack.has(schema.$ref)) { result.dynamic = true; return }
|
|
3103
|
+
refStack.add(schema.$ref)
|
|
3104
|
+
_collectEval(resolved, result, defs, schemaMap, refStack)
|
|
3105
|
+
refStack.delete(schema.$ref)
|
|
3106
|
+
}
|
|
2737
3107
|
}
|
|
2738
3108
|
// In 2020-12, $ref can coexist with siblings — don't return early if there are other keywords
|
|
2739
3109
|
const hasOtherKeywords = Object.keys(schema).some(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema' && k !== '$id')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Ultra-fast JSON Schema validator. 4.7x faster validation, 1,800x faster compilation. Works without native addon. Cross-schema $ref, Draft 2020-12 + Draft 7, V8-optimized JS codegen, simdjson, RE2, multi-core. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"test:browser": "node tests/test_browser.js",
|
|
39
39
|
"bench": "node benchmark/bench_large.js",
|
|
40
40
|
"fuzz": "node tests/fuzz_differential.js",
|
|
41
|
-
"fuzz:long": "FUZZ_ITERATIONS=100000 node tests/fuzz_differential.js"
|
|
41
|
+
"fuzz:long": "FUZZ_ITERATIONS=100000 node tests/fuzz_differential.js",
|
|
42
|
+
"test:json-suite": "node tests/run_json_test_suite.js"
|
|
42
43
|
},
|
|
43
44
|
"keywords": [
|
|
44
45
|
"json",
|
|
Binary file
|
|
Binary file
|