ata-validator 1.43.0 → 1.45.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/README.md +10 -10
- package/index.js +54 -0
- package/lib/clone-emit.js +5 -2
- package/lib/data-position-cache.js +12 -6
- package/lib/draft7.js +5 -7
- package/lib/formats.js +8 -2
- package/lib/interpreter.js +44 -51
- package/lib/js-compiler.js +197 -76
- package/lib/levenshtein.js +34 -22
- package/lib/native-positions.js +100 -0
- package/lib/plan-compiler.js +23 -25
- package/lib/rejections.js +14 -7
- package/lib/suggestions.js +27 -20
- package/lib/unique-items.js +139 -0
- package/lib/validator-core.js +71 -10
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/levenshtein.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Bounded Levenshtein. Returns Infinity if distance > maxDistance
|
|
4
|
-
//
|
|
3
|
+
// Bounded Levenshtein. Returns Infinity if distance > maxDistance (or, past
|
|
4
|
+
// the bound, possibly some number greater than it; callers compare).
|
|
5
|
+
//
|
|
6
|
+
// Only the cells within the bound of the diagonal are computed: a path
|
|
7
|
+
// through any other cell already costs more than the bound. A typo hint asks
|
|
8
|
+
// with a bound of one or two and compares the missing name with every key
|
|
9
|
+
// present, so a row is five cells instead of the whole key. Without a bound
|
|
10
|
+
// the band is the whole row.
|
|
11
|
+
//
|
|
5
12
|
// Two rows of scratch, reused across calls. The function is not recursive
|
|
6
13
|
// and the library is single-threaded, so nothing else is using them; growing
|
|
7
14
|
// them once beats allocating two arrays on every comparison, which the
|
|
@@ -10,42 +17,47 @@ let scratchA = new Int32Array(64);
|
|
|
10
17
|
let scratchB = new Int32Array(64);
|
|
11
18
|
|
|
12
19
|
function levenshtein (a, b, maxDistance) {
|
|
13
|
-
const max = maxDistance == null ? Infinity : maxDistance;
|
|
14
20
|
if (a === b) return 0;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const n = a.length;
|
|
22
|
+
const m = b.length;
|
|
23
|
+
const max = maxDistance == null || maxDistance > n + m ? n + m : maxDistance;
|
|
24
|
+
if (Math.abs(n - m) > max) return Infinity;
|
|
25
|
+
if (n === 0) return m;
|
|
26
|
+
if (m === 0) return n;
|
|
27
|
+
const big = max + 1;
|
|
28
|
+
if (scratchA.length < m + 2) {
|
|
29
|
+
scratchA = new Int32Array(m + 2);
|
|
30
|
+
scratchB = new Int32Array(m + 2);
|
|
21
31
|
}
|
|
22
32
|
let prev = scratchA;
|
|
23
33
|
let curr = scratchB;
|
|
24
|
-
|
|
25
|
-
for (let
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
for (let j = 0; j <= m; j++) prev[j] = j <= max ? j : big;
|
|
35
|
+
for (let i = 1; i <= n; i++) {
|
|
36
|
+
const lo = i - max > 1 ? i - max : 1;
|
|
37
|
+
const hi = i + max < m ? i + max : m;
|
|
38
|
+
curr[lo - 1] = lo === 1 ? i : big;
|
|
39
|
+
let rowMin = curr[lo - 1];
|
|
29
40
|
// charCodeAt rather than indexing: reading a character by index allocates
|
|
30
41
|
// a one-character string, and this is the innermost loop.
|
|
31
42
|
const ca = a.charCodeAt(i - 1);
|
|
32
|
-
for (let j =
|
|
33
|
-
|
|
34
|
-
let m = prev[j - 1] + cost;
|
|
43
|
+
for (let j = lo; j <= hi; j++) {
|
|
44
|
+
let v = prev[j - 1] + (ca === b.charCodeAt(j - 1) ? 0 : 1);
|
|
35
45
|
const del = curr[j - 1] + 1;
|
|
36
|
-
if (del <
|
|
46
|
+
if (del < v) v = del;
|
|
37
47
|
const ins = prev[j] + 1;
|
|
38
|
-
if (ins <
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
if (ins < v) v = ins;
|
|
49
|
+
if (v > big) v = big;
|
|
50
|
+
curr[j] = v;
|
|
51
|
+
if (v < rowMin) rowMin = v;
|
|
41
52
|
}
|
|
53
|
+
if (hi < m) curr[hi + 1] = big;
|
|
42
54
|
if (rowMin > max) return Infinity;
|
|
43
55
|
// A destructured swap builds an array per row; a temporary does not.
|
|
44
56
|
const t = prev;
|
|
45
57
|
prev = curr;
|
|
46
58
|
curr = t;
|
|
47
59
|
}
|
|
48
|
-
return prev[
|
|
60
|
+
return prev[m] > max ? Infinity : prev[m];
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
module.exports = { levenshtein };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The entries buildTargetedPositionMap gives, found by the native addon where
|
|
4
|
+
// it can: simdjson indexes the document's structure once and walks that
|
|
5
|
+
// index to each pointer, where the script walk reads every character. On a
|
|
6
|
+
// 5 MB log with the error near its end that is 3.1 ms against 8.3. Only for
|
|
7
|
+
// a large ASCII string, where the addon's UTF-8 byte offsets are the string's
|
|
8
|
+
// indexes, and only when every wanted pointer is found; otherwise null, and
|
|
9
|
+
// the caller builds the map in script as before.
|
|
10
|
+
const NATIVE_MIN = 32 * 1024;
|
|
11
|
+
let _native;
|
|
12
|
+
function nativeTargeted (text, wanted) {
|
|
13
|
+
if (typeof text !== 'string' || text.length < NATIVE_MIN) return null;
|
|
14
|
+
if (_native === undefined) {
|
|
15
|
+
try { _native = require('./native-load')(); } catch { _native = null; }
|
|
16
|
+
if (_native && typeof _native.locatePointers !== 'function') _native = null;
|
|
17
|
+
}
|
|
18
|
+
if (_native === null || typeof Buffer === 'undefined' || Buffer.byteLength(text, 'utf8') !== text.length) return null;
|
|
19
|
+
const ptrs = Array.from(wanted);
|
|
20
|
+
let found;
|
|
21
|
+
try { found = _native.locatePointers(text, ptrs); } catch { return null; }
|
|
22
|
+
const offs = [];
|
|
23
|
+
for (let k = 0; k < ptrs.length; k++) {
|
|
24
|
+
if (!(found[2 * k] >= 0)) return null;
|
|
25
|
+
offs.push(found[2 * k]);
|
|
26
|
+
}
|
|
27
|
+
// Line starts for the offsets that are asked about, in one forward pass.
|
|
28
|
+
const needed = [];
|
|
29
|
+
for (let k = 0; k < ptrs.length; k++) {
|
|
30
|
+
needed.push(offs[k]);
|
|
31
|
+
const ks = keyStartBefore(text, offs[k]);
|
|
32
|
+
if (ks >= 0) needed.push(ks);
|
|
33
|
+
}
|
|
34
|
+
const lineOf = lineFinder(text, needed);
|
|
35
|
+
const map = Object.create(null);
|
|
36
|
+
for (let k = 0; k < ptrs.length; k++) {
|
|
37
|
+
const off = offs[k];
|
|
38
|
+
const at = lineOf(off);
|
|
39
|
+
const entry = { byteOffset: off, length: found[2 * k + 1], line: at.line, col: off - at.start + 1, text: at.text };
|
|
40
|
+
const ks = keyStartBefore(text, off);
|
|
41
|
+
if (ks >= 0 && ptrs[k] !== '') {
|
|
42
|
+
const kat = lineOf(ks);
|
|
43
|
+
entry.keyOffset = ks;
|
|
44
|
+
entry.keyLength = keyEndAfter(text, ks) - ks + 1;
|
|
45
|
+
entry.keyLine = kat.line;
|
|
46
|
+
entry.keyCol = ks - kat.start + 1;
|
|
47
|
+
}
|
|
48
|
+
map[ptrs[k]] = entry;
|
|
49
|
+
}
|
|
50
|
+
return map;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The opening quote of the member name before a value at `off`, or -1 where
|
|
54
|
+
// the value is an array element or the root.
|
|
55
|
+
function keyStartBefore (text, off) {
|
|
56
|
+
let j = off - 1;
|
|
57
|
+
while (j >= 0) { const c = text.charCodeAt(j); if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) j--; else break; }
|
|
58
|
+
if (j < 0 || text.charCodeAt(j) !== 0x3a) return -1;
|
|
59
|
+
j--;
|
|
60
|
+
while (j >= 0) { const c = text.charCodeAt(j); if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) j--; else break; }
|
|
61
|
+
if (j < 0 || text.charCodeAt(j) !== 0x22) return -1;
|
|
62
|
+
// Back to the quote that opens the name: one not preceded by an odd run of
|
|
63
|
+
// backslashes.
|
|
64
|
+
for (let q = j - 1; q >= 0; q--) {
|
|
65
|
+
if (text.charCodeAt(q) !== 0x22) continue;
|
|
66
|
+
let b = q - 1;
|
|
67
|
+
while (b >= 0 && text.charCodeAt(b) === 0x5c) b--;
|
|
68
|
+
if (((q - 1 - b) & 1) === 0) return q;
|
|
69
|
+
}
|
|
70
|
+
return -1;
|
|
71
|
+
}
|
|
72
|
+
function keyEndAfter (text, start) {
|
|
73
|
+
let q = start + 1;
|
|
74
|
+
for (;;) {
|
|
75
|
+
const c = text.charCodeAt(q);
|
|
76
|
+
if (c === 0x5c) { q += 2; continue; }
|
|
77
|
+
if (c === 0x22 || q >= text.length) return q;
|
|
78
|
+
q++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Line number, line start and line text for any of `offsets`, from one pass.
|
|
82
|
+
function lineFinder (text, offsets) {
|
|
83
|
+
const sorted = Array.from(new Set(offsets)).sort((a, b) => a - b);
|
|
84
|
+
const info = new Map();
|
|
85
|
+
let line = 1, start = 0, s = 0;
|
|
86
|
+
while (s < sorted.length) {
|
|
87
|
+
const nl = text.indexOf('\n', start);
|
|
88
|
+
const end = nl === -1 ? text.length : nl;
|
|
89
|
+
while (s < sorted.length && sorted[s] <= end) {
|
|
90
|
+
info.set(sorted[s], { line, start, text: text.slice(start, end) });
|
|
91
|
+
s++;
|
|
92
|
+
}
|
|
93
|
+
if (nl === -1) break;
|
|
94
|
+
line++;
|
|
95
|
+
start = nl + 1;
|
|
96
|
+
}
|
|
97
|
+
return (off) => info.get(off);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { nativeTargeted };
|
package/lib/plan-compiler.js
CHANGED
|
@@ -52,12 +52,16 @@ const branchTitle = (n) => (n && n.schema && typeof n.schema.title === 'string'
|
|
|
52
52
|
|
|
53
53
|
function install(deps) {
|
|
54
54
|
const { Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
|
|
55
|
-
deepEqual, multipleOfOk, cpAtLeast, cpAtMost,
|
|
55
|
+
deepEqual, enumMember, allDistinct, multipleOfOk, cpAtLeast, cpAtMost,
|
|
56
56
|
T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri } = deps;
|
|
57
57
|
|
|
58
58
|
const TRUE_FN = () => true;
|
|
59
|
-
|
|
59
|
+
// `leaf`: the node compiled no subschema of its own (see compileNode). A
|
|
60
|
+
// collecting caller asks a leaf for its verdict first and builds the paths
|
|
61
|
+
// for its errors only when it fails.
|
|
62
|
+
const TRUE_PAIR = { v: TRUE_FN, c: TRUE_FN, leaf: true };
|
|
60
63
|
const FALSE_PAIR = {
|
|
64
|
+
leaf: true,
|
|
61
65
|
v: () => false,
|
|
62
66
|
c: (data, errors, instancePath, schemaPath) => {
|
|
63
67
|
if (errors !== NOERRORS) errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
|
|
@@ -164,12 +168,8 @@ function install(deps) {
|
|
|
164
168
|
checks.push((data, bits) => (bits & mask) !== 0);
|
|
165
169
|
}
|
|
166
170
|
if (P.enum !== null) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
else checks.push((data) => {
|
|
170
|
-
for (let i = 0; i < vals.length; i++) if (deepEqual(vals[i], data)) return true;
|
|
171
|
-
return false;
|
|
172
|
-
});
|
|
171
|
+
if (P.enum.length === 0) checks.push(() => false);
|
|
172
|
+
else checks.push((data) => enumMember(P, data));
|
|
173
173
|
}
|
|
174
174
|
if (P.hasConst) {
|
|
175
175
|
const c = P.const;
|
|
@@ -205,14 +205,7 @@ function install(deps) {
|
|
|
205
205
|
const arrs = [];
|
|
206
206
|
if (P.minItems !== undefined) { const m = P.minItems; arrs.push((d) => d.length >= m); }
|
|
207
207
|
if (P.maxItems !== undefined) { const m = P.maxItems; arrs.push((d) => d.length <= m); }
|
|
208
|
-
if (P.uniqueItems)
|
|
209
|
-
arrs.push((d) => {
|
|
210
|
-
for (let i = 0; i < d.length; i++) {
|
|
211
|
-
for (let j = i + 1; j < d.length; j++) if (deepEqual(d[i], d[j])) return false;
|
|
212
|
-
}
|
|
213
|
-
return true;
|
|
214
|
-
});
|
|
215
|
-
}
|
|
208
|
+
if (P.uniqueItems) arrs.push(allDistinct);
|
|
216
209
|
const run = combineValue(arrs);
|
|
217
210
|
if (run !== null) { needsBits = true; checks.push((data, bits) => bits !== T_ARRAY || run(data)); }
|
|
218
211
|
}
|
|
@@ -300,6 +293,7 @@ function install(deps) {
|
|
|
300
293
|
const box = { v: null, c: null };
|
|
301
294
|
const pair = {
|
|
302
295
|
open: true,
|
|
296
|
+
leaf: false,
|
|
303
297
|
v: (d, st, rec) => box.v(d, st, rec),
|
|
304
298
|
c: (d, e, ip, sp, st, rec) => box.c(d, e, ip, sp, st, rec),
|
|
305
299
|
};
|
|
@@ -308,8 +302,11 @@ function install(deps) {
|
|
|
308
302
|
// In-place children collect when this node does; child applicators
|
|
309
303
|
// never do. `rec` is threaded as the trailing argument of every step
|
|
310
304
|
// and is undefined on non-collecting nodes, where nothing reads it.
|
|
311
|
-
|
|
312
|
-
|
|
305
|
+
// Whether this node compiles any subschema: a node that does not is a
|
|
306
|
+
// leaf, whose verdict is a few comparisons.
|
|
307
|
+
let nested = false;
|
|
308
|
+
const inPlace = (n, b) => { nested = true; return compileNode(ctx, n, b, chain, collect); };
|
|
309
|
+
const child = (n) => { nested = true; return compileNode(ctx, n, base, chain, false); };
|
|
313
310
|
|
|
314
311
|
const steps = []; // collect variant: (data, errors, ip, sp, stack, rec)
|
|
315
312
|
const vsteps = []; // verdict variant: (data, stack, rec)
|
|
@@ -399,7 +396,7 @@ function install(deps) {
|
|
|
399
396
|
let ok = true;
|
|
400
397
|
const n = Math.min(fns.length, data.length);
|
|
401
398
|
for (let i = 0; i < n; i++) {
|
|
402
|
-
if (!fns[i].c(data[i], errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack)) {
|
|
399
|
+
if (!((fns[i].leaf && fns[i].v(data[i], stack)) || fns[i].c(data[i], errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack))) {
|
|
403
400
|
ok = false;
|
|
404
401
|
if (errors === NOERRORS) return false;
|
|
405
402
|
}
|
|
@@ -422,7 +419,7 @@ function install(deps) {
|
|
|
422
419
|
if (dataBits(data) !== T_ARRAY) return true;
|
|
423
420
|
let ok = true;
|
|
424
421
|
for (let i = start; i < data.length; i++) {
|
|
425
|
-
if (!fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/items', stack)) {
|
|
422
|
+
if (!((fn.leaf && fn.v(data[i], stack)) || fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/items', stack))) {
|
|
426
423
|
ok = false;
|
|
427
424
|
if (errors === NOERRORS) return false;
|
|
428
425
|
}
|
|
@@ -536,7 +533,7 @@ function install(deps) {
|
|
|
536
533
|
let evaluated = false;
|
|
537
534
|
const prop = lookup !== null ? lookup(key) : undefined;
|
|
538
535
|
if (prop !== undefined) {
|
|
539
|
-
if (!prop.fn.c(data[key], errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack)) {
|
|
536
|
+
if (!((prop.fn.leaf && prop.fn.v(data[key], stack)) || prop.fn.c(data[key], errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack))) {
|
|
540
537
|
ok = false;
|
|
541
538
|
if (errors === NOERRORS) return false;
|
|
542
539
|
}
|
|
@@ -546,7 +543,7 @@ function install(deps) {
|
|
|
546
543
|
for (let pi = 0; pi < patterns.length; pi++) {
|
|
547
544
|
const pp = patterns[pi];
|
|
548
545
|
if (pp.re.test(key)) {
|
|
549
|
-
if (!pp.fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(pp.src), stack)) {
|
|
546
|
+
if (!((pp.fn.leaf && pp.fn.v(data[key], stack)) || pp.fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(pp.src), stack))) {
|
|
550
547
|
ok = false;
|
|
551
548
|
if (errors === NOERRORS) return false;
|
|
552
549
|
}
|
|
@@ -559,7 +556,7 @@ function install(deps) {
|
|
|
559
556
|
ok = false;
|
|
560
557
|
if (errors === NOERRORS) return false;
|
|
561
558
|
errors.push(err('additionalProperties', 'additionalProperties', instancePath, schemaPath + '/additionalProperties', { additionalProperty: key }, 'must NOT have additional properties'));
|
|
562
|
-
} else if (!apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack)) {
|
|
559
|
+
} else if (!((apFn.leaf && apFn.v(data[key], stack)) || apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack))) {
|
|
563
560
|
ok = false;
|
|
564
561
|
if (errors === NOERRORS) return false;
|
|
565
562
|
}
|
|
@@ -927,7 +924,7 @@ function install(deps) {
|
|
|
927
924
|
for (let k = 0; k < keys.length; k++) {
|
|
928
925
|
const key = keys[k];
|
|
929
926
|
if (hasProp(rec, key)) continue;
|
|
930
|
-
if (!fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack)) {
|
|
927
|
+
if (!((fn.leaf && fn.v(data[key], stack)) || fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack))) {
|
|
931
928
|
ok = false;
|
|
932
929
|
if (errors === NOERRORS) return false;
|
|
933
930
|
}
|
|
@@ -983,7 +980,7 @@ function install(deps) {
|
|
|
983
980
|
let ok = true;
|
|
984
981
|
for (let i = 0; i < data.length; i++) {
|
|
985
982
|
if (hasItem(rec, i)) continue;
|
|
986
|
-
if (!fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack)) {
|
|
983
|
+
if (!((fn.leaf && fn.v(data[i], stack)) || fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack))) {
|
|
987
984
|
ok = false;
|
|
988
985
|
if (errors === NOERRORS) return false;
|
|
989
986
|
}
|
|
@@ -1068,6 +1065,7 @@ function install(deps) {
|
|
|
1068
1065
|
box.v = vfn;
|
|
1069
1066
|
pair.c = cfn;
|
|
1070
1067
|
pair.v = vfn;
|
|
1068
|
+
pair.leaf = !nested;
|
|
1071
1069
|
pair.open = false;
|
|
1072
1070
|
return pair;
|
|
1073
1071
|
}
|
package/lib/rejections.js
CHANGED
|
@@ -133,13 +133,18 @@ Object.defineProperty(RichRejection.prototype, 'errors', {
|
|
|
133
133
|
// touches none of it. The JSON text is held here rather than read back off the
|
|
134
134
|
// validator, so the result does not depend on what the instance does next.
|
|
135
135
|
class LazyJsonRejection {
|
|
136
|
-
|
|
136
|
+
// `parsed`, when given, is the document the inner path parsed from
|
|
137
|
+
// `jsonStr` and validated, after defaults, coercion and removal: reading the
|
|
138
|
+
// errors uses it instead of parsing the text again, which on a 5 MB
|
|
139
|
+
// document was a third of what reading them cost.
|
|
140
|
+
constructor(result, jsonStr, self, enrich, parsed) {
|
|
137
141
|
this.valid = false;
|
|
138
142
|
this._result = result;
|
|
139
143
|
this._jsonStr = jsonStr;
|
|
140
144
|
this._self = self;
|
|
141
145
|
this._enrich = enrich;
|
|
142
146
|
this._cached = null;
|
|
147
|
+
this._parsed = parsed;
|
|
143
148
|
}
|
|
144
149
|
toJSON() {
|
|
145
150
|
return { valid: false, errors: this.errors };
|
|
@@ -159,12 +164,14 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
|
|
|
159
164
|
|
|
160
165
|
// The enrich pass plucks `received` from the value tree and the suggestion
|
|
161
166
|
// engine (required-typo, format hints, coercion nudges) walks it too.
|
|
162
|
-
let parsedData;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
let parsedData = this._parsed;
|
|
168
|
+
if (parsedData === undefined) {
|
|
169
|
+
try { parsedData = JSON.parse(jsonStr); } catch { parsedData = undefined; }
|
|
170
|
+
// What was checked is the document after defaults, coercion and removal,
|
|
171
|
+
// so `received` has to come from that, as it does for validate(). A
|
|
172
|
+
// default that failed its own schema has no value in the text at all.
|
|
173
|
+
if (parsedData !== undefined && self._preprocess) self._preprocess(parsedData);
|
|
174
|
+
}
|
|
168
175
|
|
|
169
176
|
// Errors the inner path already enriched carry a `received` key: enrich()
|
|
170
177
|
// always sets one, even when there is no value to show. `code` is not a
|
package/lib/suggestions.js
CHANGED
|
@@ -29,22 +29,35 @@ const FORMAT_HINTS = {
|
|
|
29
29
|
ipv4: (val) => typeof val === 'string' ? 'expected four 0-255 octets separated by dots' : null,
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
// Edits that still read as a typo of a name this long. With a flat two, a
|
|
33
|
+
// missing `name` was "corrected" to `age`, and `c` to `a`.
|
|
34
|
+
function typoBudget (name) {
|
|
35
|
+
const n = name.length;
|
|
36
|
+
return n < 3 ? 0 : n < 5 ? 1 : 2;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The one string candidate nearest `target` within the budget, or null when
|
|
40
|
+
// none is near enough or two are equally near.
|
|
41
|
+
function nearest (target, candidates) {
|
|
42
|
+
const budget = typoBudget(target);
|
|
35
43
|
let best = null;
|
|
36
|
-
let bestDist =
|
|
44
|
+
let bestDist = budget + 1;
|
|
37
45
|
let tied = false;
|
|
38
|
-
for (const
|
|
39
|
-
if (typeof
|
|
40
|
-
const d = levenshtein(
|
|
41
|
-
if (d
|
|
46
|
+
for (const c of candidates) {
|
|
47
|
+
if (typeof c !== 'string') continue;
|
|
48
|
+
const d = levenshtein(target, c, budget);
|
|
49
|
+
if (d === 0 || d > budget) continue;
|
|
50
|
+
if (d < bestDist) { best = c; bestDist = d; tied = false; }
|
|
42
51
|
else if (d === bestDist) tied = true;
|
|
43
52
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
return tied ? null : best;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function suggestEnumTypo (received, enumValues) {
|
|
57
|
+
if (typeof received !== 'string') return null;
|
|
58
|
+
if (!Array.isArray(enumValues) || enumValues.length === 0 || enumValues.length > 30) return null;
|
|
59
|
+
const best = nearest(received, enumValues);
|
|
60
|
+
return best === null ? null : { text: `did you mean \`${best}\`?`, kind: 'typo' };
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
// The widest container a typo hint is offered on. `suggestEnumTypo` caps its
|
|
@@ -56,14 +69,8 @@ const MAX_TYPO_CANDIDATES = 64;
|
|
|
56
69
|
function suggestRequiredTypo (missing, presentKeys) {
|
|
57
70
|
if (!missing || !Array.isArray(presentKeys)) return null;
|
|
58
71
|
if (presentKeys.length > MAX_TYPO_CANDIDATES) return null;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const d = levenshtein(missing, k, 2);
|
|
62
|
-
if (d <= 2 && d > 0) {
|
|
63
|
-
return { text: `did you mean \`${missing}\` instead of \`${k}\`?`, kind: 'similar-key' };
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
72
|
+
const best = nearest(missing, presentKeys);
|
|
73
|
+
return best === null ? null : { text: `did you mean \`${missing}\` instead of \`${best}\`?`, kind: 'similar-key' };
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
function suggestFormat (format, raw) {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// uniqueItems for every engine. These are real functions: the interpreted
|
|
4
|
+
// engine calls them, and the code generator emits their source text
|
|
5
|
+
// (`String(fn)`) into the function it builds, so both run the same code.
|
|
6
|
+
// They read only each other, `_deq` and `_uqpI`, which the generator emits
|
|
7
|
+
// beside them.
|
|
8
|
+
//
|
|
9
|
+
// Short arrays compare pairwise and allocate nothing. Longer arrays of
|
|
10
|
+
// primitives key a Set or Map by the value itself. Longer arrays that hold an
|
|
11
|
+
// object key a Map by a numeric structural hash, and only items whose hashes
|
|
12
|
+
// collide are compared with `_deq`. Equal values always hash alike: numbers
|
|
13
|
+
// by value (so 1 and 1.0, 0 and -0 agree), strings by their characters,
|
|
14
|
+
// arrays by position, and objects by the sum of their own key-value hashes,
|
|
15
|
+
// whatever the key order. The hash replaced a sorted-key canonical string
|
|
16
|
+
// built for every item on every call, which was over a quarter of the time a
|
|
17
|
+
// generated verdict spent on SchemaStore's sample documents.
|
|
18
|
+
|
|
19
|
+
// JSON equality, the same function the generator hoists as DEQ_HELPER.
|
|
20
|
+
function _deq(a, b) {
|
|
21
|
+
if (a === b) return true;
|
|
22
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
|
|
23
|
+
var aa = Array.isArray(a);
|
|
24
|
+
if (aa !== Array.isArray(b)) return false;
|
|
25
|
+
var i;
|
|
26
|
+
if (aa) {
|
|
27
|
+
if (a.length !== b.length) return false;
|
|
28
|
+
for (i = 0; i < a.length; i++) if (!_deq(a[i], b[i])) return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
var ka = Object.keys(a);
|
|
32
|
+
if (ka.length !== Object.keys(b).length) return false;
|
|
33
|
+
for (i = 0; i < ka.length; i++) {
|
|
34
|
+
var k = ka[i];
|
|
35
|
+
if (!Object.prototype.hasOwnProperty.call(b, k) || !_deq(a[k], b[k])) return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _uqh(x) {
|
|
41
|
+
if (x === null) return 1;
|
|
42
|
+
var t = typeof x, h, i;
|
|
43
|
+
if (t === 'boolean') return x ? 2 : 3;
|
|
44
|
+
if (t === 'number') {
|
|
45
|
+
if (x === 0) return 4;
|
|
46
|
+
h = x | 0;
|
|
47
|
+
return Math.imul(h ^ (((x - h) * 1e9) | 0), 0x9e3779b1) ^ 5;
|
|
48
|
+
}
|
|
49
|
+
if (t === 'string') {
|
|
50
|
+
h = 0x811c9dc5 ^ x.length;
|
|
51
|
+
for (i = 0; i < x.length; i++) h = Math.imul(h ^ x.charCodeAt(i), 16777619);
|
|
52
|
+
return h;
|
|
53
|
+
}
|
|
54
|
+
if (t !== 'object') return 6;
|
|
55
|
+
if (Array.isArray(x)) {
|
|
56
|
+
h = 0x2545f491 ^ x.length;
|
|
57
|
+
for (i = 0; i < x.length; i++) h = Math.imul(h ^ _uqh(x[i]), 16777619);
|
|
58
|
+
return h;
|
|
59
|
+
}
|
|
60
|
+
var ks = Object.keys(x);
|
|
61
|
+
h = 0x6c8e9cf5 ^ ks.length;
|
|
62
|
+
for (i = 0; i < ks.length; i++) h = (h + Math.imul(_uqh(ks[i]) ^ Math.imul(_uqh(x[ks[i]]), 0x5bd1e995), 0x27d4eb2d)) | 0;
|
|
63
|
+
return h;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Whether every item is distinct.
|
|
67
|
+
function _uq(a) {
|
|
68
|
+
var n = a.length, i, k;
|
|
69
|
+
if (n < 2) return true;
|
|
70
|
+
if (n <= 12) {
|
|
71
|
+
for (i = 1; i < n; i++) for (k = 0; k < i; k++) if (_deq(a[i], a[k])) return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
var prim = true;
|
|
75
|
+
for (i = 0; i < n; i++) { var x = a[i]; if (x !== null && typeof x === 'object') { prim = false; break; } }
|
|
76
|
+
if (prim) {
|
|
77
|
+
var s = new Set();
|
|
78
|
+
for (i = 0; i < n; i++) { if (s.has(a[i])) return false; s.add(a[i]); }
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
var m = new Map();
|
|
82
|
+
for (i = 0; i < n; i++) {
|
|
83
|
+
var hh = _uqh(a[i]), b = m.get(hh);
|
|
84
|
+
if (b === undefined) m.set(hh, i);
|
|
85
|
+
else if (typeof b === 'number') { if (_deq(a[b], a[i])) return false; m.set(hh, [b, i]); }
|
|
86
|
+
else { for (k = 0; k < b.length; k++) if (_deq(a[b[k]], a[i])) return false; b.push(i); }
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The duplicate pair an error reports: the first item that has a later
|
|
92
|
+
// equal, and its first later equal. Returns that later index and leaves the
|
|
93
|
+
// first in _uqpI, or returns -1 when every item is distinct.
|
|
94
|
+
var _uqpI = 0;
|
|
95
|
+
function _uqp(a) {
|
|
96
|
+
var n = a.length, i, k;
|
|
97
|
+
if (n < 2) return -1;
|
|
98
|
+
if (n <= 12) {
|
|
99
|
+
for (i = 0; i < n - 1; i++) for (k = i + 1; k < n; k++) if (_deq(a[i], a[k])) { _uqpI = i; return k; }
|
|
100
|
+
return -1;
|
|
101
|
+
}
|
|
102
|
+
var prim = true;
|
|
103
|
+
for (i = 0; i < n; i++) { var x = a[i]; if (x !== null && typeof x === 'object') { prim = false; break; } }
|
|
104
|
+
var bi = -1, bj = -1, m = new Map();
|
|
105
|
+
if (prim) {
|
|
106
|
+
for (i = 0; i < n; i++) {
|
|
107
|
+
var p = m.get(a[i]);
|
|
108
|
+
if (p === undefined) m.set(a[i], i);
|
|
109
|
+
else if (p >= 0) { if (bi < 0 || p < bi) { bi = p; bj = i; } m.set(a[i], -1); }
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Each bucket holds the first occurrence of every distinct value that
|
|
113
|
+
// hashed there, in index order; a value is paired with its first later
|
|
114
|
+
// equal only once.
|
|
115
|
+
var paired = null;
|
|
116
|
+
for (i = 0; i < n; i++) {
|
|
117
|
+
var hh = _uqh(a[i]), b = m.get(hh), r = -1;
|
|
118
|
+
if (b === undefined) { m.set(hh, i); continue; }
|
|
119
|
+
if (typeof b === 'number') { if (_deq(a[b], a[i])) r = b; else m.set(hh, [b, i]); }
|
|
120
|
+
else { for (k = 0; k < b.length; k++) if (_deq(a[b[k]], a[i])) { r = b[k]; break; } if (r < 0) b.push(i); }
|
|
121
|
+
if (r >= 0) {
|
|
122
|
+
if (paired === null) paired = new Set();
|
|
123
|
+
if (!paired.has(r)) { paired.add(r); if (bi < 0 || r < bi) { bi = r; bj = i; } }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (bi < 0) return -1;
|
|
128
|
+
_uqpI = bi;
|
|
129
|
+
return bj;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// For the interpreted engine.
|
|
133
|
+
function allDistinct(a) { return _uq(a); }
|
|
134
|
+
function duplicatePair(a) {
|
|
135
|
+
var j = _uqp(a);
|
|
136
|
+
return j < 0 ? null : [_uqpI, j];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { allDistinct, duplicatePair, _deq, _uqh, _uq, _uqp };
|