ata-validator 1.53.0 → 1.54.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 +7 -0
- package/compat.js +13 -2
- package/lib/codegen-engine.js +2 -1
- package/lib/dialect.js +36 -1
- package/lib/draft7.js +17 -28
- package/lib/enrich-site.js +29 -2
- package/lib/error-runtime.js +6 -2
- package/lib/formats-source.js +43 -66
- package/lib/formats.js +37 -231
- package/lib/interpreter-compat.js +144 -0
- package/lib/interpreter-core.js +46 -142
- package/lib/js-compiler-errors.js +52 -16
- package/lib/js-compiler.js +537 -65
- package/lib/metaschemas.js +5 -3
- package/lib/plan-compiler.js +24 -7
- package/lib/regex-linear.js +81 -1
- package/lib/safe-regex.js +14 -11
- package/lib/schema-scan.js +19 -28
- package/lib/tier0.js +1 -0
- package/lib/unique-items.js +16 -5
- package/lib/uri-core.js +155 -0
- package/lib/validator-core.js +20 -11
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/formats.js
CHANGED
|
@@ -245,34 +245,6 @@ function hostname (s, from, to) {
|
|
|
245
245
|
return labelLength !== 0 && previous !== 45;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
// Character classes as tables rather than comparison chains. Nine `===` tests
|
|
249
|
-
// per character more than doubled the cost of the scan they guarded, measured
|
|
250
|
-
// against the same loop doing a single range check; an indexed byte read costs
|
|
251
|
-
// the same whatever the class holds.
|
|
252
|
-
const URI_CHAR = new Uint8Array(128);
|
|
253
|
-
for (let i = 33; i < 127; i++) URI_CHAR[i] = 1;
|
|
254
|
-
// " < > \ ^ ` { | } are printable and still not allowed in a URI.
|
|
255
|
-
for (const c of [34, 60, 62, 92, 94, 96, 123, 124, 125]) URI_CHAR[c] = 0;
|
|
256
|
-
const HEX_CHAR = new Uint8Array(128);
|
|
257
|
-
for (let i = 48; i < 58; i++) HEX_CHAR[i] = 1;
|
|
258
|
-
for (let i = 97; i < 103; i++) HEX_CHAR[i] = 1;
|
|
259
|
-
for (let i = 65; i < 71; i++) HEX_CHAR[i] = 1;
|
|
260
|
-
// Scheme characters: letters, digits, "+", "-", ".".
|
|
261
|
-
const SCHEME_CHAR = new Uint8Array(128);
|
|
262
|
-
for (let i = 48; i < 58; i++) SCHEME_CHAR[i] = 1;
|
|
263
|
-
for (let i = 97; i < 123; i++) SCHEME_CHAR[i] = 1;
|
|
264
|
-
for (let i = 65; i < 91; i++) SCHEME_CHAR[i] = 1;
|
|
265
|
-
SCHEME_CHAR[43] = 1; SCHEME_CHAR[45] = 1; SCHEME_CHAR[46] = 1;
|
|
266
|
-
|
|
267
|
-
// A scheme followed by a colon, an optional authority, and no character a URI
|
|
268
|
-
// cannot hold anywhere after the colon.
|
|
269
|
-
//
|
|
270
|
-
// One walk answers all of it. The authority's landmarks, the last "@", the
|
|
271
|
-
// colons after it and whether a bracket appeared before it, are recorded while
|
|
272
|
-
// the characters are being checked, so the authority is never cut out of the
|
|
273
|
-
// string or scanned again. Reading it out with `slice` and asking the copy for
|
|
274
|
-
// `lastIndexOf` and two regular expressions allocated a string per URI, and a
|
|
275
|
-
// document carrying a handful of URLs paid that per field.
|
|
276
248
|
// The shape almost every URI in a document has, as one regular expression: a
|
|
277
249
|
// scheme, then either "//" and a host with no userinfo, port, brackets or
|
|
278
250
|
// percent escapes, or no authority at all, then a tail of characters RFC 3986
|
|
@@ -285,78 +257,18 @@ SCHEME_CHAR[43] = 1; SCHEME_CHAR[45] = 1; SCHEME_CHAR[46] = 1;
|
|
|
285
257
|
// group merely optional, "http://[::1" could skip it and match "//[::1" as a
|
|
286
258
|
// path, accepting a string the walk rejects for its unclosed bracket.
|
|
287
259
|
// tests/test_uri_fast_path.js holds the subset relation.
|
|
288
|
-
const URI_FAST = /^[A-Za-z][A-Za-z0-9+.\-]*:(?:\/\/[!$&-.0-9;=A-Z_a-z~]*(?:[
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (c === 58) { colon = i; break; }
|
|
301
|
-
if (c > 127 || SCHEME_CHAR[c] === 0) return false;
|
|
302
|
-
}
|
|
303
|
-
if (colon === -1) return false;
|
|
304
|
-
|
|
305
|
-
let i = colon + 1;
|
|
306
|
-
let authEnd = n;
|
|
307
|
-
if (s.charCodeAt(i) === 47 && s.charCodeAt(i + 1) === 47) {
|
|
308
|
-
const authStart = i + 2;
|
|
309
|
-
let at = -1, firstColon = -1, lastColon = -1, sawBracket = 0, bracketBeforeAt = 0;
|
|
310
|
-
let hostStart = authStart;
|
|
311
|
-
authEnd = -1;
|
|
312
|
-
for (i = authStart; i < n; i++) {
|
|
313
|
-
c = s.charCodeAt(i);
|
|
314
|
-
if (c > 127 || URI_CHAR[c] === 0) return false;
|
|
315
|
-
if (c === 47 || c === 63 || c === 35) { authEnd = i; break; }
|
|
316
|
-
if (c === 37) {
|
|
317
|
-
const h1 = s.charCodeAt(i + 1), h2 = s.charCodeAt(i + 2);
|
|
318
|
-
// A "%" at the end of the string reads as NaN, which no comparison
|
|
319
|
-
// admits; `<= 127` is written that way so NaN takes the reject.
|
|
320
|
-
if (!(h1 <= 127) || !(h2 <= 127) || HEX_CHAR[h1] === 0 || HEX_CHAR[h2] === 0) return false;
|
|
321
|
-
i += 2;
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
// The last "@" ends userinfo, so everything recorded before it belongs
|
|
325
|
-
// to a part that has its own rules and is dropped here.
|
|
326
|
-
if (c === 64) { bracketBeforeAt = sawBracket; at = i; firstColon = -1; lastColon = -1; hostStart = i + 1; }
|
|
327
|
-
else if (c === 58) { if (firstColon === -1) firstColon = i; lastColon = i; }
|
|
328
|
-
else if (c === 91 || c === 93) { sawBracket = 1; }
|
|
329
|
-
}
|
|
330
|
-
if (authEnd === -1) authEnd = n;
|
|
331
|
-
// Brackets belong to the host, so one before the "@" is not userinfo.
|
|
332
|
-
if (at !== -1 && bracketBeforeAt) return false;
|
|
333
|
-
if (s.charCodeAt(hostStart) === 91) { // '['
|
|
334
|
-
let close = -1;
|
|
335
|
-
for (let j = hostStart + 1; j < authEnd; j++) { if (s.charCodeAt(j) === 93) { close = j; break; } }
|
|
336
|
-
if (close === -1) return false;
|
|
337
|
-
if (close + 1 !== authEnd) {
|
|
338
|
-
if (s.charCodeAt(close + 1) !== 58) return false;
|
|
339
|
-
if (!allDigits(s, close + 2, authEnd)) return false;
|
|
340
|
-
}
|
|
341
|
-
} else if (lastColon !== -1) {
|
|
342
|
-
// A host holding more than one colon is an IPv6 address, and those must
|
|
343
|
-
// be bracketed. Without that rule the last group reads as a port number.
|
|
344
|
-
if (firstColon !== lastColon) return false;
|
|
345
|
-
if (!allDigits(s, lastColon + 1, authEnd)) return false;
|
|
346
|
-
}
|
|
347
|
-
i = authEnd;
|
|
348
|
-
}
|
|
349
|
-
for (; i < n; i++) {
|
|
350
|
-
c = s.charCodeAt(i);
|
|
351
|
-
if (c > 127 || URI_CHAR[c] === 0) return false;
|
|
352
|
-
if (c === 37) {
|
|
353
|
-
const h1 = s.charCodeAt(i + 1), h2 = s.charCodeAt(i + 2);
|
|
354
|
-
if (!(h1 <= 127) || !(h2 <= 127) || HEX_CHAR[h1] === 0 || HEX_CHAR[h2] === 0) return false;
|
|
355
|
-
i += 2;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return true;
|
|
359
|
-
}
|
|
260
|
+
const URI_FAST = /^[A-Za-z][A-Za-z0-9+.\-]*:(?:\/\/[!$&-.0-9;=A-Z_a-z~]*(?::[0-9]*)?(?:[\/?][!$&-;=?-Z_a-z~]*)?|(?!\/\/)[!$&-;=?-Z_a-z~]*)(?:#[!$&-;=?-Z_a-z~]*)?$/;
|
|
261
|
+
|
|
262
|
+
// The walk behind the expression, shared with the code generator
|
|
263
|
+
// (lib/uri-core.js). An IRI is a URI that also admits non-ASCII, so the
|
|
264
|
+
// expression answers its common shapes too and anything else goes to the
|
|
265
|
+
// walk. A reference goes straight to the walk, which opens with a scan for
|
|
266
|
+
// the plain shapes.
|
|
267
|
+
const _uriWalk = require('./uri-core').__ataUri(ipv6);
|
|
268
|
+
function uri (s) { return URI_FAST.test(s) || _uriWalk(s, 1); }
|
|
269
|
+
function uriReference (s) { return _uriWalk(s, 0); }
|
|
270
|
+
function iri (s) { return URI_FAST.test(s) || _uriWalk(s, 3); }
|
|
271
|
+
function iriReference (s) { return _uriWalk(s, 2); }
|
|
360
272
|
|
|
361
273
|
// The characters a URI cannot hold: C0 controls, space, DEL, and the rest of
|
|
362
274
|
// Unicode whitespace. Asking for that set directly costs more than it looks:
|
|
@@ -374,128 +286,6 @@ const NON_PRINTABLE = /[^\u0021-\u007e]/;
|
|
|
374
286
|
// " < > \ ^ ` { | }, plus everything below 0x21 and above 0x7e. The old check
|
|
375
287
|
// only looked for whitespace and non-printables, so a backslash or a lone
|
|
376
288
|
// percent sign passed as a URI.
|
|
377
|
-
function uriChar (c) {
|
|
378
|
-
if (c < 33 || c > 126) return false;
|
|
379
|
-
return !(c === 34 || c === 60 || c === 62 || c === 92 ||
|
|
380
|
-
c === 94 || c === 96 || c === 123 || c === 124 || c === 125);
|
|
381
|
-
}
|
|
382
|
-
function isHex (c) {
|
|
383
|
-
return (c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70);
|
|
384
|
-
}
|
|
385
|
-
// The same set through the URI_CHAR and HEX_CHAR tables: one load per
|
|
386
|
-
// character, where uriChar() was a call and up to eleven comparisons, and on
|
|
387
|
-
// SchemaStore's SARIF samples, full of URIs, that loop was a sixth of what the
|
|
388
|
-
// interpreted engine spent. A "%" at the end reads past the string, NaN,
|
|
389
|
-
// which fails the bound as it failed isHex().
|
|
390
|
-
function uriChars (s, from) {
|
|
391
|
-
for (let i = from; i < s.length; i++) {
|
|
392
|
-
const c = s.charCodeAt(i);
|
|
393
|
-
if (c > 127 || URI_CHAR[c] === 0) return false;
|
|
394
|
-
if (c === 37) { // '%'
|
|
395
|
-
const h1 = s.charCodeAt(i + 1), h2 = s.charCodeAt(i + 2);
|
|
396
|
-
if (!(h1 <= 127) || !(h2 <= 127) || HEX_CHAR[h1] === 0 || HEX_CHAR[h2] === 0) return false;
|
|
397
|
-
i += 2;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
return true;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function allDigits (s, from, to) {
|
|
404
|
-
for (let i = from; i < to; i++) {
|
|
405
|
-
const c = s.charCodeAt(i);
|
|
406
|
-
if (c < 48 || c > 57) return false;
|
|
407
|
-
}
|
|
408
|
-
return true;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// The authority sits between "//" and the next "/", "?" or "#". A bracketed
|
|
412
|
-
// host must close, and a port is digits.
|
|
413
|
-
//
|
|
414
|
-
// Everything here reads the original string through index bounds. Cutting the
|
|
415
|
-
// authority out with `slice` and asking it for `lastIndexOf` and two regular
|
|
416
|
-
// expressions allocated a string for every URI validated, and a document
|
|
417
|
-
// carrying a handful of URLs paid that per field; the authority of an ordinary
|
|
418
|
-
// URL is a dozen characters, so walking it twice costs less than copying it
|
|
419
|
-
// once.
|
|
420
|
-
function uriAuthority (s, start) {
|
|
421
|
-
if (s.charCodeAt(start) !== 47 || s.charCodeAt(start + 1) !== 47) return true;
|
|
422
|
-
const n = s.length;
|
|
423
|
-
const authStart = start + 2;
|
|
424
|
-
let authEnd = n;
|
|
425
|
-
for (let i = authStart; i < n; i++) {
|
|
426
|
-
const c = s.charCodeAt(i);
|
|
427
|
-
if (c === 47 || c === 63 || c === 35) { authEnd = i; break; }
|
|
428
|
-
}
|
|
429
|
-
// The last "@" splits userinfo from the host. Brackets belong to the host,
|
|
430
|
-
// so one before the "@" is not userinfo.
|
|
431
|
-
let at = -1;
|
|
432
|
-
for (let i = authEnd - 1; i >= authStart; i--) {
|
|
433
|
-
if (s.charCodeAt(i) === 64) { at = i; break; }
|
|
434
|
-
}
|
|
435
|
-
if (at !== -1) {
|
|
436
|
-
for (let i = authStart; i < at; i++) {
|
|
437
|
-
const c = s.charCodeAt(i);
|
|
438
|
-
if (c === 91 || c === 93) return false;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
const hpStart = at === -1 ? authStart : at + 1;
|
|
442
|
-
if (s.charCodeAt(hpStart) === 91) { // '['
|
|
443
|
-
let close = -1;
|
|
444
|
-
for (let i = hpStart + 1; i < authEnd; i++) {
|
|
445
|
-
if (s.charCodeAt(i) === 93) { close = i; break; }
|
|
446
|
-
}
|
|
447
|
-
if (close === -1) return false;
|
|
448
|
-
if (close + 1 === authEnd) return true;
|
|
449
|
-
if (s.charCodeAt(close + 1) !== 58) return false;
|
|
450
|
-
return allDigits(s, close + 2, authEnd);
|
|
451
|
-
}
|
|
452
|
-
let firstColon = -1;
|
|
453
|
-
let lastColon = -1;
|
|
454
|
-
for (let i = hpStart; i < authEnd; i++) {
|
|
455
|
-
if (s.charCodeAt(i) === 58) {
|
|
456
|
-
if (firstColon === -1) firstColon = i;
|
|
457
|
-
lastColon = i;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
if (lastColon === -1) return true;
|
|
461
|
-
// A host holding more than one colon is an IPv6 address, and those must be
|
|
462
|
-
// bracketed. Without that rule the last group reads as a port number.
|
|
463
|
-
if (firstColon !== lastColon) return false;
|
|
464
|
-
return allDigits(s, lastColon + 1, authEnd);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// An IRI is a URI that also admits non-ASCII. The ASCII rules are identical,
|
|
468
|
-
// so the same character test runs with everything above 0x7e allowed through.
|
|
469
|
-
function iriChar (c) { return c > 126 ? c !== 127 : uriChar(c); }
|
|
470
|
-
function iriChars (s, from) {
|
|
471
|
-
for (let i = from; i < s.length; i++) {
|
|
472
|
-
const c = s.charCodeAt(i);
|
|
473
|
-
if (!iriChar(c)) return false;
|
|
474
|
-
if (c === 37) {
|
|
475
|
-
if (!isHex(s.charCodeAt(i + 1)) || !isHex(s.charCodeAt(i + 2))) return false;
|
|
476
|
-
i += 2;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return true;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function iri (s) {
|
|
483
|
-
const n = s.length;
|
|
484
|
-
if (n === 0) return false;
|
|
485
|
-
const first = s.charCodeAt(0);
|
|
486
|
-
if (!((first >= 97 && first <= 122) || (first >= 65 && first <= 90))) return false;
|
|
487
|
-
let colon = -1;
|
|
488
|
-
for (let i = 1; i < n; i++) {
|
|
489
|
-
const c = s.charCodeAt(i);
|
|
490
|
-
if (c === 58) { colon = i; break; }
|
|
491
|
-
if (!(isDigit(c) || (c >= 97 && c <= 122) || (c >= 65 && c <= 90) ||
|
|
492
|
-
c === 43 || c === 45 || c === 46)) return false;
|
|
493
|
-
}
|
|
494
|
-
if (colon === -1) return false;
|
|
495
|
-
return iriChars(s, colon + 1) && uriAuthority(s, colon + 1);
|
|
496
|
-
}
|
|
497
|
-
function iriReference (s) { return iriChars(s, 0); }
|
|
498
|
-
|
|
499
289
|
// An internationalised mailbox differs from a mailbox only in admitting
|
|
500
290
|
// non-ASCII on both sides. The separator stays the ASCII "@": a fullwidth one
|
|
501
291
|
// is a character in the local part, not a separator.
|
|
@@ -514,6 +304,10 @@ function idnEmail (s) {
|
|
|
514
304
|
}
|
|
515
305
|
if (domain.charCodeAt(0) === 46 || domain.charCodeAt(domain.length - 1) === 46) return false;
|
|
516
306
|
if (domain.indexOf('..') !== -1) return false;
|
|
307
|
+
// No label starts or ends with a hyphen (RFC 5321 sub-domain, as for
|
|
308
|
+
// `hostname`): with the dots checked above, that is a hyphen at either end
|
|
309
|
+
// or next to a dot.
|
|
310
|
+
if (domain.charCodeAt(0) === 45 || domain.charCodeAt(domain.length - 1) === 45 || domain.indexOf('-.') !== -1 || domain.indexOf('.-') !== -1) return false;
|
|
517
311
|
for (let i = 0; i < domain.length; i++) {
|
|
518
312
|
const c = domain.charCodeAt(i);
|
|
519
313
|
if (c <= 32 || c === 127 || c === 64) return false;
|
|
@@ -612,10 +406,6 @@ function time (s) {
|
|
|
612
406
|
}
|
|
613
407
|
|
|
614
408
|
// Anything without a control character or whitespace.
|
|
615
|
-
function uriReference (s) {
|
|
616
|
-
return uriChars(s, 0);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
409
|
// --- RFC 6901 JSON pointers -------------------------------------------------
|
|
620
410
|
// A pointer is empty or a run of "/"-prefixed tokens, and the only escape is
|
|
621
411
|
// "~" followed by 0 or 1. Everything else, including "#", is not a pointer:
|
|
@@ -652,14 +442,21 @@ function relativeJsonPointer(s) {
|
|
|
652
442
|
// expression: an optional operator, then a comma-separated list of varspecs,
|
|
653
443
|
// each a possibly dotted name of varchars with an optional ":n" prefix length
|
|
654
444
|
// or "*" explode.
|
|
655
|
-
const URI_TEMPLATE_EXPR = /^(?:[+#./;?&=,!@|]?)(?:[A-Za-z0-9_
|
|
445
|
+
const URI_TEMPLATE_EXPR = /^(?:[+#./;?&=,!@|]?)(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+(?:\.(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+)*(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+(?:\.(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+)*(?::[1-9][0-9]{0,3}|\*)?)*$/;
|
|
446
|
+
function isHexCode (c) { return (c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70); }
|
|
656
447
|
function uriTemplate(s) {
|
|
657
448
|
let i = 0;
|
|
658
449
|
while (i < s.length) {
|
|
659
450
|
const c = s.charCodeAt(i);
|
|
660
451
|
if (c === 125) return false; // '}' with no opening brace
|
|
661
|
-
// A literal excludes the controls, the space and DEL
|
|
452
|
+
// A literal excludes the controls, the space and DEL, and a "%" in it
|
|
453
|
+
// introduces two hex digits.
|
|
662
454
|
if (c !== 123 && (c <= 32 || c === 127)) return false;
|
|
455
|
+
if (c === 37) {
|
|
456
|
+
const h1 = s.charCodeAt(i + 1), h2 = s.charCodeAt(i + 2);
|
|
457
|
+
if (!(h1 <= 127) || !(h2 <= 127) || !isHexCode(h1) || !isHexCode(h2)) return false;
|
|
458
|
+
i += 3; continue;
|
|
459
|
+
}
|
|
663
460
|
if (c !== 123) { i++; continue; } // not '{'
|
|
664
461
|
const end = s.indexOf('}', i + 1);
|
|
665
462
|
if (end === -1) return false;
|
|
@@ -698,6 +495,14 @@ function email(s) {
|
|
|
698
495
|
at = s.lastIndexOf('@');
|
|
699
496
|
if (at < 2 || at === n - 1 || at > 64) return false;
|
|
700
497
|
if (s.charCodeAt(at - 1) !== 34) return false;
|
|
498
|
+
// Inside the quotes: printable ASCII other than the quote and the
|
|
499
|
+
// backslash, or a backslash followed by one printable ASCII character
|
|
500
|
+
// (RFC 5321 quoted-pairSMTP); "\©" is not a quoted pair.
|
|
501
|
+
for (let i = 1; i < at - 1; i++) {
|
|
502
|
+
const c = s.charCodeAt(i);
|
|
503
|
+
if (c === 92) { const q = s.charCodeAt(i + 1); if (q < 32 || q > 126 || i + 1 >= at - 1) return false; i++; continue; }
|
|
504
|
+
if (c < 32 || c > 126 || c === 34) return false;
|
|
505
|
+
}
|
|
701
506
|
} else {
|
|
702
507
|
if (s.charCodeAt(0) === 46) return false; // a leading dot is not a mailbox
|
|
703
508
|
at = -1;
|
|
@@ -718,8 +523,9 @@ function email(s) {
|
|
|
718
523
|
}
|
|
719
524
|
if (s.charCodeAt(at + 1) === 91) { // '['
|
|
720
525
|
if (s.charCodeAt(n - 1) !== 93 || n - at - 1 <= 2) return false;
|
|
721
|
-
// IPv6 literals are tagged; anything else in brackets is an
|
|
722
|
-
|
|
526
|
+
// IPv6 literals are tagged, in any case; anything else in brackets is an
|
|
527
|
+
// IPv4 address. `| 32` lowers an ASCII letter and leaves "6" and ":".
|
|
528
|
+
if ((s.charCodeAt(at + 2) | 32) === 105 && (s.charCodeAt(at + 3) | 32) === 112 && (s.charCodeAt(at + 4) | 32) === 118 && s.charCodeAt(at + 5) === 54 && s.charCodeAt(at + 6) === 58) return ipv6(s.slice(at + 7, n - 1));
|
|
723
529
|
return ipv4(s.slice(at + 2, n - 1));
|
|
724
530
|
}
|
|
725
531
|
return hostname(s, at + 1, n);
|
|
@@ -732,4 +538,4 @@ function email(s) {
|
|
|
732
538
|
const DURATION_RE = /^P(?:\d+W|(?:\d+Y(?:\d+M(?:\d+D)?)?|\d+M(?:\d+D)?|\d+D)(?:T(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S))?|T(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S))$/;
|
|
733
539
|
function duration (s) { return DURATION_RE.test(s); }
|
|
734
540
|
|
|
735
|
-
module.exports = { IPV4, IPV6_FULL, ipv4Range, URI_FAST, date, ipv4, dateTime, ipv6, hostname, uri, uriReference, uuid, time, noReserved, jsonPointer, relativeJsonPointer, uriTemplate, email, duration,
|
|
541
|
+
module.exports = { IPV4, IPV6_FULL, ipv4Range, URI_FAST, date, ipv4, dateTime, ipv6, hostname, uri, uriReference, uuid, time, noReserved, jsonPointer, relativeJsonPointer, uriTemplate, email, duration, iri, iriReference, idnEmail, DURATION_RE, URI_TEMPLATE_EXPR };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// What compat reads the reference's way while the interpreted engine walks a
|
|
4
|
+
// document: coercion where each `type` is checked, and the discriminator in
|
|
5
|
+
// place of `oneOf`. compat installs this (lib/interpreter-core.js, setCompat),
|
|
6
|
+
// so an application that never imports compat does not bundle it.
|
|
7
|
+
|
|
8
|
+
function isDiscriminated(schema) {
|
|
9
|
+
const d = schema.discriminator;
|
|
10
|
+
return d !== null && typeof d === 'object' && typeof d.propertyName === 'string';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// The reference's coercion: the first type in the list the value converts to,
|
|
14
|
+
// after unwrapping a one-element array in 'array' mode, which then converts
|
|
15
|
+
// the element.
|
|
16
|
+
const NO_COERCION = Symbol('no coercion');
|
|
17
|
+
function fitsType(d, t) {
|
|
18
|
+
switch (t) {
|
|
19
|
+
case 'string': return typeof d === 'string';
|
|
20
|
+
case 'number': return typeof d === 'number' && isFinite(d);
|
|
21
|
+
case 'integer': return typeof d === 'number' && Number.isInteger(d);
|
|
22
|
+
case 'boolean': return typeof d === 'boolean';
|
|
23
|
+
case 'null': return d === null;
|
|
24
|
+
case 'array': return Array.isArray(d);
|
|
25
|
+
case 'object': return d !== null && typeof d === 'object' && !Array.isArray(d);
|
|
26
|
+
default: return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Whether an unwrapped element is kept as it is. The reference tests a list
|
|
30
|
+
// of types here with every type at once, so a list of two or more keeps
|
|
31
|
+
// nothing (`number` stands in for `integer` beside it), and the element goes
|
|
32
|
+
// on to be converted, as one of a single type does when it does not fit.
|
|
33
|
+
function unwrappedFits(d, types) {
|
|
34
|
+
let list = types;
|
|
35
|
+
if (list.length > 1 && list.includes('number')) list = list.filter((t) => t !== 'integer');
|
|
36
|
+
if (list.length > 1 && list.includes('array') && list.includes('object')) return false;
|
|
37
|
+
for (const t of list) if (!fitsType(d, t)) return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
function coerceValue(data, types, arrayMode) {
|
|
41
|
+
if (arrayMode && Array.isArray(data) && data.length === 1) {
|
|
42
|
+
data = data[0];
|
|
43
|
+
if (unwrappedFits(data, types)) return data;
|
|
44
|
+
}
|
|
45
|
+
const kind = data === null ? 'null' : typeof data;
|
|
46
|
+
for (const t of types) {
|
|
47
|
+
switch (t) {
|
|
48
|
+
case 'number':
|
|
49
|
+
case 'integer':
|
|
50
|
+
if (kind === 'boolean' || kind === 'null') return +data;
|
|
51
|
+
// eslint-disable-next-line eqeqeq
|
|
52
|
+
if (kind === 'string' && data !== '' && data == +data && (t === 'number' || !(data % 1))) return +data;
|
|
53
|
+
break;
|
|
54
|
+
case 'string':
|
|
55
|
+
if (kind === 'number' || kind === 'boolean') return '' + data;
|
|
56
|
+
if (kind === 'null') return '';
|
|
57
|
+
break;
|
|
58
|
+
case 'boolean':
|
|
59
|
+
if (data === 'false' || data === 0 || data === null) return false;
|
|
60
|
+
if (data === 'true' || data === 1) return true;
|
|
61
|
+
break;
|
|
62
|
+
case 'null':
|
|
63
|
+
if (data === '' || data === 0 || data === false) return null;
|
|
64
|
+
break;
|
|
65
|
+
case 'array':
|
|
66
|
+
if (arrayMode && (kind === 'string' || kind === 'number' || kind === 'boolean' || kind === 'null')) return [data];
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return NO_COERCION;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = function install({ NOERRORS, DISCARD, err, mergeAnnotations, dataCtx }) {
|
|
74
|
+
// The discriminator in place of oneOf: the branch the tag names is applied,
|
|
75
|
+
// and a tag that is not a string, or names no branch, is the error.
|
|
76
|
+
function discriminate(interp, P, data, base, dynScope, errors, instancePath, schemaPath, stack, local) {
|
|
77
|
+
const d = discrMap(interp, P, base);
|
|
78
|
+
const tag = data[d.tagName];
|
|
79
|
+
if (typeof tag !== 'string') {
|
|
80
|
+
if (errors !== NOERRORS) errors.push(err('discriminator', 'discriminator', instancePath, schemaPath + '/discriminator', { error: 'tag', tag: d.tagName, tagValue: tag }, `tag "${d.tagName}" must be string`));
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const i = d.map.get(tag);
|
|
84
|
+
if (i === undefined) {
|
|
85
|
+
if (errors !== NOERRORS) errors.push(err('discriminator', 'discriminator', instancePath, schemaPath + '/discriminator', { error: 'mapping', tag: d.tagName, tagValue: tag }, `value of tag "${d.tagName}" must be in oneOf`));
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const sub = local !== DISCARD ? { props: null, items: null } : DISCARD;
|
|
89
|
+
const ok = interp.eval(P.oneOf[i], data, base, dynScope, errors, instancePath, schemaPath + '/oneOf/' + i, stack, sub);
|
|
90
|
+
if (ok && local !== DISCARD) mergeAnnotations(local, sub);
|
|
91
|
+
return ok;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Tag value to branch index, read from each branch's `properties`, through
|
|
95
|
+
// a `$ref`, with the reference's rules: a `const` or `enum` of unique
|
|
96
|
+
// strings, and the tag required by the node or by every branch.
|
|
97
|
+
function discrMap(interp, P, base) {
|
|
98
|
+
if (interp._discr === null) interp._discr = new Map();
|
|
99
|
+
let d = interp._discr.get(P);
|
|
100
|
+
if (d !== undefined) return d;
|
|
101
|
+
const tagName = P.schema.discriminator.propertyName;
|
|
102
|
+
if (P.schema.discriminator.mapping !== undefined) throw new Error('discriminator: mapping is not supported');
|
|
103
|
+
const hasRequired = (s) => s !== null && typeof s === 'object' && Array.isArray(s.required) && s.required.includes(tagName);
|
|
104
|
+
let topRequired = hasRequired(P.schema);
|
|
105
|
+
const map = new Map();
|
|
106
|
+
for (let i = 0; i < P.oneOf.length; i++) {
|
|
107
|
+
let node = P.oneOf[i];
|
|
108
|
+
if (node !== null && typeof node === 'object' && node.ref !== null && node.ref !== undefined && (node.schema.properties === undefined)) {
|
|
109
|
+
const r = interp.resolveRefCached(node, base);
|
|
110
|
+
if (r.child === undefined) throw new Error(`discriminator: cannot resolve $ref ${node.ref}`);
|
|
111
|
+
node = r.child;
|
|
112
|
+
}
|
|
113
|
+
const sch = node !== null && typeof node === 'object' ? node.schema : undefined;
|
|
114
|
+
const propSch = sch && sch.properties ? sch.properties[tagName] : undefined;
|
|
115
|
+
if (propSch === null || typeof propSch !== 'object') throw new Error(`discriminator: oneOf subschemas (or referenced schemas) must have "properties/${tagName}"`);
|
|
116
|
+
topRequired = topRequired || hasRequired(sch);
|
|
117
|
+
const values = propSch.const !== undefined ? [propSch.const] : Array.isArray(propSch.enum) ? propSch.enum : null;
|
|
118
|
+
if (values === null) throw new Error(`discriminator: "properties/${tagName}" must have "const" or "enum"`);
|
|
119
|
+
for (const v of values) {
|
|
120
|
+
if (typeof v !== 'string' || map.has(v)) throw new Error(`discriminator: "${tagName}" values must be unique strings`);
|
|
121
|
+
map.set(v, i);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!topRequired) throw new Error(`discriminator: "${tagName}" must be required`);
|
|
125
|
+
d = { tagName, map };
|
|
126
|
+
interp._discr.set(P, d);
|
|
127
|
+
return d;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Coerces the value at `instancePath` to the node's type and writes it where
|
|
131
|
+
// it sits; at the root, where it sits nowhere, the walk carries it on.
|
|
132
|
+
function coerceAt(interp, P, data, instancePath) {
|
|
133
|
+
const to = coerceValue(data, P.typeNames, interp.coerce === 'array');
|
|
134
|
+
if (to === NO_COERCION) return data;
|
|
135
|
+
if (instancePath === '') interp._ctxRoot = to;
|
|
136
|
+
else {
|
|
137
|
+
const ctx = dataCtx(instancePath, '', '');
|
|
138
|
+
if (ctx.parentData !== null && typeof ctx.parentData === 'object') ctx.parentData[ctx.parentDataProperty] = to;
|
|
139
|
+
}
|
|
140
|
+
return to;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { isDiscriminated, discriminate, coerceAt };
|
|
144
|
+
};
|