ata-validator 1.14.1 → 1.16.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 +12 -0
- package/README.md +27 -2
- package/compat.d.ts +89 -17
- package/compat.js +367 -16
- package/index.d.ts +26 -0
- package/index.js +25 -7
- package/lib/aot-impl.js +18 -0
- package/lib/buffer-gate.js +7 -1
- package/lib/compat-errors.js +343 -0
- package/lib/formats.js +404 -30
- package/lib/interpreter.js +121 -6
- package/lib/js-compiler.js +26 -13
- package/lib/keywords.js +113 -0
- package/lib/plan-compiler.js +34 -2
- package/lib/version.js +1 -1
- package/package.json +11 -9
package/lib/interpreter.js
CHANGED
|
@@ -249,19 +249,24 @@ function multipleOfOk(d, m) {
|
|
|
249
249
|
|
|
250
250
|
const _formats = require('./formats');
|
|
251
251
|
const FORMAT_CHECKS = {
|
|
252
|
-
email:
|
|
252
|
+
email: _formats.email,
|
|
253
253
|
date: _formats.date,
|
|
254
254
|
'date-time': _formats.dateTime,
|
|
255
255
|
time: _formats.time,
|
|
256
|
-
duration:
|
|
256
|
+
duration: _formats.duration,
|
|
257
257
|
uuid: _formats.uuid,
|
|
258
258
|
uri: _formats.uri,
|
|
259
259
|
'uri-reference': _formats.uriReference,
|
|
260
260
|
ipv4: _formats.ipv4,
|
|
261
261
|
ipv6: _formats.ipv6,
|
|
262
262
|
hostname: _formats.hostname,
|
|
263
|
-
regex: (s) => { try { new RegExp(s, 'u'); return true; } catch {
|
|
264
|
-
'json-pointer':
|
|
263
|
+
regex: (s) => { try { new RegExp(s, 'u'); return true; } catch { return false; } },
|
|
264
|
+
'json-pointer': _formats.jsonPointer,
|
|
265
|
+
'relative-json-pointer': _formats.relativeJsonPointer,
|
|
266
|
+
'uri-template': _formats.uriTemplate,
|
|
267
|
+
iri: _formats.iri,
|
|
268
|
+
'iri-reference': _formats.iriReference,
|
|
269
|
+
'idn-email': _formats.idnEmail,
|
|
265
270
|
};
|
|
266
271
|
|
|
267
272
|
// ---------- evaluation ----------
|
|
@@ -437,12 +442,57 @@ class Plan {
|
|
|
437
442
|
this.if = schema.if !== undefined ? child(schema.if) : undefined;
|
|
438
443
|
this.then = schema.then !== undefined ? child(schema.then) : undefined;
|
|
439
444
|
this.else = schema.else !== undefined ? child(schema.else) : undefined;
|
|
440
|
-
|
|
445
|
+
// Macro keywords: the schema the macro returns is applied in place, the
|
|
446
|
+
// way the reference validator does it, so its errors carry the keyword's
|
|
447
|
+
// path and the keyword adds an error of its own when that schema fails.
|
|
448
|
+
this.macros = null;
|
|
449
|
+
if (interp.keywords !== null) {
|
|
450
|
+
for (const key of Object.keys(schema)) {
|
|
451
|
+
const def = interp.keywords[key];
|
|
452
|
+
if (def === undefined || def.macro === null) continue;
|
|
453
|
+
const expanded = def.macro(schema[key], schema);
|
|
454
|
+
if (expanded === undefined) continue;
|
|
455
|
+
if (this.macros === null) this.macros = [];
|
|
456
|
+
this.macros.push({ keyword: key, node: child(expanded) });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
this.hasApplicators = this.allOf !== null || this.anyOf !== null || this.oneOf !== null || this.not !== undefined || this.if !== undefined || this.macros !== null;
|
|
441
460
|
|
|
442
461
|
this.unevaluatedProperties = schema.unevaluatedProperties !== undefined ? child(schema.unevaluatedProperties) : undefined;
|
|
443
462
|
this.unevaluatedItems = schema.unevaluatedItems !== undefined ? child(schema.unevaluatedItems) : undefined;
|
|
444
463
|
this.hasUnevaluated = this.unevaluatedProperties !== undefined || this.unevaluatedItems !== undefined;
|
|
445
464
|
|
|
465
|
+
// Custom keywords present on this node, in schema order. Each op holds
|
|
466
|
+
// the per-value check, built once here for the compile form, and the
|
|
467
|
+
// type mask it applies to (T_ANY when the definition names no type).
|
|
468
|
+
this.custom = null;
|
|
469
|
+
if (interp.keywords !== null) {
|
|
470
|
+
for (const key of Object.keys(schema)) {
|
|
471
|
+
const def = interp.keywords[key];
|
|
472
|
+
if (def === undefined || def.macro !== null) continue;
|
|
473
|
+
const value = schema[key];
|
|
474
|
+
let mask = T_ANY;
|
|
475
|
+
if (def.types !== null) {
|
|
476
|
+
mask = 0;
|
|
477
|
+
for (const t of def.types) mask |= typeBit(t);
|
|
478
|
+
}
|
|
479
|
+
let fn;
|
|
480
|
+
if (def.compile !== null) {
|
|
481
|
+
fn = def.compile(value, schema);
|
|
482
|
+
if (typeof fn !== 'function') throw new Error(`keyword "${key}": compile must return a function`);
|
|
483
|
+
} else {
|
|
484
|
+
const validate = def.validate;
|
|
485
|
+
fn = (data) => validate(value, data, schema);
|
|
486
|
+
// Errors written by the validate function land on the function
|
|
487
|
+
// itself, which is where the check reads them from below.
|
|
488
|
+
fn.source = validate;
|
|
489
|
+
}
|
|
490
|
+
if (this.custom === null) this.custom = [];
|
|
491
|
+
this.custom.push({ keyword: key, fn, mask });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
this.hasCustom = this.custom !== null;
|
|
495
|
+
|
|
446
496
|
// Resource bookkeeping, read once instead of two Map lookups per visit.
|
|
447
497
|
const nodeBase = interp.state.nodeBase.get(schema);
|
|
448
498
|
this.nodeBase = nodeBase !== undefined ? nodeBase : null;
|
|
@@ -514,9 +564,60 @@ function evalLeafV(P, data) {
|
|
|
514
564
|
}
|
|
515
565
|
}
|
|
516
566
|
}
|
|
567
|
+
if (P.hasCustom && !runCustomV(P, data, bits)) return false;
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Custom keyword checks for one node. The verdict variant stops at the first
|
|
572
|
+
// failing keyword; the collecting variant runs them all and reports each,
|
|
573
|
+
// taking the error objects the keyword's function left on itself when it
|
|
574
|
+
// wrote any (`fn.errors`, the same convention as the default validator's
|
|
575
|
+
// custom keywords) and a fixed message otherwise.
|
|
576
|
+
function runCustomV(P, data, bits) {
|
|
577
|
+
const ops = P.custom;
|
|
578
|
+
for (let i = 0; i < ops.length; i++) {
|
|
579
|
+
const op = ops[i];
|
|
580
|
+
if ((bits & op.mask) === 0) continue;
|
|
581
|
+
const fn = op.fn;
|
|
582
|
+
const ok = fn(data);
|
|
583
|
+
const holder = fn.source || fn;
|
|
584
|
+
if (holder.errors) holder.errors = null;
|
|
585
|
+
if (!ok) return false;
|
|
586
|
+
}
|
|
517
587
|
return true;
|
|
518
588
|
}
|
|
519
589
|
|
|
590
|
+
function runCustom(P, data, bits, errors, instancePath, schemaPath) {
|
|
591
|
+
const ops = P.custom;
|
|
592
|
+
let valid = true;
|
|
593
|
+
for (let i = 0; i < ops.length; i++) {
|
|
594
|
+
const op = ops[i];
|
|
595
|
+
if ((bits & op.mask) === 0) continue;
|
|
596
|
+
const fn = op.fn;
|
|
597
|
+
const ok = fn(data);
|
|
598
|
+
const holder = fn.source || fn;
|
|
599
|
+
const own = holder.errors;
|
|
600
|
+
if (own) holder.errors = null;
|
|
601
|
+
if (ok) continue;
|
|
602
|
+
valid = false;
|
|
603
|
+
if (errors === NOERRORS) return false;
|
|
604
|
+
const kwPath = schemaPath + '/' + op.keyword;
|
|
605
|
+
if (Array.isArray(own) && own.length > 0) {
|
|
606
|
+
for (let j = 0; j < own.length; j++) {
|
|
607
|
+
const e = own[j];
|
|
608
|
+
errors.push(err(op.keyword, e.keyword || op.keyword,
|
|
609
|
+
e.instancePath !== undefined ? instancePath + e.instancePath : instancePath,
|
|
610
|
+
e.schemaPath !== undefined ? e.schemaPath : kwPath,
|
|
611
|
+
e.params !== undefined ? e.params : {},
|
|
612
|
+
e.message !== undefined ? e.message : `must pass "${op.keyword}" keyword validation`));
|
|
613
|
+
}
|
|
614
|
+
} else {
|
|
615
|
+
errors.push(err(op.keyword, op.keyword, instancePath, kwPath, { keyword: op.keyword }, `must pass "${op.keyword}" keyword validation`));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return valid;
|
|
619
|
+
}
|
|
620
|
+
|
|
520
621
|
// Value-level checks only; the caller guarantees P.leaf. The bodies mirror
|
|
521
622
|
// the corresponding sections of eval() exactly, minus the prologue.
|
|
522
623
|
function evalLeaf(P, data, errors, instancePath, schemaPath) {
|
|
@@ -588,6 +689,7 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
|
|
|
588
689
|
}
|
|
589
690
|
}
|
|
590
691
|
}
|
|
692
|
+
if (P.hasCustom && !runCustom(P, data, bits, errors, instancePath, schemaPath)) valid = false;
|
|
591
693
|
return valid;
|
|
592
694
|
}
|
|
593
695
|
|
|
@@ -597,6 +699,8 @@ class Interpreter {
|
|
|
597
699
|
this.root = rootSchema;
|
|
598
700
|
this.state = indexSchemas(rootSchema, opts.schemaMap);
|
|
599
701
|
this.userFormats = opts.formats || null;
|
|
702
|
+
// Custom keywords, already normalized by lib/keywords.js.
|
|
703
|
+
this.keywords = opts.keywords || null;
|
|
600
704
|
// 2020-12 requires bookending for `$dynamicRef`; v1 removes the
|
|
601
705
|
// requirement. See the `$dynamicRef` branch in eval().
|
|
602
706
|
this.bookending = !opts.v1;
|
|
@@ -980,6 +1084,17 @@ class Interpreter {
|
|
|
980
1084
|
else if (collect) mergeAnnotations(local, sub);
|
|
981
1085
|
}
|
|
982
1086
|
}
|
|
1087
|
+
if (P.macros !== null) {
|
|
1088
|
+
for (let i = 0; i < P.macros.length; i++) {
|
|
1089
|
+
const m = P.macros[i];
|
|
1090
|
+
const sp = schemaPath + '/' + m.keyword;
|
|
1091
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1092
|
+
if (!this.eval(m.node, data, base, dynScope, errors, instancePath, sp, stack, sub)) {
|
|
1093
|
+
valid = false;
|
|
1094
|
+
if (errors !== NOERRORS) errors.push(err(m.keyword, m.keyword, instancePath, sp, { keyword: m.keyword }, `must pass "${m.keyword}" keyword validation`));
|
|
1095
|
+
} else if (collect) mergeAnnotations(local, sub);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
983
1098
|
if (P.anyOf !== null) {
|
|
984
1099
|
let any = false;
|
|
985
1100
|
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
@@ -1082,7 +1197,7 @@ function err(code, keyword, instancePath, schemaPath, params, message) {
|
|
|
1082
1197
|
}
|
|
1083
1198
|
|
|
1084
1199
|
const { compileInterpreter } = require('./plan-compiler').install({
|
|
1085
|
-
Plan, NOERRORS, err, evalLeaf, evalLeafV, dataBits, escapePointer,
|
|
1200
|
+
Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
|
|
1086
1201
|
deepEqual, multipleOfOk, cpAtLeast, cpAtMost,
|
|
1087
1202
|
T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
|
|
1088
1203
|
});
|
package/lib/js-compiler.js
CHANGED
|
@@ -2024,7 +2024,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2024
2024
|
if (schema.format) {
|
|
2025
2025
|
const fc = FORMAT_CODEGEN[schema.format]
|
|
2026
2026
|
if (fc) {
|
|
2027
|
-
lines.push(fc(v, isStr))
|
|
2027
|
+
lines.push(fc(v, isStr, ctx))
|
|
2028
2028
|
} else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
|
|
2029
2029
|
// User-supplied format checker: thread the function via closure and call at runtime.
|
|
2030
2030
|
const safeName = schema.format.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
@@ -2959,24 +2959,37 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2959
2959
|
}
|
|
2960
2960
|
}
|
|
2961
2961
|
|
|
2962
|
+
// Formats whose check is large enough that repeating it at every call site
|
|
2963
|
+
// costs more than the call. They are hoisted once per compiled function and
|
|
2964
|
+
// invoked by name: one real schema with four email fields grew by 20 KB when
|
|
2965
|
+
// the check was inlined at each of them.
|
|
2966
|
+
const EMAIL_HELPER = `function _em(_s){${_formats.emailSource('_s', true)}return true}`
|
|
2967
|
+
|
|
2962
2968
|
const FORMAT_CODEGEN = {
|
|
2963
|
-
email: (v, isStr) => {
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2969
|
+
email: (v, isStr, ctx) => {
|
|
2970
|
+
if (!ctx) return _formats.emailSource(v, isStr)
|
|
2971
|
+
hoistOnce(ctx, '_emHoisted', EMAIL_HELPER)
|
|
2972
|
+
return isStr ? `if(!_em(${v}))return false` : `if(typeof ${v}==='string'&&!_em(${v}))return false`
|
|
2973
|
+
},
|
|
2974
|
+
'json-pointer': _formats.jsonPointerSource,
|
|
2975
|
+
'relative-json-pointer': _formats.relativeJsonPointerSource,
|
|
2976
|
+
'uri-template': _formats.uriTemplateSource,
|
|
2977
|
+
iri: _formats.iriSource,
|
|
2978
|
+
'iri-reference': _formats.iriReferenceSource,
|
|
2979
|
+
'idn-email': _formats.idnEmailSource,
|
|
2980
|
+
regex: (v, isStr) => {
|
|
2981
|
+
const inner = `try{new RegExp(${v},'u')}catch(_er){return false}`
|
|
2982
|
+
return isStr ? `{${inner}}` : `if(typeof ${v}==='string'){${inner}}`
|
|
2968
2983
|
},
|
|
2969
2984
|
date: _formats.dateSource,
|
|
2970
2985
|
uuid: _formats.uuidSource,
|
|
2971
2986
|
'date-time': _formats.dateTimeSource,
|
|
2972
2987
|
time: _formats.timeSource,
|
|
2973
|
-
duration:
|
|
2974
|
-
? `if(!/^P(?:\\d+Y)?(?:\\d+M)?(?:\\d+W)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?$/.test(${v})||${v}==='P'||${v}.endsWith('T'))return false`
|
|
2975
|
-
: `if(typeof ${v}==='string'&&(!/^P(?:\\d+Y)?(?:\\d+M)?(?:\\d+W)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?$/.test(${v})||${v}==='P'||${v}.endsWith('T')))return false`,
|
|
2988
|
+
duration: _formats.durationSource,
|
|
2976
2989
|
uri: _formats.uriSource,
|
|
2977
2990
|
'uri-reference': (v, isStr) => isStr
|
|
2978
|
-
? `{${_formats.
|
|
2979
|
-
: `if(typeof ${v}==='string'){${_formats.
|
|
2991
|
+
? `{${_formats.uriCharsSource(v, '0')}}`
|
|
2992
|
+
: `if(typeof ${v}==='string'){${_formats.uriCharsSource(v, '0')}}`,
|
|
2980
2993
|
ipv4: _formats.ipv4Source,
|
|
2981
2994
|
ipv6: _formats.ipv6Source,
|
|
2982
2995
|
hostname: _formats.hostnameSource,
|
|
@@ -3522,7 +3535,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3522
3535
|
// let every later statement of the same check push the same error again.
|
|
3523
3536
|
if (fc) {
|
|
3524
3537
|
const ri = ctx.varCounter++
|
|
3525
|
-
const fmtCode = fc(v, isStr).replace(/return false/g, `{${failPush};break _fmt${ri}}`)
|
|
3538
|
+
const fmtCode = fc(v, isStr, ctx).replace(/return false/g, `{${failPush};break _fmt${ri}}`)
|
|
3526
3539
|
lines.push(`_fmt${ri}:{${fmtCode}}`)
|
|
3527
3540
|
} else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
|
|
3528
3541
|
// User-supplied format checker on the error path. Same closure plumbing
|
|
@@ -4158,7 +4171,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4158
4171
|
// Same rule as the error codegen: one violation, one error. The first
|
|
4159
4172
|
// failing statement pushes and leaves the labeled block.
|
|
4160
4173
|
const ri = ctx.varCounter++
|
|
4161
|
-
const code = fc(v, isStr).replace(/return false/g, `{${fail('format', 'format', `{format:'${esc(schema.format)}'}`, `'must match format "${esc(schema.format)}"'`)};break _fmt${ri}}`)
|
|
4174
|
+
const code = fc(v, isStr, ctx).replace(/return false/g, `{${fail('format', 'format', `{format:'${esc(schema.format)}'}`, `'must match format "${esc(schema.format)}"'`)};break _fmt${ri}}`)
|
|
4162
4175
|
lines.push(`_fmt${ri}:{${code}}`)
|
|
4163
4176
|
} else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
|
|
4164
4177
|
const safeName = schema.format.replace(/[^a-zA-Z0-9_]/g, '_')
|
package/lib/keywords.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Custom keywords for `new Validator(schema, { keywords })`.
|
|
4
|
+
//
|
|
5
|
+
// A definition is one of:
|
|
6
|
+
// fn shorthand for { validate: fn }
|
|
7
|
+
// { validate(value, data, parent) } called per value
|
|
8
|
+
// { compile(value, parent) -> fn } called once per schema node, the
|
|
9
|
+
// returned function per value
|
|
10
|
+
// { macro(value, parent) -> schema } the returned schema is applied in
|
|
11
|
+
// place, and the keyword reports an
|
|
12
|
+
// error of its own when it fails
|
|
13
|
+
// with optional `type`: a JSON Schema type name or a list of them. A typed
|
|
14
|
+
// keyword is skipped for data of any other type, which is what makes
|
|
15
|
+
// `{ type: 'number', ... }` safe next to `type: ['number', 'string']`.
|
|
16
|
+
//
|
|
17
|
+
// Schemas that use a registered keyword run on the interpreted engine, so
|
|
18
|
+
// every applicator around the custom check keeps its specification meaning.
|
|
19
|
+
// A function cannot be carried into a standalone module, so the ahead-of-time
|
|
20
|
+
// paths refuse these options instead of emitting a module that would ignore
|
|
21
|
+
// the keyword.
|
|
22
|
+
|
|
23
|
+
const RESERVED = new Set([
|
|
24
|
+
'$id', '$schema', '$ref', '$defs', '$anchor', '$dynamicRef', '$dynamicAnchor',
|
|
25
|
+
'$vocabulary', '$comment', 'definitions', 'type', 'enum', 'const',
|
|
26
|
+
'properties', 'patternProperties', 'additionalProperties', 'required',
|
|
27
|
+
'items', 'prefixItems', 'additionalItems', 'contains', 'allOf', 'anyOf',
|
|
28
|
+
'oneOf', 'not', 'if', 'then', 'else', 'format', 'pattern',
|
|
29
|
+
'unevaluatedProperties', 'unevaluatedItems', 'dependentSchemas',
|
|
30
|
+
'dependentRequired', 'dependencies', 'propertyNames', 'propertyDependencies',
|
|
31
|
+
'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf',
|
|
32
|
+
'minLength', 'maxLength', 'minItems', 'maxItems', 'uniqueItems',
|
|
33
|
+
'minContains', 'maxContains', 'minProperties', 'maxProperties',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// Normalize a caller's `{ name: def }` map into `{ name: { types, validate,
|
|
37
|
+
// compile, macro } }`. Throws on a definition that has no way to run, so a
|
|
38
|
+
// keyword that would otherwise be ignored is never accepted quietly.
|
|
39
|
+
function normalizeKeywords(defs) {
|
|
40
|
+
if (!defs || typeof defs !== 'object') return null;
|
|
41
|
+
const names = Object.keys(defs);
|
|
42
|
+
if (names.length === 0) return null;
|
|
43
|
+
const out = Object.create(null);
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
const raw = defs[name];
|
|
46
|
+
const def = typeof raw === 'function' ? { validate: raw } : raw;
|
|
47
|
+
if (!def || typeof def !== 'object') {
|
|
48
|
+
throw new Error(`keyword "${name}": definition must be a function or an object`);
|
|
49
|
+
}
|
|
50
|
+
if (RESERVED.has(name)) {
|
|
51
|
+
throw new Error(`keyword "${name}" is a JSON Schema keyword and cannot be redefined`);
|
|
52
|
+
}
|
|
53
|
+
const hasValidate = typeof def.validate === 'function';
|
|
54
|
+
const hasCompile = typeof def.compile === 'function';
|
|
55
|
+
const hasMacro = typeof def.macro === 'function';
|
|
56
|
+
if (!hasValidate && !hasCompile && !hasMacro) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`keyword "${name}": definition needs validate, compile or macro` +
|
|
59
|
+
(typeof def.code === 'function' ? ' (code-generating keywords are not supported)' : ''),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
let types = null;
|
|
63
|
+
if (def.type !== undefined) {
|
|
64
|
+
types = Array.isArray(def.type) ? def.type.slice() : [def.type];
|
|
65
|
+
for (const t of types) {
|
|
66
|
+
if (typeof t !== 'string') throw new Error(`keyword "${name}": type must be a string or a list of strings`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
out[name] = {
|
|
70
|
+
name,
|
|
71
|
+
types,
|
|
72
|
+
validate: hasValidate ? def.validate : null,
|
|
73
|
+
compile: hasCompile ? def.compile : null,
|
|
74
|
+
macro: hasMacro ? def.macro : null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Does the schema, or any subschema of it, use a registered keyword? Walks
|
|
81
|
+
// every object and array so keywords inside applicators and definitions are
|
|
82
|
+
// found too. Property names under `properties` are not keywords, so that map
|
|
83
|
+
// is descended into without reading its keys as keywords.
|
|
84
|
+
function schemaUsesKeywords(schema, keywords) {
|
|
85
|
+
if (!keywords) return false;
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
const walk = (node, keysAreNames) => {
|
|
88
|
+
if (node === null || typeof node !== 'object') return false;
|
|
89
|
+
if (seen.has(node)) return false;
|
|
90
|
+
seen.add(node);
|
|
91
|
+
if (Array.isArray(node)) {
|
|
92
|
+
for (const item of node) if (walk(item, false)) return true;
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
for (const key of Object.keys(node)) {
|
|
96
|
+
if (!keysAreNames && keywords[key] !== undefined) return true;
|
|
97
|
+
const v = node[key];
|
|
98
|
+
if (v !== null && typeof v === 'object') {
|
|
99
|
+
if (walk(v, !keysAreNames && NAME_MAPS.has(key))) return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
};
|
|
104
|
+
return walk(schema, false);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Maps whose keys are user-chosen names, not keywords.
|
|
108
|
+
const NAME_MAPS = new Set([
|
|
109
|
+
'properties', 'patternProperties', '$defs', 'definitions',
|
|
110
|
+
'dependentSchemas', 'dependentRequired', 'dependencies', 'propertyDependencies',
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
module.exports = { normalizeKeywords, schemaUsesKeywords };
|
package/lib/plan-compiler.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
// allocation per child.
|
|
47
47
|
|
|
48
48
|
function install(deps) {
|
|
49
|
-
const { Plan, NOERRORS, err, evalLeaf, evalLeafV, dataBits, escapePointer,
|
|
49
|
+
const { Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
|
|
50
50
|
deepEqual, multipleOfOk, cpAtLeast, cpAtMost,
|
|
51
51
|
T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri } = deps;
|
|
52
52
|
|
|
@@ -229,6 +229,10 @@ function install(deps) {
|
|
|
229
229
|
if (run !== null) { needsBits = true; checks.push((data, bits) => bits !== T_OBJECT || run(data)); }
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
if (P.hasCustom) {
|
|
233
|
+
needsBits = true;
|
|
234
|
+
checks.push((data, bits) => runCustomV(P, data, bits));
|
|
235
|
+
}
|
|
232
236
|
if (checks.length === 0) return TRUE_FN;
|
|
233
237
|
if (!needsBits) {
|
|
234
238
|
if (checks.length === 1) { const a = checks[0]; return (data) => a(data, 0); }
|
|
@@ -368,7 +372,7 @@ function install(deps) {
|
|
|
368
372
|
if (P.hasType || P.enum !== null || P.hasConst || P.hasNumber || P.hasString ||
|
|
369
373
|
P.minItems !== undefined || P.maxItems !== undefined || P.uniqueItems ||
|
|
370
374
|
P.required !== null || P.minProperties !== undefined || P.maxProperties !== undefined ||
|
|
371
|
-
P.dependentRequired !== null) {
|
|
375
|
+
P.dependentRequired !== null || P.hasCustom) {
|
|
372
376
|
steps.push((data, errors, instancePath, schemaPath) => evalLeaf(P, data, errors, instancePath, schemaPath));
|
|
373
377
|
vsteps.push(compileLeafV(P));
|
|
374
378
|
}
|
|
@@ -679,6 +683,34 @@ function install(deps) {
|
|
|
679
683
|
return true;
|
|
680
684
|
});
|
|
681
685
|
}
|
|
686
|
+
if (P.macros !== null) {
|
|
687
|
+
const ms = P.macros.map((m) => ({ keyword: m.keyword, fn: inPlace(m.node, base) }));
|
|
688
|
+
steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
|
|
689
|
+
let ok = true;
|
|
690
|
+
for (let i = 0; i < ms.length; i++) {
|
|
691
|
+
const m = ms[i];
|
|
692
|
+
const sp = schemaPath + '/' + m.keyword;
|
|
693
|
+
let passed;
|
|
694
|
+
if (collect) {
|
|
695
|
+
const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
|
|
696
|
+
passed = m.fn.c(data, errors, instancePath, sp, stack, rec);
|
|
697
|
+
if (!passed) undo(rec, pl, n0, xl);
|
|
698
|
+
} else {
|
|
699
|
+
passed = m.fn.c(data, errors, instancePath, sp, stack);
|
|
700
|
+
}
|
|
701
|
+
if (!passed) {
|
|
702
|
+
ok = false;
|
|
703
|
+
if (errors === NOERRORS) return false;
|
|
704
|
+
errors.push(err(m.keyword, m.keyword, instancePath, sp, { keyword: m.keyword }, `must pass "${m.keyword}" keyword validation`));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return ok;
|
|
708
|
+
});
|
|
709
|
+
vsteps.push((data, stack, rec) => {
|
|
710
|
+
for (let i = 0; i < ms.length; i++) if (!ms[i].fn.v(data, stack, rec)) return false;
|
|
711
|
+
return true;
|
|
712
|
+
});
|
|
713
|
+
}
|
|
682
714
|
if (P.anyOf !== null) {
|
|
683
715
|
const fns = P.anyOf.map((v) => inPlace(v, base));
|
|
684
716
|
steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
|
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.16.0",
|
|
4
4
|
"description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
|
|
52
52
|
"build": "cmake-js build --target ata",
|
|
53
53
|
"rebuild": "cmake-js rebuild --target ata",
|
|
54
|
-
"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_buffer_gate.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.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_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.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_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_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_pattern_properties_errors.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_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_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_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_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.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_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 benchmark/bench_aot_size.mjs",
|
|
54
|
+
"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_buffer_gate.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.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_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.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_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_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_ajv_parity.js && node tests/test_pattern_properties_errors.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_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_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_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_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.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_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 benchmark/bench_aot_size.mjs",
|
|
55
55
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
56
56
|
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
|
|
57
57
|
"test:compat": "node tests/test_compat.js",
|
|
@@ -122,13 +122,13 @@
|
|
|
122
122
|
"LICENSE"
|
|
123
123
|
],
|
|
124
124
|
"optionalDependencies": {
|
|
125
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
126
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
127
|
-
"@ata-validator/native-linux-
|
|
128
|
-
"@ata-validator/native-linux-arm64-
|
|
129
|
-
"@ata-validator/native-linux-x64-
|
|
130
|
-
"@ata-validator/native-linux-
|
|
131
|
-
"@ata-validator/native-win32-x64": "1.
|
|
125
|
+
"@ata-validator/native-darwin-arm64": "1.16.0",
|
|
126
|
+
"@ata-validator/native-darwin-x64": "1.16.0",
|
|
127
|
+
"@ata-validator/native-linux-arm64-gnu": "1.16.0",
|
|
128
|
+
"@ata-validator/native-linux-arm64-musl": "1.16.0",
|
|
129
|
+
"@ata-validator/native-linux-x64-gnu": "1.16.0",
|
|
130
|
+
"@ata-validator/native-linux-x64-musl": "1.16.0",
|
|
131
|
+
"@ata-validator/native-win32-x64": "1.16.0"
|
|
132
132
|
},
|
|
133
133
|
"peerDependencies": {
|
|
134
134
|
"yaml": "^2.0.0"
|
|
@@ -141,6 +141,8 @@
|
|
|
141
141
|
"devDependencies": {
|
|
142
142
|
"@sinclair/typebox": "^0.34.49",
|
|
143
143
|
"@types/node": "^25.8.0",
|
|
144
|
+
"ajv": "^8.20.0",
|
|
145
|
+
"ajv-formats": "^3.0.1",
|
|
144
146
|
"cmake-js": "^8.0.0",
|
|
145
147
|
"esbuild": "^0.28.0",
|
|
146
148
|
"jsonc-parser": "^3.3.1",
|