ata-validator 0.14.0 → 0.15.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.
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ // Single source of truth for ATA error codes.
4
+ // STABILITY: once a code is published, its keyword and category never change.
5
+ // Removal is forbidden; mark deprecated instead. Adding requires updating
6
+ // tests/error-codes.lock.json in the same PR.
7
+
8
+ const CODES = Object.freeze({
9
+ // ATA1xxx — type / shape
10
+ ATA1001: { keyword: 'type', category: 'type', headline: 'value has wrong type' },
11
+ ATA1002: { keyword: 'type', category: 'type', headline: 'value is not an object' },
12
+
13
+ // ATA2xxx — constraint
14
+ ATA2001: { keyword: 'minLength', category: 'constraint', headline: 'string shorter than minLength' },
15
+ ATA2002: { keyword: 'maxLength', category: 'constraint', headline: 'string longer than maxLength' },
16
+ ATA2003: { keyword: 'minimum', category: 'constraint', headline: 'number below minimum' },
17
+ ATA2004: { keyword: 'maximum', category: 'constraint', headline: 'number above maximum' },
18
+ ATA2005: { keyword: 'exclusiveMinimum', category: 'constraint', headline: 'number not above exclusiveMinimum' },
19
+ ATA2006: { keyword: 'exclusiveMaximum', category: 'constraint', headline: 'number not below exclusiveMaximum' },
20
+ ATA2007: { keyword: 'multipleOf', category: 'constraint', headline: 'number not a multiple of expected divisor' },
21
+ ATA2008: { keyword: 'minItems', category: 'constraint', headline: 'array shorter than minItems' },
22
+ ATA2009: { keyword: 'maxItems', category: 'constraint', headline: 'array longer than maxItems' },
23
+ ATA2010: { keyword: 'minProperties', category: 'constraint', headline: 'object has fewer than minProperties' },
24
+ ATA2011: { keyword: 'maxProperties', category: 'constraint', headline: 'object has more than maxProperties' },
25
+ ATA2012: { keyword: 'uniqueItems', category: 'constraint', headline: 'array has duplicate items' },
26
+ ATA2013: { keyword: 'pattern', category: 'constraint', headline: 'string does not match pattern' },
27
+
28
+ // ATA3xxx — format
29
+ ATA3001: { keyword: 'format', format: 'email', category: 'format', headline: 'value does not match format "email"' },
30
+ ATA3002: { keyword: 'format', format: 'date', category: 'format', headline: 'value does not match format "date"' },
31
+ ATA3003: { keyword: 'format', format: 'date-time', category: 'format', headline: 'value does not match format "date-time"' },
32
+ ATA3004: { keyword: 'format', format: 'time', category: 'format', headline: 'value does not match format "time"' },
33
+ ATA3005: { keyword: 'format', format: 'uri', category: 'format', headline: 'value does not match format "uri"' },
34
+ ATA3006: { keyword: 'format', format: 'uri-reference', category: 'format', headline: 'value does not match format "uri-reference"' },
35
+ ATA3007: { keyword: 'format', format: 'ipv4', category: 'format', headline: 'value does not match format "ipv4"' },
36
+ ATA3008: { keyword: 'format', format: 'ipv6', category: 'format', headline: 'value does not match format "ipv6"' },
37
+ ATA3009: { keyword: 'format', format: 'uuid', category: 'format', headline: 'value does not match format "uuid"' },
38
+ ATA3010: { keyword: 'format', format: 'hostname', category: 'format', headline: 'value does not match format "hostname"' },
39
+ ATA3099: { keyword: 'format', category: 'format', headline: 'value does not match user-defined format' },
40
+
41
+ // ATA4xxx — composition
42
+ ATA4001: { keyword: 'oneOf', category: 'composition', headline: 'value matched 0 of N oneOf variants' },
43
+ ATA4002: { keyword: 'oneOf', category: 'composition', headline: 'value matched more than one oneOf variant' },
44
+ ATA4003: { keyword: 'anyOf', category: 'composition', headline: 'value matched none of the anyOf variants' },
45
+ ATA4004: { keyword: 'allOf', category: 'composition', headline: 'value failed one or more allOf branches' },
46
+ ATA4005: { keyword: 'not', category: 'composition', headline: 'value matched a forbidden schema' },
47
+ ATA4006: { keyword: 'if', category: 'composition', headline: 'value violated then/else branch' },
48
+
49
+ // ATA5xxx — refs
50
+ ATA5001: { keyword: '$ref', category: 'ref', headline: '$ref could not be resolved' },
51
+ ATA5002: { keyword: '$ref', category: 'ref', headline: 'recursive $ref cycle detected at validate time' },
52
+
53
+ // ATA6xxx — enum/const
54
+ ATA6001: { keyword: 'enum', category: 'enum', headline: 'value is not one of the allowed enum values' },
55
+ ATA6002: { keyword: 'const', category: 'enum', headline: 'value does not equal const' },
56
+
57
+ // ATA7xxx — required / additional / unevaluated
58
+ ATA7001: { keyword: 'required', category: 'shape', headline: 'object missing required property' },
59
+ ATA7002: { keyword: 'additionalProperties', category: 'shape', headline: 'object has property not allowed by schema' },
60
+ ATA7003: { keyword: 'unevaluatedProperties', category: 'shape', headline: 'object has unevaluated property' },
61
+ ATA7004: { keyword: 'unevaluatedItems', category: 'shape', headline: 'array has unevaluated items' },
62
+ ATA7005: { keyword: 'dependentRequired', category: 'shape', headline: 'dependentRequired property missing' },
63
+ ATA7006: { keyword: 'propertyNames', category: 'shape', headline: 'property name violates schema' },
64
+ ATA7007: { keyword: 'contains', category: 'shape', headline: 'array does not contain a matching item' },
65
+
66
+ // ATA9xxx — system
67
+ ATA9000: { keyword: '__abort_early__', category: 'system', headline: 'validation failed (abortEarly)' },
68
+ ATA9001: { keyword: '__parse__', category: 'system', headline: 'input is not valid JSON' },
69
+ ATA9002: { keyword: '__compile__', category: 'system', headline: 'schema failed to compile' },
70
+ });
71
+
72
+ function get (code) {
73
+ return CODES[code];
74
+ }
75
+
76
+ function all () {
77
+ return Object.keys(CODES).sort();
78
+ }
79
+
80
+ // Map a (keyword, optional format) tuple back to a code. Used by the codegen
81
+ // integration to attach codes to existing error sites without rewriting them all.
82
+ function codeFor (keyword, format) {
83
+ if (keyword === 'format' && format) {
84
+ for (const c of all()) {
85
+ const meta = CODES[c];
86
+ if (meta.keyword === 'format' && meta.format === format) return c;
87
+ }
88
+ return 'ATA3099';
89
+ }
90
+ for (const c of all()) {
91
+ if (CODES[c].keyword === keyword) return c;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ module.exports = { CODES, get, all, codeFor };
@@ -1,9 +1,43 @@
1
1
  'use strict'
2
2
 
3
+ const { codeFor } = require('./error-codes')
4
+
3
5
  // Compile a JSON Schema into a pure JS validator function.
4
6
  // Closure-based validator — no new Function() or eval().
5
7
  // Returns null if the schema is too complex for JS compilation.
6
8
 
9
+ const DOC_BASE = 'https://ata-validator.com/e/'
10
+
11
+ // Build the literal fragments injected into an emitted _e.push({...}) call.
12
+ // Returns { codeStr, docUrl, frame } where `frame` is a JS source fragment
13
+ // like ",schemaSource:Object.freeze({...})" or "" if no source map hit.
14
+ // The fragment always starts with a leading comma when non-empty so callers
15
+ // can splice it inside an existing object literal.
16
+ function buildErrorLiteral (opts) {
17
+ const { keyword, format, schemaPath, sourceMap } = opts
18
+ let code = (keyword === 'format' && format) ? codeFor('format', format) : codeFor(keyword)
19
+ if (!code) code = 'ATA9001'
20
+ const docUrl = DOC_BASE + code
21
+ let frame = ''
22
+ if (sourceMap && sourceMap.file && sourceMap.map) {
23
+ const ptr = schemaPath && schemaPath.charAt(0) === '#' ? schemaPath.slice(1) : (schemaPath || '')
24
+ let hit = sourceMap.map[ptr + '#key']
25
+ if (!hit) hit = sourceMap.map[ptr]
26
+ if (hit) {
27
+ // Support both {line,col,text} (raw output of buildPositionMap) and
28
+ // [line,col,text] (compact form passed by callers that pre-flattened).
29
+ const line = Array.isArray(hit) ? hit[0] : hit.line
30
+ const col = Array.isArray(hit) ? hit[1] : hit.col
31
+ const text = Array.isArray(hit) ? hit[2] : hit.text
32
+ frame = ',schemaSource:Object.freeze({file:' + JSON.stringify(sourceMap.file) +
33
+ ',line:' + line +
34
+ ',col:' + col +
35
+ ',text:' + JSON.stringify(text) + '})'
36
+ }
37
+ }
38
+ return { codeStr: code, docUrl, frame }
39
+ }
40
+
7
41
  // Count Unicode code points, not UTF-16 code units (surrogate pairs).
8
42
  // JSON Schema: minLength/maxLength count characters per RFC 8259.
9
43
  // Fast path: if no surrogate pairs exist, .length is correct (covers >99% of real data).
@@ -2471,7 +2505,7 @@ function genCharCodeSwitch(keys, v) {
2471
2505
  // --- Error-collecting codegen: same checks, but pushes errors instead of returning false ---
2472
2506
  // Returns a function: (data, allErrors) => { valid, errors }
2473
2507
  // Valid path is still fast — only error path does extra work.
2474
- function compileToJSCodegenWithErrors(schema, schemaMap, userFormats) {
2508
+ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts) {
2475
2509
  // Bail on unevaluated keywords — error codegen doesn't support them yet
2476
2510
  if (typeof schema === 'object' && schema !== null) {
2477
2511
  const s = JSON.stringify(schema)
@@ -2532,10 +2566,20 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats) {
2532
2566
  }
2533
2567
  }
2534
2568
 
2535
- const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema, userFormats: userFormats || null }
2569
+ const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema, userFormats: userFormats || null,
2570
+ sourceMap: (sourceOpts && sourceOpts.sourceMap && sourceOpts.schemaFile)
2571
+ ? { file: sourceOpts.schemaFile, map: sourceOpts.sourceMap }
2572
+ : null }
2536
2573
  ctx.helperCode.push('const _cpLen=s=>{let n=0;for(const _ of s)n++;return n}')
2537
2574
  const lines = []
2538
2575
  genCodeE(schema, 'd', '', lines, ctx, '#')
2576
+ // Inline branch-collapse helper only when the schema actually used a
2577
+ // oneOf/anyOf branch path (tracked via ctx.usesBranchCollapse). Keeps the
2578
+ // AOT output small for schemas that don't compose. Standalone modules can't
2579
+ // require('./branch-collapse'), so the helper body is embedded verbatim.
2580
+ if (ctx.usesBranchCollapse) {
2581
+ ctx.helperCode.push("const __ATA_SEVERITY={type:10,const:8,enum:8,required:5,format:3,minLength:3,maxLength:3,minimum:3,maximum:3,pattern:3,additionalProperties:2,unevaluatedProperties:2,unevaluatedItems:2};function __ataScore(errs){if(!errs||!errs.length)return 0;let s=0;for(const e of errs)s+=__ATA_SEVERITY[e.keyword]||4;return errs.length*100+s}function __ataCollapse(kw,br,pp,sp){const pass=br.filter(b=>b.valid);if(kw==='oneOf'){if(pass.length===1)return null;if(pass.length>1)return{code:'ATA4002',keyword:'oneOf',instancePath:pp||'',path:pp||'',schemaPath:sp,message:'value matched '+pass.length+' of '+br.length+' oneOf variants, expected exactly one',params:{matched:pass.length,total:br.length}}}else{if(pass.length>=1)return null}let bi=0,bs=Infinity;for(let i=0;i<br.length;i++){const s=__ataScore(br[i].errors);if(s<bs){bs=s;bi=i}}const code=kw==='oneOf'?'ATA4001':'ATA4003';const best=br[bi];return{code,keyword:kw,instancePath:pp||'',path:pp||'',schemaPath:sp,message:'value matched 0 of '+br.length+' '+kw+' variants',params:{variants:br.length,closest:bi,closestName:best.title||('variant '+(bi+1))},branchErrors:best.errors}}")
2582
+ }
2539
2583
  if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
2540
2584
 
2541
2585
  const checkStr = lines.join('\n ')
@@ -2560,6 +2604,39 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats) {
2560
2604
  }
2561
2605
  }
2562
2606
 
2607
+ // Emit codegen for oneOf/anyOf that runs each branch with error collection,
2608
+ // aggregates results, and uses __ataCollapse to surface a single best-branch
2609
+ // error. Wraps the existing per-branch evaluation; does not replace it.
2610
+ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaPrefix) {
2611
+ ctx.usesBranchCollapse = true
2612
+ const fi = ctx.varCounter++
2613
+ const branchKw = keyword
2614
+ const branchSp = schemaPrefix + '/' + branchKw
2615
+ // Per-branch error-collecting functions, each returning {valid,errors}.
2616
+ const fns = []
2617
+ const titles = []
2618
+ for (let i = 0; i < branches.length; i++) {
2619
+ const sub = branches[i]
2620
+ const subLines = []
2621
+ const subSp = branchSp + '/' + i
2622
+ if (typeof sub === 'object' && sub !== null) {
2623
+ genCodeE(sub, '_bv', pathExpr, subLines, ctx, subSp)
2624
+ }
2625
+ const title = (sub && typeof sub === 'object' && typeof sub.title === 'string') ? sub.title : ''
2626
+ titles.push(title)
2627
+ const body = subLines.length === 0
2628
+ ? `function(_bv){return{valid:true,errors:[]}}`
2629
+ : `function(_bv){const _e=[];${subLines.join(';')};return{valid:_e.length===0,errors:_e}}`
2630
+ fns.push(body)
2631
+ }
2632
+ const fnArr = `_brf${fi}`
2633
+ const resArr = `_brr${fi}`
2634
+ const titleArr = JSON.stringify(titles)
2635
+ const collapsed = `_brc${fi}`
2636
+ const pp = pathExpr || '""'
2637
+ lines.push(`{const ${fnArr}=[${fns.join(',')}];const _bt${fi}=${titleArr};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${fnArr}[_bi](${v});${resArr}.push({valid:_br.valid,errors:_br.errors,title:_bt${fi}[_bi]})}const ${collapsed}=__ataCollapse('${branchKw}',${resArr},${pp},'${branchSp}');if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}}`)
2638
+ }
2639
+
2563
2640
  // Error-collecting code generator.
2564
2641
  // Instead of `return false`, pushes to `_e` array and optionally early-returns.
2565
2642
  // `_all` parameter: if falsy, return after first error.
@@ -2648,7 +2725,11 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2648
2725
  }
2649
2726
  })
2650
2727
  const expected = types.join(', ')
2651
- lines.push(`if(!(${conds.join('||')})){_e.push({keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/type',params:{type:'${expected}'},message:'must be ${expected}'});if(!_all)return{valid:false,errors:_e}}`)
2728
+ {
2729
+ const typeSp = `${schemaPrefix}/type`
2730
+ const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
2731
+ lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}',params:{type:'${expected}'},message:'must be ${expected}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
2732
+ }
2652
2733
  }
2653
2734
 
2654
2735
  // In error mode, never assume type — always guard (data may have failed type check but allErrors continues)
@@ -2657,9 +2738,10 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2657
2738
  const isStr = false
2658
2739
  const isNum = false
2659
2740
 
2660
- const fail = (keyword, schemaSuffix, paramsCode, msgCode) => {
2741
+ const fail = (keyword, schemaSuffix, paramsCode, msgCode, fmt) => {
2661
2742
  const sp = schemaPrefix + '/' + schemaSuffix
2662
- return `_e.push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}});if(!_all)return{valid:false,errors:_e}`
2743
+ const lit = buildErrorLiteral({ keyword, format: fmt, schemaPath: sp, sourceMap: ctx.sourceMap })
2744
+ return `_e.push({code:'${lit.codeStr}',keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode},docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}`
2663
2745
  }
2664
2746
 
2665
2747
  // enum
@@ -2692,8 +2774,10 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2692
2774
  const requiredSet = new Set(schema.required || [])
2693
2775
  const hoisted = {}
2694
2776
  if (schema.required) {
2777
+ const reqSp = `${schemaPrefix}/required`
2778
+ const reqLit = buildErrorLiteral({ keyword: 'required', schemaPath: reqSp, sourceMap: ctx.sourceMap })
2695
2779
  for (const key of schema.required) {
2696
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){_e.push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required',params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"});if(!_all)return{valid:false,errors:_e}}`)
2780
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){_e.push({code:'${reqLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${reqSp}',params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'",docUrl:'${reqLit.docUrl}'${reqLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
2697
2781
  }
2698
2782
  }
2699
2783
 
@@ -2756,7 +2840,9 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2756
2840
  }
2757
2841
  if (schema.format) {
2758
2842
  const fc = FORMAT_CODEGEN[schema.format]
2759
- const failPush = `_e.push({keyword:'format',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/format',params:{format:'${esc(schema.format)}'},message:'must match format "${esc(schema.format)}"'});if(!_all)return{valid:false,errors:_e}`
2843
+ const fmtSp = `${schemaPrefix}/format`
2844
+ const fmtLit = buildErrorLiteral({ keyword: 'format', format: schema.format, schemaPath: fmtSp, sourceMap: ctx.sourceMap })
2845
+ const failPush = `_e.push({code:'${fmtLit.codeStr}',keyword:'format',instancePath:${pathExpr||'""'},schemaPath:'${fmtSp}',params:{format:'${esc(schema.format)}'},message:'must match format "${esc(schema.format)}"',docUrl:'${fmtLit.docUrl}'${fmtLit.frame}});if(!_all)return{valid:false,errors:_e}`
2760
2846
  // Format errors use the boolean codegen — just wrap with error push
2761
2847
  if (fc) {
2762
2848
  const ri = ctx.varCounter++
@@ -2816,15 +2902,19 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2816
2902
  if (schema.additionalProperties === false && schema.properties) {
2817
2903
  const allowed = Object.keys(schema.properties).map(k => `${JSON.stringify(k)}`).join(',')
2818
2904
  const ci = ctx.varCounter++
2819
- 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({keyword:'additionalProperties',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/additionalProperties',params:{additionalProperty:_k${ci}[_i]},message:'must NOT have additional properties'});if(!_all)return{valid:false,errors:_e}}}`
2905
+ const apSp = `${schemaPrefix}/additionalProperties`
2906
+ const apLit = buildErrorLiteral({ keyword: 'additionalProperties', schemaPath: apSp, sourceMap: ctx.sourceMap })
2907
+ 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:'${apLit.codeStr}',keyword:'additionalProperties',instancePath:${pathExpr||'""'},schemaPath:'${apSp}',params:{additionalProperty:_k${ci}[_i]},message:'must NOT have additional properties',docUrl:'${apLit.docUrl}'${apLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
2820
2908
  lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
2821
2909
  }
2822
2910
 
2823
2911
  // dependentRequired
2824
2912
  if (schema.dependentRequired) {
2913
+ const drSp = `${schemaPrefix}/dependentRequired`
2914
+ const drLit = buildErrorLiteral({ keyword: 'dependentRequired', schemaPath: drSp, sourceMap: ctx.sourceMap })
2825
2915
  for (const [key, deps] of Object.entries(schema.dependentRequired)) {
2826
2916
  for (const dep of deps) {
2827
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){_e.push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired',params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"});if(!_all)return{valid:false,errors:_e}}`)
2917
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){_e.push({code:'${drLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${drSp}',params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'",docUrl:'${drLit.docUrl}'${drLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
2828
2918
  }
2829
2919
  }
2830
2920
  }
@@ -2943,26 +3033,14 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2943
3033
  }
2944
3034
  }
2945
3035
 
2946
- // anyOf
3036
+ // anyOf - collapse to single best-branch error via __ataCollapse
2947
3037
  if (schema.anyOf) {
2948
- const fi = ctx.varCounter++
2949
- const fns = schema.anyOf.map((sub, i) => {
2950
- const subLines = []
2951
- genCode(sub, '_av', subLines, ctx)
2952
- return subLines.length === 0 ? `function(_av){return true}` : `function(_av){${subLines.join(';')};return true}`
2953
- })
2954
- 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('anyOf', 'anyOf', '{}', "'must match a schema in anyOf'")}}}`)
3038
+ emitBranchCollapse(schema.anyOf, 'anyOf', v, pathExpr, lines, ctx, schemaPrefix)
2955
3039
  }
2956
3040
 
2957
- // oneOf
3041
+ // oneOf - collapse to single best-branch error via __ataCollapse
2958
3042
  if (schema.oneOf) {
2959
- const fi = ctx.varCounter++
2960
- const fns = schema.oneOf.map((sub, i) => {
2961
- const subLines = []
2962
- genCode(sub, '_ov', subLines, ctx)
2963
- return subLines.length === 0 ? `function(_ov){return true}` : `function(_ov){${subLines.join(';')};return true}`
2964
- })
2965
- 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('oneOf', 'oneOf', '{}', "'must match exactly one schema in oneOf'")}}}`)
3043
+ emitBranchCollapse(schema.oneOf, 'oneOf', v, pathExpr, lines, ctx, schemaPrefix)
2966
3044
  }
2967
3045
 
2968
3046
  // not
@@ -3011,6 +3089,11 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
3011
3089
  // Bail on additionalProperties as a schema — combined path doesn't emit the
3012
3090
  // per-key sub-schema loop yet. The jsFn + errFn fallback handles it correctly.
3013
3091
  if (hasAdditionalPropertiesSchema(schema)) return null
3092
+ // Bail on oneOf/anyOf — branch-collapse aggregation lives in genCodeE only.
3093
+ // Letting genCodeC emit the legacy "must match exactly one" placeholder
3094
+ // would short-circuit the ATA4001/ATA4002/ATA4003 contract. The boolean
3095
+ // fast path plus errFn fallback handles these schemas with collapse.
3096
+ if (s.includes('"oneOf"') || s.includes('"anyOf"')) return null
3014
3097
  }
3015
3098
  if (typeof schema === 'boolean') {
3016
3099
  return schema
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ // Bounded Levenshtein. Returns Infinity if distance > maxDistance.
4
+ // Single-row DP. O(n*m) worst case but typical strings are <30 chars.
5
+ function levenshtein (a, b, maxDistance) {
6
+ const max = maxDistance == null ? Infinity : maxDistance;
7
+ if (a === b) return 0;
8
+ if (Math.abs(a.length - b.length) > max) return Infinity;
9
+ if (a.length === 0) return b.length;
10
+ if (b.length === 0) return a.length;
11
+ let prev = new Array(b.length + 1);
12
+ let curr = new Array(b.length + 1);
13
+ for (let j = 0; j <= b.length; j++) prev[j] = j;
14
+ for (let i = 1; i <= a.length; i++) {
15
+ curr[0] = i;
16
+ let rowMin = i;
17
+ for (let j = 1; j <= b.length; j++) {
18
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
19
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
20
+ if (curr[j] < rowMin) rowMin = curr[j];
21
+ }
22
+ if (rowMin > max) return Infinity;
23
+ [prev, curr] = [curr, prev];
24
+ }
25
+ return prev[b.length];
26
+ }
27
+
28
+ module.exports = { levenshtein };
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const { color, ANSI, resolveColor, pathToDotted, trimCwd } = require('./render-shared');
4
+
5
+ function renderCompact (errors, opts) {
6
+ if (!Array.isArray(errors) || errors.length === 0) return '';
7
+ opts = opts || {};
8
+ const useColor = resolveColor(opts.color || 'auto');
9
+ const cwd = opts.cwd;
10
+ const lines = [];
11
+
12
+ for (const err of errors) {
13
+ let prefix = '';
14
+ if (err.schemaSource) {
15
+ const f = trimCwd(err.schemaSource.file, cwd);
16
+ prefix = color(useColor, ANSI.cyan, `${f}:${err.schemaSource.line}:${err.schemaSource.col}`) + ' - ';
17
+ }
18
+ const codeStr = color(useColor, ANSI.red + ANSI.bold, `error ${err.code}`);
19
+ const pathStr = color(useColor, ANSI.cyan, pathToDotted(err.path));
20
+ const got = err.received != null ? `got ${err.received}` : '';
21
+ const sugg = err.suggestion ? color(useColor, ANSI.yellow, `, ${err.suggestion.text}`) : '';
22
+ const tail = got || sugg ? ` (${got}${sugg})` : '';
23
+ lines.push(`${prefix}${codeStr}: ${pathStr} ${err.message}${tail}`);
24
+ }
25
+
26
+ const n = errors.length;
27
+ lines.push('');
28
+ lines.push(`Found ${n} error${n === 1 ? '' : 's'} in ${opts.context || 'input'}.`);
29
+ if (!(process.stdout && process.stdout.isTTY)) {
30
+ lines.push('(run with --pretty for source frames)');
31
+ }
32
+ return lines.join('\n');
33
+ }
34
+
35
+ module.exports = { renderCompact };
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ function renderJSON (errors, opts) {
4
+ opts = opts || {};
5
+ const payload = {
6
+ errors: Array.isArray(errors) ? errors : [],
7
+ summary: { count: Array.isArray(errors) ? errors.length : 0, context: opts.context || 'input' },
8
+ };
9
+ return opts.pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload);
10
+ }
11
+
12
+ module.exports = { renderJSON };
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ const { color, ANSI, resolveColor, trimCwd, truncateLine, terminalWidth } = require('./render-shared');
4
+
5
+ function caretLine (col, length, gutter) {
6
+ const pad = ' '.repeat(gutter);
7
+ const lead = ' '.repeat(Math.max(0, col - 1));
8
+ const carets = '^'.repeat(Math.max(1, length || 1));
9
+ return pad + '| ' + lead + carets;
10
+ }
11
+
12
+ function renderOne (err, useColor, opts) {
13
+ const lines = [];
14
+ const cwd = opts.cwd;
15
+ const width = terminalWidth();
16
+ const gutter = 3;
17
+
18
+ // Headline
19
+ const headline = `${err.message}`;
20
+ lines.push(color(useColor, ANSI.red + ANSI.bold, `error[${err.code}]: `) + headline);
21
+
22
+ // Schema source frame
23
+ if (err.schemaSource) {
24
+ const f = trimCwd(err.schemaSource.file, cwd);
25
+ lines.push(` --> ${color(useColor, ANSI.cyan, `${f}:${err.schemaSource.line}:${err.schemaSource.col}`)}`);
26
+ lines.push(' |');
27
+ const ln = String(err.schemaSource.line).padStart(2, ' ');
28
+ const srcText = truncateLine(err.schemaSource.text, width - 8);
29
+ lines.push(` ${ln} | ${srcText}`);
30
+ const inlineHint = err.expected ? ' ' + color(useColor, ANSI.dim, `expected ${err.expected}`) : '';
31
+ lines.push(caretLine(err.schemaSource.col, 1, gutter) + inlineHint);
32
+ lines.push(' |');
33
+ }
34
+
35
+ // Data frame
36
+ if (err.dataFrame) {
37
+ lines.push(` --> ${color(useColor, ANSI.dim, `input, byte ${err.dataFrame.byteOffset}`)}`);
38
+ lines.push(' |');
39
+ const ln = String(err.dataFrame.line).padStart(2, ' ');
40
+ const srcText = truncateLine(err.dataFrame.text, width - 8);
41
+ lines.push(` ${ln} | ${srcText}`);
42
+ const got = err.received != null ? ' ' + color(useColor, ANSI.dim, `got ${err.received}`) : '';
43
+ lines.push(caretLine(err.dataFrame.col, err.dataFrame.length, gutter) + got);
44
+ lines.push(' |');
45
+ }
46
+
47
+ if (err.suggestion) {
48
+ lines.push(' = ' + color(useColor, ANSI.yellow, 'help: ') + err.suggestion.text);
49
+ }
50
+ if (err.branchErrors && err.branchErrors.length) {
51
+ const variant = (err.params && err.params.closestName) || 'closest variant';
52
+ const n = err.branchErrors.length;
53
+ lines.push(' = ' + color(useColor, ANSI.dim, 'note: ') + `closest match was ${variant} with ${n} error${n === 1 ? '' : 's'}:`);
54
+ renderBranchErrors(err.branchErrors, 1, lines, useColor);
55
+ }
56
+ if (err.docUrl) {
57
+ lines.push(' = ' + color(useColor, ANSI.dim, 'note: see ') + err.docUrl);
58
+ }
59
+ return lines.join('\n');
60
+ }
61
+
62
+ // Render nested branchErrors with a depth cap. Stops recursing past depth 3
63
+ // and emits a placeholder. Keeps pretty output bounded for deeply nested
64
+ // oneOf chains while preserving the structured payload for JSON consumers.
65
+ function renderBranchErrors (subs, depth, lines, useColor) {
66
+ if (depth >= 3) {
67
+ lines.push(' ' + color(useColor, ANSI.dim, '... deeper branch errors omitted, see structured output'));
68
+ return;
69
+ }
70
+ const max = 3;
71
+ const shown = subs.slice(0, max);
72
+ for (const sub of shown) {
73
+ lines.push(' ' + color(useColor, ANSI.dim, `${sub.keyword}: ${sub.message || ''}`));
74
+ if (sub.branchErrors && sub.branchErrors.length) renderBranchErrors(sub.branchErrors, depth + 1, lines, useColor);
75
+ }
76
+ if (subs.length > max) {
77
+ lines.push(' ' + color(useColor, ANSI.dim, `... and ${subs.length - max} more`));
78
+ }
79
+ }
80
+
81
+ function renderPretty (errors, opts) {
82
+ if (!Array.isArray(errors) || errors.length === 0) return '';
83
+ opts = opts || {};
84
+ const useColor = resolveColor(opts.color || 'auto');
85
+ const maxErrors = opts.maxErrors != null ? opts.maxErrors : 20;
86
+ const context = opts.context || 'input';
87
+
88
+ const blocks = [];
89
+ const limit = maxErrors === 0 ? errors.length : Math.min(maxErrors, errors.length);
90
+ for (let i = 0; i < limit; i++) {
91
+ blocks.push(renderOne(errors[i], useColor, opts));
92
+ }
93
+ let out = blocks.join('\n\n');
94
+ if (limit < errors.length) {
95
+ out += `\n\n... and ${errors.length - limit} more errors (run with --pretty --max-errors=0 to see all)`;
96
+ }
97
+ const n = errors.length;
98
+ out += `\n\n` + color(useColor, ANSI.red + ANSI.bold, `error: `) + `${n} schema violation${n === 1 ? '' : 's'} in ${context}`;
99
+ return out;
100
+ }
101
+
102
+ module.exports = { renderPretty };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const ANSI = {
4
+ reset: '\x1b[0m',
5
+ bold: '\x1b[1m',
6
+ dim: '\x1b[2m',
7
+ red: '\x1b[31m',
8
+ yellow: '\x1b[33m',
9
+ cyan: '\x1b[36m',
10
+ };
11
+
12
+ function resolveColor (opt) {
13
+ if (opt === 'never') return false;
14
+ if (opt === 'always') return true;
15
+ if (process.env.NO_COLOR != null && process.env.NO_COLOR !== '') return false;
16
+ const fc = process.env.FORCE_COLOR;
17
+ if (fc === '1' || fc === '2' || fc === '3' || fc === 'true') return true;
18
+ return !!(process.stdout && process.stdout.isTTY);
19
+ }
20
+
21
+ function color (enabled, code, s) {
22
+ return enabled ? code + s + ANSI.reset : s;
23
+ }
24
+
25
+ function pathToDotted (jsonPointer) {
26
+ if (!jsonPointer || jsonPointer === '/') return 'body';
27
+ const parts = jsonPointer.replace(/^\//, '').split('/').map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
28
+ let out = 'body';
29
+ for (const p of parts) {
30
+ if (/^[0-9]+$/.test(p)) {
31
+ out += '[' + p + ']';
32
+ } else if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(p)) {
33
+ out += '.' + p;
34
+ } else {
35
+ out += '[' + JSON.stringify(p) + ']';
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function trimCwd (file, cwd) {
42
+ if (!file) return file;
43
+ const c = cwd || process.cwd();
44
+ if (file.startsWith(c + '/')) return file.slice(c.length + 1);
45
+ return file;
46
+ }
47
+
48
+ function truncateLine (text, maxWidth) {
49
+ if (!text || text.length <= maxWidth) return text;
50
+ return text.slice(0, maxWidth - 1) + '…';
51
+ }
52
+
53
+ function terminalWidth () {
54
+ const w = process.stdout && process.stdout.columns;
55
+ return (typeof w === 'number' && w > 0) ? w : 100;
56
+ }
57
+
58
+ module.exports = { ANSI, resolveColor, color, pathToDotted, trimCwd, truncateLine, terminalWidth };