ata-validator 1.6.2 → 1.7.1
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 +40 -0
- package/README.md +15 -5
- package/build.d.ts +12 -0
- package/index.d.ts +7 -0
- package/index.js +178 -57
- package/lib/aot-build.js +1 -0
- package/lib/aot.js +70 -20
- package/lib/buffer-gate.js +132 -0
- package/lib/draft7.js +24 -2
- package/lib/interpreter.js +644 -208
- package/lib/js-compiler.js +187 -55
- package/lib/metaschemas.js +21 -0
- package/lib/plan-compiler.js +545 -0
- package/lib/safe-regex-source.js +1 -1
- package/lib/safe-regex.js +209 -29
- package/lib/version.js +1 -1
- package/package.json +8 -8
package/lib/interpreter.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// schemas the JS codegen cannot represent when the native engine is absent
|
|
5
5
|
// (browser, edge workers, ATA_NO_NATIVE). It walks the schema directly with
|
|
6
6
|
// full annotation tracking (unevaluatedProperties/Items), $id/$anchor
|
|
7
|
-
// resolution, and $dynamicRef dynamic-scope semantics.
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// resolution, and $dynamicRef dynamic-scope semantics. Each schema node is
|
|
8
|
+
// resolved once into a fixed-shape Plan (type bitmask, compiled pattern,
|
|
9
|
+
// looked-up format, a flag per keyword group) so the walk itself reads no
|
|
10
|
+
// schema properties; codegen and the native engine remain the fast paths.
|
|
10
11
|
|
|
11
12
|
const { compileSafe } = require('./safe-regex');
|
|
12
13
|
|
|
@@ -135,6 +136,10 @@ function walkPointer(root, pointer) {
|
|
|
135
136
|
node = node.$defs;
|
|
136
137
|
} else if (part === '$defs' && node.definitions) {
|
|
137
138
|
node = node.definitions;
|
|
139
|
+
} else if (part === 'items' && Array.isArray(node.prefixItems)) {
|
|
140
|
+
// draft-07 array-form items is normalized to prefixItems; pointers
|
|
141
|
+
// written against the original document still land.
|
|
142
|
+
node = node.prefixItems;
|
|
138
143
|
} else {
|
|
139
144
|
return undefined;
|
|
140
145
|
}
|
|
@@ -200,13 +205,27 @@ function deepEqual(a, b) {
|
|
|
200
205
|
function codePointLength(s) {
|
|
201
206
|
let n = 0;
|
|
202
207
|
for (let i = 0; i < s.length; i++) {
|
|
203
|
-
|
|
204
|
-
if (
|
|
208
|
+
// Wraparound test: one compare classifies a high surrogate.
|
|
209
|
+
if (((s.charCodeAt(i) - 0xd800) >>> 0) < 0x400 && i + 1 < s.length) i++;
|
|
205
210
|
n++;
|
|
206
211
|
}
|
|
207
212
|
return n;
|
|
208
213
|
}
|
|
209
214
|
|
|
215
|
+
// The code point count of a UTF-16 string sits between ceil(length / 2) and
|
|
216
|
+
// length, so most length bounds are decided without counting anything.
|
|
217
|
+
function cpAtLeast(s, min) {
|
|
218
|
+
if (s.length >= 2 * min) return true;
|
|
219
|
+
if (s.length < min) return false;
|
|
220
|
+
return codePointLength(s) >= min;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function cpAtMost(s, max) {
|
|
224
|
+
if (s.length <= max) return true;
|
|
225
|
+
if (s.length > 2 * max + 1) return false;
|
|
226
|
+
return codePointLength(s) <= max;
|
|
227
|
+
}
|
|
228
|
+
|
|
210
229
|
function typeMatches(type, d) {
|
|
211
230
|
switch (type) {
|
|
212
231
|
case 'string': return typeof d === 'string';
|
|
@@ -246,7 +265,15 @@ const FORMAT_CHECKS = {
|
|
|
246
265
|
|
|
247
266
|
// ---------- evaluation ----------
|
|
248
267
|
|
|
249
|
-
|
|
268
|
+
// Sink for subschema results whose annotations the caller never reads
|
|
269
|
+
// (properties, items, propertyNames and friends). eval() skips the merge when
|
|
270
|
+
// it sees this object, so it is never mutated.
|
|
271
|
+
const DISCARD = { props: null, items: null };
|
|
272
|
+
|
|
273
|
+
// Verdict-only error sink. When eval() sees this identity it neither builds
|
|
274
|
+
// error objects nor formats messages; every push site is guarded on it. The
|
|
275
|
+
// array is frozen so a missed guard throws in tests instead of hiding.
|
|
276
|
+
const NOERRORS = Object.freeze([]);
|
|
250
277
|
|
|
251
278
|
function mergeAnnotations(target, from) {
|
|
252
279
|
if (from.props && from.props.size) {
|
|
@@ -259,6 +286,310 @@ function mergeAnnotations(target, from) {
|
|
|
259
286
|
}
|
|
260
287
|
}
|
|
261
288
|
|
|
289
|
+
// ---------- plans ----------
|
|
290
|
+
|
|
291
|
+
// Type bits, one per JSON Schema type. `integer` data also sets the `number`
|
|
292
|
+
// bit so a `number` constraint accepts it.
|
|
293
|
+
const T_STRING = 1;
|
|
294
|
+
const T_NUMBER = 2;
|
|
295
|
+
const T_INTEGER = 4;
|
|
296
|
+
const T_BOOLEAN = 8;
|
|
297
|
+
const T_NULL = 16;
|
|
298
|
+
const T_OBJECT = 32;
|
|
299
|
+
const T_ARRAY = 64;
|
|
300
|
+
const T_ANY = 127;
|
|
301
|
+
|
|
302
|
+
function typeBit(name) {
|
|
303
|
+
switch (name) {
|
|
304
|
+
case 'string': return T_STRING;
|
|
305
|
+
case 'number': return T_NUMBER;
|
|
306
|
+
case 'integer': return T_INTEGER;
|
|
307
|
+
case 'boolean': return T_BOOLEAN;
|
|
308
|
+
case 'null': return T_NULL;
|
|
309
|
+
case 'object': return T_OBJECT;
|
|
310
|
+
case 'array': return T_ARRAY;
|
|
311
|
+
default: return T_ANY; // unknown type names match everything
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function dataBits(d) {
|
|
316
|
+
switch (typeof d) {
|
|
317
|
+
case 'string': return T_STRING;
|
|
318
|
+
case 'number':
|
|
319
|
+
if (!isFinite(d)) return 0;
|
|
320
|
+
return Number.isInteger(d) ? T_NUMBER | T_INTEGER : T_NUMBER;
|
|
321
|
+
case 'boolean': return T_BOOLEAN;
|
|
322
|
+
case 'object':
|
|
323
|
+
if (d === null) return T_NULL;
|
|
324
|
+
return Array.isArray(d) ? T_ARRAY : T_OBJECT;
|
|
325
|
+
default: return 0;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isObjectMap(v) {
|
|
330
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// A plan is the schema node with every keyword resolved once: the type list
|
|
334
|
+
// folded to a bitmask, the format looked up, patterns compiled, and a flag
|
|
335
|
+
// per section so eval() can skip whole blocks without touching the schema
|
|
336
|
+
// object. Plans have a fixed shape, which keeps eval() monomorphic where the
|
|
337
|
+
// raw schema objects are not.
|
|
338
|
+
class Plan {
|
|
339
|
+
// The plan is registered in the cache before `link` runs, so a schema
|
|
340
|
+
// object that contains itself structurally resolves to the same plan
|
|
341
|
+
// instead of recursing without end.
|
|
342
|
+
constructor(schema) {
|
|
343
|
+
this.schema = schema;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
link(interp) {
|
|
347
|
+
const schema = this.schema;
|
|
348
|
+
// Children are linked as plans (or booleans) up front, so a visit never
|
|
349
|
+
// goes back through the plan cache. Reference targets are resolved at
|
|
350
|
+
// evaluation time and cached per base URI.
|
|
351
|
+
const child = (sub) => interp.node(sub);
|
|
352
|
+
this.refCache = null;
|
|
353
|
+
this.refBase0 = undefined;
|
|
354
|
+
this.refRes0 = null;
|
|
355
|
+
this.dynCache = null;
|
|
356
|
+
this.ref = typeof schema.$ref === 'string' ? schema.$ref : null;
|
|
357
|
+
this.dynamicRef = typeof schema.$dynamicRef === 'string' ? schema.$dynamicRef : null;
|
|
358
|
+
this.tracked = this.ref !== null || this.dynamicRef !== null;
|
|
359
|
+
|
|
360
|
+
this.typeMask = 0;
|
|
361
|
+
this.typeNames = null;
|
|
362
|
+
if (schema.type !== undefined) {
|
|
363
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
364
|
+
let mask = 0;
|
|
365
|
+
for (const t of types) mask |= typeBit(t);
|
|
366
|
+
this.typeMask = mask;
|
|
367
|
+
this.typeNames = types;
|
|
368
|
+
}
|
|
369
|
+
this.hasType = schema.type !== undefined;
|
|
370
|
+
this.enum = schema.enum !== undefined ? schema.enum : null;
|
|
371
|
+
this.hasConst = schema.const !== undefined;
|
|
372
|
+
this.const = schema.const;
|
|
373
|
+
|
|
374
|
+
this.minimum = typeof schema.minimum === 'number' ? schema.minimum : undefined;
|
|
375
|
+
this.maximum = typeof schema.maximum === 'number' ? schema.maximum : undefined;
|
|
376
|
+
this.exclusiveMinimum = typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined;
|
|
377
|
+
this.exclusiveMaximum = typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined;
|
|
378
|
+
this.multipleOf = typeof schema.multipleOf === 'number' ? schema.multipleOf : undefined;
|
|
379
|
+
this.hasNumber = this.minimum !== undefined || this.maximum !== undefined || this.exclusiveMinimum !== undefined || this.exclusiveMaximum !== undefined || this.multipleOf !== undefined;
|
|
380
|
+
|
|
381
|
+
this.minLength = schema.minLength;
|
|
382
|
+
this.maxLength = schema.maxLength;
|
|
383
|
+
this.pattern = schema.pattern !== undefined ? interp.pattern(schema.pattern) : null;
|
|
384
|
+
this.patternSource = schema.pattern;
|
|
385
|
+
this.format = schema.format;
|
|
386
|
+
this.formatFn = null;
|
|
387
|
+
if (schema.format !== undefined) {
|
|
388
|
+
const uf = interp.userFormats;
|
|
389
|
+
const fc = (uf && typeof uf[schema.format] === 'function') ? uf[schema.format] : FORMAT_CHECKS[schema.format];
|
|
390
|
+
this.formatFn = fc || null;
|
|
391
|
+
}
|
|
392
|
+
this.hasString = this.minLength !== undefined || this.maxLength !== undefined || this.pattern !== null || this.formatFn !== null;
|
|
393
|
+
|
|
394
|
+
this.minItems = schema.minItems;
|
|
395
|
+
this.maxItems = schema.maxItems;
|
|
396
|
+
this.uniqueItems = schema.uniqueItems === true;
|
|
397
|
+
this.prefixItems = Array.isArray(schema.prefixItems) ? schema.prefixItems.map(child) : null;
|
|
398
|
+
this.items = schema.items !== undefined ? child(schema.items) : undefined;
|
|
399
|
+
this.contains = schema.contains !== undefined ? child(schema.contains) : undefined;
|
|
400
|
+
this.minContains = schema.minContains;
|
|
401
|
+
this.maxContains = schema.maxContains;
|
|
402
|
+
this.hasArray = this.minItems !== undefined || this.maxItems !== undefined || this.uniqueItems || this.prefixItems !== null || this.items !== undefined || this.contains !== undefined;
|
|
403
|
+
|
|
404
|
+
this.required = Array.isArray(schema.required) ? schema.required : null;
|
|
405
|
+
this.minProperties = schema.minProperties;
|
|
406
|
+
this.maxProperties = schema.maxProperties;
|
|
407
|
+
this.dependentRequired = isObjectMap(schema.dependentRequired) ? Object.entries(schema.dependentRequired) : null;
|
|
408
|
+
this.propertyNames = schema.propertyNames !== undefined ? child(schema.propertyNames) : undefined;
|
|
409
|
+
this.properties = null;
|
|
410
|
+
if (isObjectMap(schema.properties)) {
|
|
411
|
+
this.properties = new Map();
|
|
412
|
+
for (const key of Object.keys(schema.properties)) {
|
|
413
|
+
const ek = escapePointer(key);
|
|
414
|
+
// Escaped once here; the walk only concatenates.
|
|
415
|
+
this.properties.set(key, { node: child(schema.properties[key]), seg: '/' + ek, schemaSeg: '/properties/' + ek });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
this.patternProperties = null;
|
|
419
|
+
if (isObjectMap(schema.patternProperties)) {
|
|
420
|
+
this.patternProperties = Object.keys(schema.patternProperties).map((src) => ({ src, re: interp.pattern(src), node: child(schema.patternProperties[src]) }));
|
|
421
|
+
}
|
|
422
|
+
this.additionalProperties = schema.additionalProperties !== undefined ? child(schema.additionalProperties) : undefined;
|
|
423
|
+
this.dependentSchemas = isObjectMap(schema.dependentSchemas) ? Object.entries(schema.dependentSchemas).map(([k, v]) => [k, child(v)]) : null;
|
|
424
|
+
this.propertyDependencies = null;
|
|
425
|
+
if (isObjectMap(schema.propertyDependencies)) {
|
|
426
|
+
this.propertyDependencies = Object.entries(schema.propertyDependencies)
|
|
427
|
+
.filter(([, choices]) => isObjectMap(choices))
|
|
428
|
+
.map(([k, choices]) => [k, new Map(Object.keys(choices).map((v) => [v, child(choices[v])]))]);
|
|
429
|
+
}
|
|
430
|
+
this.hasObject = this.required !== null || this.minProperties !== undefined || this.maxProperties !== undefined || this.dependentRequired !== null || this.propertyNames !== undefined || this.properties !== null || this.patternProperties !== null || this.additionalProperties !== undefined || this.dependentSchemas !== null || this.propertyDependencies !== null;
|
|
431
|
+
|
|
432
|
+
this.allOf = Array.isArray(schema.allOf) ? schema.allOf.map(child) : null;
|
|
433
|
+
this.anyOf = Array.isArray(schema.anyOf) ? schema.anyOf.map(child) : null;
|
|
434
|
+
this.oneOf = Array.isArray(schema.oneOf) ? schema.oneOf.map(child) : null;
|
|
435
|
+
this.not = schema.not !== undefined ? child(schema.not) : undefined;
|
|
436
|
+
this.if = schema.if !== undefined ? child(schema.if) : undefined;
|
|
437
|
+
this.then = schema.then !== undefined ? child(schema.then) : undefined;
|
|
438
|
+
this.else = schema.else !== undefined ? child(schema.else) : undefined;
|
|
439
|
+
this.hasApplicators = this.allOf !== null || this.anyOf !== null || this.oneOf !== null || this.not !== undefined || this.if !== undefined;
|
|
440
|
+
|
|
441
|
+
this.unevaluatedProperties = schema.unevaluatedProperties !== undefined ? child(schema.unevaluatedProperties) : undefined;
|
|
442
|
+
this.unevaluatedItems = schema.unevaluatedItems !== undefined ? child(schema.unevaluatedItems) : undefined;
|
|
443
|
+
this.hasUnevaluated = this.unevaluatedProperties !== undefined || this.unevaluatedItems !== undefined;
|
|
444
|
+
|
|
445
|
+
// Resource bookkeeping, read once instead of two Map lookups per visit.
|
|
446
|
+
const nodeBase = interp.state.nodeBase.get(schema);
|
|
447
|
+
this.nodeBase = nodeBase !== undefined ? nodeBase : null;
|
|
448
|
+
this.isResourceRoot = nodeBase !== undefined && interp.state.resources.get(nodeBase) === schema;
|
|
449
|
+
|
|
450
|
+
// A leaf plan checks only value-level keywords: no references, no
|
|
451
|
+
// subschemas, no annotations, no resource boundary. Those skip the whole
|
|
452
|
+
// eval() prologue (cycle guard, scope, annotation sinks) and run through
|
|
453
|
+
// evalLeaf(). Most schemas are mostly leaves.
|
|
454
|
+
this.leaf = !this.tracked && !this.hasApplicators && !this.hasUnevaluated &&
|
|
455
|
+
this.properties === null && this.patternProperties === null &&
|
|
456
|
+
this.additionalProperties === undefined && this.propertyNames === undefined &&
|
|
457
|
+
this.dependentSchemas === null && this.propertyDependencies === null &&
|
|
458
|
+
this.prefixItems === null && this.items === undefined && this.contains === undefined &&
|
|
459
|
+
this.nodeBase === null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
// Verdict-only twin of evalLeaf: no error objects, no path strings, first
|
|
465
|
+
// failure returns. Kept adjacent so the two cannot drift.
|
|
466
|
+
function evalLeafV(P, data) {
|
|
467
|
+
const bits = dataBits(data);
|
|
468
|
+
if (P.hasType && (bits & P.typeMask) === 0) return false;
|
|
469
|
+
if (P.enum !== null) {
|
|
470
|
+
let found = false;
|
|
471
|
+
for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
|
|
472
|
+
if (!found) return false;
|
|
473
|
+
}
|
|
474
|
+
if (P.hasConst && !deepEqual(P.const, data)) return false;
|
|
475
|
+
if (P.hasNumber && typeof data === 'number') {
|
|
476
|
+
if (P.minimum !== undefined && !(data >= P.minimum)) return false;
|
|
477
|
+
if (P.maximum !== undefined && !(data <= P.maximum)) return false;
|
|
478
|
+
if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) return false;
|
|
479
|
+
if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) return false;
|
|
480
|
+
if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) return false;
|
|
481
|
+
}
|
|
482
|
+
if (P.hasString && bits === T_STRING) {
|
|
483
|
+
if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) return false;
|
|
484
|
+
if (P.maxLength !== undefined && !cpAtMost(data, P.maxLength)) return false;
|
|
485
|
+
if (P.pattern !== null && !P.pattern.test(data)) return false;
|
|
486
|
+
if (P.formatFn !== null && !P.formatFn(data)) return false;
|
|
487
|
+
}
|
|
488
|
+
if (P.hasArray && bits === T_ARRAY) {
|
|
489
|
+
if (P.minItems !== undefined && data.length < P.minItems) return false;
|
|
490
|
+
if (P.maxItems !== undefined && data.length > P.maxItems) return false;
|
|
491
|
+
if (P.uniqueItems) {
|
|
492
|
+
for (let i = 0; i < data.length; i++) {
|
|
493
|
+
for (let j = i + 1; j < data.length; j++) {
|
|
494
|
+
if (deepEqual(data[i], data[j])) return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (P.hasObject && bits === T_OBJECT) {
|
|
500
|
+
if (P.required !== null) {
|
|
501
|
+
const req = P.required;
|
|
502
|
+
for (let i = 0; i < req.length; i++) {
|
|
503
|
+
if (!Object.hasOwn(data, req[i])) return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (P.minProperties !== undefined && Object.keys(data).length < P.minProperties) return false;
|
|
507
|
+
if (P.maxProperties !== undefined && Object.keys(data).length > P.maxProperties) return false;
|
|
508
|
+
if (P.dependentRequired !== null) {
|
|
509
|
+
for (const [key, deps] of P.dependentRequired) {
|
|
510
|
+
if (Object.hasOwn(data, key)) {
|
|
511
|
+
for (const dep of deps) if (!Object.hasOwn(data, dep)) return false;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Value-level checks only; the caller guarantees P.leaf. The bodies mirror
|
|
520
|
+
// the corresponding sections of eval() exactly, minus the prologue.
|
|
521
|
+
function evalLeaf(P, data, errors, instancePath, schemaPath) {
|
|
522
|
+
let valid = true;
|
|
523
|
+
const schema = P.schema;
|
|
524
|
+
const bits = dataBits(data);
|
|
525
|
+
if (P.hasType && (bits & P.typeMask) === 0) {
|
|
526
|
+
if (errors !== NOERRORS) errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${P.typeNames.join(' or ')}`));
|
|
527
|
+
valid = false;
|
|
528
|
+
}
|
|
529
|
+
if (P.enum !== null) {
|
|
530
|
+
let found = false;
|
|
531
|
+
for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
|
|
532
|
+
if (!found) {
|
|
533
|
+
if (errors !== NOERRORS) errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
|
|
534
|
+
valid = false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (P.hasConst && !deepEqual(P.const, data)) {
|
|
538
|
+
if (errors !== NOERRORS) errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
|
|
539
|
+
valid = false;
|
|
540
|
+
}
|
|
541
|
+
if (P.hasNumber && typeof data === 'number') {
|
|
542
|
+
if (P.minimum !== undefined && !(data >= P.minimum)) { errors === NOERRORS || errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
|
|
543
|
+
if (P.maximum !== undefined && !(data <= P.maximum)) { errors === NOERRORS || errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
|
|
544
|
+
if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
|
|
545
|
+
if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) { errors === NOERRORS || errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: P.exclusiveMaximum }, `must be < ${P.exclusiveMaximum}`)); valid = false; }
|
|
546
|
+
if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) { errors === NOERRORS || errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: P.multipleOf }, `must be multiple of ${P.multipleOf}`)); valid = false; }
|
|
547
|
+
}
|
|
548
|
+
if (P.hasString && bits === T_STRING) {
|
|
549
|
+
if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) { errors === NOERRORS || errors.push(err('minLength', 'minLength', instancePath, schemaPath + '/minLength', { limit: P.minLength }, `must NOT have fewer than ${P.minLength} characters`)); valid = false; }
|
|
550
|
+
if (P.maxLength !== undefined && !cpAtMost(data, P.maxLength)) { errors === NOERRORS || errors.push(err('maxLength', 'maxLength', instancePath, schemaPath + '/maxLength', { limit: P.maxLength }, `must NOT have more than ${P.maxLength} characters`)); valid = false; }
|
|
551
|
+
if (P.pattern !== null && !P.pattern.test(data)) { errors === NOERRORS || errors.push(err('pattern', 'pattern', instancePath, schemaPath + '/pattern', { pattern: P.patternSource }, `must match pattern "${P.patternSource}"`)); valid = false; }
|
|
552
|
+
if (P.formatFn !== null && !P.formatFn(data)) { errors === NOERRORS || errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: P.format }, `must match format "${P.format}"`)); valid = false; }
|
|
553
|
+
}
|
|
554
|
+
if (P.hasArray && bits === T_ARRAY) {
|
|
555
|
+
if (P.minItems !== undefined && data.length < P.minItems) { errors === NOERRORS || errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: P.minItems }, `must NOT have fewer than ${P.minItems} items`)); valid = false; }
|
|
556
|
+
if (P.maxItems !== undefined && data.length > P.maxItems) { errors === NOERRORS || errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: P.maxItems }, `must NOT have more than ${P.maxItems} items`)); valid = false; }
|
|
557
|
+
if (P.uniqueItems) {
|
|
558
|
+
outer: for (let i = 0; i < data.length; i++) {
|
|
559
|
+
for (let j = i + 1; j < data.length; j++) {
|
|
560
|
+
if (deepEqual(data[i], data[j])) {
|
|
561
|
+
if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, 'must NOT have duplicate items'));
|
|
562
|
+
valid = false;
|
|
563
|
+
break outer;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (P.hasObject && bits === T_OBJECT) {
|
|
570
|
+
const keys = Object.keys(data);
|
|
571
|
+
if (P.required !== null) {
|
|
572
|
+
const req = P.required;
|
|
573
|
+
for (let i = 0; i < req.length; i++) {
|
|
574
|
+
const key = req[i];
|
|
575
|
+
if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (P.minProperties !== undefined && keys.length < P.minProperties) { errors === NOERRORS || errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
|
|
579
|
+
if (P.maxProperties !== undefined && keys.length > P.maxProperties) { errors === NOERRORS || errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: P.maxProperties }, `must NOT have more than ${P.maxProperties} properties`)); valid = false; }
|
|
580
|
+
if (P.dependentRequired !== null) {
|
|
581
|
+
for (const [key, deps] of P.dependentRequired) {
|
|
582
|
+
if (Object.hasOwn(data, key)) {
|
|
583
|
+
for (const dep of deps) {
|
|
584
|
+
if (!Object.hasOwn(data, dep)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return valid;
|
|
591
|
+
}
|
|
592
|
+
|
|
262
593
|
class Interpreter {
|
|
263
594
|
constructor(rootSchema, options) {
|
|
264
595
|
const opts = options || {};
|
|
@@ -269,6 +600,21 @@ class Interpreter {
|
|
|
269
600
|
// requirement. See the `$dynamicRef` branch in eval().
|
|
270
601
|
this.bookending = !opts.v1;
|
|
271
602
|
this.patternCache = new Map();
|
|
603
|
+
// Plans are built on first visit; a schema node is never re-read after.
|
|
604
|
+
this.plans = new Map();
|
|
605
|
+
this.rootNode = this.node(rootSchema);
|
|
606
|
+
// Compiled closure tree: undefined = not attempted, null = out of scope.
|
|
607
|
+
this._fast = undefined;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// The closure-tree compiler covers single-resource schemas without
|
|
611
|
+
// unevaluated keywords; everything else stays on eval(). Compiled lazily so
|
|
612
|
+
// construction stays cheap for schemas that never validate.
|
|
613
|
+
_fastRoot() {
|
|
614
|
+
if (this._fast === undefined) {
|
|
615
|
+
try { this._fast = compileInterpreter(this); } catch { this._fast = null; }
|
|
616
|
+
}
|
|
617
|
+
return this._fast;
|
|
272
618
|
}
|
|
273
619
|
|
|
274
620
|
pattern(src) {
|
|
@@ -290,57 +636,142 @@ class Interpreter {
|
|
|
290
636
|
return re;
|
|
291
637
|
}
|
|
292
638
|
|
|
639
|
+
plan(schema) {
|
|
640
|
+
let p = this.plans.get(schema);
|
|
641
|
+
if (p === undefined) {
|
|
642
|
+
p = new Plan(schema);
|
|
643
|
+
this.plans.set(schema, p);
|
|
644
|
+
p.link(this);
|
|
645
|
+
}
|
|
646
|
+
return p;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// $ref resolution depends on the base URI in effect, which can differ
|
|
650
|
+
// between visits of the same node; cache per (plan, base). Nearly every
|
|
651
|
+
// plan only ever sees one base, so the last resolution lives in two plain
|
|
652
|
+
// fields and the Map exists only for the rare second base. The cached
|
|
653
|
+
// entry carries the already-planned child, so a visit does no Map lookups.
|
|
654
|
+
resolveRefCached(P, base) {
|
|
655
|
+
if (P.refBase0 === base) return P.refRes0;
|
|
656
|
+
let cache = P.refCache;
|
|
657
|
+
let r = cache !== null ? cache.get(base) : undefined;
|
|
658
|
+
if (r === undefined) {
|
|
659
|
+
const raw = resolveRef(P.ref, base, this.state);
|
|
660
|
+
r = { child: raw.node === undefined ? undefined : this.node(raw.node), base: raw.base };
|
|
661
|
+
if (P.refBase0 === undefined) { P.refBase0 = base; P.refRes0 = r; }
|
|
662
|
+
else { if (cache === null) cache = P.refCache = new Map(); cache.set(base, r); }
|
|
663
|
+
}
|
|
664
|
+
return r;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// The static half of $dynamicRef (initial target and fragment) per base;
|
|
668
|
+
// the dynamic-scope search still runs on every visit.
|
|
669
|
+
resolveDynamicRefCached(P, base) {
|
|
670
|
+
let cache = P.dynCache;
|
|
671
|
+
if (cache === null) cache = P.dynCache = new Map();
|
|
672
|
+
let r = cache.get(base);
|
|
673
|
+
if (r === undefined) {
|
|
674
|
+
const { node, base: refBase } = resolveRef(P.dynamicRef, base, this.state);
|
|
675
|
+
const [, fragment] = splitFragment(resolveUri(base, P.dynamicRef));
|
|
676
|
+
r = { node, base: refBase, fragment, byAnchor: Boolean(fragment) && !fragment.startsWith('/') };
|
|
677
|
+
cache.set(base, r);
|
|
678
|
+
}
|
|
679
|
+
return r;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Maps any schema value to what eval() accepts: a boolean or a Plan.
|
|
683
|
+
// Non-object, non-boolean values validate everything, as before.
|
|
684
|
+
node(schema) {
|
|
685
|
+
if (schema === true || schema === false) return schema;
|
|
686
|
+
if (typeof schema !== 'object' || schema === null) return true;
|
|
687
|
+
return this.plan(schema);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Verdict only: same walk as validate(), no error construction at all.
|
|
691
|
+
isValid(data) {
|
|
692
|
+
const fast = this._fastRoot();
|
|
693
|
+
if (fast !== null) return fast.v(data, []);
|
|
694
|
+
const dynScope = [this.state.rootBase];
|
|
695
|
+
return this.eval(this.rootNode, data, this.state.rootBase, dynScope, NOERRORS, '', '#', [], DISCARD);
|
|
696
|
+
}
|
|
697
|
+
|
|
293
698
|
validate(data) {
|
|
699
|
+
const fast = this._fastRoot();
|
|
700
|
+
if (fast !== null) {
|
|
701
|
+
const errors = [];
|
|
702
|
+
const valid = fast.c(data, errors, '', '#', []);
|
|
703
|
+
return valid ? { valid: true, data, errors: [] } : { valid: false, errors };
|
|
704
|
+
}
|
|
294
705
|
const errors = [];
|
|
295
706
|
const dynScope = [this.state.rootBase];
|
|
296
|
-
|
|
707
|
+
// Nothing reads the root's annotations, so they are discarded from the start.
|
|
708
|
+
const valid = this.eval(this.rootNode, data, this.state.rootBase, dynScope, errors, '', '#', [], DISCARD);
|
|
297
709
|
return valid ? { valid: true, data, errors: [] } : { valid: false, errors };
|
|
298
710
|
}
|
|
299
711
|
|
|
300
712
|
// Evaluates `schema` against `data`. Collected annotations for data are
|
|
301
713
|
// written into `sink` ({props, items}); errors append to `errors`.
|
|
302
714
|
// `stack` breaks infinite $ref recursion on identical (schema, data) pairs.
|
|
303
|
-
eval(
|
|
304
|
-
if (
|
|
305
|
-
if (
|
|
306
|
-
errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
|
|
715
|
+
eval(P, data, base, dynScope, errors, instancePath, schemaPath, stack, sink) {
|
|
716
|
+
if (P === true) return true;
|
|
717
|
+
if (P === false) {
|
|
718
|
+
if (errors !== NOERRORS) errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
|
|
307
719
|
return false;
|
|
308
720
|
}
|
|
309
|
-
if (
|
|
721
|
+
if (P.leaf) return evalLeaf(P, data, errors, instancePath, schemaPath);
|
|
722
|
+
const schema = P.schema;
|
|
310
723
|
|
|
311
|
-
|
|
312
|
-
|
|
724
|
+
// Only a reference can lead evaluation back to a schema it is already
|
|
725
|
+
// inside of, so the cycle guard is maintained on reference nodes alone.
|
|
726
|
+
// The frame is pushed in place and popped on the way out; the array is
|
|
727
|
+
// created per validate() call, so a throw leaves nothing to clean up.
|
|
728
|
+
const tracked = P.tracked;
|
|
729
|
+
if (tracked) {
|
|
730
|
+
for (let i = stack.length - 2; i >= 0; i -= 2) {
|
|
731
|
+
if (stack[i] === schema && stack[i + 1] === data) return true; // fixed point
|
|
732
|
+
}
|
|
733
|
+
stack.push(schema, data);
|
|
313
734
|
}
|
|
314
|
-
stack = stack.concat([[schema, data]]);
|
|
315
735
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
736
|
+
if (P.nodeBase !== null && P.nodeBase !== base) base = P.nodeBase;
|
|
737
|
+
// Entering a schema resource extends the dynamic scope, whether evaluation
|
|
738
|
+
// lands on the resource's root or on a subschema inside it: a reference
|
|
739
|
+
// to first#/$defs/stuff still passes through resource `first`. The scope
|
|
740
|
+
// is pushed in place and popped on the way out; evaluation is depth-first
|
|
741
|
+
// and nothing retains the array.
|
|
742
|
+
let scopePushed = false;
|
|
743
|
+
if (dynScope[dynScope.length - 1] !== base) {
|
|
744
|
+
dynScope.push(base);
|
|
745
|
+
scopePushed = true;
|
|
321
746
|
}
|
|
322
747
|
|
|
323
748
|
let valid = true;
|
|
324
|
-
|
|
749
|
+
// Annotations only matter if this node or an ancestor reads them. When
|
|
750
|
+
// nobody does, in-place children get the discard sink too and `local`
|
|
751
|
+
// stays unallocated.
|
|
752
|
+
const collect = sink !== DISCARD || P.hasUnevaluated;
|
|
753
|
+
const local = collect ? { props: null, items: null } : DISCARD;
|
|
325
754
|
|
|
326
755
|
// $ref / $dynamicRef: in-place applicators, annotations flow through
|
|
327
|
-
if (
|
|
328
|
-
const {
|
|
329
|
-
if (
|
|
330
|
-
errors.push(err('$ref', '$ref', instancePath, schemaPath + '/$ref', { ref:
|
|
756
|
+
if (P.ref !== null) {
|
|
757
|
+
const { child, base: refBase } = this.resolveRefCached(P, base);
|
|
758
|
+
if (child === undefined) {
|
|
759
|
+
if (errors !== NOERRORS) errors.push(err('$ref', '$ref', instancePath, schemaPath + '/$ref', { ref: P.ref }, `cannot resolve $ref ${P.ref}`));
|
|
331
760
|
valid = false;
|
|
332
761
|
} else {
|
|
333
|
-
const sub = { props: null, items: null };
|
|
334
|
-
if (!this.eval(
|
|
335
|
-
else mergeAnnotations(local, sub);
|
|
762
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
763
|
+
if (!this.eval(child, data, refBase, dynScope, errors, instancePath, schemaPath + '/$ref', stack, sub)) valid = false;
|
|
764
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
336
765
|
}
|
|
337
766
|
}
|
|
338
767
|
|
|
339
|
-
if (
|
|
340
|
-
const ref =
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
768
|
+
if (P.dynamicRef !== null) {
|
|
769
|
+
const ref = P.dynamicRef;
|
|
770
|
+
const initial = this.resolveDynamicRefCached(P, base);
|
|
771
|
+
let node = initial.node;
|
|
772
|
+
let refBase = initial.base;
|
|
773
|
+
const fragment = initial.fragment;
|
|
774
|
+
if (initial.byAnchor) {
|
|
344
775
|
// Under 2020-12 the initial target must itself carry the matching
|
|
345
776
|
// $dynamicAnchor before the dynamic scope is consulted (bookending);
|
|
346
777
|
// otherwise the keyword behaves like $ref. v1 removes that
|
|
@@ -364,100 +795,92 @@ class Interpreter {
|
|
|
364
795
|
}
|
|
365
796
|
}
|
|
366
797
|
if (node === undefined) {
|
|
367
|
-
errors.push(err('$dynamicRef', '$dynamicRef', instancePath, schemaPath + '/$dynamicRef', { ref }, `cannot resolve $dynamicRef ${ref}`));
|
|
798
|
+
if (errors !== NOERRORS) errors.push(err('$dynamicRef', '$dynamicRef', instancePath, schemaPath + '/$dynamicRef', { ref }, `cannot resolve $dynamicRef ${ref}`));
|
|
368
799
|
valid = false;
|
|
369
800
|
} else {
|
|
370
|
-
const sub = { props: null, items: null };
|
|
371
|
-
if (!this.eval(node, data, refBase, dynScope, errors, instancePath, schemaPath + '/$dynamicRef', stack, sub)) valid = false;
|
|
372
|
-
else mergeAnnotations(local, sub);
|
|
801
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
802
|
+
if (!this.eval(this.node(node), data, refBase, dynScope, errors, instancePath, schemaPath + '/$dynamicRef', stack, sub)) valid = false;
|
|
803
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
373
804
|
}
|
|
374
805
|
}
|
|
375
806
|
|
|
807
|
+
const bits = dataBits(data);
|
|
808
|
+
|
|
376
809
|
// type / enum / const
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${types.join(' or ')}`));
|
|
381
|
-
valid = false;
|
|
382
|
-
}
|
|
810
|
+
if (P.hasType && (bits & P.typeMask) === 0) {
|
|
811
|
+
if (errors !== NOERRORS) errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${P.typeNames.join(' or ')}`));
|
|
812
|
+
valid = false;
|
|
383
813
|
}
|
|
384
|
-
if (
|
|
385
|
-
|
|
386
|
-
|
|
814
|
+
if (P.enum !== null) {
|
|
815
|
+
let found = false;
|
|
816
|
+
for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
|
|
817
|
+
if (!found) {
|
|
818
|
+
if (errors !== NOERRORS) errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
|
|
387
819
|
valid = false;
|
|
388
820
|
}
|
|
389
821
|
}
|
|
390
|
-
if (
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
valid = false;
|
|
394
|
-
}
|
|
822
|
+
if (P.hasConst && !deepEqual(P.const, data)) {
|
|
823
|
+
if (errors !== NOERRORS) errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
|
|
824
|
+
valid = false;
|
|
395
825
|
}
|
|
396
826
|
|
|
397
827
|
// numbers
|
|
398
|
-
if (typeof data === 'number') {
|
|
399
|
-
if (
|
|
400
|
-
if (
|
|
401
|
-
if (
|
|
402
|
-
if (
|
|
403
|
-
if (
|
|
828
|
+
if (P.hasNumber && typeof data === 'number') {
|
|
829
|
+
if (P.minimum !== undefined && !(data >= P.minimum)) { errors === NOERRORS || errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
|
|
830
|
+
if (P.maximum !== undefined && !(data <= P.maximum)) { errors === NOERRORS || errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
|
|
831
|
+
if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
|
|
832
|
+
if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) { errors === NOERRORS || errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: P.exclusiveMaximum }, `must be < ${P.exclusiveMaximum}`)); valid = false; }
|
|
833
|
+
if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) { errors === NOERRORS || errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: P.multipleOf }, `must be multiple of ${P.multipleOf}`)); valid = false; }
|
|
404
834
|
}
|
|
405
835
|
|
|
406
836
|
// strings
|
|
407
|
-
if (
|
|
408
|
-
if (
|
|
409
|
-
if (
|
|
410
|
-
if (
|
|
411
|
-
if (
|
|
412
|
-
const fc = (this.userFormats && typeof this.userFormats[schema.format] === 'function') ? this.userFormats[schema.format] : FORMAT_CHECKS[schema.format];
|
|
413
|
-
if (fc && !fc(data)) { errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: schema.format }, `must match format "${schema.format}"`)); valid = false; }
|
|
414
|
-
}
|
|
837
|
+
if (P.hasString && bits === T_STRING) {
|
|
838
|
+
if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) { errors === NOERRORS || errors.push(err('minLength', 'minLength', instancePath, schemaPath + '/minLength', { limit: P.minLength }, `must NOT have fewer than ${P.minLength} characters`)); valid = false; }
|
|
839
|
+
if (P.maxLength !== undefined && !cpAtMost(data, P.maxLength)) { errors === NOERRORS || errors.push(err('maxLength', 'maxLength', instancePath, schemaPath + '/maxLength', { limit: P.maxLength }, `must NOT have more than ${P.maxLength} characters`)); valid = false; }
|
|
840
|
+
if (P.pattern !== null && !P.pattern.test(data)) { errors === NOERRORS || errors.push(err('pattern', 'pattern', instancePath, schemaPath + '/pattern', { pattern: P.patternSource }, `must match pattern "${P.patternSource}"`)); valid = false; }
|
|
841
|
+
if (P.formatFn !== null && !P.formatFn(data)) { errors === NOERRORS || errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: P.format }, `must match format "${P.format}"`)); valid = false; }
|
|
415
842
|
}
|
|
416
843
|
|
|
417
844
|
// arrays
|
|
418
|
-
if (
|
|
419
|
-
if (
|
|
420
|
-
if (
|
|
421
|
-
if (
|
|
845
|
+
if (P.hasArray && bits === T_ARRAY) {
|
|
846
|
+
if (P.minItems !== undefined && data.length < P.minItems) { errors === NOERRORS || errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: P.minItems }, `must NOT have fewer than ${P.minItems} items`)); valid = false; }
|
|
847
|
+
if (P.maxItems !== undefined && data.length > P.maxItems) { errors === NOERRORS || errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: P.maxItems }, `must NOT have more than ${P.maxItems} items`)); valid = false; }
|
|
848
|
+
if (P.uniqueItems) {
|
|
422
849
|
outer: for (let i = 0; i < data.length; i++) {
|
|
423
850
|
for (let j = i + 1; j < data.length; j++) {
|
|
424
851
|
if (deepEqual(data[i], data[j])) {
|
|
425
|
-
errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, 'must NOT have duplicate items'));
|
|
852
|
+
if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, 'must NOT have duplicate items'));
|
|
426
853
|
valid = false;
|
|
427
854
|
break outer;
|
|
428
855
|
}
|
|
429
856
|
}
|
|
430
857
|
}
|
|
431
858
|
}
|
|
432
|
-
const prefix =
|
|
433
|
-
if (prefix) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (!this.eval(prefix[i], data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack,
|
|
437
|
-
if (!local.items) local.items = new Set();
|
|
438
|
-
local.items.add(i);
|
|
859
|
+
const prefix = P.prefixItems;
|
|
860
|
+
if (prefix !== null) {
|
|
861
|
+
const n = Math.min(prefix.length, data.length);
|
|
862
|
+
for (let i = 0; i < n; i++) {
|
|
863
|
+
if (!this.eval(prefix[i], data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack, DISCARD)) valid = false;
|
|
864
|
+
if (collect) { if (!local.items) local.items = new Set(); local.items.add(i); }
|
|
439
865
|
}
|
|
440
866
|
}
|
|
441
|
-
if (
|
|
442
|
-
const start = prefix ? prefix.length : 0;
|
|
867
|
+
if (P.items !== undefined) {
|
|
868
|
+
const start = prefix !== null ? prefix.length : 0;
|
|
443
869
|
for (let i = start; i < data.length; i++) {
|
|
444
|
-
|
|
445
|
-
if (!
|
|
446
|
-
if (!local.items) local.items = new Set();
|
|
447
|
-
local.items.add(i);
|
|
870
|
+
if (!this.eval(P.items, data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/items', stack, DISCARD)) valid = false;
|
|
871
|
+
if (collect) { if (!local.items) local.items = new Set(); local.items.add(i); }
|
|
448
872
|
}
|
|
449
873
|
}
|
|
450
|
-
if (
|
|
874
|
+
if (P.contains !== undefined) {
|
|
451
875
|
const matched = [];
|
|
452
876
|
for (let i = 0; i < data.length; i++) {
|
|
453
|
-
const scratch = [];
|
|
454
|
-
|
|
455
|
-
if (this.eval(schema.contains, data[i], base, dynScope, scratch, instancePath + '/' + i, schemaPath + '/contains', stack, sub)) matched.push(i);
|
|
877
|
+
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
878
|
+
if (this.eval(P.contains, data[i], base, dynScope, scratch, instancePath + '/' + i, schemaPath + '/contains', stack, DISCARD)) matched.push(i);
|
|
456
879
|
}
|
|
457
|
-
const minC =
|
|
458
|
-
if (matched.length < minC) { errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC }, `must contain at least ${minC} valid item(s)`)); valid = false; }
|
|
459
|
-
if (
|
|
460
|
-
if (matched.length) {
|
|
880
|
+
const minC = P.minContains !== undefined ? P.minContains : 1;
|
|
881
|
+
if (matched.length < minC) { errors === NOERRORS || errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC }, `must contain at least ${minC} valid item(s)`)); valid = false; }
|
|
882
|
+
if (P.maxContains !== undefined && matched.length > P.maxContains) { errors === NOERRORS || errors.push(err('maxContains', 'maxContains', instancePath, schemaPath + '/maxContains', { limit: P.maxContains }, `must NOT contain more than ${P.maxContains} valid item(s)`)); valid = false; }
|
|
883
|
+
if (collect && matched.length) {
|
|
461
884
|
if (!local.items) local.items = new Set();
|
|
462
885
|
for (const i of matched) local.items.add(i);
|
|
463
886
|
}
|
|
@@ -465,63 +888,65 @@ class Interpreter {
|
|
|
465
888
|
}
|
|
466
889
|
|
|
467
890
|
// objects
|
|
468
|
-
if (
|
|
891
|
+
if (P.hasObject && bits === T_OBJECT) {
|
|
469
892
|
const keys = Object.keys(data);
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
893
|
+
if (P.required !== null) {
|
|
894
|
+
const req = P.required;
|
|
895
|
+
for (let i = 0; i < req.length; i++) {
|
|
896
|
+
const key = req[i];
|
|
897
|
+
if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
473
898
|
}
|
|
474
899
|
}
|
|
475
|
-
if (
|
|
476
|
-
if (
|
|
477
|
-
if (
|
|
478
|
-
for (const [key, deps] of
|
|
479
|
-
if (Object.
|
|
900
|
+
if (P.minProperties !== undefined && keys.length < P.minProperties) { errors === NOERRORS || errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
|
|
901
|
+
if (P.maxProperties !== undefined && keys.length > P.maxProperties) { errors === NOERRORS || errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: P.maxProperties }, `must NOT have more than ${P.maxProperties} properties`)); valid = false; }
|
|
902
|
+
if (P.dependentRequired !== null) {
|
|
903
|
+
for (const [key, deps] of P.dependentRequired) {
|
|
904
|
+
if (Object.hasOwn(data, key)) {
|
|
480
905
|
for (const dep of deps) {
|
|
481
|
-
if (!Object.
|
|
906
|
+
if (!Object.hasOwn(data, dep)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
|
|
482
907
|
}
|
|
483
908
|
}
|
|
484
909
|
}
|
|
485
910
|
}
|
|
486
|
-
if (
|
|
911
|
+
if (P.propertyNames !== undefined) {
|
|
487
912
|
for (const key of keys) {
|
|
488
|
-
if (!this.eval(
|
|
913
|
+
if (!this.eval(P.propertyNames, key, base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/propertyNames', stack, DISCARD)) valid = false;
|
|
489
914
|
}
|
|
490
915
|
}
|
|
491
|
-
const props =
|
|
492
|
-
const patterns =
|
|
493
|
-
for (
|
|
916
|
+
const props = P.properties;
|
|
917
|
+
const patterns = P.patternProperties;
|
|
918
|
+
for (let k = 0; k < keys.length; k++) {
|
|
919
|
+
const key = keys[k];
|
|
494
920
|
let evaluated = false;
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (!this.eval(
|
|
921
|
+
const prop = props !== null ? props.get(key) : undefined;
|
|
922
|
+
if (prop !== undefined) {
|
|
923
|
+
if (!this.eval(prop.node, data[key], base, dynScope, errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack, DISCARD)) valid = false;
|
|
498
924
|
evaluated = true;
|
|
499
925
|
}
|
|
500
|
-
if (patterns) {
|
|
501
|
-
for (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (!this.eval(
|
|
926
|
+
if (patterns !== null) {
|
|
927
|
+
for (let pi = 0; pi < patterns.length; pi++) {
|
|
928
|
+
const pp = patterns[pi];
|
|
929
|
+
if (pp.re.test(key)) {
|
|
930
|
+
if (!this.eval(pp.node, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(pp.src), stack, DISCARD)) valid = false;
|
|
505
931
|
evaluated = true;
|
|
506
932
|
}
|
|
507
933
|
}
|
|
508
934
|
}
|
|
509
|
-
if (!evaluated &&
|
|
510
|
-
|
|
511
|
-
if (!this.eval(schema.additionalProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack, sub)) valid = false;
|
|
935
|
+
if (!evaluated && P.additionalProperties !== undefined) {
|
|
936
|
+
if (!this.eval(P.additionalProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack, DISCARD)) valid = false;
|
|
512
937
|
evaluated = true;
|
|
513
938
|
}
|
|
514
|
-
if (evaluated) {
|
|
939
|
+
if (evaluated && collect) {
|
|
515
940
|
if (!local.props) local.props = new Set();
|
|
516
941
|
local.props.add(key);
|
|
517
942
|
}
|
|
518
943
|
}
|
|
519
|
-
if (
|
|
520
|
-
for (const [key, dep] of
|
|
521
|
-
if (Object.
|
|
522
|
-
const sub = { props: null, items: null };
|
|
944
|
+
if (P.dependentSchemas !== null) {
|
|
945
|
+
for (const [key, dep] of P.dependentSchemas) {
|
|
946
|
+
if (Object.hasOwn(data, key)) {
|
|
947
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
523
948
|
if (!this.eval(dep, data, base, dynScope, errors, instancePath, schemaPath + '/dependentSchemas/' + escapePointer(key), stack, sub)) valid = false;
|
|
524
|
-
else mergeAnnotations(local, sub);
|
|
949
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
525
950
|
}
|
|
526
951
|
}
|
|
527
952
|
}
|
|
@@ -529,125 +954,136 @@ class Interpreter {
|
|
|
529
954
|
// value rather than its presence. Only string values can match, since the
|
|
530
955
|
// inner keys are object keys. Equivalent to
|
|
531
956
|
// `if: { properties: { p: { const: v } }, required: [p] }, then: <schema>`.
|
|
532
|
-
if (
|
|
533
|
-
for (const [key, choices] of
|
|
534
|
-
if (!
|
|
535
|
-
if (!Object.prototype.hasOwnProperty.call(data, key)) continue;
|
|
957
|
+
if (P.propertyDependencies !== null) {
|
|
958
|
+
for (const [key, choices] of P.propertyDependencies) {
|
|
959
|
+
if (!Object.hasOwn(data, key)) continue;
|
|
536
960
|
const value = data[key];
|
|
537
961
|
if (typeof value !== 'string') continue;
|
|
538
|
-
|
|
539
|
-
|
|
962
|
+
const choice = choices.get(value);
|
|
963
|
+
if (choice === undefined) continue;
|
|
964
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
540
965
|
const branchPath = schemaPath + '/propertyDependencies/' + escapePointer(key) + '/' + escapePointer(value);
|
|
541
|
-
if (!this.eval(
|
|
542
|
-
else mergeAnnotations(local, sub);
|
|
966
|
+
if (!this.eval(choice, data, base, dynScope, errors, instancePath, branchPath, stack, sub)) valid = false;
|
|
967
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
543
968
|
}
|
|
544
969
|
}
|
|
545
970
|
}
|
|
546
971
|
|
|
547
972
|
// in-place applicators
|
|
548
|
-
if (
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
if (Array.isArray(schema.anyOf)) {
|
|
556
|
-
let any = false;
|
|
557
|
-
const scratch = [];
|
|
558
|
-
for (let i = 0; i < schema.anyOf.length; i++) {
|
|
559
|
-
const sub = { props: null, items: null };
|
|
560
|
-
if (this.eval(schema.anyOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, sub)) {
|
|
561
|
-
any = true;
|
|
562
|
-
mergeAnnotations(local, sub);
|
|
973
|
+
if (P.hasApplicators) {
|
|
974
|
+
if (P.allOf !== null) {
|
|
975
|
+
for (let i = 0; i < P.allOf.length; i++) {
|
|
976
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
977
|
+
if (!this.eval(P.allOf[i], data, base, dynScope, errors, instancePath, schemaPath + '/allOf/' + i, stack, sub)) valid = false;
|
|
978
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
563
979
|
}
|
|
564
980
|
}
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
winner = sub;
|
|
981
|
+
if (P.anyOf !== null) {
|
|
982
|
+
let any = false;
|
|
983
|
+
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
984
|
+
for (let i = 0; i < P.anyOf.length; i++) {
|
|
985
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
986
|
+
if (this.eval(P.anyOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, sub)) {
|
|
987
|
+
any = true;
|
|
988
|
+
if (collect) mergeAnnotations(local, sub);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (!any) {
|
|
992
|
+
if (errors !== NOERRORS) for (const e of scratch) errors.push(e);
|
|
993
|
+
if (errors !== NOERRORS) errors.push(err('anyOf', 'anyOf', instancePath, schemaPath + '/anyOf', {}, 'must match a schema in anyOf'));
|
|
994
|
+
valid = false;
|
|
580
995
|
}
|
|
581
996
|
}
|
|
582
|
-
if (
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
997
|
+
if (P.oneOf !== null) {
|
|
998
|
+
let count = 0;
|
|
999
|
+
let winner = null;
|
|
1000
|
+
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
1001
|
+
for (let i = 0; i < P.oneOf.length; i++) {
|
|
1002
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1003
|
+
if (this.eval(P.oneOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, sub)) {
|
|
1004
|
+
count++;
|
|
1005
|
+
winner = sub;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (count === 1) { if (collect) mergeAnnotations(local, winner); }
|
|
1009
|
+
else {
|
|
1010
|
+
if (count === 0) for (const e of scratch) errors.push(e);
|
|
1011
|
+
if (errors !== NOERRORS) errors.push(err('oneOf', 'oneOf', instancePath, schemaPath + '/oneOf', { passingSchemas: count }, 'must match exactly one schema in oneOf'));
|
|
1012
|
+
valid = false;
|
|
1013
|
+
}
|
|
587
1014
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1015
|
+
if (P.not !== undefined) {
|
|
1016
|
+
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
1017
|
+
if (this.eval(P.not, data, base, dynScope, scratch, instancePath, schemaPath + '/not', stack, DISCARD)) {
|
|
1018
|
+
if (errors !== NOERRORS) errors.push(err('not', 'not', instancePath, schemaPath + '/not', {}, 'must NOT be valid'));
|
|
1019
|
+
valid = false;
|
|
1020
|
+
}
|
|
594
1021
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1022
|
+
if (P.if !== undefined) {
|
|
1023
|
+
const ifSub = collect ? { props: null, items: null } : DISCARD;
|
|
1024
|
+
const ifScratch = errors === NOERRORS ? NOERRORS : [];
|
|
1025
|
+
const ifOk = this.eval(P.if, data, base, dynScope, ifScratch, instancePath, schemaPath + '/if', stack, ifSub);
|
|
1026
|
+
if (ifOk) {
|
|
1027
|
+
if (collect) mergeAnnotations(local, ifSub);
|
|
1028
|
+
if (P.then !== undefined) {
|
|
1029
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1030
|
+
if (!this.eval(P.then, data, base, dynScope, errors, instancePath, schemaPath + '/then', stack, sub)) valid = false;
|
|
1031
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
1032
|
+
}
|
|
1033
|
+
} else if (P.else !== undefined) {
|
|
1034
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1035
|
+
if (!this.eval(P.else, data, base, dynScope, errors, instancePath, schemaPath + '/else', stack, sub)) valid = false;
|
|
1036
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
606
1037
|
}
|
|
607
|
-
} else if (schema.else !== undefined) {
|
|
608
|
-
const sub = { props: null, items: null };
|
|
609
|
-
if (!this.eval(schema.else, data, base, dynScope, errors, instancePath, schemaPath + '/else', stack, sub)) valid = false;
|
|
610
|
-
else mergeAnnotations(local, sub);
|
|
611
1038
|
}
|
|
612
1039
|
}
|
|
613
1040
|
|
|
614
1041
|
// unevaluated*: run last, against annotations from everything above
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1042
|
+
if (P.hasUnevaluated) {
|
|
1043
|
+
if (P.unevaluatedProperties !== undefined && bits === T_OBJECT) {
|
|
1044
|
+
for (const key of Object.keys(data)) {
|
|
1045
|
+
if (local.props && local.props.has(key)) continue;
|
|
1046
|
+
if (!this.eval(P.unevaluatedProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack, DISCARD)) valid = false;
|
|
1047
|
+
if (!local.props) local.props = new Set();
|
|
1048
|
+
local.props.add(key);
|
|
1049
|
+
}
|
|
622
1050
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
local.items.add(i);
|
|
1051
|
+
if (P.unevaluatedItems !== undefined && bits === T_ARRAY) {
|
|
1052
|
+
for (let i = 0; i < data.length; i++) {
|
|
1053
|
+
if (local.items && local.items.has(i)) continue;
|
|
1054
|
+
if (!this.eval(P.unevaluatedItems, data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack, DISCARD)) valid = false;
|
|
1055
|
+
if (!local.items) local.items = new Set();
|
|
1056
|
+
local.items.add(i);
|
|
1057
|
+
}
|
|
631
1058
|
}
|
|
632
1059
|
}
|
|
633
1060
|
|
|
634
|
-
if (
|
|
1061
|
+
if (scopePushed) dynScope.pop();
|
|
1062
|
+
if (tracked) stack.length -= 2;
|
|
1063
|
+
if (valid && collect && sink !== DISCARD) mergeAnnotations(sink, local);
|
|
635
1064
|
return valid;
|
|
636
1065
|
}
|
|
637
1066
|
}
|
|
638
1067
|
|
|
639
|
-
function NO_ANNOTATIONS_SINK() {
|
|
640
|
-
return { props: null, items: null };
|
|
641
|
-
}
|
|
642
|
-
|
|
643
1068
|
function escapePointer(s) {
|
|
644
|
-
|
|
1069
|
+
if (typeof s !== 'string') s = String(s);
|
|
1070
|
+
// Keys almost never contain `~` or `/`; skip the two regex passes when so.
|
|
1071
|
+
for (let i = 0; i < s.length; i++) {
|
|
1072
|
+
const c = s.charCodeAt(i);
|
|
1073
|
+
if (c === 0x7e || c === 0x2f) return s.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
1074
|
+
}
|
|
1075
|
+
return s;
|
|
645
1076
|
}
|
|
646
1077
|
|
|
647
1078
|
function err(code, keyword, instancePath, schemaPath, params, message) {
|
|
648
1079
|
return { keyword, instancePath, schemaPath, params, message };
|
|
649
1080
|
}
|
|
650
1081
|
|
|
1082
|
+
const { compileInterpreter } = require('./plan-compiler').install({
|
|
1083
|
+
Plan, NOERRORS, err, evalLeaf, evalLeafV, deepEqual, dataBits, escapePointer,
|
|
1084
|
+
T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
|
|
1085
|
+
});
|
|
1086
|
+
|
|
651
1087
|
function createInterpreter(schema, options) {
|
|
652
1088
|
return new Interpreter(schema, options);
|
|
653
1089
|
}
|