ata-validator 1.43.0 → 1.44.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/js-compiler.js +151 -51
- package/lib/levenshtein.js +34 -22
- package/lib/native-positions.js +100 -0
- package/lib/rejections.js +14 -7
- package/lib/suggestions.js +27 -20
- package/lib/validator-core.js +71 -10
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -244,21 +244,21 @@ is the most common way to get a misleading number out of this library.
|
|
|
244
244
|
|
|
245
245
|
| | compiled with `ata build` | runtime `new Validator(schema)` |
|
|
246
246
|
|---|---|---|
|
|
247
|
-
| In a bundle, gzipped | **2.
|
|
248
|
-
| Time to a served request | **3.5 ms** |
|
|
247
|
+
| In a bundle, gzipped | **2.1 KB** | 110.7 KB |
|
|
248
|
+
| Time to a served request | **3.5 ms** | 8.2 ms |
|
|
249
249
|
| Schema known when | build time | any time |
|
|
250
250
|
|
|
251
251
|
The bundle row is the ten-field user schema in
|
|
252
252
|
`tests/fixtures/error-dx/user.schema.json`, every export of the compiled module against
|
|
253
|
-
`new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.
|
|
254
|
-
The startup row is a Hono route on Bun 1.4, median of
|
|
255
|
-
`benchmark/bundle`, against 3.
|
|
253
|
+
`new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.43.0.
|
|
254
|
+
The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
|
|
255
|
+
`benchmark/bundle`, against 3.4 ms for the same app doing no validation at all, so the
|
|
256
256
|
compiled path costs nothing measurable to start. The runtime figure is what it is because
|
|
257
257
|
a schema that arrives at run time can use any keyword, so the whole engine has to be
|
|
258
258
|
there. The compiled module imports nothing and contains only the checks your schema asks
|
|
259
259
|
for.
|
|
260
260
|
|
|
261
|
-
**On a server, use whichever fits your schemas.**
|
|
261
|
+
**On a server, use whichever fits your schemas.** 111 KB of JavaScript on a Node or Bun
|
|
262
262
|
process is not a cost anyone notices, and the runtime API is the simpler thing to reach
|
|
263
263
|
for. Speed is the same either way once warm.
|
|
264
264
|
|
|
@@ -491,7 +491,7 @@ const v = new Validator(schema, {
|
|
|
491
491
|
|
|
492
492
|
### Build-time compile (`ata compile`)
|
|
493
493
|
|
|
494
|
-
The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.
|
|
494
|
+
The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.1 KB gzipped, full error detail included, against 110.7 KB for the runtime bundled for the browser (ata 1.43.0).
|
|
495
495
|
|
|
496
496
|
```bash
|
|
497
497
|
npx ata compile schemas/user.json -o src/generated/user.validator.mjs
|
|
@@ -533,13 +533,13 @@ npx ata build 'schemas/*.json' --out-dir build/validators --check
|
|
|
533
533
|
Run with `--watch` during development for incremental rebuilds.
|
|
534
534
|
|
|
535
535
|
Bundle sizes for the 10-field user schema in `tests/fixtures/error-dx/user.schema.json`,
|
|
536
|
-
minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.
|
|
536
|
+
minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.43.0:
|
|
537
537
|
|
|
538
538
|
| What the app imports | Size | Notes |
|
|
539
539
|
|---|---|---|
|
|
540
|
-
| `Validator` from `ata-validator` |
|
|
540
|
+
| `Validator` from `ata-validator` | 110.7 KB | The compiler ships with it, because a runtime schema can use any keyword |
|
|
541
541
|
| `isValid` from the compiled module | **1.3 KB** | Nothing else is reachable, so the error collector is dropped |
|
|
542
|
-
| `validate` from the compiled module | **
|
|
542
|
+
| `validate` from the compiled module | **1.9 KB** | Adds the detailed error collector |
|
|
543
543
|
|
|
544
544
|
`--abort-early` makes the generated source about three times smaller, and after
|
|
545
545
|
bundling it makes no difference: importing only `isValid` already leaves the error
|
package/index.js
CHANGED
|
@@ -434,6 +434,9 @@ function installCodegenPaths (ctx) {
|
|
|
434
434
|
}
|
|
435
435
|
return nativeText ? getNative().rawFastValidate(this._fastSlot, Buffer.from(jsonStr)) : undefined;
|
|
436
436
|
};
|
|
437
|
+
// The parsed document is left on `_lastParsed` for the rich layer around
|
|
438
|
+
// validateJSON, which hands it to the rejection so reading the errors does
|
|
439
|
+
// not parse the text again (see LazyJsonRejection).
|
|
437
440
|
const validateText = (jsonStr) => {
|
|
438
441
|
let obj;
|
|
439
442
|
try {
|
|
@@ -442,6 +445,7 @@ function installCodegenPaths (ctx) {
|
|
|
442
445
|
if (!(e instanceof SyntaxError)) throw e;
|
|
443
446
|
return _jsonSyntaxRejection(e);
|
|
444
447
|
}
|
|
448
|
+
this._lastParsed = obj;
|
|
445
449
|
return jsonValidateFn(obj);
|
|
446
450
|
};
|
|
447
451
|
this.validateJSON = useSimdjsonForLarge && !preprocess
|
|
@@ -878,8 +882,58 @@ function buildParse (self, decline, extended) {
|
|
|
878
882
|
// preprocess pass, the JSON-text scanner, the parse() copy and the
|
|
879
883
|
// ahead-of-time bundle methods. parse() and the bundle methods load on first
|
|
880
884
|
// use.
|
|
885
|
+
// Cold start. A validator's first COLD_CALLS calls are answered by an
|
|
886
|
+
// interpreted twin, built from the same schema and options with
|
|
887
|
+
// `engine: 'interpreter'`, which the test suite holds to the same answers and
|
|
888
|
+
// errors as the generated code. Generating code and having V8 compile it is
|
|
889
|
+
// the largest part of a first call on a large schema: from process start to
|
|
890
|
+
// one rejection with its error read, SchemaStore's SARIF schema took 87 ms
|
|
891
|
+
// with code generated at the first call and 31 ms with the twin answering.
|
|
892
|
+
// A validator still in use after that compiles as before, so a server reaches
|
|
893
|
+
// the generated code's speed within its first requests, and a command line
|
|
894
|
+
// tool or a short-lived function never pays to compile. Only where compiling
|
|
895
|
+
// costs enough to matter: a schema whose size with its references followed
|
|
896
|
+
// passes COLD_MIN characters (expandedChars in lib/js-compiler.js). A small
|
|
897
|
+
// schema compiles in about a millisecond and is generated at its first call
|
|
898
|
+
// as before, which also keeps every test that compares the generated code
|
|
899
|
+
// with the interpreter comparing the generated code. Not for a validator with
|
|
900
|
+
// an extension (_extendValidate, _extendChecks), which the twin would not
|
|
901
|
+
// carry. Returns the twin to answer this call, or null to compile.
|
|
902
|
+
const COLD_CALLS = 64;
|
|
903
|
+
const COLD_MIN = 16 * 1024;
|
|
904
|
+
// Whether the schema may expand past `limit`, answered from its text where
|
|
905
|
+
// that settles it. Without a `$ref` nothing expands, and expandedChars counts
|
|
906
|
+
// at most 8 for each character of the text, so a short schema is decided
|
|
907
|
+
// without walking it. The text is the one _ensureCompiled needs anyway. Walking
|
|
908
|
+
// every small route schema here cost a Fastify boot of ten routes 0.15 ms.
|
|
909
|
+
function mayExpandPast (self, limit) {
|
|
910
|
+
const str = self._schemaStr || (self._schemaStr = JSON.stringify(self._schemaObj));
|
|
911
|
+
if (str === undefined) return false;
|
|
912
|
+
if (str.length * 8 <= limit && !str.includes('"$ref"')) return false;
|
|
913
|
+
return jsCompiler.expandedChars(self._schemaObj, limit) > limit;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function coldTwin (self, Validator) {
|
|
917
|
+
const n = self._coldCalls === undefined ? 0 : self._coldCalls;
|
|
918
|
+
if (n >= COLD_CALLS || self._validateTail !== null || self._verdictTail !== null) return null;
|
|
919
|
+
let t = self._twin;
|
|
920
|
+
if (t === undefined) {
|
|
921
|
+
t = null;
|
|
922
|
+
if (!self._interpretOnly && !self._initialized && mayExpandPast(self, COLD_MIN)) {
|
|
923
|
+
try { t = new Validator(self._rawSchema, Object.assign({}, self._options, { engine: 'interpreter' })); } catch { t = null; }
|
|
924
|
+
}
|
|
925
|
+
self._twin = t;
|
|
926
|
+
}
|
|
927
|
+
if (t === null) return null;
|
|
928
|
+
self._coldCalls = n + 1;
|
|
929
|
+
return t;
|
|
930
|
+
}
|
|
931
|
+
|
|
881
932
|
core._registerCodegen({
|
|
882
933
|
jsCompiler,
|
|
934
|
+
coldTwin,
|
|
935
|
+
// The native locator for error frames, loaded with the first frame.
|
|
936
|
+
nativePositions: () => require('./lib/native-positions').nativeTargeted,
|
|
883
937
|
installPaths: installCodegenPaths,
|
|
884
938
|
compileVerdict: installCodegenPaths.compileVerdict,
|
|
885
939
|
installScanner: installCodegenPaths.installScanner,
|
package/lib/clone-emit.js
CHANGED
|
@@ -78,8 +78,11 @@ function inlineRefsForClone(root) {
|
|
|
78
78
|
for (const k of Object.keys(cur)) {
|
|
79
79
|
if (strip && k === 'default') continue;
|
|
80
80
|
const v = cur[k];
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
// A property named "__proto__" is an own key of the copy: assigned, it
|
|
82
|
+
// set the copy's prototype and the property was gone from parse().
|
|
83
|
+
const w = (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') ? v : walk(v, active, strip);
|
|
84
|
+
if (k === '__proto__') Object.defineProperty(out, k, { value: w, writable: true, enumerable: true, configurable: true });
|
|
85
|
+
else out[k] = w;
|
|
83
86
|
}
|
|
84
87
|
if (siblingDefault !== undefined) out.default = siblingDefault;
|
|
85
88
|
return out;
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
const { buildDataPositionMap, buildTargetedPositionMap } = require('./data-positions');
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
/**
|
|
6
7
|
* Memoize the position map for the duration of a single validate() call.
|
|
7
8
|
* Caller passes the original buffer/string. Identity-keyed: same reference
|
|
8
9
|
* == same map. No global state, caller holds the cache instance.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
// `nativeTargeted`, when given, is a faster source of the same entries: the
|
|
13
|
+
// full entry point passes lib/native-positions.js, lite passes nothing.
|
|
14
|
+
function createCache (nativeTargeted) {
|
|
15
|
+
const _nativeTargeted = typeof nativeTargeted === 'function' ? nativeTargeted : null;
|
|
12
16
|
const wm = new WeakMap();
|
|
13
17
|
const sm = new Map(); // strings can't go in WeakMap; clear after each validate
|
|
14
18
|
let lastInput = null;
|
|
@@ -32,11 +36,13 @@ function createCache () {
|
|
|
32
36
|
? sm.get(input)
|
|
33
37
|
: (Buffer.isBuffer(input) ? wm.get(input) : undefined);
|
|
34
38
|
if (already) return already;
|
|
35
|
-
let filtered;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
let filtered = _nativeTargeted === null ? null : _nativeTargeted(input, wanted);
|
|
40
|
+
if (filtered === null) {
|
|
41
|
+
try {
|
|
42
|
+
filtered = buildTargetedPositionMap(input, wanted);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
40
46
|
}
|
|
41
47
|
let full;
|
|
42
48
|
const fallback = (key) => {
|
package/lib/draft7.js
CHANGED
|
@@ -49,13 +49,11 @@ function _normalize(schema) {
|
|
|
49
49
|
// dependencies → dependentSchemas + dependentRequired
|
|
50
50
|
if (schema.dependencies) {
|
|
51
51
|
for (const [key, value] of Object.entries(schema.dependencies)) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
schema.dependentSchemas[key] = value
|
|
58
|
-
}
|
|
52
|
+
// defineProperty: a dependency on a property named "__proto__" is an
|
|
53
|
+
// own key here, where assignment would set the map's prototype.
|
|
54
|
+
const target = Array.isArray(value) ? 'dependentRequired' : 'dependentSchemas'
|
|
55
|
+
if (!schema[target]) schema[target] = {}
|
|
56
|
+
Object.defineProperty(schema[target], key, { value, writable: true, enumerable: true, configurable: true })
|
|
59
57
|
}
|
|
60
58
|
delete schema.dependencies
|
|
61
59
|
}
|
package/lib/js-compiler.js
CHANGED
|
@@ -1065,6 +1065,16 @@ function hasOwnKey (d, key) {
|
|
|
1065
1065
|
return _hop.call(d, key)
|
|
1066
1066
|
}
|
|
1067
1067
|
|
|
1068
|
+
// Writes an own key. A schema can name a property, a definition or even a
|
|
1069
|
+
// keyword "__proto__", and assigning that key sets the copy's prototype
|
|
1070
|
+
// instead: the property was gone from the copy, so a constraint on it was
|
|
1071
|
+
// generated as nothing and accepted anything (a schema with such a property
|
|
1072
|
+
// and a reference the resolver rewrites, 1.40.1 to 1.43.0).
|
|
1073
|
+
function setOwn (o, k, v) {
|
|
1074
|
+
if (k === '__proto__') Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true })
|
|
1075
|
+
else o[k] = v
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1068
1078
|
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
|
|
1069
1079
|
'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString'])
|
|
1070
1080
|
|
|
@@ -1380,7 +1390,7 @@ function aliasChainEnds(def, defs) {
|
|
|
1380
1390
|
// source of evaluated properties the schema has; when it does not, the
|
|
1381
1391
|
// schema is not generated and the interpreter answers.
|
|
1382
1392
|
function unevalPropsModeled(schema, schemaMap) {
|
|
1383
|
-
const own =
|
|
1393
|
+
const own = { ...schema }
|
|
1384
1394
|
delete own.$defs
|
|
1385
1395
|
delete own.definitions
|
|
1386
1396
|
if (/"\$(?:ref|dynamicRef|recursiveRef)"/.test(JSON.stringify(own))) return false
|
|
@@ -1399,7 +1409,7 @@ function unevalPropsModeled(schema, schemaMap) {
|
|
|
1399
1409
|
if (typeof sub !== 'object' || sub === null) return false
|
|
1400
1410
|
const r = collectEvaluated(sub, schemaMap)
|
|
1401
1411
|
if (r.dynamic || r.allProps) return false
|
|
1402
|
-
const rest =
|
|
1412
|
+
const rest = { ...sub }
|
|
1403
1413
|
delete rest.patternProperties
|
|
1404
1414
|
const j = JSON.stringify(rest)
|
|
1405
1415
|
if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
|
|
@@ -1432,7 +1442,7 @@ function unevalPropsModeled(schema, schemaMap) {
|
|
|
1432
1442
|
if (typeof schema.if !== 'object' || schema.if === null) return false
|
|
1433
1443
|
const r = collectEvaluated(schema.if, schemaMap)
|
|
1434
1444
|
if (r.dynamic || r.allProps || r.props.length) return false
|
|
1435
|
-
const rest =
|
|
1445
|
+
const rest = { ...schema.if }
|
|
1436
1446
|
delete rest.patternProperties
|
|
1437
1447
|
const j = JSON.stringify(rest)
|
|
1438
1448
|
if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
|
|
@@ -1486,17 +1496,7 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1486
1496
|
}
|
|
1487
1497
|
|
|
1488
1498
|
// Keys that collide with Object.prototype
|
|
1489
|
-
|
|
1490
|
-
for (const k of schema.required) {
|
|
1491
|
-
if (UNSAFE_KEYS.has(k)) return false
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
if (schema.properties) {
|
|
1495
|
-
for (const k of Object.keys(schema.properties)) {
|
|
1496
|
-
if (UNSAFE_KEYS.has(k)) return false
|
|
1497
|
-
if (k === '$ref') return false // property named "$ref" — confusing
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1499
|
+
|
|
1500
1500
|
|
|
1501
1501
|
// Unicode property escapes in pattern need 'u' flag — codegen uses RegExp without it
|
|
1502
1502
|
|
|
@@ -1627,10 +1627,8 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1627
1627
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return false
|
|
1628
1628
|
}
|
|
1629
1629
|
}
|
|
1630
|
-
if (typeof schema.propertyNames === 'object' && schema.propertyNames !== null
|
|
1631
|
-
|
|
1632
|
-
if (Object.keys(schema.propertyNames).some(k => k !== '$schema' && !supported.includes(k))) return false
|
|
1633
|
-
}
|
|
1630
|
+
if (typeof schema.propertyNames === 'object' && schema.propertyNames !== null &&
|
|
1631
|
+
!isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return false
|
|
1634
1632
|
|
|
1635
1633
|
// Recurse into sub-schemas — bail on boolean schemas in any position
|
|
1636
1634
|
// A boolean subschema is a constant that every generator emits at its
|
|
@@ -1968,8 +1966,21 @@ function bothDefsContainers(schema) {
|
|
|
1968
1966
|
schema.definitions !== null && typeof schema.definitions === 'object'
|
|
1969
1967
|
}
|
|
1970
1968
|
|
|
1969
|
+
// The answer depends on the schema and the map alone, and the three
|
|
1970
|
+
// generators ask it about the same prepared schema in turn, so the last
|
|
1971
|
+
// answer is kept: each check walks the whole schema, and the second and third
|
|
1972
|
+
// asked again what the first had found. One entry, compared by identity,
|
|
1973
|
+
// because a WeakMap entry per schema cost more than the walk it saved on a
|
|
1974
|
+
// small schema that only ever gets its verdict generated.
|
|
1975
|
+
let _gateSchema = null, _gateMap = null, _gateOk = false
|
|
1971
1976
|
function sharedCodegenGate(schema, schemaMap) {
|
|
1972
1977
|
if (typeof schema !== 'object' || schema === null) return true
|
|
1978
|
+
if (schema === _gateSchema && schemaMap === _gateMap) return _gateOk
|
|
1979
|
+
const ok = sharedCodegenGateUncached(schema, schemaMap)
|
|
1980
|
+
_gateSchema = schema; _gateMap = schemaMap; _gateOk = ok
|
|
1981
|
+
return ok
|
|
1982
|
+
}
|
|
1983
|
+
function sharedCodegenGateUncached(schema, schemaMap) {
|
|
1973
1984
|
if (bothDefsContainers(schema)) return false
|
|
1974
1985
|
if (hasSameInstanceRefCycle(schema)) return false
|
|
1975
1986
|
if (!codegenSafe(schema, schemaMap)) return false
|
|
@@ -1985,6 +1996,15 @@ function sharedCodegenGate(schema, schemaMap) {
|
|
|
1985
1996
|
// `type: 'string'` and annotations go, on a copy. Anything else is left as
|
|
1986
1997
|
// written and the gate declines. Errors keep the interpreted engine's path.
|
|
1987
1998
|
const PN_SUPPORTED = new Set(['maxLength', 'minLength', 'pattern', 'const', 'enum'])
|
|
1999
|
+
// Whether the generators can write `propertyNames` as checks on the key in the
|
|
2000
|
+
// object's own key loop. Any other subschema is applied to each key as a
|
|
2001
|
+
// value of its own (genPropertyNamesNode and its error and combined
|
|
2002
|
+
// counterparts), which is what the interpreter does.
|
|
2003
|
+
function isSimplePN (pn) {
|
|
2004
|
+
if (pn === null || typeof pn !== 'object' || Array.isArray(pn)) return false
|
|
2005
|
+
for (const k of Object.keys(pn)) if (k !== '$schema' && !PN_SUPPORTED.has(k)) return false
|
|
2006
|
+
return true
|
|
2007
|
+
}
|
|
1988
2008
|
const PN_ANNOTATIONS = new Set(['$schema', '$comment', 'title', 'description', 'examples', 'default', 'deprecated', 'readOnly', 'writeOnly'])
|
|
1989
2009
|
function simplePropertyNames(pn, defs) {
|
|
1990
2010
|
const seen = new Set()
|
|
@@ -2006,7 +2026,7 @@ function simplePropertyNames(pn, defs) {
|
|
|
2006
2026
|
return null
|
|
2007
2027
|
}
|
|
2008
2028
|
if (!PN_SUPPORTED.has(k)) return null
|
|
2009
|
-
out
|
|
2029
|
+
setOwn(out, k, val)
|
|
2010
2030
|
}
|
|
2011
2031
|
return out
|
|
2012
2032
|
}
|
|
@@ -2037,14 +2057,14 @@ function normalizePropertyNamesWalk(root) {
|
|
|
2037
2057
|
return out
|
|
2038
2058
|
}
|
|
2039
2059
|
let out = node
|
|
2040
|
-
const set = (k, v) => { if (out === node) out = { ...node }; out
|
|
2060
|
+
const set = (k, v) => { if (out === node) out = { ...node }; setOwn(out, k, v) }
|
|
2041
2061
|
for (const k of SUBSCHEMA_MAPS) {
|
|
2042
2062
|
const map = node[k]
|
|
2043
2063
|
if (!map || typeof map !== 'object') continue
|
|
2044
2064
|
let copy = map
|
|
2045
2065
|
for (const [name, sub] of Object.entries(map)) {
|
|
2046
2066
|
const w = walk(sub)
|
|
2047
|
-
if (w !== sub) { if (copy === map) copy = { ...map }; copy
|
|
2067
|
+
if (w !== sub) { if (copy === map) copy = { ...map }; setOwn(copy, name, w) }
|
|
2048
2068
|
}
|
|
2049
2069
|
if (copy !== map) set(k, copy)
|
|
2050
2070
|
}
|
|
@@ -2162,7 +2182,7 @@ function normalizeDeepRefsWalk(root) {
|
|
|
2162
2182
|
if (inner) return node
|
|
2163
2183
|
let out = node
|
|
2164
2184
|
memo.set(node, out)
|
|
2165
|
-
const own = (k, v) => { if (out === node) { out = { ...node }; memo.set(node, out) } out
|
|
2185
|
+
const own = (k, v) => { if (out === node) { out = { ...node }; memo.set(node, out) } setOwn(out, k, v) }
|
|
2166
2186
|
if (typeof node.$ref === 'string' && names.has(node.$ref)) own('$ref', '#/' + defsKey + '/' + names.get(node.$ref))
|
|
2167
2187
|
for (const k of Object.keys(node)) {
|
|
2168
2188
|
if (k === '$ref' || k === 'enum' || k === 'const' || k === 'default' || k === 'examples') continue
|
|
@@ -2211,10 +2231,39 @@ function normalizeRefs(root, schemaMap) {
|
|
|
2211
2231
|
return out
|
|
2212
2232
|
}
|
|
2213
2233
|
const SKIP_VALUE_KEYS = new Set(['enum', 'const', 'default', 'examples'])
|
|
2234
|
+
// A schema whose every reference is `#` or a plain `#/$defs/name` (or
|
|
2235
|
+
// `definitions`), with no `$id` or `$anchor` below the root to move the base
|
|
2236
|
+
// they resolve against, needs nothing from the resolver: each reference
|
|
2237
|
+
// already reads as the generators read it. Telling that from the text skips
|
|
2238
|
+
// loading the interpreted engine and indexing the schema, which was most of
|
|
2239
|
+
// what preparing a small schema for code generation cost.
|
|
2240
|
+
const LOCAL_ONLY_REF = /^#(?:\/(?:\$defs|definitions)\/[^/~%]+)?$/
|
|
2241
|
+
function refsAllPlainLocal (root, text) {
|
|
2242
|
+
const ids = text.split('"$id":').length - 1
|
|
2243
|
+
if (ids > (typeof root.$id === 'string' ? 1 : 0) || text.includes('"$anchor":')) return false
|
|
2244
|
+
const re = /"\$ref":("(?:[^"\\]|\\.)*")/g
|
|
2245
|
+
let m
|
|
2246
|
+
while ((m = re.exec(text)) !== null) {
|
|
2247
|
+
let ref
|
|
2248
|
+
try { ref = JSON.parse(m[1]) } catch { return false }
|
|
2249
|
+
if (!LOCAL_ONLY_REF.test(ref)) return false
|
|
2250
|
+
}
|
|
2251
|
+
return true
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// What the text of a schema prepared for code generation says about its size,
|
|
2255
|
+
// kept from the stringify normalizeRefsWalk makes anyway: its length when it
|
|
2256
|
+
// holds no `$ref`, -1 when it does. expandedChars counts at most 8 for each
|
|
2257
|
+
// character of a schema without references, so a short one is known not to
|
|
2258
|
+
// reach EXPANDED_SHARE without walking it again at every compile.
|
|
2259
|
+
const _refFreeLength = new WeakMap()
|
|
2214
2260
|
function normalizeRefsWalk(root, schemaMap) {
|
|
2215
2261
|
const text = JSON.stringify(root)
|
|
2216
|
-
|
|
2262
|
+
const hasRef = text.includes('"$ref"')
|
|
2263
|
+
_refFreeLength.set(root, hasRef ? -1 : text.length)
|
|
2264
|
+
if (!hasRef) return root
|
|
2217
2265
|
if (/"\$(?:dynamicRef|recursiveRef|dynamicAnchor|recursiveAnchor)"/.test(text)) return null
|
|
2266
|
+
if (refsAllPlainLocal(root, text)) return root
|
|
2218
2267
|
const { indexSchemas, resolveRef } = require('./interpreter')._refInternals
|
|
2219
2268
|
const state = indexSchemas(root, schemaMap && schemaMap.size ? schemaMap : null)
|
|
2220
2269
|
const rootDialect = typeof root.$schema === 'string' ? root.$schema.replace(/#$/, '') : null
|
|
@@ -2280,7 +2329,7 @@ function normalizeRefsWalk(root, schemaMap) {
|
|
|
2280
2329
|
out.$ref = t === root ? '#' : '#/' + defsKey + '/' + names.get(t)
|
|
2281
2330
|
continue
|
|
2282
2331
|
}
|
|
2283
|
-
out
|
|
2332
|
+
setOwn(out, k, SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k]))
|
|
2284
2333
|
}
|
|
2285
2334
|
return out
|
|
2286
2335
|
}
|
|
@@ -2299,6 +2348,46 @@ function prepareForCodegen(schema, schemaMap) {
|
|
|
2299
2348
|
}
|
|
2300
2349
|
|
|
2301
2350
|
const VERDICT_SPLIT = 64 * 1024
|
|
2351
|
+
// The schema's size with every local reference followed, each definition
|
|
2352
|
+
// sized once, annotations left out. A rough measure of the expanded verdict
|
|
2353
|
+
// source: on SchemaStore that runs about 1.4 times it at the median, and far
|
|
2354
|
+
// less where allOf merges. Past EXPANDED_SHARE the verdict is generated with
|
|
2355
|
+
// shared definitions from the start, instead of expanded first and thrown
|
|
2356
|
+
// away: SARIF comes to 2.1 million, and generating its 2.8 MB of expanded
|
|
2357
|
+
// source only to discard it was most of a cold first validate() (70 ms of the
|
|
2358
|
+
// 87 a command line run took to report one error). The bound is set where
|
|
2359
|
+
// only schemas that split anyway reach it: SARIF 2.1 measures about 706
|
|
2360
|
+
// thousand, while utcm-monitor (370 thousand) and accelerator (262 thousand)
|
|
2361
|
+
// stay expanded and keep their speed.
|
|
2362
|
+
const EXPANDED_SHARE = 400 * 1024
|
|
2363
|
+
const SIZE_SKIPS = new Set(['$defs', 'definitions', 'description', 'title', 'examples', 'default', '$comment', 'markdownDescription', 'deprecated', 'readOnly', 'writeOnly'])
|
|
2364
|
+
function expandedChars (root, limit) {
|
|
2365
|
+
const defs = (root && (root.$defs || root.definitions)) || {}
|
|
2366
|
+
const memo = new Map()
|
|
2367
|
+
const size = (n, stack) => {
|
|
2368
|
+
if (n === null || typeof n !== 'object') return 8
|
|
2369
|
+
let t = 2
|
|
2370
|
+
if (Array.isArray(n)) { for (const x of n) { t += size(x, stack) + 1; if (t > limit) return t } return t }
|
|
2371
|
+
for (const k of Object.keys(n)) {
|
|
2372
|
+
if (SIZE_SKIPS.has(k)) continue
|
|
2373
|
+
const v = n[k]
|
|
2374
|
+
if (k === '$ref' && typeof v === 'string') {
|
|
2375
|
+
const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(v)
|
|
2376
|
+
if (m && defs[m[1]] !== undefined && !stack.has(m[1])) {
|
|
2377
|
+
let d = memo.get(m[1])
|
|
2378
|
+
if (d === undefined) { stack.add(m[1]); d = size(defs[m[1]], stack); stack.delete(m[1]); memo.set(m[1], d) }
|
|
2379
|
+
t += d
|
|
2380
|
+
if (t > limit) return t
|
|
2381
|
+
continue
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
t += k.length + 3 + size(v, stack)
|
|
2385
|
+
if (t > limit) return t
|
|
2386
|
+
}
|
|
2387
|
+
return t
|
|
2388
|
+
}
|
|
2389
|
+
return size(root, new Set())
|
|
2390
|
+
}
|
|
2302
2391
|
function sourceSize (lines, ctx) {
|
|
2303
2392
|
let n = 0
|
|
2304
2393
|
for (const l of lines) n += l.length
|
|
@@ -2340,13 +2429,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2340
2429
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
2341
2430
|
}
|
|
2342
2431
|
}
|
|
2343
|
-
|
|
2344
|
-
if (schema.propertyNames) {
|
|
2345
|
-
const pn = schema.propertyNames
|
|
2346
|
-
const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
|
|
2347
|
-
const keys = Object.keys(pn).filter(k => k !== '$schema')
|
|
2348
|
-
if (keys.some(k => !supported.includes(k))) return null
|
|
2349
|
-
}
|
|
2432
|
+
if (schema.propertyNames && typeof schema.propertyNames === 'object' && !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return null
|
|
2350
2433
|
|
|
2351
2434
|
// Build anchors map for $ref/#anchor and $dynamicRef resolution
|
|
2352
2435
|
const anchors = {}
|
|
@@ -2389,7 +2472,11 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2389
2472
|
// (79 KB of schema) came to 2.8 MB of verdict source, and compiling it was
|
|
2390
2473
|
// half of the first validate() call.
|
|
2391
2474
|
let ctx, lines
|
|
2392
|
-
|
|
2475
|
+
const refFree = _refFreeLength.get(inputSchema)
|
|
2476
|
+
const shareFirst = refFree !== undefined && refFree >= 0 && refFree * 8 <= EXPANDED_SHARE
|
|
2477
|
+
? false
|
|
2478
|
+
: expandedChars(schema, EXPANDED_SHARE) > EXPANDED_SHARE
|
|
2479
|
+
for (const share of shareFirst ? [true] : [false, true]) {
|
|
2393
2480
|
ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: inputSchema, userFormats: userFormats || null, removeNodes,
|
|
2394
2481
|
keywords: (opts && opts.keywords) || null, kwEmitted: null, shareDefs: share }
|
|
2395
2482
|
if (ctx.keywords) ctx.kwEmitted = new Set()
|
|
@@ -3438,6 +3525,13 @@ function genPropertyNamesNode(schema, v, lines, ctx, knownType, isObj) {
|
|
|
3438
3525
|
const pn = schema.propertyNames
|
|
3439
3526
|
const ki = ctx.varCounter++
|
|
3440
3527
|
const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
3528
|
+
if (!isSimplePN(pn)) {
|
|
3529
|
+
const sub = []
|
|
3530
|
+
genCode(pn, '_pv', sub, ctx)
|
|
3531
|
+
if (sub.length === 0) return
|
|
3532
|
+
lines.push(`${guard}{const _pnf${ki}=function(_pv){${sub.join(';')};return true};for(const _k${ki} in ${v}){if(!_pnf${ki}(_k${ki}))return false}}`)
|
|
3533
|
+
return
|
|
3534
|
+
}
|
|
3441
3535
|
lines.push(`${guard}{for(const _k${ki} in ${v}){`)
|
|
3442
3536
|
if (pn.minLength !== undefined) lines.push(`if(_k${ki}.length<${pn.minLength})return false`)
|
|
3443
3537
|
if (pn.maxLength !== undefined) lines.push(`if(_k${ki}.length>${pn.maxLength})return false`)
|
|
@@ -3760,7 +3854,7 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
|
|
|
3760
3854
|
function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
|
|
3761
3855
|
let handledPropertyNames = false
|
|
3762
3856
|
const ppEntries = Object.entries(schema.patternProperties)
|
|
3763
|
-
const pn = schema.propertyNames
|
|
3857
|
+
const pn = isSimplePN(schema.propertyNames) ? schema.propertyNames : null
|
|
3764
3858
|
const pi = ctx.varCounter++
|
|
3765
3859
|
const kVar = `_ppk${pi}`
|
|
3766
3860
|
|
|
@@ -4821,12 +4915,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
4821
4915
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
4822
4916
|
}
|
|
4823
4917
|
}
|
|
4824
|
-
if (schema.propertyNames)
|
|
4825
|
-
const pn = schema.propertyNames
|
|
4826
|
-
const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
|
|
4827
|
-
const keys = Object.keys(pn).filter(k => k !== '$schema')
|
|
4828
|
-
if (keys.some(k => !supported.includes(k))) return null
|
|
4829
|
-
}
|
|
4918
|
+
if (schema.propertyNames && typeof schema.propertyNames === 'object' && !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return null
|
|
4830
4919
|
|
|
4831
4920
|
// Build anchors map for $ref/#anchor and $dynamicRef resolution
|
|
4832
4921
|
const eRootDefs = schema.$defs || schema.definitions || null
|
|
@@ -5791,6 +5880,13 @@ function genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
|
5791
5880
|
const pn = schema.propertyNames
|
|
5792
5881
|
const ki = ctx.varCounter++
|
|
5793
5882
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
|
|
5883
|
+
// The subschema checks the key as a value, and its errors are reported at
|
|
5884
|
+
// the object, as the interpreter reports them.
|
|
5885
|
+
if (!isSimplePN(pn)) {
|
|
5886
|
+
genCodeE(pn, `_k${ki}`, pathExpr, lines, ctx, schemaPrefix + '/propertyNames')
|
|
5887
|
+
lines.push(`}}`)
|
|
5888
|
+
return
|
|
5889
|
+
}
|
|
5794
5890
|
if (pn.minLength !== undefined) {
|
|
5795
5891
|
lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
|
|
5796
5892
|
}
|
|
@@ -5978,12 +6074,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
5978
6074
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
5979
6075
|
}
|
|
5980
6076
|
}
|
|
5981
|
-
if (schema.propertyNames)
|
|
5982
|
-
const pn = schema.propertyNames
|
|
5983
|
-
const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
|
|
5984
|
-
const keys = Object.keys(pn).filter(k => k !== '$schema')
|
|
5985
|
-
if (keys.some(k => !supported.includes(k))) return null
|
|
5986
|
-
}
|
|
6077
|
+
if (schema.propertyNames && typeof schema.propertyNames === 'object' && !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return null
|
|
5987
6078
|
|
|
5988
6079
|
// Build anchors map for $ref/#anchor and $dynamicRef resolution
|
|
5989
6080
|
const cRootDefs = schema.$defs || schema.definitions || null
|
|
@@ -6135,7 +6226,7 @@ function typeMismatchRest(schema, types) {
|
|
|
6135
6226
|
for (const k of Object.keys(schema)) {
|
|
6136
6227
|
if (drop.has(k)) continue
|
|
6137
6228
|
if (rest === null) rest = {}
|
|
6138
|
-
rest
|
|
6229
|
+
setOwn(rest, k, schema[k])
|
|
6139
6230
|
}
|
|
6140
6231
|
if (rest === null) return null
|
|
6141
6232
|
// A node whose reference was already written (genCodeCNode) keeps that
|
|
@@ -6236,7 +6327,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
6236
6327
|
if (Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))) {
|
|
6237
6328
|
if (!ctx.inCombined || ctx.inBranchFn) throw DECLINE
|
|
6238
6329
|
genCodeCNode({ $ref: schema.$ref }, v, pathExpr, lines, ctx, schemaPrefix)
|
|
6239
|
-
const rest =
|
|
6330
|
+
const rest = { ...schema }
|
|
6240
6331
|
Object.defineProperty(rest, REF_DONE, { value: true })
|
|
6241
6332
|
genCodeCNode(rest, v, pathExpr, lines, ctx, schemaPrefix)
|
|
6242
6333
|
return
|
|
@@ -6905,7 +6996,7 @@ function emitAnnot(sub, ctx) {
|
|
|
6905
6996
|
ctx.annFns.set(sub, null)
|
|
6906
6997
|
const name = `_an${ctx.varCounter++}`
|
|
6907
6998
|
const local = {}
|
|
6908
|
-
for (const k of Object.keys(sub)) if (!ANNOT_APPLICATORS.has(k)) local
|
|
6999
|
+
for (const k of Object.keys(sub)) if (!ANNOT_APPLICATORS.has(k)) setOwn(local, k, sub[k])
|
|
6909
7000
|
const vl = []
|
|
6910
7001
|
nestedGenCode(local, '_av', vl, ctx)
|
|
6911
7002
|
const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
|
|
@@ -7053,7 +7144,7 @@ function emitAnnotI(sub, ctx) {
|
|
|
7053
7144
|
annItemHelpers(ctx)
|
|
7054
7145
|
const name = `_ai${ctx.varCounter++}`
|
|
7055
7146
|
const local = {}
|
|
7056
|
-
for (const k of Object.keys(sub)) if (!ANNOT_ITEM_APPLICATORS.has(k)) local
|
|
7147
|
+
for (const k of Object.keys(sub)) if (!ANNOT_ITEM_APPLICATORS.has(k)) setOwn(local, k, sub[k])
|
|
7057
7148
|
const vl = []
|
|
7058
7149
|
nestedGenCode(local, '_av', vl, ctx)
|
|
7059
7150
|
const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
|
|
@@ -7169,7 +7260,7 @@ function genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
|
|
|
7169
7260
|
function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
|
|
7170
7261
|
let handledPropertyNames = false
|
|
7171
7262
|
const ppEntries = Object.entries(schema.patternProperties)
|
|
7172
|
-
const pn = schema.propertyNames
|
|
7263
|
+
const pn = isSimplePN(schema.propertyNames) ? schema.propertyNames : null
|
|
7173
7264
|
const pi = ctx.varCounter++
|
|
7174
7265
|
|
|
7175
7266
|
// Build pattern matchers: prefer charCodeAt for simple prefixes
|
|
@@ -7290,7 +7381,16 @@ function genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
7290
7381
|
function genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
7291
7382
|
const pn = schema.propertyNames
|
|
7292
7383
|
const ki = ctx.varCounter++
|
|
7384
|
+
// In the rich function an error carries the value it was raised on, and
|
|
7385
|
+
// here that would be the key where the interpreter's error points at the
|
|
7386
|
+
// object, so that one is left to the plain function and enrichment.
|
|
7387
|
+
if (!isSimplePN(pn) && ctx.rich) throw DECLINE
|
|
7293
7388
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
|
|
7389
|
+
if (!isSimplePN(pn)) {
|
|
7390
|
+
genCodeC(pn, `_k${ki}`, pathExpr, lines, ctx, schemaPrefix + '/propertyNames')
|
|
7391
|
+
lines.push(`}}`)
|
|
7392
|
+
return
|
|
7393
|
+
}
|
|
7294
7394
|
if (pn.minLength !== undefined) {
|
|
7295
7395
|
lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
|
|
7296
7396
|
}
|
|
@@ -7508,6 +7608,6 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
|
|
|
7508
7608
|
// not → contributes nothing (spec: annotations from not are discarded)
|
|
7509
7609
|
}
|
|
7510
7610
|
|
|
7511
|
-
module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, AJV_MESSAGES, _setCodeBudget }
|
|
7611
|
+
module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, expandedChars, AJV_MESSAGES, _setCodeBudget }
|
|
7512
7612
|
// For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
|
|
7513
7613
|
Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
|
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/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) {
|
package/lib/validator-core.js
CHANGED
|
@@ -604,8 +604,10 @@ function resolveSchemaForPreprocess(schema, schemaMap) {
|
|
|
604
604
|
if (p && typeof p === 'object' && p.$ref && !p.type) {
|
|
605
605
|
const t = resolveRefForPreprocess(p.$ref, schemaMap)
|
|
606
606
|
if (t && typeof t === 'object') {
|
|
607
|
-
|
|
608
|
-
|
|
607
|
+
// Spread and defineProperty: a property named "__proto__" stays an own
|
|
608
|
+
// key of the copy (assignment would set the copy's prototype).
|
|
609
|
+
if (!cloned) { cloned = { ...s }; cloned.properties = { ...s.properties } }
|
|
610
|
+
Object.defineProperty(cloned.properties, key, { value: t, writable: true, enumerable: true, configurable: true })
|
|
609
611
|
}
|
|
610
612
|
}
|
|
611
613
|
}
|
|
@@ -775,6 +777,7 @@ class Validator {
|
|
|
775
777
|
// to attach dataFrame entries to each enriched error.
|
|
776
778
|
this._posCache = null; // created by _pos() on first use, only the JSON text path needs it
|
|
777
779
|
this._lastRawInput = null;
|
|
780
|
+
this._lastParsed = undefined;
|
|
778
781
|
// undefined: not built yet. null: this schema has no scanner.
|
|
779
782
|
this._scanner = undefined;
|
|
780
783
|
// Checks a wrapper registered through _extendChecks. Declared here so
|
|
@@ -815,7 +818,7 @@ class Validator {
|
|
|
815
818
|
}
|
|
816
819
|
|
|
817
820
|
_pos() {
|
|
818
|
-
return this._posCache || (this._posCache = require('./data-position-cache').createCache());
|
|
821
|
+
return this._posCache || (this._posCache = require('./data-position-cache').createCache(_codegen !== null && _codegen.nativePositions ? _codegen.nativePositions() : null));
|
|
819
822
|
}
|
|
820
823
|
|
|
821
824
|
_ensureVocabularies() {
|
|
@@ -847,7 +850,12 @@ class Validator {
|
|
|
847
850
|
// "validate this schema against its dialect" needs no network and no
|
|
848
851
|
// caller-supplied registry. Only schemas that mention json-schema.org in a
|
|
849
852
|
// reference pay for the lookup.
|
|
850
|
-
|
|
853
|
+
// A reference, not `$schema`: nearly every schema names its dialect, and
|
|
854
|
+
// matching that loaded the vendored meta-schemas into every validator's
|
|
855
|
+
// map, 27 entries that made its compile cache key a 30 KB string, rebuilt
|
|
856
|
+
// in 20 microseconds at each compile, and sent every compile through the
|
|
857
|
+
// external-document checks.
|
|
858
|
+
if (this._schemaStr.includes('json-schema.org/') && /"\$(?:ref|dynamicRef|recursiveRef)":"[^"]*json-schema\.org\//.test(this._schemaStr)) {
|
|
851
859
|
const { METASCHEMAS } = require('./metaschemas');
|
|
852
860
|
this._ownSchemaMap();
|
|
853
861
|
for (const [id, meta] of METASCHEMAS) {
|
|
@@ -1104,6 +1112,17 @@ class Validator {
|
|
|
1104
1112
|
_bindVerdict(this, this._fastVerdict
|
|
1105
1113
|
? this._fastVerdict
|
|
1106
1114
|
: (data) => run(data).valid);
|
|
1115
|
+
// Parsed, then the verdict first and the errors only for a document that
|
|
1116
|
+
// fails: collecting them on every document cost an accepted 5 MB one 9 ms
|
|
1117
|
+
// over JSON.parse and validate(). At this layer, below the presentation
|
|
1118
|
+
// that the text path wraps around it: going through validate() as it
|
|
1119
|
+
// stands once every layer is in place handed the text path errors
|
|
1120
|
+
// already presented, and they lost their source anchor.
|
|
1121
|
+
const runText = options.abortEarly
|
|
1122
|
+
? run
|
|
1123
|
+
: (preprocess
|
|
1124
|
+
? (data) => { preprocess(data); return interp.isValid(data) ? VALID_RESULT : interp.validate(data); }
|
|
1125
|
+
: (data) => (interp.isValid(data) ? VALID_RESULT : interp.validate(data)));
|
|
1107
1126
|
this.validateJSON = (jsonStr) => {
|
|
1108
1127
|
let data;
|
|
1109
1128
|
try {
|
|
@@ -1112,9 +1131,19 @@ class Validator {
|
|
|
1112
1131
|
if (!(e instanceof SyntaxError)) throw e;
|
|
1113
1132
|
return _jsonSyntaxRejection(e);
|
|
1114
1133
|
}
|
|
1115
|
-
|
|
1134
|
+
this._lastParsed = data;
|
|
1135
|
+
return runText(data);
|
|
1136
|
+
};
|
|
1137
|
+
this.isValidJSON = (jsonStr) => {
|
|
1138
|
+
let data;
|
|
1139
|
+
try {
|
|
1140
|
+
data = JSON.parse(jsonStr);
|
|
1141
|
+
} catch (e) {
|
|
1142
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
return this.isValidObject(data);
|
|
1116
1146
|
};
|
|
1117
|
-
this.isValidJSON = (jsonStr) => this.validateJSON(jsonStr).valid;
|
|
1118
1147
|
}
|
|
1119
1148
|
|
|
1120
1149
|
// Error presentation, one lazy layer: declaration-order sorting, rich
|
|
@@ -1163,21 +1192,38 @@ class Validator {
|
|
|
1163
1192
|
// rejection it builds. Cleared as soon as it returns: the rejection
|
|
1164
1193
|
// carries the text itself, so nothing outlives the call.
|
|
1165
1194
|
this._lastRawInput = jsonStr;
|
|
1166
|
-
|
|
1195
|
+
this._lastParsed = undefined;
|
|
1196
|
+
let result, parsed;
|
|
1167
1197
|
try {
|
|
1168
1198
|
result = innerJson(jsonStr);
|
|
1169
1199
|
} finally {
|
|
1170
1200
|
this._lastRawInput = null;
|
|
1201
|
+
parsed = this._lastParsed;
|
|
1202
|
+
this._lastParsed = undefined;
|
|
1171
1203
|
}
|
|
1172
1204
|
// Every diagnostic the text path adds is deferred. Deciding here
|
|
1173
1205
|
// whether there is anything to add would mean reading `result.errors`,
|
|
1174
1206
|
// and on the codegen path that realizes the inner lazy layer, which is
|
|
1175
1207
|
// the document walk this exists to avoid.
|
|
1176
1208
|
if (result && result.valid === false) {
|
|
1177
|
-
return new LazyJsonRejection(result, jsonStr, this, enrich);
|
|
1209
|
+
return new LazyJsonRejection(result, jsonStr, this, enrich, parsed);
|
|
1178
1210
|
}
|
|
1179
1211
|
return result;
|
|
1180
1212
|
};
|
|
1213
|
+
} else if (this.validateJSON) {
|
|
1214
|
+
// Without enrichment the text path still presents its errors as
|
|
1215
|
+
// validate() does, in declaration order: on generated code they came
|
|
1216
|
+
// out in the order the function found them, so the same document gave
|
|
1217
|
+
// its errors in one order from validate() and another from here.
|
|
1218
|
+
const innerJson = this.validateJSON;
|
|
1219
|
+
this.validateJSON = (jsonStr) => {
|
|
1220
|
+
this._lastParsed = undefined;
|
|
1221
|
+
const result = innerJson(jsonStr);
|
|
1222
|
+
const parsed = this._lastParsed;
|
|
1223
|
+
this._lastParsed = undefined;
|
|
1224
|
+
if (!(result && result.valid === false && parsed !== undefined)) return result;
|
|
1225
|
+
return present(result, parsed);
|
|
1226
|
+
};
|
|
1181
1227
|
}
|
|
1182
1228
|
}
|
|
1183
1229
|
|
|
@@ -1211,10 +1257,12 @@ class Validator {
|
|
|
1211
1257
|
// errorMessage keep the validate hot path untouched. Layered after rich
|
|
1212
1258
|
// enrichment so `code`/`keyword`/`path` are already final and only the
|
|
1213
1259
|
// human-facing message changes.
|
|
1260
|
+
// The module loads only for a schema that has the keyword: loading it
|
|
1261
|
+
// to ask was a file read on every process's first compile.
|
|
1214
1262
|
{
|
|
1215
|
-
const emLib = require('./error-messages');
|
|
1216
1263
|
const schemaStr = this._schemaStr || (this._schemaObj ? JSON.stringify(this._schemaObj) : '');
|
|
1217
|
-
if (
|
|
1264
|
+
if (schemaStr.indexOf('"errorMessage"') !== -1) {
|
|
1265
|
+
const emLib = require('./error-messages');
|
|
1218
1266
|
const root = this._schemaObj;
|
|
1219
1267
|
const wrap = (inner) => (arg) => {
|
|
1220
1268
|
const result = inner(arg);
|
|
@@ -2184,7 +2232,15 @@ for (const [name, pick] of [
|
|
|
2184
2232
|
});
|
|
2185
2233
|
}
|
|
2186
2234
|
|
|
2235
|
+
// Cold start: an interpreted twin answers a large schema's first calls where
|
|
2236
|
+
// the code generator is registered (see coldTwin in index.js). Lite has none.
|
|
2237
|
+
Validator.prototype._coldTwin = function () {
|
|
2238
|
+
return _codegen === null || _codegen.coldTwin === undefined ? null : _codegen.coldTwin(this, Validator);
|
|
2239
|
+
};
|
|
2240
|
+
|
|
2187
2241
|
_defineLazyMethod('validate', (self) => (data) => {
|
|
2242
|
+
const t = self._coldTwin();
|
|
2243
|
+
if (t !== null) return t.validate(data);
|
|
2188
2244
|
self._ensureCompiled();
|
|
2189
2245
|
return self.validate(data);
|
|
2190
2246
|
});
|
|
@@ -2195,6 +2251,7 @@ _defineLazyMethod('isValidObject', (self) => {
|
|
|
2195
2251
|
// again on every call, about 200 ns, ten times the check.
|
|
2196
2252
|
let bound = false;
|
|
2197
2253
|
return (data) => {
|
|
2254
|
+
if (!bound) { const t = self._coldTwin(); if (t !== null) return t.isValidObject(data); }
|
|
2198
2255
|
if (bound) return self.isValidObject(data);
|
|
2199
2256
|
bound = true;
|
|
2200
2257
|
// A validator that rewrites its input goes through the full compile, which
|
|
@@ -2234,10 +2291,14 @@ _defineLazyMethod('isValidObject', (self) => {
|
|
|
2234
2291
|
};
|
|
2235
2292
|
});
|
|
2236
2293
|
_defineLazyMethod('validateJSON', (self) => (jsonStr) => {
|
|
2294
|
+
const t = self._coldTwin();
|
|
2295
|
+
if (t !== null) return t.validateJSON(jsonStr);
|
|
2237
2296
|
self._ensureCompiled();
|
|
2238
2297
|
return self.validateJSON(jsonStr);
|
|
2239
2298
|
});
|
|
2240
2299
|
_defineLazyMethod('isValidJSON', (self) => (jsonStr) => {
|
|
2300
|
+
const t = self._coldTwin();
|
|
2301
|
+
if (t !== null) return t.isValidJSON(jsonStr);
|
|
2241
2302
|
self._ensureCompiled();
|
|
2242
2303
|
return self.isValidJSON(jsonStr);
|
|
2243
2304
|
});
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.44.0",
|
|
4
4
|
"description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"release:check": "node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_embed.js && node tests/test_version_sync.js",
|
|
65
65
|
"build": "cmake-js build --target ata",
|
|
66
66
|
"rebuild": "cmake-js rebuild --target ata",
|
|
67
|
-
"test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_number_text_parity.js && node tests/test_text_path_errors.js && node tests/test_compiled_parity.js && node tests/test_compiled_extension_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_error_list_differential.js && node tests/test_output_format.js && node tests/test_standard_json_schema.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_esm_exports.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_native_lazy.js && node tests/test_buffer_apis_without_native.js && node tests/test_cold_start_modules.js && node tests/test_safe_regex_embed.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_regex_ecma_whitespace.js && node tests/test_regex_native_linear.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_proto_accessor.js && node tests/test_hybrid_tier.js && node tests/test_first_rejection_builds_one.js && node tests/test_container_local.js && node tests/test_one_shot_validate.js && node tests/test_fixed_pattern_inline.js && node tests/test_plan_source.js && node tests/test_fused_remove_additional.js && node tests/test_runtime_parse.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_encoded_refs.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_schema_code_injection.js && node tests/test_aot_html_safe.js && node tests/test_ref_annotation_siblings.js && node tests/test_gate_wrapped_schemas.js && node tests/test_pattern_message_escape.js && node tests/test_nested_defaults_engines.js && node tests/test_nested_coercion_parity.js && node tests/test_preprocess_cache.js && node tests/test_large_schema_codegen.js && node tests/test_base_uri.js && node tests/test_hoisted_def_recursion.js && node tests/test_alias_definitions.js && node tests/test_property_names_ref.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_custom_keyword_codegen.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_unevaluated_differential.js && node tests/fuzz_differential.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_ref_error_order.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_error_read_paths.js && node tests/test_unique_items_error_pair.js && node tests/test_rich_errors_generated.js && node tests/test_captured_verdict_stub.js && node tests/test_generated_returns_rewrite.js && node tests/test_deep_pointer_refs.js && node tests/test_uneval_props_dynamic.js && node tests/test_one_pass_no_fallback.js && node tests/test_scanner_size_cap.js && node tests/test_scanner_index_keys.js && node tests/test_both_defs_containers.js && node tests/test_uneval_branch_counts.js && node tests/test_branch_verdicts.js && node tests/test_shared_definitions.js && node tests/test_compile_cache_bound.js && node tests/test_uneval_items_dynamic.js && node tests/test_escaped_property_names.js && node tests/test_ref_siblings_combined.js && node tests/test_unicode_property_patterns.js && node tests/test_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_lite_parity.js && node tests/test_lite_bundle_size.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_render_without_process.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_aot_source_frames.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node tests/test_collect_evaluated.js && node tests/test_differential_tiers.js && node tests/test_draft7.js && node tests/test_dual_path.js && node tests/test_keywords_codegen.js && node tests/test_ref_cross.js && node tests/test_shape_classifier.js && node tests/test_tier0.js && node benchmark/bench_aot_size.mjs",
|
|
67
|
+
"test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_number_text_parity.js && node tests/test_text_path_errors.js && node tests/test_compiled_parity.js && node tests/test_compiled_extension_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_error_list_differential.js && node tests/test_output_format.js && node tests/test_standard_json_schema.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_esm_exports.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_native_lazy.js && node tests/test_buffer_apis_without_native.js && node tests/test_cold_start_modules.js && node tests/test_safe_regex_embed.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_regex_ecma_whitespace.js && node tests/test_regex_native_linear.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_proto_accessor.js && node tests/test_hybrid_tier.js && node tests/test_first_rejection_builds_one.js && node tests/test_container_local.js && node tests/test_one_shot_validate.js && node tests/test_fixed_pattern_inline.js && node tests/test_plan_source.js && node tests/test_fused_remove_additional.js && node tests/test_runtime_parse.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_encoded_refs.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_schema_code_injection.js && node tests/test_aot_html_safe.js && node tests/test_ref_annotation_siblings.js && node tests/test_gate_wrapped_schemas.js && node tests/test_pattern_message_escape.js && node tests/test_nested_defaults_engines.js && node tests/test_nested_coercion_parity.js && node tests/test_preprocess_cache.js && node tests/test_large_schema_codegen.js && node tests/test_base_uri.js && node tests/test_hoisted_def_recursion.js && node tests/test_alias_definitions.js && node tests/test_property_names_ref.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_custom_keyword_codegen.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_unevaluated_differential.js && node tests/fuzz_differential.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_ref_error_order.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_error_read_paths.js && node tests/test_unique_items_error_pair.js && node tests/test_rich_errors_generated.js && node tests/test_captured_verdict_stub.js && node tests/test_generated_returns_rewrite.js && node tests/test_deep_pointer_refs.js && node tests/test_uneval_props_dynamic.js && node tests/test_one_pass_no_fallback.js && node tests/test_scanner_size_cap.js && node tests/test_scanner_index_keys.js && node tests/test_both_defs_containers.js && node tests/test_uneval_branch_counts.js && node tests/test_branch_verdicts.js && node tests/test_property_names_subschema.js && node tests/test_property_named_ref.js && node tests/test_validate_json_engines_same_errors.js && node tests/test_prototype_property_names.js && node tests/test_proto_name_rename_invariance.js && node tests/test_shared_definitions.js && node tests/test_compile_cache_bound.js && node tests/test_uneval_items_dynamic.js && node tests/test_escaped_property_names.js && node tests/test_ref_siblings_combined.js && node tests/test_unicode_property_patterns.js && node tests/test_cold_twin.js && node tests/test_native_positions.js && node tests/test_validate_json_single_parse.js && node tests/test_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_lite_parity.js && node tests/test_lite_bundle_size.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_render_without_process.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_aot_source_frames.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_levenshtein_banded.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node tests/test_collect_evaluated.js && node tests/test_differential_tiers.js && node tests/test_draft7.js && node tests/test_dual_path.js && node tests/test_keywords_codegen.js && node tests/test_ref_cross.js && node tests/test_shape_classifier.js && node tests/test_tier0.js && node benchmark/bench_aot_size.mjs",
|
|
68
68
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
69
69
|
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
|
|
70
70
|
"test:compat": "node tests/test_compat.js",
|
|
@@ -143,13 +143,13 @@
|
|
|
143
143
|
"LICENSE"
|
|
144
144
|
],
|
|
145
145
|
"optionalDependencies": {
|
|
146
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
147
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
148
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
149
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
150
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
151
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
152
|
-
"@ata-validator/native-win32-x64": "1.
|
|
146
|
+
"@ata-validator/native-darwin-arm64": "1.44.0",
|
|
147
|
+
"@ata-validator/native-darwin-x64": "1.44.0",
|
|
148
|
+
"@ata-validator/native-linux-arm64-gnu": "1.44.0",
|
|
149
|
+
"@ata-validator/native-linux-arm64-musl": "1.44.0",
|
|
150
|
+
"@ata-validator/native-linux-x64-gnu": "1.44.0",
|
|
151
|
+
"@ata-validator/native-linux-x64-musl": "1.44.0",
|
|
152
|
+
"@ata-validator/native-win32-x64": "1.44.0"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
155
|
"yaml": "^2.0.0"
|