ata-validator 0.4.15 → 0.5.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.
@@ -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
- if (!defs) return null
377
- // #/$defs/Name or #/definitions/Name
378
- const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
379
- if (!m) return null
380
- const name = m[1]
381
- const entry = defs[name]
382
- if (!entry) return null
383
- return (d) => {
384
- const fn = entry.fn
385
- return fn ? fn(d) : true
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 only simple local refs (#/$defs/Name), no $id, no sibling keywords
461
+ // $ref — allow local refs (#/$defs/Name) and non-local refs if in schemaMap
457
462
  if (schema.$ref) {
458
- if (!/^#\/(?:\$defs|definitions)\/[^/]+$/.test(schema.$ref)) return false
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
- if (schema.patternProperties ||
518
- schema.dependentSchemas ||
519
- schema.propertyNames) return null
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 && ctx.rootDefs) {
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 // circular — bail
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 (schema.additionalProperties === false && schema.properties) {
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
- if (!schema.patternProperties) {
763
- const inner = `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
764
- if (!ctx.deferredChecks) ctx.deferredChecks = []
765
- ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
766
- } else {
767
- const allowed = Object.keys(schema.properties).map(k => `'${esc(k)}'`).join(',')
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 || schema.dependentSchemas || schema.propertyNames) return null
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 && ctx.rootDefs) {
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 || schema.dependentSchemas || schema.propertyNames) return null
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 && ctx.rootDefs) {
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
- const fail = (code, msg) => `(_e||(_e=[])).push({code:'${code}',path:${pathExpr||'""'},message:${msg}})`
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