ata-validator 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/index.js +240 -115
- package/lib/enrich-error.js +10 -3
- package/lib/error-codes.js +54 -1
- package/lib/formats.js +390 -4
- package/lib/interpreter.js +7 -7
- package/lib/js-compiler.js +118 -40
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
|
|
4
4
|
|
|
5
|
+
## 1.12.0 - 2026-09-06
|
|
6
|
+
|
|
7
|
+
### Performance
|
|
8
|
+
|
|
9
|
+
- `date-time` stopped asking every character where it sits. The scan tested each of nineteen positions against the separator indices before checking the digit; the separators are now read directly by index and each digit becomes its value in the same read that validates it, so the field numbers cost nothing extra. 40.8 to 19.5 ns on a valid value, medians across separate processes. Same answers as before on every month, day and clock boundary and under 150k random mutations.
|
|
10
|
+
|
|
11
|
+
- `uuid` reads the four hyphens by index and the 32 hex digits in five runs with fixed bounds, instead of a case-insensitive regular expression. A digit is one unsigned compare and a letter one more after folding case with a single OR. 48.4 to 35.1 ns; 300k mutated and random strings against the old expression with 0 mismatches.
|
|
12
|
+
|
|
13
|
+
- `time` reads fixed positions the same way instead of running a regular expression: 15.6 to 12.1 ns, and 200k mutations against the old pattern with 0 mismatches.
|
|
14
|
+
|
|
15
|
+
- `uri` reads the scheme from the front and scans the rest once instead of running two regular expressions: 39.3 to 32.8 ns, interleaved medians. The remaining scan then became two tiers, because the measured answer was not the expected one: a hand-written character loop beat the old `\s` expression in isolation but lost to the engine's scanner inside a compiled validator, since `\s` is what forced the Unicode machinery in. The first tier asks whether anything sits outside printable ASCII, a one-byte class the engine scans at its own speed and no ordinary URI ever trips; only a string that trips it pays for the loop that knows the exact reserved set. On a nested schema carrying six URLs, 215.5 to 189.4 ns per document, medians across separate processes. `uri-reference` shares the same scan. Fuzzed over every code point in the BMP in three positions, 0 mismatches.
|
|
16
|
+
|
|
17
|
+
- The Standard Schema bridge stopped paying for enrichment it throws away. An issue carries a message and a path and nothing else, but `~standard.validate` read the rich error list, which builds suggestions, ranks and source frames per error. It now takes the raw, schema-ordered list through the same build the rich path uses, and `parsePointerPath` walks the pointer in one pass instead of split, filter, map and a regex per segment. A 16-error rejection went from 17.6 to 4.3 microseconds; messages and order are unchanged, and `tests/test_standard_schema.js` holds issues to exact parity with `validate().errors`. Schemas using `errorMessage` keep their custom messages.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- The compiled engine refused a lowercase zone letter in `time` that the interpreter accepted: `00:00:00z` answered differently depending on which engine ran the schema, and `date-time` took either case in both. One implementation now answers for every engine, and it takes both cases, per RFC 3339.
|
|
22
|
+
|
|
23
|
+
- The ReDoS integration test measured the first call, which includes compiling the schema and the pattern, against a 50 ms budget. On a loaded CI machine that reads as a failure without anything being wrong: the gate exists to separate linear matching from catastrophic backtracking, which differ by minutes, not by milliseconds. It now warms up first and allows 500 ms.
|
|
24
|
+
|
|
25
|
+
## 1.11.0 - 2026-08-31
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- The verdict methods answer the same question as `validate()` again. With `coerceTypes`, `removeAdditional` or a schema `default` in play, `validate()` ran the preprocess pass and `isValidObject()`, `isValidJSON()` and `validateJSON()` did not, so the same validator answered `true` from one and `false` from another for the same document: `validate({ age: '26' })` accepted where `isValidObject({ age: '26' })` rejected. Every path now runs the same pass. Verdict methods on a validator configured this way rewrite the input in place, as `validate()` already did, and the cost of the correction is 0.5 ns on `isValidObject` and 2.7 ns on `isValidJSON`, measured interleaved; validators without those options are unchanged. `tests/test_verdict_preprocess.js` holds all four methods to the same answers.
|
|
30
|
+
|
|
31
|
+
- The native engine's error codes no longer reach callers untranslated. A type failure answered by the addon came back as `code: 3` with no `keyword` and a `docUrl` pointing at a page that does not exist, while the same failure from the JavaScript engines came back as `ATA1001`; both now report the documented code, keyword and link. `tests/test_native_error_codes.js` holds the table against the enum in `include/ata.h`, so the two cannot drift apart silently.
|
|
32
|
+
- The error generator declined self-referencing schemas by emitting nothing for the reference, which accepted whatever that reference guarded: `{ properties: { foo: { $ref: "#" } }, additionalProperties: false }` accepted `{ foo: { bar: false } }` on that path. It now declines the schema outright and the validator falls back to an engine that answers it correctly. The entry-point agreement test covers the shape.
|
|
33
|
+
|
|
34
|
+
- `ipv6` gave two different answers depending on which engine ran it, and neither was right. The compiled path refused an IPv4-mapped address such as `::ffff:192.168.1.1`, the interpreted path accepted `::ffff:1.2.3.4.5`, and both accepted a group of five hex digits like `12345::1`. One implementation now answers for every engine, following RFC 4291, checked against Node's own `net.isIPv6` and the suite's corpus.
|
|
35
|
+
|
|
36
|
+
- `date-time` refuses dates that do not exist. The check ran a regular expression for the shape and then handed the string to `Date.parse`, which rolls an out-of-range day into the next month, so `2026-02-30T00:00:00Z`, `2026-02-29T00:00:00Z` and `2026-12-31T24:00:00Z` were all accepted. The month, the day count for that month in that year, the clock and the offset are now checked directly, per RFC 3339. Schemas that relied on the old leniency will see those values rejected.
|
|
37
|
+
|
|
38
|
+
- Data that points back at itself no longer exhausts the stack. A document with a cycle, which JSON text cannot express but an in-memory object graph can, threw `RangeError: Maximum call stack size exceeded` on the compiled path while the interpreted engine settled on an answer, so the two engines disagreed. Both now follow the same rule: a value already being checked against a schema is a fixed point and counts as satisfied, and a cycle no longer hides a real violation elsewhere in the document. Validation runs a fast pass that only counts depth and a guarded pass that runs when that depth is exceeded, so ordinary documents pay one integer operation per recursive call. Measured interleaved on a self-referencing schema: a four-node document 22.2 to 30.6 ns, a 200-node document 2077 to 1178 ns, non-recursive schemas unchanged at 3.8 ns. `tests/test_cyclic_input.js` holds all three engines to the same answers.
|
|
39
|
+
|
|
40
|
+
### Performance
|
|
41
|
+
|
|
42
|
+
- `ipv6` and `hostname` read the string once as well: 54.7 to 29.6 ns and 45.5 to 28.2 ns, interleaved medians. `ipv6` no longer allocates two arrays per check; `hostname` keeps the answers of the expression it replaces, fuzzed over 300k strings with 0 mismatches.
|
|
43
|
+
|
|
44
|
+
- `date-time` reads the string once, with no regular expression, no date object and no allocation: 95.0 to 39.4 ns on a valid value with a `Z`, 103.8 to 45.3 ns with a numeric offset, interleaved medians. Fuzzed against a reference that spells out RFC 3339, with 0 mismatches over 300k strings; `tests/test_formats_single_pass.js` keeps both the predicate and the generated form on it.
|
|
45
|
+
|
|
46
|
+
- A constructed Validator is roughly three times smaller on the heap until it is used. The public methods and the Standard Schema entry moved from per-instance closures built in the constructor to memoized prototype accessors, and the JSON position cache is only allocated when the JSON text path first needs it. Measured per instance on a 10-key object schema, double-gc deltas over 2000 instances: 1.61 KB to 0.43 KB with a shared schema object, 2.33 KB to 1.12 KB when each instance owns its schema, 3.93 KB to 3.30 KB once compiled and used. Construction alone went from 1504 to 855 ns; construction plus first validate pays about 0.9 microseconds more, once, because the compile step's method assignments now go through a defining setter. The hot validate() path is unchanged, measured interleaved. Detached method references (`const f = v.validate`) still work; `tests/test_lazy_instance.js` pins the shape.
|
|
47
|
+
|
|
5
48
|
## 1.10.0 - 2026-08-30
|
|
6
49
|
|
|
7
50
|
### Performance
|
package/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const { needsNormalization } = require("./lib/schema-scan");
|
|
|
15
15
|
const { isV1Dialect } = require("./lib/dialect");
|
|
16
16
|
const { classify } = require("./lib/shape-classifier");
|
|
17
17
|
const { buildTier0Plan, tier0Validate } = require("./lib/tier0");
|
|
18
|
+
const { createCache: _createPosCache } = require("./lib/data-position-cache");
|
|
18
19
|
|
|
19
20
|
// Extract default values from a schema tree. Returns a function that applies
|
|
20
21
|
// defaults to an object in-place (mutates), or null if no defaults exist.
|
|
@@ -325,15 +326,22 @@ const ABORT_EARLY_RESULT = Object.freeze({
|
|
|
325
326
|
// the eager shape. Note for tests: deepStrictEqual against a plain object
|
|
326
327
|
// compares prototypes; read `.errors` and compare that.
|
|
327
328
|
class LazyRejection {
|
|
328
|
-
constructor(build, data) {
|
|
329
|
+
constructor(build, data, buildRaw) {
|
|
329
330
|
this.valid = false;
|
|
330
331
|
this._build = build;
|
|
331
332
|
this._data = data;
|
|
332
333
|
this._errors = null;
|
|
334
|
+
this._buildRaw = buildRaw;
|
|
333
335
|
}
|
|
334
336
|
toJSON() {
|
|
335
337
|
return { valid: false, errors: this.errors };
|
|
336
338
|
}
|
|
339
|
+
// Raw shape for consumers that carry only message and path, such as the
|
|
340
|
+
// Standard Schema bridge: schema order, no enrichment. Reading `errors`
|
|
341
|
+
// afterwards still enriches through its own build.
|
|
342
|
+
_ataRaw() {
|
|
343
|
+
return this._buildRaw ? this._buildRaw(this._data) : this.errors;
|
|
344
|
+
}
|
|
337
345
|
}
|
|
338
346
|
Object.defineProperty(LazyRejection.prototype, 'errors', {
|
|
339
347
|
enumerable: true,
|
|
@@ -462,18 +470,30 @@ function sortErrorsBySchemaOrder(rootSchema, errors) {
|
|
|
462
470
|
|
|
463
471
|
function parsePointerPath(path) {
|
|
464
472
|
if (!path) return [];
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
473
|
+
// One pass, no intermediate arrays. Per Standard Schema V1 an array index
|
|
474
|
+
// is emitted as a number and an object key as a string; a segment is an
|
|
475
|
+
// index when it is all digits with no leading zero.
|
|
476
|
+
const out = [];
|
|
477
|
+
const n = path.length;
|
|
478
|
+
let start = 1;
|
|
479
|
+
for (let i = 1; i <= n; i++) {
|
|
480
|
+
if (i !== n && path.charCodeAt(i) !== 47) continue;
|
|
481
|
+
if (i > start) {
|
|
482
|
+
let seg = path.slice(start, i);
|
|
483
|
+
if (seg.indexOf('~') >= 0) seg = seg.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
484
|
+
const c0 = seg.charCodeAt(0);
|
|
485
|
+
let numeric = c0 >= 48 && c0 <= 57 && (seg.length === 1 || c0 !== 48);
|
|
486
|
+
if (numeric) {
|
|
487
|
+
for (let k = 1; k < seg.length; k++) {
|
|
488
|
+
const c = seg.charCodeAt(k);
|
|
489
|
+
if (c < 48 || c > 57) { numeric = false; break; }
|
|
490
|
+
}
|
|
474
491
|
}
|
|
475
|
-
|
|
476
|
-
}
|
|
492
|
+
out.push({ key: numeric ? Number(seg) : seg });
|
|
493
|
+
}
|
|
494
|
+
start = i + 1;
|
|
495
|
+
}
|
|
496
|
+
return out;
|
|
477
497
|
}
|
|
478
498
|
|
|
479
499
|
function createPaddedBuffer(jsonStr) {
|
|
@@ -788,89 +808,15 @@ class Validator {
|
|
|
788
808
|
// Per-validate data position cache. Populated by validateJSON before
|
|
789
809
|
// dispatching to inner validate(); consulted by the rich-error wrap
|
|
790
810
|
// to attach dataFrame entries to each enriched error.
|
|
791
|
-
this._posCache =
|
|
811
|
+
this._posCache = null; // created by _pos() on first use, only the JSON text path needs it
|
|
792
812
|
this._lastRawInput = null;
|
|
793
813
|
|
|
794
|
-
//
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
return this.validate(data);
|
|
798
|
-
};
|
|
799
|
-
this.isValidObject = (data) => {
|
|
800
|
-
// Lazy: classify + build tier 0 plan on first call, not in constructor.
|
|
801
|
-
const _tier = classify(this._schemaObj);
|
|
802
|
-
if (_tier.tier === 0) {
|
|
803
|
-
const _plan = buildTier0Plan(this._schemaObj);
|
|
804
|
-
let _n = 0;
|
|
805
|
-
this.isValidObject = (d) => {
|
|
806
|
-
const r = tier0Validate(_plan, d);
|
|
807
|
-
if (++_n === 2) {
|
|
808
|
-
try { this._ensureCodegen(); } catch {}
|
|
809
|
-
}
|
|
810
|
-
return r;
|
|
811
|
-
};
|
|
812
|
-
} else {
|
|
813
|
-
this._ensureCodegen();
|
|
814
|
-
// Codegen can bail on shapes it cannot represent; the full compile
|
|
815
|
-
// binds the native path or the unsupported thrower instead of
|
|
816
|
-
// leaving this stub to re-dispatch to itself.
|
|
817
|
-
if (!this._jsFn) this._ensureCompiled();
|
|
818
|
-
}
|
|
819
|
-
return this.isValidObject(data);
|
|
820
|
-
};
|
|
821
|
-
this.validateJSON = (jsonStr) => {
|
|
822
|
-
this._ensureCompiled();
|
|
823
|
-
return this.validateJSON(jsonStr);
|
|
824
|
-
};
|
|
825
|
-
this.isValidJSON = (jsonStr) => {
|
|
826
|
-
this._ensureCompiled();
|
|
827
|
-
return this.isValidJSON(jsonStr);
|
|
828
|
-
};
|
|
829
|
-
this.validateAndParse = (jsonStr) => {
|
|
830
|
-
if (!native) throw new Error('Native addon required for validateAndParse()');
|
|
831
|
-
this._ensureCompiled();
|
|
832
|
-
return this.validateAndParse(jsonStr);
|
|
833
|
-
};
|
|
834
|
-
this.isValid = (buf) => {
|
|
835
|
-
if (!native) throw new Error('Native addon required for isValid() — use validate() or isValidObject() instead');
|
|
836
|
-
this._ensureCompiled();
|
|
837
|
-
return this.isValid(buf);
|
|
838
|
-
};
|
|
839
|
-
this.countValid = (ndjsonBuf) => {
|
|
840
|
-
if (!native) throw new Error('Native addon required for countValid()');
|
|
841
|
-
this._ensureCompiled();
|
|
842
|
-
return this.countValid(ndjsonBuf);
|
|
843
|
-
};
|
|
844
|
-
this.batchIsValid = (buffers) => {
|
|
845
|
-
if (!native) throw new Error('Native addon required for batchIsValid()');
|
|
846
|
-
this._ensureCompiled();
|
|
847
|
-
return this.batchIsValid(buffers);
|
|
848
|
-
};
|
|
814
|
+
// Public methods start as memoized accessors on the prototype; nothing is
|
|
815
|
+
// allocated per instance until one is first read. See _defineLazyMethod
|
|
816
|
+
// below the class.
|
|
849
817
|
|
|
850
|
-
// ~standard
|
|
851
|
-
// the
|
|
852
|
-
const self = this;
|
|
853
|
-
Object.defineProperty(this, "~standard", {
|
|
854
|
-
value: Object.freeze({
|
|
855
|
-
version: 1,
|
|
856
|
-
vendor: "ata-validator",
|
|
857
|
-
validate(value) {
|
|
858
|
-
const result = self.validate(value);
|
|
859
|
-
if (result.valid) {
|
|
860
|
-
return { value };
|
|
861
|
-
}
|
|
862
|
-
return {
|
|
863
|
-
issues: result.errors.map((err) => ({
|
|
864
|
-
message: err.message,
|
|
865
|
-
path: parsePointerPath(err.instancePath),
|
|
866
|
-
})),
|
|
867
|
-
};
|
|
868
|
-
},
|
|
869
|
-
}),
|
|
870
|
-
writable: false,
|
|
871
|
-
enumerable: false,
|
|
872
|
-
configurable: false,
|
|
873
|
-
});
|
|
818
|
+
// "~standard" (Standard Schema V1) is a lazy prototype accessor too;
|
|
819
|
+
// see below the class. Consumers only pay for it if they read it.
|
|
874
820
|
|
|
875
821
|
// Populate identity cache so repeated `new Validator(sameSchema)` short-circuits.
|
|
876
822
|
if (!opts && typeof schema === "object" && schema !== null) {
|
|
@@ -882,6 +828,22 @@ class Validator {
|
|
|
882
828
|
// meta-schema, which addSchema() may only have registered just now. Run once,
|
|
883
829
|
// before anything reads the schema, and before `_schemaStr` is computed from
|
|
884
830
|
// it. After this addSchema() is refused, so the answer cannot go stale.
|
|
831
|
+
// Whether validation is preceded by a pass that rewrites the input:
|
|
832
|
+
// coercion, removal of undeclared keys, or filling in defaults. The verdict
|
|
833
|
+
// methods have to take the same path when it is, so the quick bindings that
|
|
834
|
+
// answer from the compiled function alone are not used for these validators.
|
|
835
|
+
_needsPreprocess() {
|
|
836
|
+
const o = this._options;
|
|
837
|
+
if (o.coerceTypes || o.removeAdditional) return true;
|
|
838
|
+
if (o.useDefaults === false) return false;
|
|
839
|
+
if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
|
|
840
|
+
return this._schemaStr.includes('"default"');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
_pos() {
|
|
844
|
+
return this._posCache || (this._posCache = _createPosCache());
|
|
845
|
+
}
|
|
846
|
+
|
|
885
847
|
_ensureVocabularies() {
|
|
886
848
|
if (this._vocabulariesApplied) return;
|
|
887
849
|
this._vocabulariesApplied = true;
|
|
@@ -1180,14 +1142,26 @@ class Validator {
|
|
|
1180
1142
|
return result;
|
|
1181
1143
|
};
|
|
1182
1144
|
}
|
|
1183
|
-
|
|
1145
|
+
// The verdict methods answer validate()'s question without building the
|
|
1146
|
+
// error list, so they run the same preprocess pass. Skipping it made the
|
|
1147
|
+
// two disagree on input that coercion or a default would have fixed.
|
|
1148
|
+
this.isValidObject = preprocess
|
|
1149
|
+
? (data) => { preprocess(data); return jsFn(data) }
|
|
1150
|
+
: jsFn;
|
|
1184
1151
|
const hybridFn = jsFn._hybridFactory
|
|
1185
1152
|
? jsFn._hybridFactory(VALID_RESULT, errFn)
|
|
1186
1153
|
: null;
|
|
1187
|
-
const
|
|
1154
|
+
const jsonValidateInner = safeCombinedFn
|
|
1188
1155
|
|| hybridFn
|
|
1189
1156
|
|| ((obj) => (jsFn(obj) ? VALID_RESULT : errFn(obj)));
|
|
1190
|
-
|
|
1157
|
+
// Parsed text takes the same preprocess pass as a parsed object, so
|
|
1158
|
+
// validate(obj) and validateJSON(text) answer the same for the same
|
|
1159
|
+
// document. Without it, coercion, removal and defaults applied on one
|
|
1160
|
+
// path and not the other.
|
|
1161
|
+
const jsonValidateFn = preprocess
|
|
1162
|
+
? (obj) => { preprocess(obj); return jsonValidateInner(obj) }
|
|
1163
|
+
: jsonValidateInner;
|
|
1164
|
+
this.validateJSON = useSimdjsonForLarge && native && !preprocess
|
|
1191
1165
|
? (jsonStr) => {
|
|
1192
1166
|
if (jsonStr.length >= SIMDJSON_THRESHOLD) {
|
|
1193
1167
|
this._ensureNative();
|
|
@@ -1214,7 +1188,22 @@ class Validator {
|
|
|
1214
1188
|
this._ensureNative();
|
|
1215
1189
|
return this._compiled.validateJSON(jsonStr);
|
|
1216
1190
|
};
|
|
1217
|
-
|
|
1191
|
+
// The addon validates the bytes as they are, which is the wrong answer
|
|
1192
|
+
// when the schema asks for coercion, removal or defaults: those change
|
|
1193
|
+
// what counts as valid. With a preprocess pass configured the text is
|
|
1194
|
+
// parsed and run through the same path validate() takes.
|
|
1195
|
+
const verdictFromText = (jsonStr) => {
|
|
1196
|
+
let parsed;
|
|
1197
|
+
try {
|
|
1198
|
+
parsed = JSON.parse(jsonStr);
|
|
1199
|
+
} catch (e) {
|
|
1200
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
if (preprocess) preprocess(parsed);
|
|
1204
|
+
return jsFn(parsed);
|
|
1205
|
+
};
|
|
1206
|
+
this.isValidJSON = useSimdjsonForLarge && native && !preprocess
|
|
1218
1207
|
? (jsonStr) => {
|
|
1219
1208
|
if (jsonStr.length >= SIMDJSON_THRESHOLD) {
|
|
1220
1209
|
this._ensureNative();
|
|
@@ -1223,21 +1212,9 @@ class Validator {
|
|
|
1223
1212
|
Buffer.from(jsonStr),
|
|
1224
1213
|
);
|
|
1225
1214
|
}
|
|
1226
|
-
|
|
1227
|
-
return jsFn(JSON.parse(jsonStr));
|
|
1228
|
-
} catch (e) {
|
|
1229
|
-
if (!(e instanceof SyntaxError)) throw e;
|
|
1230
|
-
return false;
|
|
1231
|
-
}
|
|
1215
|
+
return verdictFromText(jsonStr);
|
|
1232
1216
|
}
|
|
1233
|
-
:
|
|
1234
|
-
try {
|
|
1235
|
-
return jsFn(JSON.parse(jsonStr));
|
|
1236
|
-
} catch (e) {
|
|
1237
|
-
if (!(e instanceof SyntaxError)) throw e;
|
|
1238
|
-
return false;
|
|
1239
|
-
}
|
|
1240
|
-
};
|
|
1217
|
+
: verdictFromText;
|
|
1241
1218
|
// validateAndParse: parse the JSON, then validate. Pure JS (JSON.parse +
|
|
1242
1219
|
// validate) so it works with or without the native addon and in browsers.
|
|
1243
1220
|
{
|
|
@@ -1416,11 +1393,19 @@ class Validator {
|
|
|
1416
1393
|
if (result && result.valid === false && result !== ABORT_EARLY_RESULT) {
|
|
1417
1394
|
// Positions come from the raw input when validateJSON set one;
|
|
1418
1395
|
// resolved eagerly since the cache is reset per call.
|
|
1419
|
-
const positions = (enrich && self._lastRawInput != null) ? self.
|
|
1396
|
+
const positions = (enrich && self._lastRawInput != null) ? self._pos().get(self._lastRawInput) : null;
|
|
1420
1397
|
if (positions) self._posCache.reset();
|
|
1421
1398
|
let cached = null;
|
|
1422
1399
|
return {
|
|
1423
1400
|
valid: false,
|
|
1401
|
+
// Raw shape for consumers that carry only message and path, such
|
|
1402
|
+
// as the Standard Schema bridge: schema order, no enrichment.
|
|
1403
|
+
// Reading `errors` afterwards still enriches from the same build.
|
|
1404
|
+
_ataRaw() {
|
|
1405
|
+
let raw = result.errors || [];
|
|
1406
|
+
if (raw.length > 1) raw = sortErrorsBySchemaOrder(root, raw);
|
|
1407
|
+
return raw;
|
|
1408
|
+
},
|
|
1424
1409
|
get errors() {
|
|
1425
1410
|
if (cached === null) {
|
|
1426
1411
|
let raw = result.errors || [];
|
|
@@ -1481,7 +1466,7 @@ class Validator {
|
|
|
1481
1466
|
let parsedData;
|
|
1482
1467
|
try { parsedData = JSON.parse(jsonStr); } catch { parsedData = undefined; }
|
|
1483
1468
|
if (!first || !first.docUrl) {
|
|
1484
|
-
const positions = (this._lastRawInput != null) ? this.
|
|
1469
|
+
const positions = (this._lastRawInput != null) ? this._pos().get(this._lastRawInput) : null;
|
|
1485
1470
|
const enriched = result.errors.map((e) => enrich(e, {
|
|
1486
1471
|
data: parsedData,
|
|
1487
1472
|
positions,
|
|
@@ -1501,7 +1486,7 @@ class Validator {
|
|
|
1501
1486
|
return { valid: false, errors: enriched };
|
|
1502
1487
|
}
|
|
1503
1488
|
// Already-enriched path: still attach dataFrame if missing.
|
|
1504
|
-
const positions = (this._lastRawInput != null) ? this.
|
|
1489
|
+
const positions = (this._lastRawInput != null) ? this._pos().get(this._lastRawInput) : null;
|
|
1505
1490
|
if (positions) {
|
|
1506
1491
|
for (const e of result.errors) {
|
|
1507
1492
|
if (e && !e.dataFrame) {
|
|
@@ -1592,17 +1577,24 @@ class Validator {
|
|
|
1592
1577
|
const _full = this.validate;
|
|
1593
1578
|
const _fast = this._fastVerdict;
|
|
1594
1579
|
const EMPTY_ERRORS = Object.freeze([]);
|
|
1580
|
+
const _verdictFallback = [{ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' }];
|
|
1595
1581
|
const _buildErrors = (data) => {
|
|
1596
1582
|
const r = _full(data);
|
|
1597
1583
|
return (r && r.valid === false && r.errors && r.errors.length)
|
|
1598
1584
|
? r.errors
|
|
1599
1585
|
// The data changed between the verdict and this read; keep the
|
|
1600
1586
|
// verdict and say so rather than inventing a specific error.
|
|
1601
|
-
:
|
|
1587
|
+
: _verdictFallback;
|
|
1588
|
+
};
|
|
1589
|
+
const _buildRawErrors = (data) => {
|
|
1590
|
+
const r = _full(data);
|
|
1591
|
+
if (!r || r.valid !== false) return _verdictFallback;
|
|
1592
|
+
const raw = typeof r._ataRaw === 'function' ? r._ataRaw() : r.errors;
|
|
1593
|
+
return raw && raw.length ? raw : _verdictFallback;
|
|
1602
1594
|
};
|
|
1603
1595
|
this.validate = (data) => {
|
|
1604
1596
|
if (_fast(data)) return { valid: true, data, errors: EMPTY_ERRORS };
|
|
1605
|
-
return new LazyRejection(_buildErrors, data);
|
|
1597
|
+
return new LazyRejection(_buildErrors, data, _buildRawErrors);
|
|
1606
1598
|
};
|
|
1607
1599
|
}
|
|
1608
1600
|
|
|
@@ -1673,6 +1665,13 @@ class Validator {
|
|
|
1673
1665
|
|
|
1674
1666
|
_ensureCodegen() {
|
|
1675
1667
|
if (this._jsFn) return;
|
|
1668
|
+
// A validator that rewrites its input cannot use the binding below: that
|
|
1669
|
+
// one answers from the compiled function alone and would skip the rewrite,
|
|
1670
|
+
// so isValidObject() and validate() would disagree.
|
|
1671
|
+
if (this._needsPreprocess()) {
|
|
1672
|
+
this._ensureCompiled();
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1676
1675
|
this._ensureVocabularies();
|
|
1677
1676
|
if (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI) return;
|
|
1678
1677
|
if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
|
|
@@ -2032,6 +2031,132 @@ function defineSchema (schema) {
|
|
|
2032
2031
|
return schema;
|
|
2033
2032
|
}
|
|
2034
2033
|
|
|
2034
|
+
// Public methods start as memoized accessors on the prototype. A fresh
|
|
2035
|
+
// Validator allocates none of them; the first read of a method builds the
|
|
2036
|
+
// bound closure, stores it on the instance as an ordinary writable property
|
|
2037
|
+
// and returns it. The setter keeps the compile step's plain assignments
|
|
2038
|
+
// (`this.validate = fn`) working before the getter has ever run. Detached
|
|
2039
|
+
// use (`const f = v.validate`) keeps working because the closure binds the
|
|
2040
|
+
// instance.
|
|
2041
|
+
// Standard Schema V1. Built on first read, then pinned to the instance with
|
|
2042
|
+
// the same descriptor the constructor used to install eagerly.
|
|
2043
|
+
Object.defineProperty(Validator.prototype, "~standard", {
|
|
2044
|
+
configurable: true,
|
|
2045
|
+
get() {
|
|
2046
|
+
const self = this;
|
|
2047
|
+
const std = Object.freeze({
|
|
2048
|
+
version: 1,
|
|
2049
|
+
vendor: "ata-validator",
|
|
2050
|
+
validate(value) {
|
|
2051
|
+
const result = self.validate(value);
|
|
2052
|
+
if (result.valid) {
|
|
2053
|
+
return { value };
|
|
2054
|
+
}
|
|
2055
|
+
// An issue carries a message and a path and nothing else, so the
|
|
2056
|
+
// suggestion and source-frame work the rich error path does would be
|
|
2057
|
+
// thrown away here. Take the raw, schema-ordered list when the result
|
|
2058
|
+
// offers one; fall back to the public list otherwise.
|
|
2059
|
+
const raw = typeof result._ataRaw === 'function' ? result._ataRaw() : result.errors;
|
|
2060
|
+
const issues = new Array(raw.length);
|
|
2061
|
+
for (let i = 0; i < raw.length; i++) {
|
|
2062
|
+
const err = raw[i];
|
|
2063
|
+
const path = err.instancePath != null ? err.instancePath : (err.path || '');
|
|
2064
|
+
let message = err.message;
|
|
2065
|
+
if (!message) {
|
|
2066
|
+
// The native engine reports numeric codes without a message; the
|
|
2067
|
+
// enrich pass knows how to word those. Rare, so required lazily.
|
|
2068
|
+
message = require('./lib/enrich-error').enrich(err, {}).message;
|
|
2069
|
+
}
|
|
2070
|
+
issues[i] = { message, path: parsePointerPath(path) };
|
|
2071
|
+
}
|
|
2072
|
+
return { issues };
|
|
2073
|
+
},
|
|
2074
|
+
});
|
|
2075
|
+
Object.defineProperty(this, "~standard", {
|
|
2076
|
+
value: std,
|
|
2077
|
+
writable: false,
|
|
2078
|
+
enumerable: false,
|
|
2079
|
+
configurable: false,
|
|
2080
|
+
});
|
|
2081
|
+
return std;
|
|
2082
|
+
},
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
function _defineLazyMethod(name, maker) {
|
|
2086
|
+
Object.defineProperty(Validator.prototype, name, {
|
|
2087
|
+
configurable: true,
|
|
2088
|
+
get() {
|
|
2089
|
+
const fn = maker(this);
|
|
2090
|
+
Object.defineProperty(this, name, { value: fn, writable: true, configurable: true, enumerable: true });
|
|
2091
|
+
return fn;
|
|
2092
|
+
},
|
|
2093
|
+
set(fn) {
|
|
2094
|
+
Object.defineProperty(this, name, { value: fn, writable: true, configurable: true, enumerable: true });
|
|
2095
|
+
},
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
_defineLazyMethod('validate', (self) => (data) => {
|
|
2100
|
+
self._ensureCompiled();
|
|
2101
|
+
return self.validate(data);
|
|
2102
|
+
});
|
|
2103
|
+
_defineLazyMethod('isValidObject', (self) => (data) => {
|
|
2104
|
+
// A validator that rewrites its input goes through the full compile, which
|
|
2105
|
+
// binds a verdict method that runs the rewrite first.
|
|
2106
|
+
if (self._needsPreprocess()) {
|
|
2107
|
+
self._ensureCompiled();
|
|
2108
|
+
return self.isValidObject(data);
|
|
2109
|
+
}
|
|
2110
|
+
// Lazy: classify + build tier 0 plan on first call, not in constructor.
|
|
2111
|
+
const _tier = classify(self._schemaObj);
|
|
2112
|
+
if (_tier.tier === 0) {
|
|
2113
|
+
const _plan = buildTier0Plan(self._schemaObj);
|
|
2114
|
+
let _n = 0;
|
|
2115
|
+
self.isValidObject = (d) => {
|
|
2116
|
+
const r = tier0Validate(_plan, d);
|
|
2117
|
+
if (++_n === 2) {
|
|
2118
|
+
try { self._ensureCodegen(); } catch {}
|
|
2119
|
+
}
|
|
2120
|
+
return r;
|
|
2121
|
+
};
|
|
2122
|
+
} else {
|
|
2123
|
+
self._ensureCodegen();
|
|
2124
|
+
// Codegen can bail on shapes it cannot represent; the full compile
|
|
2125
|
+
// binds the native path or the unsupported thrower instead of
|
|
2126
|
+
// leaving this stub to re-dispatch to itself.
|
|
2127
|
+
if (!self._jsFn) self._ensureCompiled();
|
|
2128
|
+
}
|
|
2129
|
+
return self.isValidObject(data);
|
|
2130
|
+
});
|
|
2131
|
+
_defineLazyMethod('validateJSON', (self) => (jsonStr) => {
|
|
2132
|
+
self._ensureCompiled();
|
|
2133
|
+
return self.validateJSON(jsonStr);
|
|
2134
|
+
});
|
|
2135
|
+
_defineLazyMethod('isValidJSON', (self) => (jsonStr) => {
|
|
2136
|
+
self._ensureCompiled();
|
|
2137
|
+
return self.isValidJSON(jsonStr);
|
|
2138
|
+
});
|
|
2139
|
+
_defineLazyMethod('validateAndParse', (self) => (jsonStr) => {
|
|
2140
|
+
if (!native) throw new Error('Native addon required for validateAndParse()');
|
|
2141
|
+
self._ensureCompiled();
|
|
2142
|
+
return self.validateAndParse(jsonStr);
|
|
2143
|
+
});
|
|
2144
|
+
_defineLazyMethod('isValid', (self) => (buf) => {
|
|
2145
|
+
if (!native) throw new Error('Native addon required for isValid() — use validate() or isValidObject() instead');
|
|
2146
|
+
self._ensureCompiled();
|
|
2147
|
+
return self.isValid(buf);
|
|
2148
|
+
});
|
|
2149
|
+
_defineLazyMethod('countValid', (self) => (ndjsonBuf) => {
|
|
2150
|
+
if (!native) throw new Error('Native addon required for countValid()');
|
|
2151
|
+
self._ensureCompiled();
|
|
2152
|
+
return self.countValid(ndjsonBuf);
|
|
2153
|
+
});
|
|
2154
|
+
_defineLazyMethod('batchIsValid', (self) => (buffers) => {
|
|
2155
|
+
if (!native) throw new Error('Native addon required for batchIsValid()');
|
|
2156
|
+
self._ensureCompiled();
|
|
2157
|
+
return self.batchIsValid(buffers);
|
|
2158
|
+
});
|
|
2159
|
+
|
|
2035
2160
|
module.exports = {
|
|
2036
2161
|
Validator,
|
|
2037
2162
|
compile,
|
package/lib/enrich-error.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { CODES, codeFor } = require('./error-codes');
|
|
3
|
+
const { CODES, codeFor, fromNative } = require('./error-codes');
|
|
4
4
|
const { suggestFor } = require('./suggestions');
|
|
5
5
|
|
|
6
6
|
const DOC_BASE = 'https://ata-validator.com/e/';
|
|
@@ -130,12 +130,19 @@ function rankFor (keyword) {
|
|
|
130
130
|
function enrich (rawErr, opts) {
|
|
131
131
|
const data = opts && opts.data;
|
|
132
132
|
const positions = opts && opts.positions;
|
|
133
|
-
const keyword = rawErr.keyword;
|
|
134
133
|
const format = rawErr.params && rawErr.params.format;
|
|
134
|
+
// The native engine reports its own enum as a number. Translate it to the
|
|
135
|
+
// keyword and public code the JavaScript engines use, so an error means the
|
|
136
|
+
// same thing whichever engine produced it.
|
|
137
|
+
const fromAddon = typeof rawErr.code === 'number' ? fromNative(rawErr.code, format) : null;
|
|
138
|
+
const keyword = rawErr.keyword || (fromAddon && fromAddon.keyword);
|
|
135
139
|
// Prefer a code the codegen already attached (e.g. branch-collapse emits
|
|
136
140
|
// ATA4001/4002/4003 distinguishing zero/multi/anyOf failure modes). The
|
|
137
141
|
// keyword-derived lookup only finds the first match for `keyword: 'oneOf'`.
|
|
138
|
-
const code =
|
|
142
|
+
const code = (fromAddon && fromAddon.code) ||
|
|
143
|
+
(typeof rawErr.code === 'string' && rawErr.code) ||
|
|
144
|
+
codeFor(keyword, format) ||
|
|
145
|
+
'ATA9001';
|
|
139
146
|
const meta = CODES[code];
|
|
140
147
|
const path = rawErr.instancePath != null ? rawErr.instancePath : (rawErr.path || '');
|
|
141
148
|
|
package/lib/error-codes.js
CHANGED
|
@@ -100,4 +100,57 @@ function codeFor (keyword, format) {
|
|
|
100
100
|
return hit === undefined ? null : hit;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
|
|
104
|
+
// The native engine reports its own `error_code` enum from include/ata.h as an
|
|
105
|
+
// ordinal. Nothing outside the addon should ever see one: an error that came
|
|
106
|
+
// from there carries the same public code and keyword as the same failure from
|
|
107
|
+
// the JavaScript engines. Ordinals are positional, so this table follows the
|
|
108
|
+
// enum's declaration order and `tests/test_native_error_codes.js` fails if the
|
|
109
|
+
// two drift apart.
|
|
110
|
+
const NATIVE_KEYWORDS = [
|
|
111
|
+
null, // 0 ok
|
|
112
|
+
'__parse__', // 1 invalid_json
|
|
113
|
+
'__compile__', // 2 invalid_schema
|
|
114
|
+
'type', // 3 type_mismatch
|
|
115
|
+
'required', // 4 required_property_missing
|
|
116
|
+
'additionalProperties', // 5 additional_property_not_allowed
|
|
117
|
+
'enum', // 6 enum_mismatch
|
|
118
|
+
'const', // 7 const_mismatch
|
|
119
|
+
'minimum', // 8 minimum_violation
|
|
120
|
+
'maximum', // 9 maximum_violation
|
|
121
|
+
'exclusiveMinimum', // 10 exclusive_minimum_violation
|
|
122
|
+
'exclusiveMaximum', // 11 exclusive_maximum_violation
|
|
123
|
+
'minLength', // 12 min_length_violation
|
|
124
|
+
'maxLength', // 13 max_length_violation
|
|
125
|
+
'pattern', // 14 pattern_mismatch
|
|
126
|
+
'format', // 15 format_mismatch
|
|
127
|
+
'minItems', // 16 min_items_violation
|
|
128
|
+
'maxItems', // 17 max_items_violation
|
|
129
|
+
'uniqueItems', // 18 unique_items_violation
|
|
130
|
+
'minProperties', // 19 min_properties_violation
|
|
131
|
+
'maxProperties', // 20 max_properties_violation
|
|
132
|
+
'multipleOf', // 21 multiple_of_violation
|
|
133
|
+
'allOf', // 22 all_of_failed
|
|
134
|
+
'anyOf', // 23 any_of_failed
|
|
135
|
+
'oneOf', // 24 one_of_failed
|
|
136
|
+
'not', // 25 not_failed
|
|
137
|
+
'$ref', // 26 ref_not_found
|
|
138
|
+
'if', // 27 if_then_else_failed
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
// Keyword and public code for a native ordinal, or null when the ordinal is
|
|
142
|
+
// outside the enum, which would mean the addon is newer than this table.
|
|
143
|
+
function fromNative (ordinal, format) {
|
|
144
|
+
if (typeof ordinal !== 'number' || !Number.isInteger(ordinal)) return null
|
|
145
|
+
const keyword = NATIVE_KEYWORDS[ordinal]
|
|
146
|
+
if (!keyword) return null
|
|
147
|
+
// codeFor('format') answers with the first format code, which names a
|
|
148
|
+
// specific format. Without knowing which format failed, the generic one is
|
|
149
|
+
// the honest answer.
|
|
150
|
+
const code = keyword === 'format'
|
|
151
|
+
? (format ? codeFor('format', format) || 'ATA3099' : 'ATA3099')
|
|
152
|
+
: codeFor(keyword) || 'ATA9001'
|
|
153
|
+
return { keyword, code }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { CODES, get, all, codeFor, fromNative, NATIVE_KEYWORDS };
|