ata-validator 1.8.2 → 1.10.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 +23 -0
- package/index.js +67 -7
- package/lib/correlate.js +106 -0
- package/lib/data-positions.js +21 -3
- package/lib/diagnose.js +312 -0
- package/lib/diagnostic-source.js +46 -0
- package/lib/enrich-error.js +76 -0
- package/lib/formats.js +57 -0
- package/lib/interpreter.js +3 -2
- package/lib/js-compiler.js +244 -126
- package/lib/levenshtein.js +13 -2
- package/lib/render-compact.js +21 -11
- package/lib/render-pretty.js +89 -40
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/enrich-error.js
CHANGED
|
@@ -35,6 +35,7 @@ function expectedFor (err) {
|
|
|
35
35
|
case 'minimum': return err.params && err.params.limit != null ? `≥${err.params.limit}` : undefined;
|
|
36
36
|
case 'maximum': return err.params && err.params.limit != null ? `≤${err.params.limit}` : undefined;
|
|
37
37
|
case 'format': return err.params && err.params.format ? `format '${err.params.format}'` : undefined;
|
|
38
|
+
case 'pattern': return err.params && err.params.pattern ? `string matching /${err.params.pattern}/` : undefined;
|
|
38
39
|
case 'enum': return err.params && err.params.allowedValues
|
|
39
40
|
? `one of [${err.params.allowedValues.map(reprValue).join(', ')}]`
|
|
40
41
|
: undefined;
|
|
@@ -73,6 +74,59 @@ function pickReceived (err, data) {
|
|
|
73
74
|
* Pure: returns a new object. Source frames and suggestions are added by
|
|
74
75
|
* other helpers later in the pipeline.
|
|
75
76
|
*/
|
|
77
|
+
// Observation-first wording. `message` stays as it is for parity, so this is a
|
|
78
|
+
// separate field a consumer opts into.
|
|
79
|
+
function detailFor (err, out) {
|
|
80
|
+
const p = err.params || {};
|
|
81
|
+
switch (err.keyword) {
|
|
82
|
+
case 'type':
|
|
83
|
+
return `expected ${p.type}, found ${typeNameOf(out.received)}`;
|
|
84
|
+
case 'required':
|
|
85
|
+
return `missing required property "${p.missingProperty}"`;
|
|
86
|
+
case 'additionalProperties':
|
|
87
|
+
return `unknown property "${p.additionalProperty}"`;
|
|
88
|
+
case 'unevaluatedProperties':
|
|
89
|
+
return `unevaluated property "${p.unevaluatedProperty}"`;
|
|
90
|
+
case 'enum':
|
|
91
|
+
return out.expected ? `expected ${out.expected}, found ${out.received}` : undefined;
|
|
92
|
+
case 'const':
|
|
93
|
+
return out.expected ? `expected ${out.expected}, found ${out.received}` : undefined;
|
|
94
|
+
case 'format':
|
|
95
|
+
return `not a valid ${p.format}: ${out.received}`;
|
|
96
|
+
case 'minimum': case 'maximum': case 'exclusiveMinimum': case 'exclusiveMaximum':
|
|
97
|
+
return `expected ${out.expected}, found ${out.received}`;
|
|
98
|
+
case 'minLength': case 'maxLength':
|
|
99
|
+
return `expected ${out.expected}, found ${out.received}`;
|
|
100
|
+
default:
|
|
101
|
+
return out.expected ? `expected ${out.expected}, found ${out.received}` : undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Derived from the repr in `received`, which is already a JSON-ish string.
|
|
106
|
+
function typeNameOf (received) {
|
|
107
|
+
if (received === undefined) return 'nothing';
|
|
108
|
+
if (received === 'null') return 'null';
|
|
109
|
+
if (received === 'true' || received === 'false') return 'boolean';
|
|
110
|
+
if (received.startsWith('"')) return 'string';
|
|
111
|
+
if (received.startsWith('[array')) return 'array';
|
|
112
|
+
if (received.startsWith('{') || received.startsWith('[object')) return 'object';
|
|
113
|
+
if (/^-?\d/.test(received)) return 'number';
|
|
114
|
+
return 'value';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Cause before effect, applied only as a tie-break within one container.
|
|
118
|
+
// Ordering across the document is by position, done in the renderer.
|
|
119
|
+
const RANK = {
|
|
120
|
+
required: 0, additionalProperties: 0, unevaluatedProperties: 0,
|
|
121
|
+
unevaluatedItems: 0, dependentRequired: 0, propertyNames: 0,
|
|
122
|
+
type: 1,
|
|
123
|
+
oneOf: 3, anyOf: 3, allOf: 3, not: 3,
|
|
124
|
+
};
|
|
125
|
+
function rankFor (keyword) {
|
|
126
|
+
const r = RANK[keyword];
|
|
127
|
+
return r === undefined ? 2 : r;
|
|
128
|
+
}
|
|
129
|
+
|
|
76
130
|
function enrich (rawErr, opts) {
|
|
77
131
|
const data = opts && opts.data;
|
|
78
132
|
const positions = opts && opts.positions;
|
|
@@ -129,6 +183,28 @@ function enrich (rawErr, opts) {
|
|
|
129
183
|
const sugg = suggestFor(out, opts && opts.data);
|
|
130
184
|
if (sugg) out.suggestion = sugg;
|
|
131
185
|
|
|
186
|
+
const detail = detailFor(rawErr, out);
|
|
187
|
+
if (detail !== undefined) out.detail = detail;
|
|
188
|
+
out.rank = rankFor(keyword);
|
|
189
|
+
|
|
190
|
+
// Token-level anchor. `dataFrame` already carries the value span; `anchor`
|
|
191
|
+
// adds the key span so a caret can sit on the property that is wrong rather
|
|
192
|
+
// than on the object containing it.
|
|
193
|
+
if (opts && opts.positions) {
|
|
194
|
+
const named = (rawErr.params && (rawErr.params.additionalProperty || rawErr.params.unevaluatedProperty)) || null;
|
|
195
|
+
const own = opts.positions[path];
|
|
196
|
+
const child = named ? opts.positions[(path === '' ? '' : path) + '/' + named] : null;
|
|
197
|
+
const src = child || own;
|
|
198
|
+
if (src) {
|
|
199
|
+
out.anchor = { line: src.line, col: src.col, length: src.length };
|
|
200
|
+
if (src.keyOffset !== undefined) {
|
|
201
|
+
out.anchor.keyLine = src.keyLine;
|
|
202
|
+
out.anchor.keyCol = src.keyCol;
|
|
203
|
+
out.anchor.keyLength = src.keyLength;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
132
208
|
return out;
|
|
133
209
|
}
|
|
134
210
|
|
package/lib/formats.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Format predicates that read the string once, with no regular expression
|
|
4
|
+
// and no allocation. Each has a twin that emits the same check as source for
|
|
5
|
+
// the code generator; tests/test_formats_single_pass.js holds the two
|
|
6
|
+
// together and fuzzes both against the regular-expression forms they replaced.
|
|
7
|
+
// Measured, interleaved medians: date 45.7 to 14.2 ns, ipv4 54.5 to 27.1 ns.
|
|
8
|
+
// uuid was tried the same way and measured 57.3 against 59.4 ns: V8's regular
|
|
9
|
+
// expression for a fixed-length hex pattern is already that fast, so it stays.
|
|
10
|
+
|
|
11
|
+
function isDigit (c) { return c >= 48 && c <= 57; }
|
|
12
|
+
|
|
13
|
+
function date (s) {
|
|
14
|
+
if (s.length !== 10) return false;
|
|
15
|
+
for (let i = 0; i < 10; i++) {
|
|
16
|
+
const c = s.charCodeAt(i);
|
|
17
|
+
if (i === 4 || i === 7) { if (c !== 45) return false; } else if (!isDigit(c)) return false;
|
|
18
|
+
}
|
|
19
|
+
const m = (s.charCodeAt(5) - 48) * 10 + (s.charCodeAt(6) - 48);
|
|
20
|
+
const d = (s.charCodeAt(8) - 48) * 10 + (s.charCodeAt(9) - 48);
|
|
21
|
+
return m >= 1 && m <= 12 && d >= 1 && d <= 31;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Four decimal octets, no leading zeros, no empty octets, nothing else.
|
|
25
|
+
function ipv4 (s) {
|
|
26
|
+
const n = s.length;
|
|
27
|
+
if (n < 7 || n > 15) return false;
|
|
28
|
+
let octets = 0, value = 0, digits = 0;
|
|
29
|
+
for (let i = 0; i <= n; i++) {
|
|
30
|
+
const c = i < n ? s.charCodeAt(i) : 46;
|
|
31
|
+
if (c === 46) {
|
|
32
|
+
if (digits === 0 || value > 255) return false;
|
|
33
|
+
octets++; value = 0; digits = 0;
|
|
34
|
+
if (octets > 4) return false;
|
|
35
|
+
} else if (isDigit(c)) {
|
|
36
|
+
if (digits === 1 && value === 0) return false;
|
|
37
|
+
value = value * 10 + (c - 48); digits++;
|
|
38
|
+
if (digits > 3) return false;
|
|
39
|
+
} else return false;
|
|
40
|
+
}
|
|
41
|
+
return octets === 4;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Source twins. `v` is the expression holding the string; when `isStr` is
|
|
45
|
+
// false the check is wrapped in a typeof guard, matching FORMAT_CODEGEN's
|
|
46
|
+
// convention. Each returns a statement that `return false`s on mismatch.
|
|
47
|
+
function guard (v, isStr, body) { return isStr ? `{${body}}` : `if(typeof ${v}==='string'){${body}}`; }
|
|
48
|
+
|
|
49
|
+
function dateSource (v, isStr) {
|
|
50
|
+
return guard(v, isStr, `if(${v}.length!==10)return false;for(let _i=0;_i<10;_i++){const _c=${v}.charCodeAt(_i);if(_i===4||_i===7){if(_c!==45)return false}else if(_c<48||_c>57)return false}const _m=(${v}.charCodeAt(5)-48)*10+(${v}.charCodeAt(6)-48),_d=(${v}.charCodeAt(8)-48)*10+(${v}.charCodeAt(9)-48);if(_m<1||_m>12||_d<1||_d>31)return false`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ipv4Source (v, isStr) {
|
|
54
|
+
return guard(v, isStr, `const _n=${v}.length;if(_n<7||_n>15)return false;let _o=0,_val=0,_dg=0;for(let _i=0;_i<=_n;_i++){const _c=_i<_n?${v}.charCodeAt(_i):46;if(_c===46){if(_dg===0||_val>255)return false;_o++;_val=0;_dg=0;if(_o>4)return false}else if(_c>=48&&_c<=57){if(_dg===1&&_val===0)return false;_val=_val*10+(_c-48);_dg++;if(_dg>3)return false}else return false}if(_o!==4)return false`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { date, ipv4, dateSource, ipv4Source };
|
package/lib/interpreter.js
CHANGED
|
@@ -247,16 +247,17 @@ function multipleOfOk(d, m) {
|
|
|
247
247
|
return Math.abs(q - Math.round(q)) < 1e-9;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
const _formats = require('./formats');
|
|
250
251
|
const FORMAT_CHECKS = {
|
|
251
252
|
email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1; },
|
|
252
|
-
date:
|
|
253
|
+
date: _formats.date,
|
|
253
254
|
'date-time': (s) => /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/.test(s) && !isNaN(Date.parse(s)),
|
|
254
255
|
time: (s) => /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?([Zz]|[+-]\d{2}:\d{2})?$/.test(s),
|
|
255
256
|
duration: (s) => /^P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/.test(s) && s !== 'P' && !s.endsWith('T'),
|
|
256
257
|
uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
|
|
257
258
|
uri: (s) => /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(s) && !/[\s\u0000-\u001f\u007f]/.test(s),
|
|
258
259
|
'uri-reference': (s) => !/[\s\u0000-\u001f\u007f]/.test(s),
|
|
259
|
-
ipv4:
|
|
260
|
+
ipv4: _formats.ipv4,
|
|
260
261
|
ipv6: (s) => s !== '' && /^[0-9a-fA-F:.]+$/.test(s) && s.split(':').length >= 3 && s.split(':').length <= 8,
|
|
261
262
|
hostname: (s) => s.length > 0 && s.length <= 253 && /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(s),
|
|
262
263
|
regex: (s) => { try { new RegExp(s, 'u'); return true; } catch { try { new RegExp(s); return true; } catch { return false; } } },
|