ata-validator 0.4.15 → 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -21
- package/index.d.ts +4 -0
- package/index.js +147 -31
- package/lib/draft7.js +82 -0
- package/lib/js-compiler.js +563 -62
- package/package.json +7 -3
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/prebuilds/linux-arm64/ata-validator.node +0 -0
- package/prebuilds/linux-x64/ata-validator.node +0 -0
package/lib/js-compiler.js
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
// Closure-based validator — no new Function() or eval().
|
|
5
5
|
// Returns null if the schema is too complex for JS compilation.
|
|
6
6
|
|
|
7
|
-
function compileToJS(schema, defs) {
|
|
7
|
+
function compileToJS(schema, defs, schemaMap) {
|
|
8
8
|
if (typeof schema === 'boolean') {
|
|
9
9
|
return schema ? () => true : () => false
|
|
10
10
|
}
|
|
11
11
|
if (typeof schema !== 'object' || schema === null) return null
|
|
12
12
|
|
|
13
13
|
// Bail if schema has edge cases that JS fast path gets wrong
|
|
14
|
-
if (!defs && !codegenSafe(schema)) return null
|
|
14
|
+
if (!defs && !codegenSafe(schema, schemaMap)) return null
|
|
15
15
|
|
|
16
16
|
// Collect $defs early so sub-schemas can resolve $ref
|
|
17
17
|
const rootDefs = defs || collectDefs(schema)
|
|
@@ -27,7 +27,7 @@ function compileToJS(schema, defs) {
|
|
|
27
27
|
|
|
28
28
|
// $ref (local only)
|
|
29
29
|
if (schema.$ref) {
|
|
30
|
-
const refFn = resolveRef(schema.$ref, rootDefs)
|
|
30
|
+
const refFn = resolveRef(schema.$ref, rootDefs, schemaMap)
|
|
31
31
|
if (!refFn) return null
|
|
32
32
|
checks.push(refFn)
|
|
33
33
|
}
|
|
@@ -372,18 +372,23 @@ function collectDefs(schema) {
|
|
|
372
372
|
return defs
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
function resolveRef(ref, defs) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
375
|
+
function resolveRef(ref, defs, schemaMap) {
|
|
376
|
+
// 1. Local ref
|
|
377
|
+
if (defs) {
|
|
378
|
+
const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
379
|
+
if (m) {
|
|
380
|
+
const name = m[1]
|
|
381
|
+
const entry = defs[name]
|
|
382
|
+
if (entry) return (d) => { const fn = entry.fn; return fn ? fn(d) : true }
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// 2. Cross-schema ref
|
|
386
|
+
if (schemaMap && schemaMap.has(ref)) {
|
|
387
|
+
const resolved = schemaMap.get(ref)
|
|
388
|
+
const fn = compileToJS(resolved, null, schemaMap)
|
|
389
|
+
return fn || (() => true)
|
|
386
390
|
}
|
|
391
|
+
return null
|
|
387
392
|
}
|
|
388
393
|
|
|
389
394
|
function buildTypeCheck(types) {
|
|
@@ -423,7 +428,7 @@ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
|
|
|
423
428
|
|
|
424
429
|
// Recursively check if a schema can be safely compiled to JS codegen.
|
|
425
430
|
// Returns false if any sub-schema contains features codegen gets wrong.
|
|
426
|
-
function codegenSafe(schema) {
|
|
431
|
+
function codegenSafe(schema, schemaMap) {
|
|
427
432
|
if (typeof schema === 'boolean') return true
|
|
428
433
|
if (typeof schema !== 'object' || schema === null) return true
|
|
429
434
|
|
|
@@ -433,7 +438,7 @@ function codegenSafe(schema) {
|
|
|
433
438
|
if (schema.properties) {
|
|
434
439
|
for (const v of Object.values(schema.properties)) {
|
|
435
440
|
if (typeof v === 'boolean') return false
|
|
436
|
-
if (!codegenSafe(v)) return false
|
|
441
|
+
if (!codegenSafe(v, schemaMap)) return false
|
|
437
442
|
}
|
|
438
443
|
}
|
|
439
444
|
|
|
@@ -453,14 +458,15 @@ function codegenSafe(schema) {
|
|
|
453
458
|
// Unicode property escapes in pattern need 'u' flag — codegen uses RegExp without it
|
|
454
459
|
if (schema.pattern && /\\[pP]\{/.test(schema.pattern)) return false
|
|
455
460
|
|
|
456
|
-
// $ref — allow
|
|
461
|
+
// $ref — allow local refs (#/$defs/Name) and non-local refs if in schemaMap
|
|
457
462
|
if (schema.$ref) {
|
|
458
|
-
|
|
463
|
+
const isLocal = /^#\/(?:\$defs|definitions)\/[^/]+$/.test(schema.$ref)
|
|
464
|
+
const isResolvable = !isLocal && schemaMap && schemaMap.has(schema.$ref)
|
|
465
|
+
if (!isLocal && !isResolvable) return false
|
|
459
466
|
// Bail if $ref has sibling keywords (complex interaction)
|
|
460
|
-
const siblings = Object.keys(schema).filter(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema')
|
|
467
|
+
const siblings = Object.keys(schema).filter(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema' && k !== '$id')
|
|
461
468
|
if (siblings.length > 0) return false
|
|
462
469
|
}
|
|
463
|
-
if (schema.$id) return false
|
|
464
470
|
|
|
465
471
|
// additionalProperties as schema — bail entirely, too many edge cases with allOf interaction
|
|
466
472
|
if (typeof schema.additionalProperties === 'object') return false
|
|
@@ -476,8 +482,9 @@ function codegenSafe(schema) {
|
|
|
476
482
|
if (/[~/"']/.test(name)) return false // special chars in def name
|
|
477
483
|
if (typeof def === 'boolean') return false
|
|
478
484
|
if (typeof def === 'object' && def !== null) {
|
|
485
|
+
if (def.$id) return false // $id in $defs creates new resolution scope — bail
|
|
479
486
|
if (def.$ref) return false // nested ref chain — bail
|
|
480
|
-
if (!codegenSafe(def)) return false
|
|
487
|
+
if (!codegenSafe(def, schemaMap)) return false
|
|
481
488
|
}
|
|
482
489
|
}
|
|
483
490
|
}
|
|
@@ -495,7 +502,7 @@ function codegenSafe(schema) {
|
|
|
495
502
|
for (const s of subs) {
|
|
496
503
|
if (s === undefined || s === null) continue
|
|
497
504
|
if (typeof s === 'boolean') return false // boolean sub-schema
|
|
498
|
-
if (!codegenSafe(s)) return false
|
|
505
|
+
if (!codegenSafe(s, schemaMap)) return false
|
|
499
506
|
}
|
|
500
507
|
|
|
501
508
|
return true
|
|
@@ -503,22 +510,42 @@ function codegenSafe(schema) {
|
|
|
503
510
|
|
|
504
511
|
// --- Codegen mode: generates a single Function (NOT CSP-safe) ---
|
|
505
512
|
// This matches ajv's approach: one monolithic function, V8 JIT fully inlines it
|
|
506
|
-
function compileToJSCodegen(schema) {
|
|
513
|
+
function compileToJSCodegen(schema, schemaMap) {
|
|
507
514
|
if (typeof schema === 'boolean') return schema ? () => true : () => false
|
|
508
515
|
if (typeof schema !== 'object' || schema === null) return null
|
|
509
516
|
|
|
510
517
|
// Bail if schema contains features that codegen can't handle correctly
|
|
511
|
-
if (!codegenSafe(schema)) return null
|
|
518
|
+
if (!codegenSafe(schema, schemaMap)) return null
|
|
512
519
|
|
|
513
520
|
// Collect defs for $ref resolution
|
|
514
521
|
const rootDefs = schema.$defs || schema.definitions || null
|
|
515
522
|
|
|
516
523
|
// Bail only on truly unsupported features
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
524
|
+
// patternProperties: bail only on boolean sub-schemas or unicode property escapes
|
|
525
|
+
if (schema.patternProperties) {
|
|
526
|
+
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
527
|
+
if (typeof sub === 'boolean') return null
|
|
528
|
+
if (/\\[pP]\{/.test(pat)) return null
|
|
529
|
+
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// dependentSchemas: bail on boolean sub-schemas
|
|
533
|
+
if (schema.dependentSchemas) {
|
|
534
|
+
for (const sub of Object.values(schema.dependentSchemas)) {
|
|
535
|
+
if (typeof sub === 'boolean') return null
|
|
536
|
+
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// propertyNames: only codegen simple string constraints
|
|
540
|
+
if (schema.propertyNames) {
|
|
541
|
+
if (typeof schema.propertyNames === 'boolean') return null
|
|
542
|
+
const pn = schema.propertyNames
|
|
543
|
+
const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
|
|
544
|
+
const keys = Object.keys(pn).filter(k => k !== '$schema')
|
|
545
|
+
if (keys.some(k => !supported.includes(k))) return null
|
|
546
|
+
}
|
|
520
547
|
|
|
521
|
-
const ctx = { varCounter: 0, helpers: [], helperCode: [], closureVars: [], closureVals: [], rootDefs, refStack: new Set() }
|
|
548
|
+
const ctx = { varCounter: 0, helpers: [], helperCode: [], closureVars: [], closureVals: [], rootDefs, refStack: new Set(), schemaMap: schemaMap || null }
|
|
522
549
|
const lines = []
|
|
523
550
|
genCode(schema, 'd', lines, ctx)
|
|
524
551
|
if (lines.length === 0) return () => true
|
|
@@ -614,16 +641,25 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
614
641
|
if (typeof schema !== 'object' || schema === null) return
|
|
615
642
|
|
|
616
643
|
// $ref — guard against circular references
|
|
617
|
-
if (schema.$ref
|
|
644
|
+
if (schema.$ref) {
|
|
645
|
+
// 1. Local ref
|
|
618
646
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
619
|
-
if (m && ctx.rootDefs[m[1]]) {
|
|
620
|
-
if (ctx.refStack.has(schema.$ref)) return
|
|
647
|
+
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
648
|
+
if (ctx.refStack.has(schema.$ref)) return
|
|
621
649
|
ctx.refStack.add(schema.$ref)
|
|
622
650
|
genCode(ctx.rootDefs[m[1]], v, lines, ctx, knownType)
|
|
623
651
|
ctx.refStack.delete(schema.$ref)
|
|
624
|
-
} else {
|
|
625
652
|
return
|
|
626
653
|
}
|
|
654
|
+
// 2. Cross-schema ref
|
|
655
|
+
if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
656
|
+
if (ctx.refStack.has(schema.$ref)) return
|
|
657
|
+
ctx.refStack.add(schema.$ref)
|
|
658
|
+
genCode(ctx.schemaMap.get(schema.$ref), v, lines, ctx, knownType)
|
|
659
|
+
ctx.refStack.delete(schema.$ref)
|
|
660
|
+
return
|
|
661
|
+
}
|
|
662
|
+
return
|
|
627
663
|
}
|
|
628
664
|
|
|
629
665
|
// Determine the single known type after this schema's type check
|
|
@@ -757,19 +793,15 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
757
793
|
|
|
758
794
|
// additionalProperties -- deferred to end of function for better V8 optimization
|
|
759
795
|
// (type checks run first in hot path, expensive prop count check last)
|
|
760
|
-
if
|
|
796
|
+
// Skip if patternProperties is present — it will handle additionalProperties in a unified loop
|
|
797
|
+
if (schema.additionalProperties === false && schema.properties && !schema.patternProperties) {
|
|
761
798
|
const propCount = Object.keys(schema.properties).length
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
const ci = ctx.varCounter++
|
|
769
|
-
const inner = `const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++)if(!_a${ci}.has(_k${ci}[_i]))return false`
|
|
770
|
-
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
771
|
-
ctx.deferredChecks.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
772
|
-
}
|
|
799
|
+
const allRequired = schema.required && schema.required.length === propCount
|
|
800
|
+
const inner = allRequired
|
|
801
|
+
? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
|
|
802
|
+
: `for(var _k in ${v})if(${Object.keys(schema.properties).map(k => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
|
|
803
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
804
|
+
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
773
805
|
}
|
|
774
806
|
|
|
775
807
|
// dependentRequired
|
|
@@ -780,6 +812,162 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
780
812
|
}
|
|
781
813
|
}
|
|
782
814
|
|
|
815
|
+
// patternProperties + propertyNames + additionalProperties — unified key iteration
|
|
816
|
+
// Merges up to 3 separate for..in loops into one pass.
|
|
817
|
+
if (schema.patternProperties) {
|
|
818
|
+
const ppEntries = Object.entries(schema.patternProperties)
|
|
819
|
+
const pn = schema.propertyNames && typeof schema.propertyNames === 'object' ? schema.propertyNames : null
|
|
820
|
+
const pi = ctx.varCounter++
|
|
821
|
+
const kVar = `_ppk${pi}`
|
|
822
|
+
|
|
823
|
+
// Build pattern matchers: prefer charCodeAt for simple prefixes, fall back to regex
|
|
824
|
+
const matchers = []
|
|
825
|
+
for (const [pat] of ppEntries) {
|
|
826
|
+
const fast = fastPrefixCheck(pat, kVar)
|
|
827
|
+
if (fast) {
|
|
828
|
+
matchers.push({ check: fast })
|
|
829
|
+
} else {
|
|
830
|
+
const ri = ctx.varCounter++
|
|
831
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
832
|
+
ctx.closureVals.push(new RegExp(pat))
|
|
833
|
+
matchers.push({ check: `_re${ri}.test(${kVar})` })
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Build sub-schema validators as closure vars
|
|
838
|
+
for (let i = 0; i < ppEntries.length; i++) {
|
|
839
|
+
const [, sub] = ppEntries[i]
|
|
840
|
+
const subLines = []
|
|
841
|
+
genCode(sub, `_ppv`, subLines, ctx)
|
|
842
|
+
const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
|
|
843
|
+
const fnVar = `_ppf${pi}_${i}`
|
|
844
|
+
ctx.closureVars.push(fnVar)
|
|
845
|
+
ctx.closureVals.push(new Function('_ppv', fnBody))
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
849
|
+
|
|
850
|
+
if (schema.additionalProperties === false && schema.properties) {
|
|
851
|
+
// Unified loop: properties + patterns + propertyNames + additionalProperties
|
|
852
|
+
ctx._ppHandledAdditional = true
|
|
853
|
+
ctx._ppHandledPropertyNames = !!pn
|
|
854
|
+
const propKeys = Object.keys(schema.properties)
|
|
855
|
+
// Inline key comparison instead of Set.has for small property counts (faster, no allocation)
|
|
856
|
+
const keyCheck = propKeys.length <= 8
|
|
857
|
+
? propKeys.map(k => `${kVar}===${JSON.stringify(k)}`).join('||')
|
|
858
|
+
: null
|
|
859
|
+
if (!keyCheck) {
|
|
860
|
+
const allowedSet = `_as${pi}`
|
|
861
|
+
ctx.closureVars.push(allowedSet)
|
|
862
|
+
ctx.closureVals.push(new Set(propKeys))
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
lines.push(`${guard}{for(const ${kVar} in ${v}){`)
|
|
866
|
+
// propertyNames checks (merged into same loop)
|
|
867
|
+
if (pn) {
|
|
868
|
+
if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength})return false`)
|
|
869
|
+
if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength})return false`)
|
|
870
|
+
if (pn.pattern) {
|
|
871
|
+
const fast = fastPrefixCheck(pn.pattern, kVar)
|
|
872
|
+
if (fast) {
|
|
873
|
+
lines.push(`if(!(${fast}))return false`)
|
|
874
|
+
} else {
|
|
875
|
+
const ri = ctx.varCounter++
|
|
876
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
877
|
+
ctx.closureVals.push(new RegExp(pn.pattern))
|
|
878
|
+
lines.push(`if(!_re${ri}.test(${kVar}))return false`)
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)})return false`)
|
|
882
|
+
if (pn.enum) {
|
|
883
|
+
const ei = ctx.varCounter++
|
|
884
|
+
ctx.closureVars.push(`_es${ei}`)
|
|
885
|
+
ctx.closureVals.push(new Set(pn.enum))
|
|
886
|
+
lines.push(`if(!_es${ei}.has(${kVar}))return false`)
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// Check: is key in declared properties?
|
|
890
|
+
const matchExpr = keyCheck || `_as${pi}.has(${kVar})`
|
|
891
|
+
lines.push(`let _m${pi}=${matchExpr}`)
|
|
892
|
+
// Check pattern matches
|
|
893
|
+
for (let i = 0; i < ppEntries.length; i++) {
|
|
894
|
+
lines.push(`if(${matchers[i].check}){_m${pi}=true;if(!_ppf${pi}_${i}(${v}[${kVar}]))return false}`)
|
|
895
|
+
}
|
|
896
|
+
lines.push(`if(!_m${pi})return false`)
|
|
897
|
+
lines.push(`}}`)
|
|
898
|
+
} else {
|
|
899
|
+
// No additionalProperties: validate matching keys + propertyNames
|
|
900
|
+
ctx._ppHandledPropertyNames = !!pn
|
|
901
|
+
lines.push(`${guard}{for(const ${kVar} in ${v}){`)
|
|
902
|
+
// propertyNames checks (merged)
|
|
903
|
+
if (pn) {
|
|
904
|
+
if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength})return false`)
|
|
905
|
+
if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength})return false`)
|
|
906
|
+
if (pn.pattern) {
|
|
907
|
+
const fast = fastPrefixCheck(pn.pattern, kVar)
|
|
908
|
+
if (fast) {
|
|
909
|
+
lines.push(`if(!(${fast}))return false`)
|
|
910
|
+
} else {
|
|
911
|
+
const ri = ctx.varCounter++
|
|
912
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
913
|
+
ctx.closureVals.push(new RegExp(pn.pattern))
|
|
914
|
+
lines.push(`if(!_re${ri}.test(${kVar}))return false`)
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)})return false`)
|
|
918
|
+
if (pn.enum) {
|
|
919
|
+
const ei = ctx.varCounter++
|
|
920
|
+
ctx.closureVars.push(`_es${ei}`)
|
|
921
|
+
ctx.closureVals.push(new Set(pn.enum))
|
|
922
|
+
lines.push(`if(!_es${ei}.has(${kVar}))return false`)
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
for (let i = 0; i < ppEntries.length; i++) {
|
|
926
|
+
lines.push(`if(${matchers[i].check}&&!_ppf${pi}_${i}(${v}[${kVar}]))return false`)
|
|
927
|
+
}
|
|
928
|
+
lines.push(`}}`)
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// dependentSchemas
|
|
933
|
+
if (schema.dependentSchemas) {
|
|
934
|
+
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
935
|
+
const guard = isObj ? '' : `typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&`
|
|
936
|
+
lines.push(`if(${guard}${JSON.stringify(key)} in ${v}){`)
|
|
937
|
+
genCode(depSchema, v, lines, ctx, effectiveType)
|
|
938
|
+
lines.push(`}`)
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// propertyNames — only emit if not already merged into patternProperties loop
|
|
943
|
+
if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ctx._ppHandledPropertyNames) {
|
|
944
|
+
const pn = schema.propertyNames
|
|
945
|
+
const ki = ctx.varCounter++
|
|
946
|
+
const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
947
|
+
lines.push(`${guard}{for(const _k${ki} in ${v}){`)
|
|
948
|
+
if (pn.minLength !== undefined) lines.push(`if(_k${ki}.length<${pn.minLength})return false`)
|
|
949
|
+
if (pn.maxLength !== undefined) lines.push(`if(_k${ki}.length>${pn.maxLength})return false`)
|
|
950
|
+
if (pn.pattern) {
|
|
951
|
+
const fast = fastPrefixCheck(pn.pattern, `_k${ki}`)
|
|
952
|
+
if (fast) {
|
|
953
|
+
lines.push(`if(!(${fast}))return false`)
|
|
954
|
+
} else {
|
|
955
|
+
const ri = ctx.varCounter++
|
|
956
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
957
|
+
ctx.closureVals.push(new RegExp(pn.pattern))
|
|
958
|
+
lines.push(`if(!_re${ri}.test(_k${ki}))return false`)
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (pn.const !== undefined) lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)})return false`)
|
|
962
|
+
if (pn.enum) {
|
|
963
|
+
const ei = ctx.varCounter++
|
|
964
|
+
ctx.closureVars.push(`_es${ei}`)
|
|
965
|
+
ctx.closureVals.push(new Set(pn.enum))
|
|
966
|
+
lines.push(`if(!_es${ei}.has(_k${ki}))return false`)
|
|
967
|
+
}
|
|
968
|
+
lines.push(`}}`)
|
|
969
|
+
}
|
|
970
|
+
|
|
783
971
|
// properties — use hoisted vars for required props, hoist optional to locals too
|
|
784
972
|
if (schema.properties) {
|
|
785
973
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
@@ -937,28 +1125,86 @@ const FORMAT_CODEGEN = {
|
|
|
937
1125
|
uuid: (v, isStr) => isStr
|
|
938
1126
|
? `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`
|
|
939
1127
|
: `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`,
|
|
1128
|
+
'date-time': (v, isStr) => isStr
|
|
1129
|
+
? `if(!/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$/.test(${v})||isNaN(Date.parse(${v})))return false`
|
|
1130
|
+
: `if(typeof ${v}==='string'&&(!/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$/.test(${v})||isNaN(Date.parse(${v}))))return false`,
|
|
1131
|
+
time: (v, isStr) => isStr
|
|
1132
|
+
? `if(!/^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$/.test(${v}))return false`
|
|
1133
|
+
: `if(typeof ${v}==='string'&&!/^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$/.test(${v}))return false`,
|
|
1134
|
+
duration: (v, isStr) => isStr
|
|
1135
|
+
? `if(!/^P(?:\\d+Y)?(?:\\d+M)?(?:\\d+W)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?$/.test(${v})||${v}==='P')return false`
|
|
1136
|
+
: `if(typeof ${v}==='string'&&(!/^P(?:\\d+Y)?(?:\\d+M)?(?:\\d+W)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?$/.test(${v})||${v}==='P'))return false`,
|
|
1137
|
+
uri: (v, isStr) => isStr
|
|
1138
|
+
? `if(!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(${v}))return false`
|
|
1139
|
+
: `if(typeof ${v}==='string'&&!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(${v}))return false`,
|
|
1140
|
+
'uri-reference': (v, isStr) => isStr
|
|
1141
|
+
? `if(${v}===''||/\\s/.test(${v}))return false`
|
|
1142
|
+
: `if(typeof ${v}==='string'&&(${v}===''||/\\s/.test(${v})))return false`,
|
|
940
1143
|
ipv4: (v, isStr) => isStr
|
|
941
1144
|
? `{const _p=${v}.split('.');if(_p.length!==4||!_p.every(function(n){var x=+n;return x>=0&&x<=255&&String(x)===n}))return false}`
|
|
942
1145
|
: `if(typeof ${v}==='string'){const _p=${v}.split('.');if(_p.length!==4||!_p.every(function(n){var x=+n;return x>=0&&x<=255&&String(x)===n}))return false}`,
|
|
1146
|
+
ipv6: (v, isStr) => isStr
|
|
1147
|
+
? `{const _s=${v};if(_s===''||!/^[0-9a-fA-F:]+$/.test(_s)||_s.split(':').length<3||_s.split(':').length>8)return false}`
|
|
1148
|
+
: `if(typeof ${v}==='string'){const _s=${v};if(_s===''||!/^[0-9a-fA-F:]+$/.test(_s)||_s.split(':').length<3||_s.split(':').length>8)return false}`,
|
|
1149
|
+
hostname: (v, isStr) => isStr
|
|
1150
|
+
? `if(!/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(${v}))return false`
|
|
1151
|
+
: `if(typeof ${v}==='string'&&!/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(${v}))return false`,
|
|
943
1152
|
}
|
|
944
1153
|
|
|
945
1154
|
// Safe key escaping: use JSON.stringify to handle all special chars (newlines, null bytes, etc.)
|
|
946
1155
|
function esc(s) { return JSON.stringify(s).slice(1, -1) }
|
|
947
1156
|
|
|
1157
|
+
// Detect simple prefix patterns like "^x-", "^_", "^prefix" and generate fast charCodeAt checks
|
|
1158
|
+
// Returns a JS expression string or null if pattern is too complex
|
|
1159
|
+
function fastPrefixCheck(pattern, keyVar) {
|
|
1160
|
+
// Match patterns like ^literal (no regex metacharacters after ^)
|
|
1161
|
+
const m = pattern.match(/^\^([a-zA-Z0-9_\-./]+)$/)
|
|
1162
|
+
if (!m) return null
|
|
1163
|
+
const prefix = m[1]
|
|
1164
|
+
if (prefix.length === 0 || prefix.length > 8) return null // too long = diminishing returns
|
|
1165
|
+
if (prefix.length === 1) {
|
|
1166
|
+
return `${keyVar}.charCodeAt(0)===${prefix.charCodeAt(0)}`
|
|
1167
|
+
}
|
|
1168
|
+
if (prefix.length === 2) {
|
|
1169
|
+
return `${keyVar}.charCodeAt(0)===${prefix.charCodeAt(0)}&&${keyVar}.charCodeAt(1)===${prefix.charCodeAt(1)}`
|
|
1170
|
+
}
|
|
1171
|
+
// For longer prefixes, startsWith is cleaner and still faster than regex
|
|
1172
|
+
return `${keyVar}.startsWith(${JSON.stringify(prefix)})`
|
|
1173
|
+
}
|
|
1174
|
+
|
|
948
1175
|
// --- Error-collecting codegen: same checks, but pushes errors instead of returning false ---
|
|
949
1176
|
// Returns a function: (data, allErrors) => { valid, errors }
|
|
950
1177
|
// Valid path is still fast — only error path does extra work.
|
|
951
|
-
function compileToJSCodegenWithErrors(schema) {
|
|
1178
|
+
function compileToJSCodegenWithErrors(schema, schemaMap) {
|
|
952
1179
|
if (typeof schema === 'boolean') {
|
|
953
1180
|
return schema
|
|
954
1181
|
? () => ({ valid: true, errors: [] })
|
|
955
1182
|
: () => ({ valid: false, errors: [{ code: 'type_mismatch', path: '', message: 'schema is false' }] })
|
|
956
1183
|
}
|
|
957
1184
|
if (typeof schema !== 'object' || schema === null) return null
|
|
958
|
-
if (!codegenSafe(schema)) return null
|
|
959
|
-
if (schema.patternProperties
|
|
1185
|
+
if (!codegenSafe(schema, schemaMap)) return null
|
|
1186
|
+
if (schema.patternProperties) {
|
|
1187
|
+
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
1188
|
+
if (typeof sub === 'boolean') return null
|
|
1189
|
+
if (/\\[pP]\{/.test(pat)) return null
|
|
1190
|
+
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (schema.dependentSchemas) {
|
|
1194
|
+
for (const sub of Object.values(schema.dependentSchemas)) {
|
|
1195
|
+
if (typeof sub === 'boolean') return null
|
|
1196
|
+
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (schema.propertyNames) {
|
|
1200
|
+
if (typeof schema.propertyNames === 'boolean') return null
|
|
1201
|
+
const pn = schema.propertyNames
|
|
1202
|
+
const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
|
|
1203
|
+
const keys = Object.keys(pn).filter(k => k !== '$schema')
|
|
1204
|
+
if (keys.some(k => !supported.includes(k))) return null
|
|
1205
|
+
}
|
|
960
1206
|
|
|
961
|
-
const ctx = { varCounter: 0, helperCode: [], rootDefs: schema.$defs || schema.definitions || null, refStack: new Set() }
|
|
1207
|
+
const ctx = { varCounter: 0, helperCode: [], rootDefs: schema.$defs || schema.definitions || null, refStack: new Set(), schemaMap: schemaMap || null }
|
|
962
1208
|
const lines = []
|
|
963
1209
|
genCodeE(schema, 'd', '', lines, ctx)
|
|
964
1210
|
if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
|
|
@@ -982,11 +1228,22 @@ function compileToJSCodegenWithErrors(schema) {
|
|
|
982
1228
|
function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
983
1229
|
if (typeof schema !== 'object' || schema === null) return
|
|
984
1230
|
|
|
985
|
-
// $ref — resolve local refs
|
|
986
|
-
if (schema.$ref
|
|
1231
|
+
// $ref — resolve local and cross-schema refs
|
|
1232
|
+
if (schema.$ref) {
|
|
987
1233
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
988
|
-
if (m && ctx.rootDefs[m[1]]) {
|
|
1234
|
+
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
1235
|
+
if (ctx.refStack.has(schema.$ref)) return
|
|
1236
|
+
ctx.refStack.add(schema.$ref)
|
|
989
1237
|
genCodeE(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx)
|
|
1238
|
+
ctx.refStack.delete(schema.$ref)
|
|
1239
|
+
return
|
|
1240
|
+
}
|
|
1241
|
+
if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
1242
|
+
if (ctx.refStack.has(schema.$ref)) return
|
|
1243
|
+
ctx.refStack.add(schema.$ref)
|
|
1244
|
+
genCodeE(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx)
|
|
1245
|
+
ctx.refStack.delete(schema.$ref)
|
|
1246
|
+
return
|
|
990
1247
|
}
|
|
991
1248
|
}
|
|
992
1249
|
|
|
@@ -1162,6 +1419,55 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1162
1419
|
}
|
|
1163
1420
|
}
|
|
1164
1421
|
|
|
1422
|
+
// patternProperties
|
|
1423
|
+
if (schema.patternProperties) {
|
|
1424
|
+
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
1425
|
+
const ri = ctx.varCounter++
|
|
1426
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(pat)})`)
|
|
1427
|
+
const ki = ctx.varCounter++
|
|
1428
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){if(_re${ri}.test(_k${ki})){`)
|
|
1429
|
+
const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
|
|
1430
|
+
genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx)
|
|
1431
|
+
lines.push(`}}}`)
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// dependentSchemas
|
|
1436
|
+
if (schema.dependentSchemas) {
|
|
1437
|
+
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
1438
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
1439
|
+
genCodeE(depSchema, v, pathExpr, lines, ctx)
|
|
1440
|
+
lines.push(`}`)
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// propertyNames
|
|
1445
|
+
if (schema.propertyNames && typeof schema.propertyNames === 'object') {
|
|
1446
|
+
const pn = schema.propertyNames
|
|
1447
|
+
const ki = ctx.varCounter++
|
|
1448
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
|
|
1449
|
+
if (pn.minLength !== undefined) {
|
|
1450
|
+
lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('min_length_violation', `'propertyNames: key too short: '+_k${ki}`)}}`)
|
|
1451
|
+
}
|
|
1452
|
+
if (pn.maxLength !== undefined) {
|
|
1453
|
+
lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('max_length_violation', `'propertyNames: key too long: '+_k${ki}`)}}`)
|
|
1454
|
+
}
|
|
1455
|
+
if (pn.pattern) {
|
|
1456
|
+
const ri = ctx.varCounter++
|
|
1457
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(pn.pattern)})`)
|
|
1458
|
+
lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern_mismatch', `'propertyNames: pattern mismatch: '+_k${ki}`)}}`)
|
|
1459
|
+
}
|
|
1460
|
+
if (pn.const !== undefined) {
|
|
1461
|
+
lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const_mismatch', `'propertyNames: expected '+${JSON.stringify(pn.const)}`)}}`)
|
|
1462
|
+
}
|
|
1463
|
+
if (pn.enum) {
|
|
1464
|
+
const ei = ctx.varCounter++
|
|
1465
|
+
ctx.helperCode.push(`const _es${ei}=new Set(${JSON.stringify(pn.enum)})`)
|
|
1466
|
+
lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum_mismatch', `'propertyNames: key not in enum: '+_k${ki}`)}}`)
|
|
1467
|
+
}
|
|
1468
|
+
lines.push(`}}`)
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1165
1471
|
// items — starts after prefixItems (Draft 2020-12 semantics)
|
|
1166
1472
|
if (schema.items) {
|
|
1167
1473
|
const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
|
|
@@ -1265,18 +1571,37 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1265
1571
|
// Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
|
|
1266
1572
|
// Avoids double-pass (jsFn → false → errFn runs same checks again).
|
|
1267
1573
|
// Uses type-aware optimizations: after type check passes, skip guards.
|
|
1268
|
-
function compileToJSCombined(schema, VALID_RESULT) {
|
|
1574
|
+
function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
1269
1575
|
if (typeof schema === 'boolean') {
|
|
1270
1576
|
return schema
|
|
1271
1577
|
? () => VALID_RESULT
|
|
1272
1578
|
: () => ({ valid: false, errors: [{ code: 'type_mismatch', path: '', message: 'schema is false' }] })
|
|
1273
1579
|
}
|
|
1274
1580
|
if (typeof schema !== 'object' || schema === null) return null
|
|
1275
|
-
if (!codegenSafe(schema)) return null
|
|
1276
|
-
if (schema.patternProperties
|
|
1581
|
+
if (!codegenSafe(schema, schemaMap)) return null
|
|
1582
|
+
if (schema.patternProperties) {
|
|
1583
|
+
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
1584
|
+
if (typeof sub === 'boolean') return null
|
|
1585
|
+
if (/\\[pP]\{/.test(pat)) return null
|
|
1586
|
+
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (schema.dependentSchemas) {
|
|
1590
|
+
for (const sub of Object.values(schema.dependentSchemas)) {
|
|
1591
|
+
if (typeof sub === 'boolean') return null
|
|
1592
|
+
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (schema.propertyNames) {
|
|
1596
|
+
if (typeof schema.propertyNames === 'boolean') return null
|
|
1597
|
+
const pn = schema.propertyNames
|
|
1598
|
+
const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
|
|
1599
|
+
const keys = Object.keys(pn).filter(k => k !== '$schema')
|
|
1600
|
+
if (keys.some(k => !supported.includes(k))) return null
|
|
1601
|
+
}
|
|
1277
1602
|
|
|
1278
1603
|
const ctx = { varCounter: 0, helperCode: [], closureVars: [], closureVals: [],
|
|
1279
|
-
rootDefs: schema.$defs || schema.definitions || null, refStack: new Set() }
|
|
1604
|
+
rootDefs: schema.$defs || schema.definitions || null, refStack: new Set(), schemaMap: schemaMap || null }
|
|
1280
1605
|
const lines = []
|
|
1281
1606
|
genCodeC(schema, 'd', '', lines, ctx)
|
|
1282
1607
|
if (lines.length === 0) return () => VALID_RESULT
|
|
@@ -1305,18 +1630,45 @@ function compileToJSCombined(schema, VALID_RESULT) {
|
|
|
1305
1630
|
function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
1306
1631
|
if (typeof schema !== 'object' || schema === null) return
|
|
1307
1632
|
|
|
1308
|
-
// $ref — resolve local refs
|
|
1309
|
-
if (schema.$ref
|
|
1633
|
+
// $ref — resolve local and cross-schema refs
|
|
1634
|
+
if (schema.$ref) {
|
|
1310
1635
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
1311
|
-
if (m && ctx.rootDefs[m[1]]) {
|
|
1636
|
+
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
1637
|
+
if (ctx.refStack.has(schema.$ref)) return
|
|
1638
|
+
ctx.refStack.add(schema.$ref)
|
|
1312
1639
|
genCodeC(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx)
|
|
1640
|
+
ctx.refStack.delete(schema.$ref)
|
|
1641
|
+
return
|
|
1642
|
+
}
|
|
1643
|
+
if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
1644
|
+
if (ctx.refStack.has(schema.$ref)) return
|
|
1645
|
+
ctx.refStack.add(schema.$ref)
|
|
1646
|
+
genCodeC(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx)
|
|
1647
|
+
ctx.refStack.delete(schema.$ref)
|
|
1648
|
+
return
|
|
1313
1649
|
}
|
|
1314
1650
|
}
|
|
1315
1651
|
|
|
1316
1652
|
const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
|
|
1317
1653
|
let isObj = false, isArr = false, isStr = false, isNum = false
|
|
1318
1654
|
|
|
1319
|
-
|
|
1655
|
+
// Pre-allocate error objects as closure variables for static paths.
|
|
1656
|
+
// This shrinks the generated function body → better V8 JIT on valid path.
|
|
1657
|
+
const isStaticPath = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
1658
|
+
const fail = (code, msg) => {
|
|
1659
|
+
if (isStaticPath && msg.startsWith("'") && !msg.includes('+')) {
|
|
1660
|
+
// Static error: pre-allocate as frozen closure variable
|
|
1661
|
+
const ei = ctx.varCounter++
|
|
1662
|
+
const errVar = `_E${ei}`
|
|
1663
|
+
const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
|
|
1664
|
+
const msgVal = msg.slice(1, -1)
|
|
1665
|
+
ctx.closureVars.push(errVar)
|
|
1666
|
+
ctx.closureVals.push(Object.freeze({code, path: pathVal, message: msgVal}))
|
|
1667
|
+
return `(_e||(_e=[])).push(${errVar})`
|
|
1668
|
+
}
|
|
1669
|
+
// Dynamic path (e.g., array index): inline as before
|
|
1670
|
+
return `(_e||(_e=[])).push({code:'${code}',path:${pathExpr||'""'},message:${msg}})`
|
|
1671
|
+
}
|
|
1320
1672
|
|
|
1321
1673
|
if (types) {
|
|
1322
1674
|
const conds = types.map(t => {
|
|
@@ -1391,7 +1743,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1391
1743
|
} else if (schema.required) {
|
|
1392
1744
|
for (const key of schema.required) {
|
|
1393
1745
|
const p = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
|
|
1394
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){_e.push({code:'required_missing',path:${p},message:'missing: ${esc(key)}'})}`)
|
|
1746
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){(_e||(_e=[])).push({code:'required_missing',path:${p},message:'missing: ${esc(key)}'})}`)
|
|
1395
1747
|
}
|
|
1396
1748
|
}
|
|
1397
1749
|
|
|
@@ -1444,8 +1796,8 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1444
1796
|
if (schema.minProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('min_properties_violation', `'minProperties ${schema.minProperties}'`)}}`)
|
|
1445
1797
|
if (schema.maxProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('max_properties_violation', `'maxProperties ${schema.maxProperties}'`)}}`)
|
|
1446
1798
|
|
|
1447
|
-
// additionalProperties
|
|
1448
|
-
if (schema.additionalProperties === false && schema.properties) {
|
|
1799
|
+
// additionalProperties — skip if patternProperties present (handled in unified loop below)
|
|
1800
|
+
if (schema.additionalProperties === false && schema.properties && !schema.patternProperties) {
|
|
1449
1801
|
const allowed = Object.keys(schema.properties).map(k => JSON.stringify(k)).join(',')
|
|
1450
1802
|
const ci = ctx.varCounter++
|
|
1451
1803
|
lines.push(isObj
|
|
@@ -1485,6 +1837,155 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
1485
1837
|
}
|
|
1486
1838
|
}
|
|
1487
1839
|
|
|
1840
|
+
// patternProperties — same optimizations as genCode: charCodeAt + inline key comparison + merged propertyNames
|
|
1841
|
+
if (schema.patternProperties) {
|
|
1842
|
+
const ppEntries = Object.entries(schema.patternProperties)
|
|
1843
|
+
const pn = schema.propertyNames && typeof schema.propertyNames === 'object' ? schema.propertyNames : null
|
|
1844
|
+
const pi = ctx.varCounter++
|
|
1845
|
+
|
|
1846
|
+
// Build pattern matchers: prefer charCodeAt for simple prefixes
|
|
1847
|
+
const matchers = []
|
|
1848
|
+
for (const [pat] of ppEntries) {
|
|
1849
|
+
const kVar = `_k${pi}`
|
|
1850
|
+
const fast = fastPrefixCheck(pat, kVar)
|
|
1851
|
+
if (fast) {
|
|
1852
|
+
matchers.push({ check: fast })
|
|
1853
|
+
} else {
|
|
1854
|
+
const ri = ctx.varCounter++
|
|
1855
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
1856
|
+
ctx.closureVals.push(new RegExp(pat))
|
|
1857
|
+
matchers.push({ check: `_re${ri}.test(_k${pi})` })
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// Build sub-schema validators as closure vars
|
|
1862
|
+
for (let i = 0; i < ppEntries.length; i++) {
|
|
1863
|
+
const [, sub] = ppEntries[i]
|
|
1864
|
+
const subLines = []
|
|
1865
|
+
genCode(sub, `_ppv`, subLines, ctx)
|
|
1866
|
+
const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
|
|
1867
|
+
const fnVar = `_ppf${pi}_${i}`
|
|
1868
|
+
ctx.closureVars.push(fnVar)
|
|
1869
|
+
ctx.closureVals.push(new Function('_ppv', fnBody))
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
1873
|
+
const kVar = `_k${pi}`
|
|
1874
|
+
|
|
1875
|
+
if (schema.additionalProperties === false && schema.properties) {
|
|
1876
|
+
ctx._ppHandledPropertyNamesC = !!pn
|
|
1877
|
+
const propKeys = Object.keys(schema.properties)
|
|
1878
|
+
// Inline key comparison for small property sets
|
|
1879
|
+
const keyCheck = propKeys.length <= 8
|
|
1880
|
+
? propKeys.map(k => `${kVar}===${JSON.stringify(k)}`).join('||')
|
|
1881
|
+
: null
|
|
1882
|
+
if (!keyCheck) {
|
|
1883
|
+
const allowedSet = `_as${pi}`
|
|
1884
|
+
ctx.closureVars.push(allowedSet)
|
|
1885
|
+
ctx.closureVals.push(new Set(propKeys))
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
lines.push(`${guard}{for(const ${kVar} in ${v}){`)
|
|
1889
|
+
// propertyNames checks (merged)
|
|
1890
|
+
if (pn) {
|
|
1891
|
+
if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('min_length_violation', `'propertyNames: key too short: '+${kVar}`)}}`)
|
|
1892
|
+
if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('max_length_violation', `'propertyNames: key too long: '+${kVar}`)}}`)
|
|
1893
|
+
if (pn.pattern) {
|
|
1894
|
+
const fast = fastPrefixCheck(pn.pattern, kVar)
|
|
1895
|
+
if (fast) {
|
|
1896
|
+
lines.push(`if(!(${fast})){${fail('pattern_mismatch', `'propertyNames: pattern mismatch: '+${kVar}`)}}`)
|
|
1897
|
+
} else {
|
|
1898
|
+
const ri = ctx.varCounter++
|
|
1899
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
1900
|
+
ctx.closureVals.push(new RegExp(pn.pattern))
|
|
1901
|
+
lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern_mismatch', `'propertyNames: pattern mismatch: '+${kVar}`)}}`)
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const_mismatch', `'propertyNames: expected '+${JSON.stringify(pn.const)}`)}}`)
|
|
1905
|
+
if (pn.enum) {
|
|
1906
|
+
const ei = ctx.varCounter++
|
|
1907
|
+
ctx.closureVars.push(`_es${ei}`)
|
|
1908
|
+
ctx.closureVals.push(new Set(pn.enum))
|
|
1909
|
+
lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum_mismatch', `'propertyNames: key not in enum: '+${kVar}`)}}`)
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
const matchExpr = keyCheck || `_as${pi}.has(${kVar})`
|
|
1913
|
+
lines.push(`let _m${pi}=${matchExpr}`)
|
|
1914
|
+
for (let i = 0; i < ppEntries.length; i++) {
|
|
1915
|
+
lines.push(`if(${matchers[i].check}){_m${pi}=true;if(!_ppf${pi}_${i}(${v}[${kVar}])){${fail('pattern_mismatch', `'patternProperties: value invalid for key '+${kVar}`)}}}`)
|
|
1916
|
+
}
|
|
1917
|
+
lines.push(`if(!_m${pi}){${fail('additional_property', `'extra: '+${kVar}`)}}`)
|
|
1918
|
+
lines.push(`}}`)
|
|
1919
|
+
} else {
|
|
1920
|
+
ctx._ppHandledPropertyNamesC = !!pn
|
|
1921
|
+
lines.push(`${guard}{for(const ${kVar} in ${v}){`)
|
|
1922
|
+
if (pn) {
|
|
1923
|
+
if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('min_length_violation', `'propertyNames: key too short: '+${kVar}`)}}`)
|
|
1924
|
+
if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('max_length_violation', `'propertyNames: key too long: '+${kVar}`)}}`)
|
|
1925
|
+
if (pn.pattern) {
|
|
1926
|
+
const fast = fastPrefixCheck(pn.pattern, kVar)
|
|
1927
|
+
if (fast) {
|
|
1928
|
+
lines.push(`if(!(${fast})){${fail('pattern_mismatch', `'propertyNames: pattern mismatch: '+${kVar}`)}}`)
|
|
1929
|
+
} else {
|
|
1930
|
+
const ri = ctx.varCounter++
|
|
1931
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
1932
|
+
ctx.closureVals.push(new RegExp(pn.pattern))
|
|
1933
|
+
lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern_mismatch', `'propertyNames: pattern mismatch: '+${kVar}`)}}`)
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const_mismatch', `'propertyNames: expected '+${JSON.stringify(pn.const)}`)}}`)
|
|
1937
|
+
if (pn.enum) {
|
|
1938
|
+
const ei = ctx.varCounter++
|
|
1939
|
+
ctx.closureVars.push(`_es${ei}`)
|
|
1940
|
+
ctx.closureVals.push(new Set(pn.enum))
|
|
1941
|
+
lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum_mismatch', `'propertyNames: key not in enum: '+${kVar}`)}}`)
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
for (let i = 0; i < ppEntries.length; i++) {
|
|
1945
|
+
lines.push(`if(${matchers[i].check}&&!_ppf${pi}_${i}(${v}[${kVar}])){${fail('pattern_mismatch', `'patternProperties: value invalid for key '+${kVar}`)}}`)
|
|
1946
|
+
}
|
|
1947
|
+
lines.push(`}}`)
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// dependentSchemas
|
|
1952
|
+
if (schema.dependentSchemas) {
|
|
1953
|
+
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
1954
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
1955
|
+
genCodeC(depSchema, v, pathExpr, lines, ctx)
|
|
1956
|
+
lines.push(`}`)
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// propertyNames — skip if already merged into patternProperties loop
|
|
1961
|
+
if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ctx._ppHandledPropertyNamesC) {
|
|
1962
|
+
const pn = schema.propertyNames
|
|
1963
|
+
const ki = ctx.varCounter++
|
|
1964
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
|
|
1965
|
+
if (pn.minLength !== undefined) {
|
|
1966
|
+
lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('min_length_violation', `'propertyNames: key too short: '+_k${ki}`)}}`)
|
|
1967
|
+
}
|
|
1968
|
+
if (pn.maxLength !== undefined) {
|
|
1969
|
+
lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('max_length_violation', `'propertyNames: key too long: '+_k${ki}`)}}`)
|
|
1970
|
+
}
|
|
1971
|
+
if (pn.pattern) {
|
|
1972
|
+
const ri = ctx.varCounter++
|
|
1973
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
1974
|
+
ctx.closureVals.push(new RegExp(pn.pattern))
|
|
1975
|
+
lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern_mismatch', `'propertyNames: pattern mismatch: '+_k${ki}`)}}`)
|
|
1976
|
+
}
|
|
1977
|
+
if (pn.const !== undefined) {
|
|
1978
|
+
lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const_mismatch', `'propertyNames: expected '+${JSON.stringify(pn.const)}`)}}`)
|
|
1979
|
+
}
|
|
1980
|
+
if (pn.enum) {
|
|
1981
|
+
const ei = ctx.varCounter++
|
|
1982
|
+
ctx.closureVars.push(`_es${ei}`)
|
|
1983
|
+
ctx.closureVals.push(new Set(pn.enum))
|
|
1984
|
+
lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum_mismatch', `'propertyNames: key not in enum: '+_k${ki}`)}}`)
|
|
1985
|
+
}
|
|
1986
|
+
lines.push(`}}`)
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1488
1989
|
// items
|
|
1489
1990
|
if (schema.items) {
|
|
1490
1991
|
const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
|