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/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~]*(?:[\/?#][!#$&-;=?-\[\]_a-z~]*)?|(?!\/\/)[!#$&-;=?-\[\]_a-z~]*)$/;
289
-
290
- function uri (s) {
291
- if (URI_FAST.test(s)) return true;
292
- const n = s.length;
293
- if (n === 0) return false;
294
- let c = s.charCodeAt(0);
295
- // A scheme opens with a letter, never a digit or a sign.
296
- if (c > 127 || SCHEME_CHAR[c] === 0 || (c >= 48 && c <= 57) || c === 43 || c === 45 || c === 46) return false;
297
- let colon = -1;
298
- for (let i = 1; i < n; i++) {
299
- c = s.charCodeAt(i);
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_%]|%[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}|\*)?)*$/;
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 IPv4 address.
722
- if (s.startsWith('IPv6:', at + 2)) return ipv6(s.slice(at + 7, n - 1));
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, uriChars, uriAuthority, iri, iriReference, idnEmail, DURATION_RE, URI_TEMPLATE_EXPR };
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
+ };