ata-validator 1.58.1 → 1.60.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 +5 -3
- package/codegen.js +2 -1
- package/index.js +13 -1
- package/lib/aot-impl.js +59 -7
- package/lib/compiled.js +6 -0
- package/lib/standard-schema.js +145 -0
- package/lib/validator-core.js +22 -144
- package/lib/version.js +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -661,9 +661,11 @@ through a `setFormats()` export the module carries. `docs/API.md` has the
|
|
|
661
661
|
details.
|
|
662
662
|
|
|
663
663
|
**Fastify startup, 10 route schemas, from a cold process to the first validated request:
|
|
664
|
-
ajv
|
|
665
|
-
|
|
666
|
-
|
|
664
|
+
ajv 30.3 ms, ata 9.7 ms, no build step required** (Node 25, ajv 8.20.0, loading each
|
|
665
|
+
library included). ata loads in 3.7 ms, registers the routes in 0.3 ms and compiles on the
|
|
666
|
+
first request, so counting only registration would overstate the gap. The first request
|
|
667
|
+
here comes in the same tick as setup; a server that listens first has the code generator
|
|
668
|
+
loaded by then. Reproduce with `node benchmark/bench_fastify_boot.mjs`.
|
|
667
669
|
|
|
668
670
|
### Standard Schema V1
|
|
669
671
|
|
package/codegen.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// loads it on demand, once a validator gets hot; importing this once at
|
|
5
5
|
// startup has every schema compiled to generated code from its first call,
|
|
6
6
|
// and makes parse() and the ahead-of-time bundle methods available in the
|
|
7
|
-
// browser. On Node, ata-validator
|
|
7
|
+
// browser. On Node, ata-validator loads it when a validator first compiles or
|
|
8
|
+
// once the event loop is free; importing this loads it now.
|
|
8
9
|
const core = require('./lib/validator-core.js');
|
|
9
10
|
if (!core._codegenRegistered()) core._registerCodegen(require('./lib/codegen-engine.js'));
|
|
10
11
|
module.exports = {};
|
package/index.js
CHANGED
|
@@ -6,7 +6,19 @@
|
|
|
6
6
|
// lib/codegen-engine.js, so that the edge entry (index.edge.js) and
|
|
7
7
|
// ata-validator/lite can load the core without it.
|
|
8
8
|
const core = require('./lib/validator-core.js');
|
|
9
|
-
|
|
9
|
+
// The code generator loads on first need, or once the event loop is free,
|
|
10
|
+
// not with the package: it is most of what requiring the package parsed. The
|
|
11
|
+
// timer is unref'd, so a script that has finished does not stay to load it.
|
|
12
|
+
// The loader lives here rather than in the core, which the browser build
|
|
13
|
+
// shares and which has no use for it there.
|
|
14
|
+
let engine = null;
|
|
15
|
+
const loadCodegen = () => {
|
|
16
|
+
if (engine === null) core._registerCodegen(engine = require('./lib/codegen-engine.js'));
|
|
17
|
+
return engine;
|
|
18
|
+
};
|
|
19
|
+
core._setCodegenLoader(loadCodegen);
|
|
20
|
+
const warm = setTimeout(loadCodegen, 0);
|
|
21
|
+
if (typeof warm === 'object' && typeof warm.unref === 'function') warm.unref();
|
|
10
22
|
|
|
11
23
|
// The public names. lib/public-api.js holds the same for index.edge.js; they
|
|
12
24
|
// are written out here rather than required so that loading the package in
|
package/lib/aot-impl.js
CHANGED
|
@@ -371,6 +371,30 @@ function defaultsDecl(validator) {
|
|
|
371
371
|
`let _ataFill;\nfunction applyDefaults(d) { (_ataFill || (_ataFill = _ataDefaults(JSON.parse(${text}))))(d); }\n`;
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
// What a bundled validator runs on the data before it checks it, for the
|
|
375
|
+
// options the caller of bundleStandalone named: the runtime's pass for
|
|
376
|
+
// `coerceTypes`, `removeAdditional` and `useDefaults: true`, as one generated
|
|
377
|
+
// function (preprocessSource), in the runtime's order. A bundle used to leave
|
|
378
|
+
// coerceTypes and removeAdditional out without a word, and then rejected what
|
|
379
|
+
// the runtime accepts. Where the generated pass declines and the runtime
|
|
380
|
+
// would change the data with its closure pass, there is nothing to carry it
|
|
381
|
+
// in, and the bundle refuses. `useDefaults` alone runs applyDefaults.
|
|
382
|
+
// Returns { decl, call }, the call naming the data parameter `arg`.
|
|
383
|
+
function bundlePreprocess(validator, opts, fillDecl, label, arg = 'd') {
|
|
384
|
+
const coerceTypes = opts && opts.coerceTypes ? opts.coerceTypes : false;
|
|
385
|
+
const removeAdditional = opts && opts.removeAdditional ? opts.removeAdditional : false;
|
|
386
|
+
const useDefaults = !!(opts && opts.useDefaults === true);
|
|
387
|
+
if (!coerceTypes && !removeAdditional) return { decl: '', call: useDefaults && fillDecl ? `applyDefaults(${arg});` : '' };
|
|
388
|
+
const internals = require('./validator-core')._internals;
|
|
389
|
+
const schema = internals.resolveSchemaForPreprocess(validator._schemaObj, validator._schemaMap);
|
|
390
|
+
let src = null;
|
|
391
|
+
try { src = require('./codegen-engine').preprocessSource(schema, { coerceTypes, removeAdditional, useDefaults }); } catch { src = null; }
|
|
392
|
+
if (src !== null) return { decl: `function _ataPre(d) {\n${src}\n}\n`, call: `_ataPre(${arg});` };
|
|
393
|
+
const changes = (coerceTypes && internals.buildCoercer(schema) !== null) || (removeAdditional && internals.buildRemover(schema) !== null);
|
|
394
|
+
if (changes) throw new Error(`${label}: ${coerceTypes ? 'coerceTypes' : 'removeAdditional'} cannot be compiled into a standalone module for this schema; validate with the runtime Validator`);
|
|
395
|
+
return { decl: '', call: useDefaults && fillDecl ? `applyDefaults(${arg});` : '' };
|
|
396
|
+
}
|
|
397
|
+
|
|
374
398
|
function toStandaloneModule(validator, opts) {
|
|
375
399
|
assertEmittable(validator, 'toStandaloneModule');
|
|
376
400
|
validator._ensureCompiled();
|
|
@@ -384,6 +408,12 @@ function toStandaloneModule(validator, opts) {
|
|
|
384
408
|
const source = !!(opts && opts.source);
|
|
385
409
|
const sourceMap = opts && opts.sourceMap ? opts.sourceMap : null;
|
|
386
410
|
const schemaFile = opts && opts.schemaFile ? opts.schemaFile : null;
|
|
411
|
+
// The validator's own coerceTypes and removeAdditional run before every
|
|
412
|
+
// check, as they do in the validator; a module used to leave them out and
|
|
413
|
+
// reject what the validator accepts. Defaults are not filled here: a module
|
|
414
|
+
// exports applyDefaults for that, and validate() leaves the data as it is.
|
|
415
|
+
const vopts = validator._options || {};
|
|
416
|
+
const pre = bundlePreprocess(validator, { coerceTypes: vopts.coerceTypes, removeAdditional: vopts.removeAdditional }, '', 'toStandaloneModule', 'data');
|
|
387
417
|
const src = jsFn._source;
|
|
388
418
|
|
|
389
419
|
let errCore = '';
|
|
@@ -612,7 +642,7 @@ function toStandaloneModule(validator, opts) {
|
|
|
612
642
|
// reading patterns. A second declaration of that name shadowed it and the
|
|
613
643
|
// prelude called this one instead, reaching _fn before its initialiser ran.
|
|
614
644
|
const parseCore = cloneExpr
|
|
615
|
-
? `function _ataParse(data) {\n if (!_fn(data)) { const e = new Error('validation failed'); e.name = 'AtaValidationError'; throw e; }\n return ${cloneExpr};\n}\n`
|
|
645
|
+
? `function _ataParse(data) {\n ${pre.call}if (!_fn(data)) { const e = new Error('validation failed'); e.name = 'AtaValidationError'; throw e; }\n return ${cloneExpr};\n}\n`
|
|
616
646
|
: '';
|
|
617
647
|
|
|
618
648
|
// validateJSON(): parse the text, validate, and on failure attach a
|
|
@@ -701,8 +731,8 @@ const ABORT = Object.freeze({
|
|
|
701
731
|
${closureDecls}${sharedBlock}${preambleDecls}${formatDecls}const _fn = function(d) {
|
|
702
732
|
${src}
|
|
703
733
|
};
|
|
704
|
-
${errCore}${orderDecl}${compactCore}function isValid(data) { return _fn(data); }
|
|
705
|
-
function validate(data) { ${validBody}; }
|
|
734
|
+
${errCore}${orderDecl}${compactCore}${pre.decl}function isValid(data) { ${pre.call}return _fn(data); }
|
|
735
|
+
function validate(data) { ${pre.call}${validBody}; }
|
|
706
736
|
${parseCore}${positionsCore}${fillDecl}${hashDecl}${exports}`;
|
|
707
737
|
}
|
|
708
738
|
|
|
@@ -912,6 +942,12 @@ function bundle(Validator, schemas, opts) {
|
|
|
912
942
|
// Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
|
|
913
943
|
// opts.format: 'cjs' (default) or 'esm'.
|
|
914
944
|
// opts.formats: { name: fn } — embedded in the output via Function#toString.
|
|
945
|
+
// opts.order: true hands each validator's errors out in the runtime's order,
|
|
946
|
+
// as toStandaloneModule does (lib/aot-order.js).
|
|
947
|
+
// Each validator of a schema with a default carries `applyDefaults(data)`, the
|
|
948
|
+
// runtime's defaults pass, as a property; validate does not run it unless
|
|
949
|
+
// `useDefaults: true` is passed. `coerceTypes` and `removeAdditional` run
|
|
950
|
+
// before the check, as at runtime (bundlePreprocess).
|
|
915
951
|
function bundleStandalone(Validator, schemas, opts) {
|
|
916
952
|
// A custom keyword is a function, and a standalone module imports nothing,
|
|
917
953
|
// so there is no way to carry it. Refuse rather than emit a module that
|
|
@@ -930,11 +966,14 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
930
966
|
// Pass one compiles; pass two emits. The helpers that several schemas hoist
|
|
931
967
|
// are the same text declaring the same name, so the bundle keeps one copy at
|
|
932
968
|
// module scope, and pass two needs to know the set before it writes anything.
|
|
969
|
+
const ordered = !!(opts && opts.order === true);
|
|
970
|
+
const validatorOpts = ordered ? Object.fromEntries(Object.entries(bundleOpts).filter(([k]) => k !== 'order')) : bundleOpts;
|
|
933
971
|
const compiledEntries = schemas.map((schema) => {
|
|
934
|
-
const v = new Validator(schema,
|
|
972
|
+
const v = new Validator(schema, validatorOpts);
|
|
935
973
|
v._ensureCompiled();
|
|
936
974
|
const jsFn = v._jsFn;
|
|
937
975
|
if (!jsFn || !jsFn._hybridSource) return null;
|
|
976
|
+
const noOrdinal = !ordered || !require(AOT_ORDER_MODULE).wantsOrdinals(v._schemaObj);
|
|
938
977
|
// The schema the validator compiled, not the one the caller passed. They
|
|
939
978
|
// differ whenever anything prepared the schema: draft-07 normalization,
|
|
940
979
|
// `format` removed under assertFormat: false, keywords outside the
|
|
@@ -944,7 +983,7 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
944
983
|
v._schemaObj,
|
|
945
984
|
v._schemaMap,
|
|
946
985
|
v._userFormats,
|
|
947
|
-
{ noOrdinal
|
|
986
|
+
{ noOrdinal },
|
|
948
987
|
);
|
|
949
988
|
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
950
989
|
return { v, jsFn, jsErrFn };
|
|
@@ -990,12 +1029,25 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
990
1029
|
if (preambleSrc) preamble = preamble ? `${preamble}\n${preambleSrc}` : preambleSrc;
|
|
991
1030
|
const closureSrc = closureDeclLines(jsFn, 'var').join('\n');
|
|
992
1031
|
if (closureSrc) preamble = preamble ? `${preamble}\n${closureSrc}\n` : closureSrc + '\n';
|
|
1032
|
+
// The runtime's passes over the data, for the options asked for, and its
|
|
1033
|
+
// defaults pass as the validator's applyDefaults.
|
|
1034
|
+
const fillDecl = defaultsDecl(v);
|
|
1035
|
+
const pre = bundlePreprocess(v, opts, fillDecl, 'bundleStandalone');
|
|
1036
|
+
preamble = preamble ? `${preamble}\n${fillDecl}${pre.decl}` : fillDecl + pre.decl;
|
|
1037
|
+
const ret = (body) => fillDecl
|
|
1038
|
+
? `var _f=function(d){${pre.call}${body}};_f.applyDefaults=applyDefaults;return _f`
|
|
1039
|
+
: `return function(d){${pre.call}${body}}`;
|
|
1040
|
+
// In order: the collector's errors sorted as the runtime sorts them.
|
|
1041
|
+
const hasErr = !!(jsErrFn && jsErrFn._errSource);
|
|
1042
|
+
const errDecl = ordered && hasErr
|
|
1043
|
+
? `${require(AOT_ORDER_MODULE).orderDecl(v._schemaObj)}var E=function(d){var r=(function(d){var _all=true;${errBody}})(d);return r&&r.errors?{valid:false,errors:_ataOrder(r.errors)}:r};`
|
|
1044
|
+
: `var E=function(d){var _all=true;${errBody}};`;
|
|
993
1045
|
if (opts && opts.verbose) {
|
|
994
1046
|
// Embed the schema and a small resolver so errors carry parentSchema.
|
|
995
1047
|
const schemaLit = JSON.stringify(v._schemaObj);
|
|
996
|
-
return `(function(R){${preamble}var _S=${schemaLit};function _PS(p){if(!p||p[0]!=='#')return undefined;var s=p.slice(1);if(!s)return _S;var ps=s.split('/').filter(Boolean).map(function(x){return x.replace(/~1/g,'/').replace(/~0/g,'~')});var t=_S;for(var i=0;i<ps.length-1;i++){if(t==null||typeof t!=='object')return undefined;t=t[ps[i]]}return t}
|
|
1048
|
+
return `(function(R){${preamble}var _S=${schemaLit};function _PS(p){if(!p||p[0]!=='#')return undefined;var s=p.slice(1);if(!s)return _S;var ps=s.split('/').filter(Boolean).map(function(x){return x.replace(/~1/g,'/').replace(/~0/g,'~')});var t=_S;for(var i=0;i<ps.length-1;i++){if(t==null||typeof t!=='object')return undefined;t=t[ps[i]]}return t}${errDecl}var _v=function(d){${jsFn._hybridSource}};${ret('var r=_v(d);if(r&&r.valid===false&&r.errors){var es=[];for(var i=0;i<r.errors.length;i++){var e=r.errors[i];es.push(Object.assign({},e,{parentSchema:_PS(e.schemaPath)}))}return{valid:false,errors:es}}return r')}})(R)`;
|
|
997
1049
|
}
|
|
998
|
-
return `(function(R){${preamble}
|
|
1050
|
+
return `(function(R){${preamble}${errDecl}${ret(jsFn._hybridSource)}})(R)`;
|
|
999
1051
|
});
|
|
1000
1052
|
const arr = `[${fns.join(',')}]`;
|
|
1001
1053
|
// The code-point counter the length checks call. toStandaloneModule embeds
|
package/lib/compiled.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
const { LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy } = require('./rejections');
|
|
19
19
|
const { COMPILED_OPTIONS, prepare, parse, installExtension, extendJSON, EXT_FALLBACK } = require('./compiled-common');
|
|
20
|
+
const { installStandard } = require('./standard-schema');
|
|
20
21
|
|
|
21
22
|
const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
22
23
|
|
|
@@ -99,6 +100,11 @@ function fromCompiled(mod, schema, options) {
|
|
|
99
100
|
isValidJSON: json.isValidJSON,
|
|
100
101
|
};
|
|
101
102
|
});
|
|
103
|
+
// Standard Schema V1, as the runtime Validator hands it out, so a validator
|
|
104
|
+
// the build compiled away still works where the runtime one did (a form
|
|
105
|
+
// library, a router, an RPC layer). The schema it publishes is the one the
|
|
106
|
+
// module was built from, as the runtime reads it.
|
|
107
|
+
installStandard(api, () => schema, (self, value) => self.validate(value));
|
|
102
108
|
return api;
|
|
103
109
|
}
|
|
104
110
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Standard Schema V1 for a validator: the `~standard` member a form library,
|
|
4
|
+
// a router or an RPC layer reads instead of ata's own API. One module, so the
|
|
5
|
+
// runtime Validator (lib/validator-core.js) and the wrapper around a module
|
|
6
|
+
// written ahead of time (lib/compiled.js) hand out the same object built the
|
|
7
|
+
// same way, and a validator the build compiled away still works where the
|
|
8
|
+
// runtime one did.
|
|
9
|
+
|
|
10
|
+
// Paths repeat across rejections of the same shape, so the parsed segment
|
|
11
|
+
// list is cached per path string. Entries are frozen: the same array is
|
|
12
|
+
// handed to every issue that names the path.
|
|
13
|
+
//
|
|
14
|
+
// The cache is bucketed by length and searched with ===, not keyed in a Map.
|
|
15
|
+
// A path with an array index in it is concatenated afresh by every rejection,
|
|
16
|
+
// and a Map has to hash each such string from scratch before it can look
|
|
17
|
+
// anything up; that hashing was 30% of a Standard Schema rejection with
|
|
18
|
+
// sixteen issues. Comparing against the few paths of the same length costs
|
|
19
|
+
// less. Both levels are bounded so a stream of array indexes cannot grow it
|
|
20
|
+
// without limit: a full bucket stops caching, too many lengths clear it.
|
|
21
|
+
const _pathBuckets = new Map();
|
|
22
|
+
const PATH_BUCKET_MAX = 32;
|
|
23
|
+
const PATH_LENGTHS_MAX = 256;
|
|
24
|
+
const EMPTY_PATH = Object.freeze([]);
|
|
25
|
+
function parsePointerPath(path) {
|
|
26
|
+
if (!path) return EMPTY_PATH;
|
|
27
|
+
const n = path.length;
|
|
28
|
+
let bucket = _pathBuckets.get(n);
|
|
29
|
+
if (bucket !== undefined) {
|
|
30
|
+
for (let i = 0; i < bucket.length; i += 2) if (bucket[i] === path) return bucket[i + 1];
|
|
31
|
+
} else {
|
|
32
|
+
if (_pathBuckets.size >= PATH_LENGTHS_MAX) _pathBuckets.clear();
|
|
33
|
+
bucket = [];
|
|
34
|
+
_pathBuckets.set(n, bucket);
|
|
35
|
+
}
|
|
36
|
+
const segs = Object.freeze(parsePointerPathUncached(path));
|
|
37
|
+
if (bucket.length < PATH_BUCKET_MAX * 2) bucket.push(path, segs);
|
|
38
|
+
return segs;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parsePointerPathUncached(path) {
|
|
42
|
+
// One pass, no intermediate arrays. Per Standard Schema V1 an array index
|
|
43
|
+
// is emitted as a number and an object key as a string; a segment is an
|
|
44
|
+
// index when it is all digits with no leading zero.
|
|
45
|
+
const out = [];
|
|
46
|
+
const n = path.length;
|
|
47
|
+
let start = 1;
|
|
48
|
+
for (let i = 1; i <= n; i++) {
|
|
49
|
+
if (i !== n && path.charCodeAt(i) !== 47) continue;
|
|
50
|
+
if (i > start) {
|
|
51
|
+
let seg = path.slice(start, i);
|
|
52
|
+
if (seg.indexOf('~') >= 0) seg = seg.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
53
|
+
const c0 = seg.charCodeAt(0);
|
|
54
|
+
let numeric = c0 >= 48 && c0 <= 57 && (seg.length === 1 || c0 !== 48);
|
|
55
|
+
if (numeric) {
|
|
56
|
+
for (let k = 1; k < seg.length; k++) {
|
|
57
|
+
const c = seg.charCodeAt(k);
|
|
58
|
+
if (c < 48 || c > 57) { numeric = false; break; }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
out.push({ key: numeric ? Number(seg) : seg });
|
|
62
|
+
}
|
|
63
|
+
start = i + 1;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Standard JSON Schema, the `jsonSchema` member of `~standard`: what a consumer
|
|
69
|
+
// such as the MCP SDK reads to publish a schema, from `input({ target })`.
|
|
70
|
+
// ata validates the JSON Schema it was given and does not transform values, so
|
|
71
|
+
// input and output are the same document, handed back as a copy when the
|
|
72
|
+
// target is the schema's dialect: draft-07 when it declares draft-07, 2020-12
|
|
73
|
+
// when it declares 2020-12 or nothing, as ata reads it. Any other target
|
|
74
|
+
// throws, as the specification asks, rather than returning a document that
|
|
75
|
+
// would mean something else there.
|
|
76
|
+
function standardJsonSchema(getSchema) {
|
|
77
|
+
const convert = (options) => {
|
|
78
|
+
const target = options && options.target;
|
|
79
|
+
const schema = getSchema();
|
|
80
|
+
if (schema === true) return {};
|
|
81
|
+
if (schema === false) return { not: {} };
|
|
82
|
+
const declared = typeof schema.$schema === 'string' ? schema.$schema : '';
|
|
83
|
+
const dialect = declared === '' || declared.includes('draft/2020-12/schema') ? 'draft-2020-12'
|
|
84
|
+
: declared.includes('draft-07/schema') ? 'draft-07' : declared;
|
|
85
|
+
if (target !== dialect) {
|
|
86
|
+
throw new TypeError(`ata-validator cannot provide a ${dialect} schema as ${String(target)}; it does not convert between dialects`);
|
|
87
|
+
}
|
|
88
|
+
return JSON.parse(JSON.stringify(schema));
|
|
89
|
+
};
|
|
90
|
+
return Object.freeze({ input: convert, output: convert });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The `~standard` object for `self`, a validator whose validate(value)
|
|
94
|
+
// answers as ata's does: `getSchema(self)` returns the schema as the caller
|
|
95
|
+
// wrote it, `validate(self, value)` is called each time, so an extension
|
|
96
|
+
// installed later counts.
|
|
97
|
+
function standardSchemaFor(self, getSchema, validate) {
|
|
98
|
+
return Object.freeze({
|
|
99
|
+
version: 1,
|
|
100
|
+
vendor: 'ata-validator',
|
|
101
|
+
jsonSchema: standardJsonSchema(() => getSchema(self)),
|
|
102
|
+
validate(value) {
|
|
103
|
+
const result = validate(self, value);
|
|
104
|
+
if (result.valid) {
|
|
105
|
+
return { value };
|
|
106
|
+
}
|
|
107
|
+
// An issue carries a message and a path and nothing else, so the
|
|
108
|
+
// suggestion and source-frame work the rich error path does would be
|
|
109
|
+
// thrown away here. Take the raw, schema-ordered list when the result
|
|
110
|
+
// offers one; fall back to the public list otherwise.
|
|
111
|
+
const raw = typeof result._ataRaw === 'function' ? result._ataRaw() : result.errors;
|
|
112
|
+
const issues = new Array(raw.length);
|
|
113
|
+
for (let i = 0; i < raw.length; i++) {
|
|
114
|
+
const err = raw[i];
|
|
115
|
+
const path = err.instancePath != null ? err.instancePath : (err.path || '');
|
|
116
|
+
let message = err.message;
|
|
117
|
+
if (!message) {
|
|
118
|
+
// The native engine reports numeric codes without a message; the
|
|
119
|
+
// enrich pass knows how to word those. Rare, so required lazily.
|
|
120
|
+
message = require('./enrich-error').enrich(err, {}).message;
|
|
121
|
+
}
|
|
122
|
+
issues[i] = { message, path: parsePointerPath(path) };
|
|
123
|
+
}
|
|
124
|
+
return { issues };
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Installs `~standard` on `target` (an instance, or a prototype) as a lazy
|
|
130
|
+
// accessor: built on first read for the object it is read from, then pinned
|
|
131
|
+
// to that object as a plain, non-enumerable value, so a validator nobody asks
|
|
132
|
+
// never builds it.
|
|
133
|
+
function installStandard(target, getSchema, validate) {
|
|
134
|
+
Object.defineProperty(target, '~standard', {
|
|
135
|
+
configurable: true,
|
|
136
|
+
enumerable: false,
|
|
137
|
+
get() {
|
|
138
|
+
const std = standardSchemaFor(this, getSchema, validate);
|
|
139
|
+
Object.defineProperty(this, '~standard', { value: std, writable: false, enumerable: false, configurable: false });
|
|
140
|
+
return std;
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { parsePointerPath, standardJsonSchema, standardSchemaFor, installStandard };
|
package/lib/validator-core.js
CHANGED
|
@@ -25,6 +25,9 @@ function _registerCodegen(engine) {
|
|
|
25
25
|
_codegen = engine;
|
|
26
26
|
({ compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined } = engine.jsCompiler);
|
|
27
27
|
}
|
|
28
|
+
// The code generator, loading it first where the entry loads it on need
|
|
29
|
+
// (index.js); everywhere else, whatever is registered.
|
|
30
|
+
let _loadCodegen = () => _codegen;
|
|
28
31
|
// The ahead-of-time loaders without the code generator: the edge entry
|
|
29
32
|
// (index.edge.js) registers lib/aot.browser.js here, so modules written by
|
|
30
33
|
// `ata build` or Validator.bundle() load where generated code cannot run.
|
|
@@ -219,7 +222,7 @@ function collectRemovals(schema, actions, path) {
|
|
|
219
222
|
// change within a realm and the probe itself is a code generation attempt.
|
|
220
223
|
let _codegenAvailable = null;
|
|
221
224
|
function codegenAvailable() {
|
|
222
|
-
if (
|
|
225
|
+
if (_loadCodegen() === null) return false;
|
|
223
226
|
if (_codegenAvailable === null) {
|
|
224
227
|
try {
|
|
225
228
|
_codegenAvailable = compileFunction('return 1')() === 1;
|
|
@@ -355,64 +358,6 @@ function _jsonSyntaxRejection(e) {
|
|
|
355
358
|
|
|
356
359
|
|
|
357
360
|
|
|
358
|
-
// Paths repeat across rejections of the same shape, so the parsed segment
|
|
359
|
-
// list is cached per path string. Entries are frozen: the same array is
|
|
360
|
-
// handed to every issue that names the path.
|
|
361
|
-
//
|
|
362
|
-
// The cache is bucketed by length and searched with ===, not keyed in a Map.
|
|
363
|
-
// A path with an array index in it is concatenated afresh by every rejection,
|
|
364
|
-
// and a Map has to hash each such string from scratch before it can look
|
|
365
|
-
// anything up; that hashing was 30% of a Standard Schema rejection with
|
|
366
|
-
// sixteen issues. Comparing against the few paths of the same length costs
|
|
367
|
-
// less. Both levels are bounded so a stream of array indexes cannot grow it
|
|
368
|
-
// without limit: a full bucket stops caching, too many lengths clear it.
|
|
369
|
-
const _pathBuckets = new Map();
|
|
370
|
-
const PATH_BUCKET_MAX = 32;
|
|
371
|
-
const PATH_LENGTHS_MAX = 256;
|
|
372
|
-
function parsePointerPath(path) {
|
|
373
|
-
if (!path) return EMPTY_PATH;
|
|
374
|
-
const n = path.length;
|
|
375
|
-
let bucket = _pathBuckets.get(n);
|
|
376
|
-
if (bucket !== undefined) {
|
|
377
|
-
for (let i = 0; i < bucket.length; i += 2) if (bucket[i] === path) return bucket[i + 1];
|
|
378
|
-
} else {
|
|
379
|
-
if (_pathBuckets.size >= PATH_LENGTHS_MAX) _pathBuckets.clear();
|
|
380
|
-
bucket = [];
|
|
381
|
-
_pathBuckets.set(n, bucket);
|
|
382
|
-
}
|
|
383
|
-
const segs = Object.freeze(parsePointerPathUncached(path));
|
|
384
|
-
if (bucket.length < PATH_BUCKET_MAX * 2) bucket.push(path, segs);
|
|
385
|
-
return segs;
|
|
386
|
-
}
|
|
387
|
-
const EMPTY_PATH = Object.freeze([]);
|
|
388
|
-
|
|
389
|
-
function parsePointerPathUncached(path) {
|
|
390
|
-
// One pass, no intermediate arrays. Per Standard Schema V1 an array index
|
|
391
|
-
// is emitted as a number and an object key as a string; a segment is an
|
|
392
|
-
// index when it is all digits with no leading zero.
|
|
393
|
-
const out = [];
|
|
394
|
-
const n = path.length;
|
|
395
|
-
let start = 1;
|
|
396
|
-
for (let i = 1; i <= n; i++) {
|
|
397
|
-
if (i !== n && path.charCodeAt(i) !== 47) continue;
|
|
398
|
-
if (i > start) {
|
|
399
|
-
let seg = path.slice(start, i);
|
|
400
|
-
if (seg.indexOf('~') >= 0) seg = seg.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
401
|
-
const c0 = seg.charCodeAt(0);
|
|
402
|
-
let numeric = c0 >= 48 && c0 <= 57 && (seg.length === 1 || c0 !== 48);
|
|
403
|
-
if (numeric) {
|
|
404
|
-
for (let k = 1; k < seg.length; k++) {
|
|
405
|
-
const c = seg.charCodeAt(k);
|
|
406
|
-
if (c < 48 || c > 57) { numeric = false; break; }
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
out.push({ key: numeric ? Number(seg) : seg });
|
|
410
|
-
}
|
|
411
|
-
start = i + 1;
|
|
412
|
-
}
|
|
413
|
-
return out;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
361
|
function createPaddedBuffer(jsonStr) {
|
|
417
362
|
if (typeof Buffer === 'undefined') throw new Error('createPaddedBuffer requires Node.js Buffer');
|
|
418
363
|
const jsonBuf = Buffer.from(jsonStr);
|
|
@@ -951,7 +896,7 @@ class Validator {
|
|
|
951
896
|
}
|
|
952
897
|
|
|
953
898
|
_pos() {
|
|
954
|
-
return this._posCache || (this._posCache = require('./data-position-cache').createCache(
|
|
899
|
+
return this._posCache || (this._posCache = require('./data-position-cache').createCache(_loadCodegen() !== null && _codegen.nativePositions ? _codegen.nativePositions() : null));
|
|
955
900
|
}
|
|
956
901
|
|
|
957
902
|
_ensureVocabularies() {
|
|
@@ -1102,7 +1047,7 @@ class Validator {
|
|
|
1102
1047
|
// pass looks for one. One reached through a reference into another
|
|
1103
1048
|
// document is walked, since its defaults are not in this text.
|
|
1104
1049
|
const mayHaveDefaults = preprocessSchema !== schemaObj || this._schemaStr.includes('"default"');
|
|
1105
|
-
if (!this._interpretOnly &&
|
|
1050
|
+
if (!this._interpretOnly && _loadCodegen() !== null) {
|
|
1106
1051
|
const cacheable = !this._userFormats && !this._usesKeywords;
|
|
1107
1052
|
const ppKey = cacheable ? mapKey + '\u0000' + String(options.coerceTypes) + '|' + (options.useDefaults !== false) + '|' + String(options.removeAdditional) : null;
|
|
1108
1053
|
const hit = ppKey !== null ? _preprocessCache.get(ppKey) : undefined;
|
|
@@ -1726,7 +1671,7 @@ class Validator {
|
|
|
1726
1671
|
return;
|
|
1727
1672
|
}
|
|
1728
1673
|
this._ensureVocabularies();
|
|
1729
|
-
if (this._interpretOnly ||
|
|
1674
|
+
if (this._interpretOnly || env('ATA_FORCE_NAPI') || _loadCodegen() === null) return;
|
|
1730
1675
|
_codegen.compileVerdict.call(this);
|
|
1731
1676
|
}
|
|
1732
1677
|
|
|
@@ -1736,7 +1681,7 @@ class Validator {
|
|
|
1736
1681
|
static fromStandalone(mod, schema, opts) {
|
|
1737
1682
|
// It loads what Validator.bundleStandalone() writes, and compiles an error
|
|
1738
1683
|
// function when the module carries none, so it goes with those methods.
|
|
1739
|
-
if (
|
|
1684
|
+
if (_loadCodegen() === null && _aotLoader === null && _tiering === null) throw new TypeError('Validator.fromStandalone is not part of ata-validator/lite, like the bundle methods it loads for. Import it from ata-validator, or import a module from `ata build` directly.');
|
|
1740
1685
|
const options = opts || {};
|
|
1741
1686
|
const schemaObj = typeof schema === "string" ? JSON.parse(schema) : schema;
|
|
1742
1687
|
|
|
@@ -1835,23 +1780,9 @@ class Validator {
|
|
|
1835
1780
|
}
|
|
1836
1781
|
};
|
|
1837
1782
|
|
|
1838
|
-
// Standard Schema V1
|
|
1783
|
+
// Standard Schema V1, the same object a Validator hands out.
|
|
1839
1784
|
Object.defineProperty(v, "~standard", {
|
|
1840
|
-
value:
|
|
1841
|
-
version: 1,
|
|
1842
|
-
vendor: "ata-validator",
|
|
1843
|
-
jsonSchema: standardJsonSchema(() => schemaObj),
|
|
1844
|
-
validate(value) {
|
|
1845
|
-
const result = v.validate(value);
|
|
1846
|
-
if (result.valid) return { value };
|
|
1847
|
-
return {
|
|
1848
|
-
issues: result.errors.map((e) => ({
|
|
1849
|
-
message: e.message,
|
|
1850
|
-
path: parsePointerPath(e.instancePath),
|
|
1851
|
-
})),
|
|
1852
|
-
};
|
|
1853
|
-
},
|
|
1854
|
-
}),
|
|
1785
|
+
value: require('./standard-schema').standardSchemaFor(v, () => schemaObj, (self, value) => self.validate(value)),
|
|
1855
1786
|
writable: false,
|
|
1856
1787
|
enumerable: false,
|
|
1857
1788
|
configurable: false,
|
|
@@ -1922,7 +1853,7 @@ function version() {
|
|
|
1922
1853
|
// generated module shapes; browser bundles get `lib/aot.browser.js` (a stub
|
|
1923
1854
|
// that throws) via the package.json `browser` field.
|
|
1924
1855
|
function _aot() {
|
|
1925
|
-
if (
|
|
1856
|
+
if (_loadCodegen() !== null) return _codegen.aot();
|
|
1926
1857
|
if (_aotLoader !== null) return _aotLoader();
|
|
1927
1858
|
// The browser build: emitters stay out of the page, as they did when the
|
|
1928
1859
|
// generator was in it (the package.json browser field maps lib/aot.js to
|
|
@@ -1991,74 +1922,19 @@ function defineSchema (schema) {
|
|
|
1991
1922
|
// bound closure, stores it on the instance as an ordinary writable property
|
|
1992
1923
|
// and returns it. The setter keeps the compile step's plain assignments
|
|
1993
1924
|
// (`this.validate = fn`) working before the getter has ever run. Detached
|
|
1994
|
-
// Standard JSON Schema, the `jsonSchema` member of `~standard`: what a consumer
|
|
1995
|
-
// such as the MCP SDK reads to publish a schema, from `input({ target })`.
|
|
1996
|
-
// ata validates the JSON Schema it was given and does not transform values, so
|
|
1997
|
-
// input and output are the same document, handed back as a copy when the
|
|
1998
|
-
// target is the schema's dialect: draft-07 when it declares draft-07, 2020-12
|
|
1999
|
-
// when it declares 2020-12 or nothing, as ata reads it. Any other target
|
|
2000
|
-
// throws, as the specification asks, rather than returning a document that
|
|
2001
|
-
// would mean something else there.
|
|
2002
|
-
function standardJsonSchema(getSchema) {
|
|
2003
|
-
const convert = (options) => {
|
|
2004
|
-
const target = options && options.target;
|
|
2005
|
-
const schema = getSchema();
|
|
2006
|
-
if (schema === true) return {};
|
|
2007
|
-
if (schema === false) return { not: {} };
|
|
2008
|
-
const declared = typeof schema.$schema === 'string' ? schema.$schema : '';
|
|
2009
|
-
const dialect = declared === '' || declared.includes('draft/2020-12/schema') ? 'draft-2020-12'
|
|
2010
|
-
: declared.includes('draft-07/schema') ? 'draft-07' : declared;
|
|
2011
|
-
if (target !== dialect) {
|
|
2012
|
-
throw new TypeError(`ata-validator cannot provide a ${dialect} schema as ${String(target)}; it does not convert between dialects`);
|
|
2013
|
-
}
|
|
2014
|
-
return JSON.parse(JSON.stringify(schema));
|
|
2015
|
-
};
|
|
2016
|
-
return Object.freeze({ input: convert, output: convert });
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
1925
|
// use (`const f = v.validate`) keeps working because the closure binds the
|
|
2020
1926
|
// instance.
|
|
2021
1927
|
// Standard Schema V1. Built on first read, then pinned to the instance with
|
|
2022
1928
|
// the same descriptor the constructor used to install eagerly.
|
|
1929
|
+
// Lazy on the prototype, pinned to the instance on first read; the object
|
|
1930
|
+
// comes from lib/standard-schema.js, shared with the wrapper around a module
|
|
1931
|
+
// written ahead of time, and loaded only when something reads `~standard`.
|
|
2023
1932
|
Object.defineProperty(Validator.prototype, "~standard", {
|
|
2024
1933
|
configurable: true,
|
|
1934
|
+
enumerable: false,
|
|
2025
1935
|
get() {
|
|
2026
|
-
const
|
|
2027
|
-
|
|
2028
|
-
version: 1,
|
|
2029
|
-
vendor: "ata-validator",
|
|
2030
|
-
jsonSchema: standardJsonSchema(() => self._rawSchema),
|
|
2031
|
-
validate(value) {
|
|
2032
|
-
const result = self.validate(value);
|
|
2033
|
-
if (result.valid) {
|
|
2034
|
-
return { value };
|
|
2035
|
-
}
|
|
2036
|
-
// An issue carries a message and a path and nothing else, so the
|
|
2037
|
-
// suggestion and source-frame work the rich error path does would be
|
|
2038
|
-
// thrown away here. Take the raw, schema-ordered list when the result
|
|
2039
|
-
// offers one; fall back to the public list otherwise.
|
|
2040
|
-
const raw = typeof result._ataRaw === 'function' ? result._ataRaw() : result.errors;
|
|
2041
|
-
const issues = new Array(raw.length);
|
|
2042
|
-
for (let i = 0; i < raw.length; i++) {
|
|
2043
|
-
const err = raw[i];
|
|
2044
|
-
const path = err.instancePath != null ? err.instancePath : (err.path || '');
|
|
2045
|
-
let message = err.message;
|
|
2046
|
-
if (!message) {
|
|
2047
|
-
// The native engine reports numeric codes without a message; the
|
|
2048
|
-
// enrich pass knows how to word those. Rare, so required lazily.
|
|
2049
|
-
message = require('./enrich-error').enrich(err, {}).message;
|
|
2050
|
-
}
|
|
2051
|
-
issues[i] = { message, path: parsePointerPath(path) };
|
|
2052
|
-
}
|
|
2053
|
-
return { issues };
|
|
2054
|
-
},
|
|
2055
|
-
});
|
|
2056
|
-
Object.defineProperty(this, "~standard", {
|
|
2057
|
-
value: std,
|
|
2058
|
-
writable: false,
|
|
2059
|
-
enumerable: false,
|
|
2060
|
-
configurable: false,
|
|
2061
|
-
});
|
|
1936
|
+
const std = require('./standard-schema').standardSchemaFor(this, (self) => self._rawSchema, (self, value) => self.validate(value));
|
|
1937
|
+
Object.defineProperty(this, "~standard", { value: std, writable: false, enumerable: false, configurable: false });
|
|
2062
1938
|
return std;
|
|
2063
1939
|
},
|
|
2064
1940
|
});
|
|
@@ -2346,7 +2222,7 @@ function _buildParse(self) {
|
|
|
2346
2222
|
later._untilCodegen = true;
|
|
2347
2223
|
return later;
|
|
2348
2224
|
}
|
|
2349
|
-
if (
|
|
2225
|
+
if (_loadCodegen() === null) return decline('this entry has no code generator to build the copy with', 'Use validate(); a valid result carries the input as it is.');
|
|
2350
2226
|
return _codegen.buildParse(self, decline, extended);
|
|
2351
2227
|
}
|
|
2352
2228
|
|
|
@@ -2417,7 +2293,8 @@ for (const [name, pick] of [
|
|
|
2417
2293
|
// Cold start: an interpreted twin answers a large schema's first calls where
|
|
2418
2294
|
// the code generator is registered (see coldTwin in index.js). Lite has none.
|
|
2419
2295
|
Validator.prototype._coldTwin = function () {
|
|
2420
|
-
|
|
2296
|
+
const cg = _loadCodegen();
|
|
2297
|
+
return cg === null || cg.coldTwin === undefined ? null : cg.coldTwin(this, Validator);
|
|
2421
2298
|
};
|
|
2422
2299
|
|
|
2423
2300
|
_defineLazyMethod('validate', (self) => (data) => {
|
|
@@ -2529,9 +2406,10 @@ module.exports = {
|
|
|
2529
2406
|
// How an entry registers the code generator. Not enumerable, so the exported
|
|
2530
2407
|
// surface of `require('ata-validator')` is what it was.
|
|
2531
2408
|
Object.defineProperty(module.exports, '_registerCodegen', { value: _registerCodegen, enumerable: false });
|
|
2409
|
+
Object.defineProperty(module.exports, '_setCodegenLoader', { value: (load) => { _loadCodegen = load; }, enumerable: false });
|
|
2532
2410
|
Object.defineProperty(module.exports, '_registerTiering', { value: _registerTiering, enumerable: false });
|
|
2533
2411
|
Object.defineProperty(module.exports, '_codegenRegistered', { value: () => _codegen !== null, enumerable: false });
|
|
2534
2412
|
Object.defineProperty(module.exports, '_tiering', { get: () => _tiering, enumerable: false });
|
|
2535
2413
|
Object.defineProperty(module.exports, '_registerAot', { value: _registerAot, enumerable: false });
|
|
2536
2414
|
// What the compiled paths in index.js need from this module.
|
|
2537
|
-
Object.defineProperty(module.exports, '_internals', { value: { _interpArgs, _normalizeCallerSchemaUncached, resolveSchemaForPreprocess, _jsonSyntaxRejection, ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindEntry, _bindVerdict, _compileCache, _compileCacheSet, _mustReject, _rememberInstance, _verboseWrap, compileCacheKey, getNative, isV1Dialect, resolveSchemaByPath }, enumerable: false });
|
|
2415
|
+
Object.defineProperty(module.exports, '_internals', { value: { _interpArgs, _normalizeCallerSchemaUncached, resolveSchemaForPreprocess, buildCoercer, buildRemover, _jsonSyntaxRejection, ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindEntry, _bindVerdict, _compileCache, _compileCacheSet, _mustReject, _rememberInstance, _verboseWrap, compileCacheKey, getNative, isV1Dialect, resolveSchemaByPath }, enumerable: false });
|
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.60.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
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|
|
@@ -186,13 +186,13 @@
|
|
|
186
186
|
"codegen.d.ts"
|
|
187
187
|
],
|
|
188
188
|
"optionalDependencies": {
|
|
189
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
190
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
191
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
192
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
193
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
194
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
195
|
-
"@ata-validator/native-win32-x64": "1.
|
|
189
|
+
"@ata-validator/native-darwin-arm64": "1.60.0",
|
|
190
|
+
"@ata-validator/native-darwin-x64": "1.60.0",
|
|
191
|
+
"@ata-validator/native-linux-arm64-gnu": "1.60.0",
|
|
192
|
+
"@ata-validator/native-linux-arm64-musl": "1.60.0",
|
|
193
|
+
"@ata-validator/native-linux-x64-gnu": "1.60.0",
|
|
194
|
+
"@ata-validator/native-linux-x64-musl": "1.60.0",
|
|
195
|
+
"@ata-validator/native-win32-x64": "1.60.0"
|
|
196
196
|
},
|
|
197
197
|
"peerDependencies": {
|
|
198
198
|
"yaml": "^2.0.0"
|