ata-validator 0.4.2 → 0.4.3

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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const native = require("node-gyp-build")(__dirname);
2
- const { compileToJS, compileToJSCodegen } = require("./lib/js-compiler");
2
+ const { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined } = require("./lib/js-compiler");
3
3
 
4
4
  // Extract default values from a schema tree. Returns a function that applies
5
5
  // defaults to an object in-place (mutates), or null if no defaults exist.
@@ -214,6 +214,14 @@ class Validator {
214
214
  const jsFn = process.env.ATA_FORCE_NAPI
215
215
  ? null
216
216
  : (compileToJSCodegen(schemaObj) || compileToJS(schemaObj));
217
+ // Combined validator: single pass, validates + collects errors, all optimized
218
+ const jsCombinedFn = process.env.ATA_FORCE_NAPI
219
+ ? null
220
+ : compileToJSCombined(schemaObj, VALID_RESULT);
221
+ // Fallback error-collecting codegen (less optimized, for schemas combined can't handle)
222
+ const jsErrFn = (!jsCombinedFn && !process.env.ATA_FORCE_NAPI)
223
+ ? compileToJSCodegenWithErrors(schemaObj)
224
+ : null;
217
225
  this._jsFn = jsFn;
218
226
 
219
227
  // Data mutators — applied in-place before validation
@@ -242,34 +250,31 @@ class Validator {
242
250
  const useSimdjsonForLarge = !hasArrayTraversal;
243
251
 
244
252
  if (jsFn) {
253
+ // Error handler: combined (optimized) → jsErrFn → NAPI fallback
254
+ const errFn = jsCombinedFn
255
+ ? (d) => { try { return jsCombinedFn(d); } catch { return compiled.validate(d); } }
256
+ : jsErrFn
257
+ ? (d) => { try { return jsErrFn(d, true); } catch { return compiled.validate(d); } }
258
+ : (d) => compiled.validate(d);
245
259
  this.validate = preprocess
246
- ? (data) => { preprocess(data); return jsFn(data) ? VALID_RESULT : compiled.validate(data); }
247
- : (data) => jsFn(data) ? VALID_RESULT : compiled.validate(data);
260
+ ? (data) => { preprocess(data); return jsFn(data) ? VALID_RESULT : errFn(data); }
261
+ : (data) => jsFn(data) ? VALID_RESULT : errFn(data);
248
262
  this.isValidObject = jsFn;
263
+ const jsonValidateFn = (obj) => jsFn(obj) ? VALID_RESULT : errFn(obj);
249
264
  this.validateJSON = useSimdjsonForLarge
250
265
  ? (jsonStr) => {
251
- // Selective schema: large docs use simdjson (skips irrelevant data)
252
266
  if (jsonStr.length >= SIMDJSON_THRESHOLD) {
253
267
  const buf = Buffer.from(jsonStr);
254
268
  if (native.rawFastValidate(fastSlot, buf)) return VALID_RESULT;
255
269
  return compiled.validateJSON(jsonStr);
256
270
  }
257
- try {
258
- const obj = JSON.parse(jsonStr);
259
- if (jsFn(obj)) return VALID_RESULT;
260
- } catch (e) {
261
- if (!(e instanceof SyntaxError)) throw e;
262
- }
271
+ try { return jsonValidateFn(JSON.parse(jsonStr)); }
272
+ catch (e) { if (!(e instanceof SyntaxError)) throw e; }
263
273
  return compiled.validateJSON(jsonStr);
264
274
  }
265
275
  : (jsonStr) => {
266
- // Non-selective schema: JSON.parse + jsFn always wins
267
- try {
268
- const obj = JSON.parse(jsonStr);
269
- if (jsFn(obj)) return VALID_RESULT;
270
- } catch (e) {
271
- if (!(e instanceof SyntaxError)) throw e;
272
- }
276
+ try { return jsonValidateFn(JSON.parse(jsonStr)); }
277
+ catch (e) { if (!(e instanceof SyntaxError)) throw e; }
273
278
  return compiled.validateJSON(jsonStr);
274
279
  };
275
280
  this.isValidJSON = useSimdjsonForLarge
@@ -437,18 +437,30 @@ function codegenSafe(schema) {
437
437
  }
438
438
  }
439
439
 
440
- // Required keys that collide with Object.prototype
440
+ // Keys that collide with Object.prototype
441
441
  if (schema.required) {
442
442
  for (const k of schema.required) {
443
443
  if (UNSAFE_KEYS.has(k)) return false
444
444
  }
445
445
  }
446
+ if (schema.properties) {
447
+ for (const k of Object.keys(schema.properties)) {
448
+ if (UNSAFE_KEYS.has(k)) return false
449
+ if (k === '$ref') return false // property named "$ref" — confusing
450
+ }
451
+ }
446
452
 
447
453
  // Unicode property escapes in pattern need 'u' flag — codegen uses RegExp without it
448
454
  if (schema.pattern && /\\[pP]\{/.test(schema.pattern)) return false
449
455
 
450
- // $ref — bail entirely from codegen; ref resolution has too many edge cases
451
- if (schema.$ref) return false
456
+ // $ref — allow only simple local refs (#/$defs/Name), no $id, no sibling keywords
457
+ if (schema.$ref) {
458
+ if (!/^#\/(?:\$defs|definitions)\/[^/]+$/.test(schema.$ref)) return false
459
+ // Bail if $ref has sibling keywords (complex interaction)
460
+ const siblings = Object.keys(schema).filter(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema')
461
+ if (siblings.length > 0) return false
462
+ }
463
+ if (schema.$id) return false
452
464
 
453
465
  // additionalProperties as schema — bail entirely, too many edge cases with allOf interaction
454
466
  if (typeof schema.additionalProperties === 'object') return false
@@ -457,6 +469,19 @@ function codegenSafe(schema) {
457
469
  // propertyNames: false — codegen doesn't handle this
458
470
  if (schema.propertyNames === false) return false
459
471
 
472
+ // Check $defs: targets must be safe, names must be simple, no nested $ref chains
473
+ const defs = schema.$defs || schema.definitions
474
+ if (defs) {
475
+ for (const [name, def] of Object.entries(defs)) {
476
+ if (/[~/"']/.test(name)) return false // special chars in def name
477
+ if (typeof def === 'boolean') return false
478
+ if (typeof def === 'object' && def !== null) {
479
+ if (def.$ref) return false // nested ref chain — bail
480
+ if (!codegenSafe(def)) return false
481
+ }
482
+ }
483
+ }
484
+
460
485
  // Recurse into sub-schemas — bail on boolean schemas in any position
461
486
  const subs = [
462
487
  schema.items, schema.contains, schema.not,
@@ -498,8 +523,10 @@ function compileToJSCodegen(schema) {
498
523
  genCode(schema, 'd', lines, ctx)
499
524
  if (lines.length === 0) return () => true
500
525
 
501
- const body = (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
502
- lines.join('\n ') + '\n return true'
526
+ const helperStr = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
527
+ const checkStr = lines.join('\n ')
528
+ const body = helperStr + checkStr + '\n return true'
529
+
503
530
  try {
504
531
  return new Function('d', body)
505
532
  } catch {
@@ -648,16 +675,19 @@ function genCode(schema, v, lines, ctx, knownType) {
648
675
  if (fc) lines.push(fc(v, isStr))
649
676
  }
650
677
 
651
- // uniqueItems — fast path for primitive arrays (no JSON.stringify)
678
+ // uniqueItems — tiered strategy based on expected array size
652
679
  if (schema.uniqueItems) {
653
680
  const si = ctx.varCounter++
654
- // If items schema is a primitive type, skip JSON.stringify entirely
655
681
  const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
656
682
  const isPrimItems = itemType === 'string' || itemType === 'number' || itemType === 'integer'
657
- // For objects: use sorted-key JSON.stringify to handle key order correctly per spec
658
- const inner = isPrimItems
659
- ? `const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){if(_s${si}.has(${v}[_i]))return false;_s${si}.add(${v}[_i])}`
660
- : `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k))return false;_s${si}.add(_k)}`
683
+ const maxItems = schema.maxItems
684
+ // Small primitive arrays (maxItems <= 16): nested loop is 6x faster than Set
685
+ // No allocation, no hash computation — just direct === comparison
686
+ const inner = isPrimItems && maxItems && maxItems <= 16
687
+ ? `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k])return false}}`
688
+ : isPrimItems
689
+ ? `const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){if(_s${si}.has(${v}[_i]))return false;_s${si}.add(${v}[_i])}`
690
+ : `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k))return false;_s${si}.add(_k)}`
661
691
  lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
662
692
  }
663
693
 
@@ -842,4 +872,620 @@ const FORMAT_CODEGEN = {
842
872
  // Safe key escaping: use JSON.stringify to handle all special chars (newlines, null bytes, etc.)
843
873
  function esc(s) { return JSON.stringify(s).slice(1, -1) }
844
874
 
845
- module.exports = { compileToJS, compileToJSCodegen }
875
+ // --- Error-collecting codegen: same checks, but pushes errors instead of returning false ---
876
+ // Returns a function: (data, allErrors) => { valid, errors }
877
+ // Valid path is still fast — only error path does extra work.
878
+ function compileToJSCodegenWithErrors(schema) {
879
+ if (typeof schema === 'boolean') {
880
+ return schema
881
+ ? () => ({ valid: true, errors: [] })
882
+ : () => ({ valid: false, errors: [{ code: 'type_mismatch', path: '', message: 'schema is false' }] })
883
+ }
884
+ if (typeof schema !== 'object' || schema === null) return null
885
+ if (!codegenSafe(schema)) return null
886
+ if (schema.patternProperties || schema.dependentSchemas || schema.propertyNames) return null
887
+
888
+ const ctx = { varCounter: 0, helperCode: [], rootDefs: schema.$defs || schema.definitions || null, refStack: new Set() }
889
+ const lines = []
890
+ genCodeE(schema, 'd', '', lines, ctx)
891
+ if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
892
+
893
+ const body = `const _e=[];\n ` +
894
+ (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
895
+ lines.join('\n ') +
896
+ `\n return{valid:_e.length===0,errors:_e}`
897
+ try {
898
+ return new Function('d', '_all', body)
899
+ } catch {
900
+ return null
901
+ }
902
+ }
903
+
904
+ // Error-collecting code generator.
905
+ // Instead of `return false`, pushes to `_e` array and optionally early-returns.
906
+ // `_all` parameter: if falsy, return after first error.
907
+ function genCodeE(schema, v, pathExpr, lines, ctx) {
908
+ if (typeof schema !== 'object' || schema === null) return
909
+
910
+ // $ref — resolve local refs
911
+ if (schema.$ref && ctx.rootDefs) {
912
+ const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
913
+ if (m && ctx.rootDefs[m[1]]) {
914
+ genCodeE(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx)
915
+ }
916
+ }
917
+
918
+ const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
919
+ if (types) {
920
+ const conds = types.map(t => {
921
+ switch (t) {
922
+ case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
923
+ case 'array': return `Array.isArray(${v})`
924
+ case 'string': return `typeof ${v}==='string'`
925
+ case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
926
+ case 'integer': return `Number.isInteger(${v})`
927
+ case 'boolean': return `typeof ${v}==='boolean'`
928
+ case 'null': return `${v}===null`
929
+ default: return 'true'
930
+ }
931
+ })
932
+ const expected = types.join(', ')
933
+ lines.push(`if(!(${conds.join('||')})){_e.push({code:'type_mismatch',path:${pathExpr||'""'},message:'expected ${expected}'});if(!_all)return{valid:false,errors:_e}}`)
934
+ }
935
+
936
+ // In error mode, never assume type — always guard (data may have failed type check but allErrors continues)
937
+ const isObj = false
938
+ const isArr = false
939
+ const isStr = false
940
+ const isNum = false
941
+
942
+ const fail = (code, msg) => `_e.push({code:'${code}',path:${pathExpr||'""'},message:${msg}});if(!_all)return{valid:false,errors:_e}`
943
+
944
+ // enum
945
+ if (schema.enum) {
946
+ const vals = schema.enum
947
+ const primitives = vals.filter(v => v === null || typeof v !== 'object')
948
+ const objects = vals.filter(v => v !== null && typeof v === 'object')
949
+ const primChecks = primitives.map(p => `${v}===${JSON.stringify(p)}`).join('||')
950
+ const objChecks = objects.map(o => `JSON.stringify(${v})===${JSON.stringify(JSON.stringify(o))}`).join('||')
951
+ const allChecks = [primChecks, objChecks].filter(Boolean).join('||')
952
+ lines.push(`if(!(${allChecks || 'false'})){${fail('enum_mismatch', "'value not in enum'")}}`)
953
+ }
954
+
955
+ // const — use canonical (sorted-key) comparison for objects
956
+ if (schema.const !== undefined) {
957
+ const cv = schema.const
958
+ if (cv === null || typeof cv !== 'object') {
959
+ lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const_mismatch', "'value does not match const'")}}`)
960
+ } else {
961
+ // Pre-compute canonical form of const value
962
+ const ci = ctx.varCounter++
963
+ const canonFn = `_cnE${ci}`
964
+ 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(',')+'}'};`)
965
+ const expected = canonFn + '(' + JSON.stringify(cv) + ')'
966
+ lines.push(`if(${canonFn}(${v})!==${expected}){${fail('const_mismatch', "'value does not match const'")}}`)
967
+ }
968
+ }
969
+
970
+ // required — no destructuring in error mode (data might not be an object)
971
+ const requiredSet = new Set(schema.required || [])
972
+ const hoisted = {}
973
+ if (schema.required) {
974
+ for (const key of schema.required) {
975
+ const p = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
976
+ 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 required: ${esc(key)}'});if(!_all)return{valid:false,errors:_e}}`)
977
+ }
978
+ }
979
+
980
+ // numeric
981
+ if (schema.minimum !== undefined) {
982
+ const c = isNum ? `${v}<${schema.minimum}` : `typeof ${v}==='number'&&${v}<${schema.minimum}`
983
+ lines.push(`if(${c}){${fail('minimum_violation', `'minimum ${schema.minimum}'`)}}`)
984
+ }
985
+ if (schema.maximum !== undefined) {
986
+ const c = isNum ? `${v}>${schema.maximum}` : `typeof ${v}==='number'&&${v}>${schema.maximum}`
987
+ lines.push(`if(${c}){${fail('maximum_violation', `'maximum ${schema.maximum}'`)}}`)
988
+ }
989
+ if (schema.exclusiveMinimum !== undefined) {
990
+ const c = isNum ? `${v}<=${schema.exclusiveMinimum}` : `typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum}`
991
+ lines.push(`if(${c}){${fail('exclusive_minimum_violation', `'exclusiveMinimum ${schema.exclusiveMinimum}'`)}}`)
992
+ }
993
+ if (schema.exclusiveMaximum !== undefined) {
994
+ const c = isNum ? `${v}>=${schema.exclusiveMaximum}` : `typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum}`
995
+ lines.push(`if(${c}){${fail('exclusive_maximum_violation', `'exclusiveMaximum ${schema.exclusiveMaximum}'`)}}`)
996
+ }
997
+ if (schema.multipleOf !== undefined) {
998
+ const m = schema.multipleOf
999
+ const ci = ctx.varCounter++
1000
+ // Use tolerance-based check for floating point (matches C++ behavior)
1001
+ lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multiple_of_violation', `'multipleOf ${m}'`)}}}`)
1002
+ }
1003
+
1004
+ // string
1005
+ if (schema.minLength !== undefined) {
1006
+ const c = isStr ? `${v}.length<${schema.minLength}` : `typeof ${v}==='string'&&${v}.length<${schema.minLength}`
1007
+ lines.push(`if(${c}){${fail('min_length_violation', `'minLength ${schema.minLength}'`)}}`)
1008
+ }
1009
+ if (schema.maxLength !== undefined) {
1010
+ const c = isStr ? `${v}.length>${schema.maxLength}` : `typeof ${v}==='string'&&${v}.length>${schema.maxLength}`
1011
+ lines.push(`if(${c}){${fail('max_length_violation', `'maxLength ${schema.maxLength}'`)}}`)
1012
+ }
1013
+ if (schema.pattern) {
1014
+ const ri = ctx.varCounter++
1015
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(schema.pattern)})`)
1016
+ const c = isStr ? `!_re${ri}.test(${v})` : `typeof ${v}==='string'&&!_re${ri}.test(${v})`
1017
+ lines.push(`if(${c}){${fail('pattern_mismatch', `'pattern mismatch'`)}}`)
1018
+ }
1019
+ if (schema.format) {
1020
+ const fc = FORMAT_CODEGEN[schema.format]
1021
+ // Format errors use the boolean codegen — just wrap with error push
1022
+ if (fc) {
1023
+ const ri = ctx.varCounter++
1024
+ const boolLines = []
1025
+ boolLines.push(fc(v, isStr))
1026
+ // Replace `return false` with error push in the format check
1027
+ const fmtCode = boolLines.join(';').replace(/return false/g,
1028
+ `{_e.push({code:'format_mismatch',path:${pathExpr||'""'},message:'format ${esc(schema.format)}'});if(!_all)return{valid:false,errors:_e}}`)
1029
+ lines.push(fmtCode)
1030
+ }
1031
+ }
1032
+
1033
+ // array size
1034
+ if (schema.minItems !== undefined) {
1035
+ const c = isArr ? `${v}.length<${schema.minItems}` : `Array.isArray(${v})&&${v}.length<${schema.minItems}`
1036
+ lines.push(`if(${c}){${fail('min_items_violation', `'minItems ${schema.minItems}'`)}}`)
1037
+ }
1038
+ if (schema.maxItems !== undefined) {
1039
+ const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`
1040
+ lines.push(`if(${c}){${fail('max_items_violation', `'maxItems ${schema.maxItems}'`)}}`)
1041
+ }
1042
+
1043
+ // uniqueItems
1044
+ if (schema.uniqueItems) {
1045
+ const si = ctx.varCounter++
1046
+ const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
1047
+ const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
1048
+ const inner = isPrim
1049
+ ? `const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){if(_s${si}.has(${v}[_i])){${fail('unique_items_violation', "'duplicate items'")};break};_s${si}.add(${v}[_i])}`
1050
+ : `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k)){${fail('unique_items_violation', "'duplicate items'")};break};_s${si}.add(_k)}`
1051
+ lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
1052
+ }
1053
+
1054
+ // object size
1055
+ if (schema.minProperties !== undefined) {
1056
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('min_properties_violation', `'minProperties ${schema.minProperties}'`)}}`)
1057
+ }
1058
+ if (schema.maxProperties !== undefined) {
1059
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('max_properties_violation', `'maxProperties ${schema.maxProperties}'`)}}`)
1060
+ }
1061
+
1062
+ // additionalProperties: false
1063
+ if (schema.additionalProperties === false && schema.properties) {
1064
+ const allowed = Object.keys(schema.properties).map(k => `${JSON.stringify(k)}`).join(',')
1065
+ const ci = ctx.varCounter++
1066
+ 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])){_e.push({code:'additional_property',path:${pathExpr||'""'},message:'additional property: '+_k${ci}[_i]});if(!_all)return{valid:false,errors:_e}}}`
1067
+ lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
1068
+ }
1069
+
1070
+ // dependentRequired
1071
+ if (schema.dependentRequired) {
1072
+ for (const [key, deps] of Object.entries(schema.dependentRequired)) {
1073
+ for (const dep of deps) {
1074
+ const p = pathExpr ? `${pathExpr}+'/${esc(dep)}'` : `'/${esc(dep)}'`
1075
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){_e.push({code:'required_missing',path:${p},message:'${esc(key)} requires ${esc(dep)}'});if(!_all)return{valid:false,errors:_e}}`)
1076
+ }
1077
+ }
1078
+ }
1079
+
1080
+ // properties — always guard (error mode, data may not be an object or may be array)
1081
+ if (schema.properties) {
1082
+ for (const [key, prop] of Object.entries(schema.properties)) {
1083
+ const childPath = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
1084
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
1085
+ genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx)
1086
+ lines.push(`}`)
1087
+ }
1088
+ }
1089
+
1090
+ // items — starts after prefixItems (Draft 2020-12 semantics)
1091
+ if (schema.items) {
1092
+ const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
1093
+ const idx = `_j${ctx.varCounter}`
1094
+ const elem = `_ei${ctx.varCounter}`
1095
+ ctx.varCounter++
1096
+ const childPath = pathExpr ? `${pathExpr}+'/'+${idx}` : `'/'+${idx}`
1097
+ lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
1098
+ genCodeE(schema.items, elem, childPath, lines, ctx)
1099
+ lines.push(`}}`)
1100
+ }
1101
+
1102
+ // prefixItems
1103
+ if (schema.prefixItems) {
1104
+ for (let i = 0; i < schema.prefixItems.length; i++) {
1105
+ const childPath = pathExpr ? `${pathExpr}+'/${i}'` : `'/${i}'`
1106
+ lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
1107
+ genCodeE(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx)
1108
+ lines.push(`}`)
1109
+ }
1110
+ }
1111
+
1112
+ // contains
1113
+ if (schema.contains) {
1114
+ const ci = ctx.varCounter++
1115
+ const subLines = []
1116
+ genCode(schema.contains, `_cv`, subLines, ctx)
1117
+ const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
1118
+ const minC = schema.minContains !== undefined ? schema.minContains : 1
1119
+ const maxC = schema.maxContains
1120
+ lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
1121
+ lines.push(`if(_cc${ci}<${minC}){${fail('contains_violation', `'contains: need at least ${minC} match(es)'`)}}`)
1122
+ if (maxC !== undefined) {
1123
+ lines.push(`if(_cc${ci}>${maxC}){${fail('contains_violation', `'contains: at most ${maxC} match(es)'`)}}`)
1124
+ }
1125
+ lines.push(`}`)
1126
+ }
1127
+
1128
+ // allOf
1129
+ if (schema.allOf) {
1130
+ for (const sub of schema.allOf) {
1131
+ genCodeE(sub, v, pathExpr, lines, ctx)
1132
+ }
1133
+ }
1134
+
1135
+ // anyOf
1136
+ if (schema.anyOf) {
1137
+ const fi = ctx.varCounter++
1138
+ const fns = schema.anyOf.map((sub, i) => {
1139
+ const subLines = []
1140
+ genCode(sub, '_av', subLines, ctx)
1141
+ return subLines.length === 0 ? `function(_av){return true}` : `function(_av){${subLines.join(';')};return true}`
1142
+ })
1143
+ lines.push(`{const _af${fi}=[${fns.join(',')}];let _am${fi}=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am${fi}=true;break}}if(!_am${fi}){${fail('any_of_failed', "'no anyOf matched'")}}}`)
1144
+ }
1145
+
1146
+ // oneOf
1147
+ if (schema.oneOf) {
1148
+ const fi = ctx.varCounter++
1149
+ const fns = schema.oneOf.map((sub, i) => {
1150
+ const subLines = []
1151
+ genCode(sub, '_ov', subLines, ctx)
1152
+ return subLines.length === 0 ? `function(_ov){return true}` : `function(_ov){${subLines.join(';')};return true}`
1153
+ })
1154
+ lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc${fi}=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc${fi}++;if(_oc${fi}>1)break}if(_oc${fi}!==1){${fail('one_of_failed', "'oneOf: expected 1 match, got '+_oc"+fi)}}}`)
1155
+ }
1156
+
1157
+ // not
1158
+ if (schema.not) {
1159
+ const subLines = []
1160
+ genCode(schema.not, '_nv', subLines, ctx)
1161
+ const nfn = subLines.length === 0 ? `function(_nv){return true}` : `function(_nv){${subLines.join(';')};return true}`
1162
+ const fi = ctx.varCounter++
1163
+ lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not_failed', "'should not match'")}}}`)
1164
+ }
1165
+
1166
+ // if/then/else
1167
+ if (schema.if) {
1168
+ const ifLines = []
1169
+ genCode(schema.if, '_iv', ifLines, ctx)
1170
+ const fi = ctx.varCounter++
1171
+ const ifFn = ifLines.length === 0
1172
+ ? `function(_iv){return true}`
1173
+ : `function(_iv){${ifLines.join(';')};return true}`
1174
+ lines.push(`{const _if${fi}=${ifFn}`)
1175
+ if (schema.then) {
1176
+ lines.push(`if(_if${fi}(${v})){`)
1177
+ genCodeE(schema.then, v, pathExpr, lines, ctx)
1178
+ lines.push(`}`)
1179
+ }
1180
+ if (schema.else) {
1181
+ lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`)
1182
+ genCodeE(schema.else, v, pathExpr, lines, ctx)
1183
+ lines.push(`}`)
1184
+ }
1185
+ lines.push(`}`)
1186
+ }
1187
+ }
1188
+
1189
+ // --- Combined validator: single pass, validates + collects errors ---
1190
+ // Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
1191
+ // Avoids double-pass (jsFn → false → errFn runs same checks again).
1192
+ // Uses type-aware optimizations: after type check passes, skip guards.
1193
+ function compileToJSCombined(schema, VALID_RESULT) {
1194
+ if (typeof schema === 'boolean') {
1195
+ return schema
1196
+ ? () => VALID_RESULT
1197
+ : () => ({ valid: false, errors: [{ code: 'type_mismatch', path: '', message: 'schema is false' }] })
1198
+ }
1199
+ if (typeof schema !== 'object' || schema === null) return null
1200
+ if (!codegenSafe(schema)) return null
1201
+ if (schema.patternProperties || schema.dependentSchemas || schema.propertyNames) return null
1202
+
1203
+ const ctx = { varCounter: 0, helperCode: [], closureVars: [], closureVals: [],
1204
+ rootDefs: schema.$defs || schema.definitions || null, refStack: new Set() }
1205
+ const lines = []
1206
+ genCodeC(schema, 'd', '', lines, ctx)
1207
+ if (lines.length === 0) return () => VALID_RESULT
1208
+
1209
+ // Use factory pattern: closure vars (regexes, etc.) created once, not per call
1210
+ const closureParams = ctx.closureVars.join(',')
1211
+ const inner = `const _e=[];\n ` +
1212
+ (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
1213
+ lines.join('\n ') +
1214
+ `\n return _e.length===0?R:{valid:false,errors:_e}`
1215
+
1216
+ try {
1217
+ const factory = new Function('R' + (closureParams ? ',' + closureParams : ''),
1218
+ `return function(d){${inner}}`)
1219
+ return factory(VALID_RESULT, ...ctx.closureVals)
1220
+ } catch {
1221
+ return null
1222
+ }
1223
+ }
1224
+
1225
+ // Combined code generator: type-aware like genCode, error-collecting like genCodeE.
1226
+ // After type check passes → use optimizations (destructuring, no guards).
1227
+ // If type check fails → push error, skip property checks (they'd crash).
1228
+ function genCodeC(schema, v, pathExpr, lines, ctx) {
1229
+ if (typeof schema !== 'object' || schema === null) return
1230
+
1231
+ // $ref — resolve local refs
1232
+ if (schema.$ref && ctx.rootDefs) {
1233
+ const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
1234
+ if (m && ctx.rootDefs[m[1]]) {
1235
+ genCodeC(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx)
1236
+ }
1237
+ }
1238
+
1239
+ const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
1240
+ let isObj = false, isArr = false, isStr = false, isNum = false
1241
+
1242
+ const fail = (code, msg) => `_e.push({code:'${code}',path:${pathExpr||'""'},message:${msg}})`
1243
+
1244
+ if (types) {
1245
+ const conds = types.map(t => {
1246
+ switch (t) {
1247
+ case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
1248
+ case 'array': return `Array.isArray(${v})`
1249
+ case 'string': return `typeof ${v}==='string'`
1250
+ case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
1251
+ case 'integer': return `Number.isInteger(${v})`
1252
+ case 'boolean': return `typeof ${v}==='boolean'`
1253
+ case 'null': return `${v}===null`
1254
+ default: return 'true'
1255
+ }
1256
+ })
1257
+ const expected = types.join(', ')
1258
+ // Type check: push error but continue — wrap remaining in type-success block
1259
+ const typeOk = `_tok${ctx.varCounter++}`
1260
+ lines.push(`const ${typeOk}=${conds.join('||')}`)
1261
+ lines.push(`if(!${typeOk}){${fail('type_mismatch', `'expected ${expected}'`)}}`)
1262
+ // Subsequent optimized code runs inside if(typeOk){...}
1263
+ if (types.length === 1) {
1264
+ isObj = types[0] === 'object'
1265
+ isArr = types[0] === 'array'
1266
+ isStr = types[0] === 'string'
1267
+ isNum = types[0] === 'number' || types[0] === 'integer'
1268
+ }
1269
+ lines.push(`if(${typeOk}){`)
1270
+ // We'll close this block at the end of genCodeC — mark it
1271
+ ctx._typeBlock = true
1272
+ }
1273
+
1274
+ // enum
1275
+ if (schema.enum) {
1276
+ const vals = schema.enum
1277
+ const primitives = vals.filter(v => v === null || typeof v !== 'object')
1278
+ const objects = vals.filter(v => v !== null && typeof v === 'object')
1279
+ const primChecks = primitives.map(p => `${v}===${JSON.stringify(p)}`).join('||')
1280
+ const objChecks = objects.map(o => `JSON.stringify(${v})===${JSON.stringify(JSON.stringify(o))}`).join('||')
1281
+ const allChecks = [primChecks, objChecks].filter(Boolean).join('||')
1282
+ lines.push(`if(!(${allChecks || 'false'})){${fail('enum_mismatch', "'value not in enum'")}}`)
1283
+ }
1284
+
1285
+ // const
1286
+ if (schema.const !== undefined) {
1287
+ const cv = schema.const
1288
+ if (cv === null || typeof cv !== 'object') {
1289
+ lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const_mismatch', "'const mismatch'")}}`)
1290
+ } else {
1291
+ const ci = ctx.varCounter++
1292
+ const canonFn = `_cn${ci}`
1293
+ 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(',')+'}'};`)
1294
+ lines.push(`if(${canonFn}(${v})!==${canonFn}(${JSON.stringify(cv)})){${fail('const_mismatch', "'const mismatch'")}}`)
1295
+ }
1296
+ }
1297
+
1298
+ // required — use destructuring when type is object (SAFE because type check already passed)
1299
+ const requiredSet = new Set(schema.required || [])
1300
+ const hoisted = {}
1301
+ if (schema.required && schema.properties && isObj) {
1302
+ const destructKeys = []
1303
+ for (const key of schema.required) {
1304
+ if (schema.properties[key]) {
1305
+ const lv = `_h${ctx.varCounter++}`
1306
+ hoisted[key] = lv
1307
+ destructKeys.push(`${JSON.stringify(key)}:${lv}`)
1308
+ }
1309
+ }
1310
+ if (destructKeys.length > 0) lines.push(`const{${destructKeys.join(',')}}=${v}`)
1311
+ for (const key of schema.required) {
1312
+ const check = hoisted[key] ? `${hoisted[key]}===undefined` : `${v}[${JSON.stringify(key)}]===undefined`
1313
+ const p = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
1314
+ lines.push(`if(${check}){${`_e.push({code:'required_missing',path:${p},message:'missing: ${esc(key)}'})`}}`)
1315
+ }
1316
+ } else if (schema.required) {
1317
+ for (const key of schema.required) {
1318
+ const p = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
1319
+ 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)}'})}`)
1320
+ }
1321
+ }
1322
+
1323
+ // numeric — skip type guard if known
1324
+ if (schema.minimum !== undefined) { const c = isNum ? `${v}<${schema.minimum}` : `typeof ${v}==='number'&&${v}<${schema.minimum}`; lines.push(`if(${c}){${fail('minimum_violation', `'min ${schema.minimum}'`)}}`) }
1325
+ if (schema.maximum !== undefined) { const c = isNum ? `${v}>${schema.maximum}` : `typeof ${v}==='number'&&${v}>${schema.maximum}`; lines.push(`if(${c}){${fail('maximum_violation', `'max ${schema.maximum}'`)}}`) }
1326
+ if (schema.exclusiveMinimum !== undefined) { const c = isNum ? `${v}<=${schema.exclusiveMinimum}` : `typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum}`; lines.push(`if(${c}){${fail('exclusive_minimum_violation', `'excMin ${schema.exclusiveMinimum}'`)}}`) }
1327
+ if (schema.exclusiveMaximum !== undefined) { const c = isNum ? `${v}>=${schema.exclusiveMaximum}` : `typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum}`; lines.push(`if(${c}){${fail('exclusive_maximum_violation', `'excMax ${schema.exclusiveMaximum}'`)}}`) }
1328
+ if (schema.multipleOf !== undefined) {
1329
+ const m = schema.multipleOf
1330
+ const ci = ctx.varCounter++
1331
+ lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multiple_of_violation', `'multipleOf ${m}'`)}}}`)
1332
+ }
1333
+
1334
+ // string — skip guard if known
1335
+ if (schema.minLength !== undefined) { const c = isStr ? `${v}.length<${schema.minLength}` : `typeof ${v}==='string'&&${v}.length<${schema.minLength}`; lines.push(`if(${c}){${fail('min_length_violation', `'minLength ${schema.minLength}'`)}}`) }
1336
+ if (schema.maxLength !== undefined) { const c = isStr ? `${v}.length>${schema.maxLength}` : `typeof ${v}==='string'&&${v}.length>${schema.maxLength}`; lines.push(`if(${c}){${fail('max_length_violation', `'maxLength ${schema.maxLength}'`)}}`) }
1337
+ if (schema.pattern) {
1338
+ const ri = ctx.varCounter++
1339
+ const reVar = `_re${ri}`
1340
+ ctx.closureVars.push(reVar)
1341
+ ctx.closureVals.push(new RegExp(schema.pattern))
1342
+ const c = isStr ? `!${reVar}.test(${v})` : `typeof ${v}==='string'&&!${reVar}.test(${v})`
1343
+ lines.push(`if(${c}){${fail('pattern_mismatch', "'pattern mismatch'")}}`)
1344
+ }
1345
+ if (schema.format) {
1346
+ const fc = FORMAT_CODEGEN[schema.format]
1347
+ if (fc) {
1348
+ const code = fc(v, isStr).replace(/return false/g, `{${fail('format_mismatch', `'format ${esc(schema.format)}'`)}}`)
1349
+ lines.push(code)
1350
+ }
1351
+ }
1352
+
1353
+ // array size
1354
+ if (schema.minItems !== undefined) { const c = isArr ? `${v}.length<${schema.minItems}` : `Array.isArray(${v})&&${v}.length<${schema.minItems}`; lines.push(`if(${c}){${fail('min_items_violation', `'minItems ${schema.minItems}'`)}}`) }
1355
+ if (schema.maxItems !== undefined) { const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`; lines.push(`if(${c}){${fail('max_items_violation', `'maxItems ${schema.maxItems}'`)}}`) }
1356
+
1357
+ // uniqueItems
1358
+ if (schema.uniqueItems) {
1359
+ const si = ctx.varCounter++
1360
+ const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
1361
+ const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
1362
+ const inner = isPrim
1363
+ ? `const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){if(_s${si}.has(${v}[_i])){${fail('unique_items_violation', "'duplicates'")};break};_s${si}.add(${v}[_i])}`
1364
+ : `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k)){${fail('unique_items_violation', "'duplicates'")};break};_s${si}.add(_k)}`
1365
+ lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
1366
+ }
1367
+
1368
+ // object size
1369
+ 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}'`)}}`)
1370
+ 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}'`)}}`)
1371
+
1372
+ // additionalProperties
1373
+ if (schema.additionalProperties === false && schema.properties) {
1374
+ const allowed = Object.keys(schema.properties).map(k => JSON.stringify(k)).join(',')
1375
+ const ci = ctx.varCounter++
1376
+ lines.push(isObj
1377
+ ? `{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])){${fail('additional_property', `'extra: '+_k${ci}[_i]`)}}}`
1378
+ : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){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])){${fail('additional_property', `'extra: '+_k${ci}[_i]`)}}}`)
1379
+ }
1380
+
1381
+ // dependentRequired
1382
+ if (schema.dependentRequired) {
1383
+ for (const [key, deps] of Object.entries(schema.dependentRequired)) {
1384
+ for (const dep of deps) {
1385
+ const p = pathExpr ? `${pathExpr}+'/${esc(dep)}'` : `'/${esc(dep)}'`
1386
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){_e.push({code:'required_missing',path:${p},message:'${esc(key)} requires ${esc(dep)}'})}`)
1387
+ }
1388
+ }
1389
+ }
1390
+
1391
+ // properties — use hoisted vars for required+known-object, full guard otherwise
1392
+ if (schema.properties) {
1393
+ for (const [key, prop] of Object.entries(schema.properties)) {
1394
+ const pv = hoisted[key] || `${v}[${JSON.stringify(key)}]`
1395
+ const childPath = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
1396
+ if (requiredSet.has(key) && isObj) {
1397
+ lines.push(`if(${pv}!==undefined){`)
1398
+ genCodeC(prop, pv, childPath, lines, ctx)
1399
+ lines.push(`}`)
1400
+ } else if (isObj) {
1401
+ const oi = ctx.varCounter++
1402
+ lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined){`)
1403
+ genCodeC(prop, `_o${oi}`, childPath, lines, ctx)
1404
+ lines.push(`}}`)
1405
+ } else {
1406
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
1407
+ genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx)
1408
+ lines.push(`}`)
1409
+ }
1410
+ }
1411
+ }
1412
+
1413
+ // items
1414
+ if (schema.items) {
1415
+ const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
1416
+ const idx = `_j${ctx.varCounter}`, elem = `_ei${ctx.varCounter}`
1417
+ ctx.varCounter++
1418
+ const childPath = pathExpr ? `${pathExpr}+'/'+${idx}` : `'/'+${idx}`
1419
+ lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
1420
+ genCodeC(schema.items, elem, childPath, lines, ctx)
1421
+ lines.push(`}}`)
1422
+ }
1423
+
1424
+ // prefixItems
1425
+ if (schema.prefixItems) {
1426
+ for (let i = 0; i < schema.prefixItems.length; i++) {
1427
+ const childPath = pathExpr ? `${pathExpr}+'/${i}'` : `'/${i}'`
1428
+ lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
1429
+ genCodeC(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx)
1430
+ lines.push(`}`)
1431
+ }
1432
+ }
1433
+
1434
+ // contains
1435
+ if (schema.contains) {
1436
+ const ci = ctx.varCounter++
1437
+ const subLines = []
1438
+ genCode(schema.contains, `_cv`, subLines, ctx)
1439
+ const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
1440
+ const minC = schema.minContains !== undefined ? schema.minContains : 1
1441
+ const maxC = schema.maxContains
1442
+ lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
1443
+ lines.push(`if(_cc${ci}<${minC}){${fail('contains_violation', `'need ${minC}+ matches'`)}}`)
1444
+ if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains_violation', `'max ${maxC} matches'`)}}`)
1445
+ lines.push(`}`)
1446
+ }
1447
+
1448
+ // allOf
1449
+ if (schema.allOf) { for (const sub of schema.allOf) genCodeC(sub, v, pathExpr, lines, ctx) }
1450
+
1451
+ // anyOf
1452
+ if (schema.anyOf) {
1453
+ const fi = ctx.varCounter++
1454
+ const fns = schema.anyOf.map(sub => { const sl = []; genCode(sub, '_av', sl, ctx); return sl.length === 0 ? `function(_av){return true}` : `function(_av){${sl.join(';')};return true}` })
1455
+ lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('any_of_failed', "'no match'")}}}`)
1456
+ }
1457
+
1458
+ // oneOf
1459
+ if (schema.oneOf) {
1460
+ const fi = ctx.varCounter++
1461
+ const fns = schema.oneOf.map(sub => { const sl = []; genCode(sub, '_ov', sl, ctx); return sl.length === 0 ? `function(_ov){return true}` : `function(_ov){${sl.join(';')};return true}` })
1462
+ lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc++;if(_oc>1)break}if(_oc!==1){${fail('one_of_failed', "'need exactly 1'")}}}`)
1463
+ }
1464
+
1465
+ // not
1466
+ if (schema.not) {
1467
+ const sl = []; genCode(schema.not, '_nv', sl, ctx)
1468
+ const nfn = sl.length === 0 ? `function(_nv){return true}` : `function(_nv){${sl.join(';')};return true}`
1469
+ const fi = ctx.varCounter++
1470
+ lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not_failed', "'should not match'")}}}`)
1471
+ }
1472
+
1473
+ // if/then/else
1474
+ if (schema.if) {
1475
+ const sl = []; genCode(schema.if, '_iv', sl, ctx)
1476
+ const fi = ctx.varCounter++
1477
+ const ifFn = sl.length === 0 ? `function(_iv){return true}` : `function(_iv){${sl.join(';')};return true}`
1478
+ lines.push(`{const _if${fi}=${ifFn}`)
1479
+ if (schema.then) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx); lines.push(`}`) }
1480
+ if (schema.else) { lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx); lines.push(`}`) }
1481
+ lines.push(`}`)
1482
+ }
1483
+
1484
+ // Close type-success block if opened
1485
+ if (ctx._typeBlock) {
1486
+ lines.push(`}`)
1487
+ ctx._typeBlock = false
1488
+ }
1489
+ }
1490
+
1491
+ module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Ultra-fast JSON Schema validator. Beats ajv on every valid-path benchmark: 1.1x–2.7x faster validate(obj), 151x faster compilation, 5.9x faster parallel batch. Speculative validation with V8-optimized JS codegen, simdjson, multi-core. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/src/ata.cpp CHANGED
@@ -626,6 +626,12 @@ static void validate_node(const schema_node_ptr& node,
626
626
  std::vector<validation_error>& errors,
627
627
  bool all_errors = true);
628
628
 
629
+ // Fast boolean-only tree walker — no error collection, no string allocation.
630
+ // Uses [[likely]]/[[unlikely]] hints. Returns true if valid.
631
+ static bool validate_fast(const schema_node_ptr& node,
632
+ dom::element value,
633
+ const compiled_schema& ctx);
634
+
629
635
  // Macro for early termination
630
636
  #define ATA_CHECK_EARLY() if (!all_errors && !errors.empty()) return
631
637
 
@@ -1200,6 +1206,221 @@ static void validate_node(const schema_node_ptr& node,
1200
1206
  }
1201
1207
  }
1202
1208
 
1209
+ // Fast boolean-only tree walker — stripped of all error collection.
1210
+ // No std::string allocation, no path tracking, no error messages.
1211
+ // Returns true if valid. Uses [[likely]]/[[unlikely]] branch hints.
1212
+ static bool validate_fast(const schema_node_ptr& node,
1213
+ dom::element value,
1214
+ const compiled_schema& ctx) {
1215
+ if (!node) [[unlikely]] return true;
1216
+
1217
+ if (node->boolean_schema.has_value()) [[unlikely]]
1218
+ return node->boolean_schema.value();
1219
+
1220
+ // $ref
1221
+ if (!node->ref.empty()) [[unlikely]] {
1222
+ auto it = ctx.defs.find(node->ref);
1223
+ if (it != ctx.defs.end()) {
1224
+ if (!validate_fast(it->second, value, ctx)) return false;
1225
+ } else if (node->ref == "#" && ctx.root) {
1226
+ if (!validate_fast(ctx.root, value, ctx)) return false;
1227
+ } else {
1228
+ return false;
1229
+ }
1230
+ }
1231
+
1232
+ // type
1233
+ if (!node->types.empty()) {
1234
+ bool match = false;
1235
+ for (const auto& t : node->types) {
1236
+ if (type_matches(value, t)) { match = true; break; }
1237
+ }
1238
+ if (!match) [[unlikely]] return false;
1239
+ }
1240
+
1241
+ // enum
1242
+ if (!node->enum_values_minified.empty()) {
1243
+ auto val_str = canonical_json(value);
1244
+ bool found = false;
1245
+ for (const auto& ev : node->enum_values_minified) {
1246
+ if (ev == val_str) { found = true; break; }
1247
+ }
1248
+ if (!found) [[unlikely]] return false;
1249
+ }
1250
+
1251
+ // const
1252
+ if (node->const_value_raw.has_value()) {
1253
+ if (canonical_json(value) != node->const_value_raw.value()) [[unlikely]] return false;
1254
+ }
1255
+
1256
+ auto actual_type = type_of_sv(value);
1257
+
1258
+ // Numeric
1259
+ if (actual_type == "integer" || actual_type == "number") {
1260
+ double v = to_double(value);
1261
+ if (node->minimum.has_value() && v < node->minimum.value()) return false;
1262
+ if (node->maximum.has_value() && v > node->maximum.value()) return false;
1263
+ if (node->exclusive_minimum.has_value() && v <= node->exclusive_minimum.value()) return false;
1264
+ if (node->exclusive_maximum.has_value() && v >= node->exclusive_maximum.value()) return false;
1265
+ if (node->multiple_of.has_value()) {
1266
+ double rem = std::fmod(v, node->multiple_of.value());
1267
+ if (std::abs(rem) > 1e-8 && std::abs(rem - node->multiple_of.value()) > 1e-8) return false;
1268
+ }
1269
+ }
1270
+
1271
+ // String
1272
+ if (actual_type == "string") {
1273
+ std::string_view sv;
1274
+ value.get(sv);
1275
+ uint64_t len = utf8_length(sv);
1276
+ if (node->min_length.has_value() && len < node->min_length.value()) return false;
1277
+ if (node->max_length.has_value() && len > node->max_length.value()) return false;
1278
+ if (node->compiled_pattern) {
1279
+ if (!re2::RE2::PartialMatch(re2::StringPiece(sv.data(), sv.size()), *node->compiled_pattern))
1280
+ return false;
1281
+ }
1282
+ if (node->format.has_value() && !check_format(sv, node->format.value())) return false;
1283
+ }
1284
+
1285
+ // Array
1286
+ if (actual_type == "array" && value.is<dom::array>()) {
1287
+ dom::array arr; value.get(arr);
1288
+ uint64_t arr_size = 0;
1289
+ for ([[maybe_unused]] auto _ : arr) ++arr_size;
1290
+
1291
+ if (node->min_items.has_value() && arr_size < node->min_items.value()) return false;
1292
+ if (node->max_items.has_value() && arr_size > node->max_items.value()) return false;
1293
+
1294
+ if (node->unique_items) {
1295
+ std::set<std::string> seen;
1296
+ for (auto item : arr) {
1297
+ if (!seen.insert(canonical_json(item)).second) return false;
1298
+ }
1299
+ }
1300
+
1301
+ { uint64_t idx = 0;
1302
+ for (auto item : arr) {
1303
+ if (idx < node->prefix_items.size()) {
1304
+ if (!validate_fast(node->prefix_items[idx], item, ctx)) return false;
1305
+ } else if (node->items_schema) {
1306
+ if (!validate_fast(node->items_schema, item, ctx)) return false;
1307
+ }
1308
+ ++idx;
1309
+ }
1310
+ }
1311
+
1312
+ if (node->contains_schema) {
1313
+ uint64_t match_count = 0;
1314
+ for (auto item : arr) {
1315
+ if (validate_fast(node->contains_schema, item, ctx)) ++match_count;
1316
+ }
1317
+ uint64_t min_c = node->min_contains.value_or(1);
1318
+ uint64_t max_c = node->max_contains.value_or(arr_size);
1319
+ if (match_count < min_c || match_count > max_c) return false;
1320
+ }
1321
+ }
1322
+
1323
+ // Object
1324
+ if (actual_type == "object" && value.is<dom::object>()) {
1325
+ dom::object obj; value.get(obj);
1326
+
1327
+ if (node->min_properties.has_value() || node->max_properties.has_value()) {
1328
+ uint64_t n = 0;
1329
+ for ([[maybe_unused]] auto _ : obj) ++n;
1330
+ if (node->min_properties.has_value() && n < node->min_properties.value()) return false;
1331
+ if (node->max_properties.has_value() && n > node->max_properties.value()) return false;
1332
+ }
1333
+
1334
+ for (const auto& req : node->required) {
1335
+ dom::element d;
1336
+ if (obj[req].get(d) != SUCCESS) [[unlikely]] return false;
1337
+ }
1338
+
1339
+ for (auto [key, val] : obj) {
1340
+ std::string_view key_sv(key);
1341
+ bool matched = false;
1342
+
1343
+ auto it = node->properties.find(std::string(key_sv));
1344
+ if (it != node->properties.end()) {
1345
+ if (!validate_fast(it->second, val, ctx)) return false;
1346
+ matched = true;
1347
+ }
1348
+
1349
+ for (const auto& pp : node->pattern_properties) {
1350
+ if (pp.compiled && re2::RE2::PartialMatch(
1351
+ re2::StringPiece(key_sv.data(), key_sv.size()), *pp.compiled)) {
1352
+ if (!validate_fast(pp.schema, val, ctx)) return false;
1353
+ matched = true;
1354
+ }
1355
+ }
1356
+
1357
+ if (!matched) {
1358
+ if (node->additional_properties_bool.has_value() &&
1359
+ !node->additional_properties_bool.value()) return false;
1360
+ if (node->additional_properties_schema &&
1361
+ !validate_fast(node->additional_properties_schema, val, ctx)) return false;
1362
+ }
1363
+ }
1364
+
1365
+ for (const auto& [prop, deps] : node->dependent_required) {
1366
+ dom::element d;
1367
+ if (obj[prop].get(d) == SUCCESS) {
1368
+ for (const auto& dep : deps) {
1369
+ dom::element dd;
1370
+ if (obj[dep].get(dd) != SUCCESS) return false;
1371
+ }
1372
+ }
1373
+ }
1374
+
1375
+ for (const auto& [prop, schema] : node->dependent_schemas) {
1376
+ dom::element d;
1377
+ if (obj[prop].get(d) == SUCCESS) {
1378
+ if (!validate_fast(schema, value, ctx)) return false;
1379
+ }
1380
+ }
1381
+ }
1382
+
1383
+ // allOf
1384
+ for (const auto& sub : node->all_of) {
1385
+ if (!validate_fast(sub, value, ctx)) return false;
1386
+ }
1387
+
1388
+ // anyOf
1389
+ if (!node->any_of.empty()) {
1390
+ bool any = false;
1391
+ for (const auto& sub : node->any_of) {
1392
+ if (validate_fast(sub, value, ctx)) { any = true; break; }
1393
+ }
1394
+ if (!any) return false;
1395
+ }
1396
+
1397
+ // oneOf
1398
+ if (!node->one_of.empty()) {
1399
+ int n = 0;
1400
+ for (const auto& sub : node->one_of) {
1401
+ if (validate_fast(sub, value, ctx)) ++n;
1402
+ if (n > 1) return false;
1403
+ }
1404
+ if (n != 1) return false;
1405
+ }
1406
+
1407
+ // not
1408
+ if (node->not_schema) {
1409
+ if (validate_fast(node->not_schema, value, ctx)) return false;
1410
+ }
1411
+
1412
+ // if/then/else
1413
+ if (node->if_schema) {
1414
+ if (validate_fast(node->if_schema, value, ctx)) {
1415
+ if (node->then_schema && !validate_fast(node->then_schema, value, ctx)) return false;
1416
+ } else {
1417
+ if (node->else_schema && !validate_fast(node->else_schema, value, ctx)) return false;
1418
+ }
1419
+ }
1420
+
1421
+ return true;
1422
+ }
1423
+
1203
1424
  // --- Codegen compiler ---
1204
1425
  static void cg_compile(const schema_node* n, cg::plan& p,
1205
1426
  std::vector<cg::ins>& out) {
@@ -1660,9 +1881,8 @@ bool is_valid_prepadded(const schema_ref& schema, const char* data, size_t lengt
1660
1881
  return cg_exec(schema.impl->gen_plan, schema.impl->gen_plan.code, result.value());
1661
1882
  }
1662
1883
 
1663
- std::vector<validation_error> errors;
1664
- validate_node(schema.impl->root, result.value(), "", *schema.impl, errors, false);
1665
- return errors.empty();
1884
+ // Use fast boolean-only tree walker — no error collection overhead
1885
+ return validate_fast(schema.impl->root, result.value(), *schema.impl);
1666
1886
  }
1667
1887
 
1668
1888
  } // namespace ata