ata-validator 0.13.4 → 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.
- package/CHANGELOG.md +42 -0
- package/README.md +69 -1
- package/bin/ata.js +189 -13
- package/build.d.ts +6 -0
- package/index.browser.mjs +1 -1
- package/index.d.ts +87 -19
- package/index.js +188 -5
- package/index.mjs +1 -1
- package/lib/aot-build.js +25 -1
- package/lib/branch-collapse.js +75 -0
- package/lib/data-position-cache.js +43 -0
- package/lib/data-positions.js +104 -0
- package/lib/enrich-error.js +125 -0
- package/lib/error-codes.js +96 -0
- package/lib/js-compiler.js +108 -25
- package/lib/levenshtein.js +28 -0
- package/lib/render-compact.js +35 -0
- package/lib/render-json.js +12 -0
- package/lib/render-pretty.js +102 -0
- package/lib/render-shared.js +58 -0
- package/lib/source-positions.js +148 -0
- package/lib/suggestions.js +132 -0
- package/package.json +12 -5
- package/scripts/check-doc-coverage.js +34 -0
- package/scripts/regen-error-codes-doc.js +40 -0
- package/scripts/regen-lock.js +19 -0
|
@@ -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 };
|
package/lib/js-compiler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:'${
|
|
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
|
|
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
|
|
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:'${
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|