@depup/joi 18.1.1-depup.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.
@@ -0,0 +1,986 @@
1
+ 'use strict';
2
+
3
+ const { assert, escapeRegex } = require('@hapi/hoek');
4
+ const { isDomainValid, isEmailValid, ipRegex, uriRegex } = require('@hapi/address');
5
+ const Tlds = require('@hapi/tlds');
6
+
7
+ const Any = require('./any');
8
+ const Common = require('../common');
9
+
10
+
11
+ const internals = {
12
+ tlds: Tlds.tlds instanceof Set ? { tlds: { allow: Tlds.tlds, deny: null } } : false, // $lab:coverage:ignore$
13
+ base64Regex: {
14
+ // paddingRequired
15
+ true: {
16
+ // urlSafe
17
+ true: /^(?:[\w\-]{2}[\w\-]{2})*(?:[\w\-]{2}==|[\w\-]{3}=)?$/,
18
+ false: /^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/
19
+ },
20
+ false: {
21
+ true: /^(?:[\w\-]{2}[\w\-]{2})*(?:[\w\-]{2}(==)?|[\w\-]{3}=?)?$/,
22
+ false: /^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}(==)?|[A-Za-z0-9+\/]{3}=?)?$/
23
+ }
24
+ },
25
+ dataUriRegex: /^data:[\w+.-]+\/[\w+.-]+;((charset=[\w-]+|base64),)?(.*)$/,
26
+ hexRegex: {
27
+ withPrefix: /^0x[0-9a-f]+$/i,
28
+ withOptionalPrefix: /^(?:0x)?[0-9a-f]+$/i,
29
+ withoutPrefix: /^[0-9a-f]+$/i
30
+ },
31
+ ipRegex: ipRegex({ cidr: 'forbidden' }).regex,
32
+ isoDurationRegex: /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/,
33
+
34
+ guidBrackets: {
35
+ '{': '}', '[': ']', '(': ')', '': ''
36
+ },
37
+ guidVersions: {
38
+ uuidv1: '1',
39
+ uuidv2: '2',
40
+ uuidv3: '3',
41
+ uuidv4: '4',
42
+ uuidv5: '5',
43
+ uuidv6: '6',
44
+ uuidv7: '7',
45
+ uuidv8: '8'
46
+ },
47
+ guidSeparators: new Set([undefined, true, false, '-', ':']),
48
+
49
+ normalizationForms: ['NFC', 'NFD', 'NFKC', 'NFKD']
50
+ };
51
+
52
+
53
+ module.exports = Any.extend({
54
+
55
+ type: 'string',
56
+
57
+ flags: {
58
+
59
+ insensitive: { default: false },
60
+ truncate: { default: false }
61
+ },
62
+
63
+ terms: {
64
+
65
+ replacements: { init: null }
66
+ },
67
+
68
+ coerce: {
69
+ from: 'string',
70
+ method(value, { schema, state, prefs }) {
71
+
72
+ const normalize = schema.$_getRule('normalize');
73
+ if (normalize) {
74
+ value = value.normalize(normalize.args.form);
75
+ }
76
+
77
+ const casing = schema.$_getRule('case');
78
+ if (casing) {
79
+ value = casing.args.direction === 'upper' ? value.toLocaleUpperCase() : value.toLocaleLowerCase();
80
+ }
81
+
82
+ const trim = schema.$_getRule('trim');
83
+ if (trim &&
84
+ trim.args.enabled) {
85
+
86
+ value = value.trim();
87
+ }
88
+
89
+ if (schema.$_terms.replacements) {
90
+ for (const replacement of schema.$_terms.replacements) {
91
+ value = value.replace(replacement.pattern, replacement.replacement);
92
+ }
93
+ }
94
+
95
+ const hex = schema.$_getRule('hex');
96
+ if (hex &&
97
+ hex.args.options.byteAligned &&
98
+ value.length % 2 !== 0) {
99
+
100
+ value = `0${value}`;
101
+ }
102
+
103
+ if (schema.$_getRule('isoDate')) {
104
+ const iso = internals.isoDate(value);
105
+ if (iso) {
106
+ value = iso;
107
+ }
108
+ }
109
+
110
+ if (schema._flags.truncate) {
111
+ const rule = schema.$_getRule('max');
112
+ if (rule) {
113
+ let limit = rule.args.limit;
114
+ if (Common.isResolvable(limit)) {
115
+ limit = limit.resolve(value, state, prefs);
116
+ if (!Common.limit(limit)) {
117
+ return { value, errors: schema.$_createError('any.ref', limit, { ref: rule.args.limit, arg: 'limit', reason: 'must be a positive integer' }, state, prefs) };
118
+ }
119
+ }
120
+
121
+ value = value.slice(0, limit);
122
+ }
123
+ }
124
+
125
+ return { value };
126
+ }
127
+ },
128
+
129
+ validate(value, { schema, error }) {
130
+
131
+ if (typeof value !== 'string') {
132
+ return { value, errors: error('string.base') };
133
+ }
134
+
135
+ if (value === '') {
136
+ const min = schema.$_getRule('min');
137
+ if (min &&
138
+ min.args.limit === 0) {
139
+
140
+ return;
141
+ }
142
+
143
+ return { value, errors: error('string.empty') };
144
+ }
145
+ },
146
+
147
+ jsonSchema(schema, res, mode, options) {
148
+
149
+ const noEmpty = !schema._valids?.has('') && !schema._flags.only;
150
+ if (noEmpty) {
151
+ const min = schema.$_getRule('min');
152
+ const length = schema.$_getRule('length');
153
+
154
+ if ((!min || min.args.limit > 0) &&
155
+ (!length || length.args.limit > 0)) {
156
+
157
+ res.minLength = 1;
158
+ }
159
+ }
160
+
161
+ return res;
162
+ },
163
+
164
+ rules: {
165
+
166
+ alphanum: {
167
+ method() {
168
+
169
+ return this.$_addRule('alphanum');
170
+ },
171
+ validate(value, helpers) {
172
+
173
+ if (/^[a-zA-Z0-9]+$/.test(value)) {
174
+ return value;
175
+ }
176
+
177
+ return helpers.error('string.alphanum');
178
+ }
179
+ },
180
+
181
+ base64: {
182
+ method(options = {}) {
183
+
184
+ Common.assertOptions(options, ['paddingRequired', 'urlSafe']);
185
+
186
+ options = { urlSafe: false, paddingRequired: true, ...options };
187
+ assert(typeof options.paddingRequired === 'boolean', 'paddingRequired must be boolean');
188
+ assert(typeof options.urlSafe === 'boolean', 'urlSafe must be boolean');
189
+
190
+ return this.$_addRule({ name: 'base64', args: { options } });
191
+ },
192
+ validate(value, helpers, { options }) {
193
+
194
+ const regex = internals.base64Regex[options.paddingRequired][options.urlSafe];
195
+ if (regex.test(value)) {
196
+ return value;
197
+ }
198
+
199
+ return helpers.error('string.base64');
200
+ },
201
+ jsonSchema(rule, res) {
202
+
203
+ res.format = 'base64';
204
+ return res;
205
+ }
206
+ },
207
+
208
+ case: {
209
+ method(direction) {
210
+
211
+ assert(['lower', 'upper'].includes(direction), 'Invalid case:', direction);
212
+
213
+ return this.$_addRule({ name: 'case', args: { direction } });
214
+ },
215
+ validate(value, helpers, { direction }) {
216
+
217
+ if (direction === 'lower' && value === value.toLocaleLowerCase() ||
218
+ direction === 'upper' && value === value.toLocaleUpperCase()) {
219
+
220
+ return value;
221
+ }
222
+
223
+ return helpers.error(`string.${direction}case`);
224
+ },
225
+ convert: true
226
+ },
227
+
228
+ creditCard: {
229
+ method() {
230
+
231
+ return this.$_addRule('creditCard');
232
+ },
233
+ validate(value, helpers) {
234
+
235
+ let i = value.length;
236
+ let sum = 0;
237
+ let mul = 1;
238
+
239
+ while (i--) {
240
+ const char = value.charAt(i) * mul;
241
+ sum = sum + (char - (char > 9) * 9);
242
+ mul = mul ^ 3;
243
+ }
244
+
245
+ if (sum > 0 &&
246
+ sum % 10 === 0) {
247
+
248
+ return value;
249
+ }
250
+
251
+ return helpers.error('string.creditCard');
252
+ }
253
+ },
254
+
255
+ dataUri: {
256
+ method(options = {}) {
257
+
258
+ Common.assertOptions(options, ['paddingRequired']);
259
+
260
+ options = { paddingRequired: true, ...options };
261
+ assert(typeof options.paddingRequired === 'boolean', 'paddingRequired must be boolean');
262
+
263
+ return this.$_addRule({ name: 'dataUri', args: { options } });
264
+ },
265
+ validate(value, helpers, { options }) {
266
+
267
+ const matches = value.match(internals.dataUriRegex);
268
+
269
+ if (matches) {
270
+ if (!matches[2]) {
271
+ return value;
272
+ }
273
+
274
+ if (matches[2] !== 'base64') {
275
+ return value;
276
+ }
277
+
278
+ const base64regex = internals.base64Regex[options.paddingRequired].false;
279
+ if (base64regex.test(matches[3])) {
280
+ return value;
281
+ }
282
+ }
283
+
284
+ return helpers.error('string.dataUri');
285
+ },
286
+ jsonSchema(rule, res) {
287
+
288
+ res.format = 'data-uri';
289
+ return res;
290
+ }
291
+ },
292
+
293
+ domain: {
294
+ method(options) {
295
+
296
+ if (options) {
297
+ Common.assertOptions(options, ['allowFullyQualified', 'allowUnicode', 'allowUnderscore', 'maxDomainSegments', 'minDomainSegments', 'tlds']);
298
+ }
299
+
300
+ const address = internals.addressOptions(options);
301
+ return this.$_addRule({ name: 'domain', args: { options }, address });
302
+ },
303
+ validate(value, helpers, args, { address }) {
304
+
305
+ if (isDomainValid(value, address)) {
306
+ return value;
307
+ }
308
+
309
+ return helpers.error('string.domain');
310
+ }
311
+ },
312
+
313
+ email: {
314
+ method(options = {}) {
315
+
316
+ Common.assertOptions(options, ['allowFullyQualified', 'allowUnicode', 'ignoreLength', 'maxDomainSegments', 'minDomainSegments', 'multiple', 'separator', 'tlds']);
317
+ assert(options.multiple === undefined || typeof options.multiple === 'boolean', 'multiple option must be an boolean');
318
+
319
+ const address = internals.addressOptions(options);
320
+ const regex = new RegExp(`\\s*[${options.separator ? escapeRegex(options.separator) : ','}]\\s*`);
321
+
322
+ return this.$_addRule({ name: 'email', args: { options }, regex, address });
323
+ },
324
+ validate(value, helpers, { options }, { regex, address }) {
325
+
326
+ const emails = options.multiple ? value.split(regex) : [value];
327
+ const invalids = [];
328
+ for (const email of emails) {
329
+ if (!isEmailValid(email, address)) {
330
+ invalids.push(email);
331
+ }
332
+ }
333
+
334
+ if (!invalids.length) {
335
+ return value;
336
+ }
337
+
338
+ return helpers.error('string.email', { value, invalids });
339
+ },
340
+ jsonSchema(rule, res) {
341
+
342
+ res.format = 'email';
343
+ return res;
344
+ }
345
+ },
346
+
347
+ guid: {
348
+ alias: 'uuid',
349
+ method(options = {}) {
350
+
351
+ Common.assertOptions(options, ['version', 'separator', 'wrapper']);
352
+
353
+ assert(
354
+ options.wrapper === undefined ||
355
+ typeof options.wrapper === 'boolean' ||
356
+ (typeof options.wrapper === 'string' && typeof internals.guidBrackets[options.wrapper] === 'string'),
357
+ `"wrapper" must be true, false, or one of "${Object.keys(internals.guidBrackets).filter(Boolean).join('", "')}"`
358
+ );
359
+
360
+ let versionNumbers = '';
361
+
362
+ if (options.version) {
363
+ const versions = [].concat(options.version);
364
+
365
+ assert(versions.length >= 1, 'version must have at least 1 valid version specified');
366
+ const set = new Set();
367
+
368
+ for (let i = 0; i < versions.length; ++i) {
369
+ const version = versions[i];
370
+ assert(typeof version === 'string', 'version at position ' + i + ' must be a string');
371
+ const versionNumber = internals.guidVersions[version.toLowerCase()];
372
+ assert(versionNumber, 'version at position ' + i + ' must be one of ' + Object.keys(internals.guidVersions).join(', '));
373
+ assert(!set.has(versionNumber), 'version at position ' + i + ' must not be a duplicate');
374
+
375
+ versionNumbers += versionNumber;
376
+ set.add(versionNumber);
377
+ }
378
+ }
379
+
380
+ assert(internals.guidSeparators.has(options.separator), 'separator must be one of true, false, "-", or ":"');
381
+ const separator = options.separator === undefined ? '[:-]?' :
382
+ options.separator === true ? '[:-]' :
383
+ options.separator === false ? '[]?' : `\\${options.separator}`;
384
+
385
+ let wrapperStart;
386
+ let wrapperEnd;
387
+
388
+ if (options.wrapper === undefined) {
389
+ wrapperStart = '[\\[{\\(]?';
390
+ wrapperEnd = '[\\]}\\)]?';
391
+ }
392
+ else if (options.wrapper === true) {
393
+ wrapperStart = '[\\[{\\(]';
394
+ wrapperEnd = '[\\]}\\)]';
395
+ }
396
+ else if (options.wrapper === false) {
397
+ wrapperStart = '';
398
+ wrapperEnd = '';
399
+ }
400
+ else {
401
+ wrapperStart = escapeRegex(options.wrapper);
402
+ wrapperEnd = escapeRegex(internals.guidBrackets[options.wrapper]);
403
+ }
404
+
405
+ const regex = new RegExp(
406
+ `^(${wrapperStart})[0-9A-F]{8}(${separator})[0-9A-F]{4}\\2?[${
407
+ versionNumbers || '0-9A-F'
408
+ }][0-9A-F]{3}\\2?[${
409
+ versionNumbers ? '89AB' : '0-9A-F'
410
+ }][0-9A-F]{3}\\2?[0-9A-F]{12}(${wrapperEnd})$`,
411
+ 'i'
412
+ );
413
+
414
+ return this.$_addRule({ name: 'guid', args: { options }, regex });
415
+ },
416
+
417
+ validate(value, helpers, args, { regex }) {
418
+
419
+ const results = regex.exec(value);
420
+
421
+ if (!results) {
422
+ return helpers.error('string.guid');
423
+ }
424
+
425
+ const open = results[1];
426
+ const close = results[results.length - 1];
427
+
428
+ if ((open || close) && internals.guidBrackets[open] !== close) {
429
+ return helpers.error('string.guid');
430
+ }
431
+
432
+ return value;
433
+ },
434
+ jsonSchema(rule, res) {
435
+
436
+ res.format = 'uuid';
437
+ return res;
438
+ }
439
+ },
440
+
441
+ hex: {
442
+ method(options = {}) {
443
+
444
+ Common.assertOptions(options, ['byteAligned', 'prefix']);
445
+
446
+ options = { byteAligned: false, prefix: false, ...options };
447
+ assert(typeof options.byteAligned === 'boolean', 'byteAligned must be boolean');
448
+ assert(typeof options.prefix === 'boolean' || options.prefix === 'optional', 'prefix must be boolean or "optional"');
449
+
450
+ return this.$_addRule({ name: 'hex', args: { options } });
451
+ },
452
+ validate(value, helpers, { options }) {
453
+
454
+ const re = options.prefix === 'optional' ?
455
+ internals.hexRegex.withOptionalPrefix :
456
+ options.prefix === true ?
457
+ internals.hexRegex.withPrefix :
458
+ internals.hexRegex.withoutPrefix;
459
+ if (!re.test(value)) {
460
+ return helpers.error('string.hex');
461
+ }
462
+
463
+ if (options.byteAligned &&
464
+ value.length % 2 !== 0) {
465
+
466
+ return helpers.error('string.hexAlign');
467
+ }
468
+
469
+ return value;
470
+ },
471
+ jsonSchema(rule, res) {
472
+
473
+ res.format = 'hex';
474
+ return res;
475
+ }
476
+ },
477
+
478
+ hostname: {
479
+ method() {
480
+
481
+ return this.$_addRule('hostname');
482
+ },
483
+ validate(value, helpers) {
484
+
485
+ if (isDomainValid(value, { minDomainSegments: 1 }) ||
486
+ internals.ipRegex.test(value)) {
487
+
488
+ return value;
489
+ }
490
+
491
+ return helpers.error('string.hostname');
492
+ },
493
+ jsonSchema(rule, res) {
494
+
495
+ res.format = 'hostname';
496
+ return res;
497
+ }
498
+ },
499
+
500
+ insensitive: {
501
+ method() {
502
+
503
+ return this.$_setFlag('insensitive', true);
504
+ }
505
+ },
506
+
507
+ ip: {
508
+ method(options = {}) {
509
+
510
+ Common.assertOptions(options, ['cidr', 'version']);
511
+
512
+ const { cidr, versions, regex } = ipRegex(options);
513
+ const version = options.version ? versions : undefined;
514
+ return this.$_addRule({ name: 'ip', args: { options: { cidr, version } }, regex });
515
+ },
516
+ validate(value, helpers, { options }, { regex }) {
517
+
518
+ if (regex.test(value)) {
519
+ return value;
520
+ }
521
+
522
+ if (options.version) {
523
+ return helpers.error('string.ipVersion', { value, cidr: options.cidr, version: options.version });
524
+ }
525
+
526
+ return helpers.error('string.ip', { value, cidr: options.cidr });
527
+ },
528
+ jsonSchema(rule, res) {
529
+
530
+ const version = rule.args.options.version;
531
+ if (version && version.length === 1) {
532
+ res.format = version[0];
533
+ }
534
+ else {
535
+ res.format = 'ip';
536
+ }
537
+
538
+ return res;
539
+ }
540
+ },
541
+
542
+ isoDate: {
543
+ method() {
544
+
545
+ return this.$_addRule('isoDate');
546
+ },
547
+ validate(value, { error }) {
548
+
549
+ if (internals.isoDate(value)) {
550
+ return value;
551
+ }
552
+
553
+ return error('string.isoDate');
554
+ },
555
+ jsonSchema(rule, res) {
556
+
557
+ res.format = 'date-time';
558
+ return res;
559
+ }
560
+ },
561
+
562
+ isoDuration: {
563
+ method() {
564
+
565
+ return this.$_addRule('isoDuration');
566
+ },
567
+ validate(value, helpers) {
568
+
569
+ if (internals.isoDurationRegex.test(value)) {
570
+ return value;
571
+ }
572
+
573
+ return helpers.error('string.isoDuration');
574
+ },
575
+ jsonSchema(rule, res) {
576
+
577
+ res.format = 'duration';
578
+ return res;
579
+ }
580
+ },
581
+
582
+ length: {
583
+ method(limit, encoding) {
584
+
585
+ return internals.length(this, 'length', limit, '=', encoding);
586
+ },
587
+ validate(value, helpers, { limit, encoding }, { name, operator, args }) {
588
+
589
+ const length = encoding ? Buffer && Buffer.byteLength(value, encoding) : value.length; // $lab:coverage:ignore$
590
+ if (Common.compare(length, limit, operator)) {
591
+ return value;
592
+ }
593
+
594
+ return helpers.error('string.' + name, { limit: args.limit, value, encoding });
595
+ },
596
+ jsonSchema(rule, res) {
597
+
598
+ res.minLength = rule.args.limit;
599
+ res.maxLength = rule.args.limit;
600
+ return res;
601
+ },
602
+ args: [
603
+ {
604
+ name: 'limit',
605
+ ref: true,
606
+ assert: Common.limit,
607
+ message: 'must be a positive integer'
608
+ },
609
+ 'encoding'
610
+ ]
611
+ },
612
+
613
+ lowercase: {
614
+ method() {
615
+
616
+ return this.case('lower');
617
+ }
618
+ },
619
+
620
+ max: {
621
+ method(limit, encoding) {
622
+
623
+ return internals.length(this, 'max', limit, '<=', encoding);
624
+ },
625
+ jsonSchema(rule, res) {
626
+
627
+ res.maxLength = rule.args.limit;
628
+ return res;
629
+ },
630
+ args: ['limit', 'encoding']
631
+ },
632
+
633
+ min: {
634
+ method(limit, encoding) {
635
+
636
+ return internals.length(this, 'min', limit, '>=', encoding);
637
+ },
638
+ jsonSchema(rule, res) {
639
+
640
+ if (rule.args.limit > 0) {
641
+
642
+ res.minLength = rule.args.limit;
643
+ }
644
+
645
+ return res;
646
+ },
647
+ args: ['limit', 'encoding']
648
+ },
649
+
650
+ normalize: {
651
+ method(form = 'NFC') {
652
+
653
+ assert(internals.normalizationForms.includes(form), 'normalization form must be one of ' + internals.normalizationForms.join(', '));
654
+
655
+ return this.$_addRule({ name: 'normalize', args: { form } });
656
+ },
657
+ validate(value, { error }, { form }) {
658
+
659
+ if (value === value.normalize(form)) {
660
+ return value;
661
+ }
662
+
663
+ return error('string.normalize', { value, form });
664
+ },
665
+ convert: true
666
+ },
667
+
668
+ pattern: {
669
+ alias: 'regex',
670
+ method(regex, options = {}) {
671
+
672
+ assert(regex instanceof RegExp, 'regex must be a RegExp');
673
+ assert(!regex.flags.includes('g') && !regex.flags.includes('y'), 'regex should not use global or sticky mode');
674
+
675
+ if (typeof options === 'string') {
676
+ options = { name: options };
677
+ }
678
+
679
+ Common.assertOptions(options, ['invert', 'name']);
680
+
681
+ const errorCode = ['string.pattern', options.invert ? '.invert' : '', options.name ? '.name' : '.base'].join('');
682
+ return this.$_addRule({ name: 'pattern', args: { regex, options }, errorCode });
683
+ },
684
+ validate(value, helpers, { regex, options }, { errorCode }) {
685
+
686
+ const patternMatch = regex.test(value);
687
+
688
+ if (patternMatch ^ options.invert) {
689
+ return value;
690
+ }
691
+
692
+ return helpers.error(errorCode, { name: options.name, regex, value });
693
+ },
694
+ jsonSchema(rule, res) {
695
+
696
+ res.pattern = rule.args.regex.source;
697
+ return res;
698
+ },
699
+ args: ['regex', 'options'],
700
+ multi: true
701
+ },
702
+
703
+ replace: {
704
+ method(pattern, replacement) {
705
+
706
+ if (typeof pattern === 'string') {
707
+ pattern = new RegExp(escapeRegex(pattern), 'g');
708
+ }
709
+
710
+ assert(pattern instanceof RegExp, 'pattern must be a RegExp');
711
+ assert(typeof replacement === 'string', 'replacement must be a String');
712
+
713
+ const obj = this.clone();
714
+
715
+ if (!obj.$_terms.replacements) {
716
+ obj.$_terms.replacements = [];
717
+ }
718
+
719
+ obj.$_terms.replacements.push({ pattern, replacement });
720
+ return obj;
721
+ }
722
+ },
723
+
724
+ token: {
725
+ method() {
726
+
727
+ return this.$_addRule('token');
728
+ },
729
+ validate(value, helpers) {
730
+
731
+ if (/^\w+$/.test(value)) {
732
+ return value;
733
+ }
734
+
735
+ return helpers.error('string.token');
736
+ },
737
+ jsonSchema(rule, res) {
738
+
739
+ res.format = 'token';
740
+ return res;
741
+ }
742
+ },
743
+
744
+ trim: {
745
+ method(enabled = true) {
746
+
747
+ assert(typeof enabled === 'boolean', 'enabled must be a boolean');
748
+
749
+ return this.$_addRule({ name: 'trim', args: { enabled } });
750
+ },
751
+ validate(value, helpers, { enabled }) {
752
+
753
+ if (!enabled ||
754
+ value === value.trim()) {
755
+
756
+ return value;
757
+ }
758
+
759
+ return helpers.error('string.trim');
760
+ },
761
+ convert: true
762
+ },
763
+
764
+ truncate: {
765
+ method(enabled = true) {
766
+
767
+ assert(typeof enabled === 'boolean', 'enabled must be a boolean');
768
+
769
+ return this.$_setFlag('truncate', enabled);
770
+ }
771
+ },
772
+
773
+ uppercase: {
774
+ method() {
775
+
776
+ return this.case('upper');
777
+ }
778
+ },
779
+
780
+ uri: {
781
+ method(options = {}) {
782
+
783
+ Common.assertOptions(options, ['allowRelative', 'allowQuerySquareBrackets', 'domain', 'relativeOnly', 'scheme', 'encodeUri']);
784
+
785
+ if (options.domain) {
786
+ Common.assertOptions(options.domain, ['allowFullyQualified', 'allowUnicode', 'maxDomainSegments', 'minDomainSegments', 'tlds']);
787
+ }
788
+
789
+ const { regex, scheme } = uriRegex(options);
790
+ const domain = options.domain ? internals.addressOptions(options.domain) : null;
791
+ return this.$_addRule({ name: 'uri', args: { options }, regex, domain, scheme });
792
+ },
793
+ validate(value, helpers, { options }, { regex, domain, scheme }) {
794
+
795
+ if (['http:/', 'https:/'].includes(value)) { // scheme:/ is technically valid but makes no sense
796
+ return helpers.error('string.uri');
797
+ }
798
+
799
+ let match = regex.exec(value);
800
+
801
+ if (!match && helpers.prefs.convert && options.encodeUri) {
802
+ const encoded = encodeURI(value);
803
+ match = regex.exec(encoded);
804
+ if (match) {
805
+ value = encoded;
806
+ }
807
+ }
808
+
809
+ if (match) {
810
+ const matched = match[1] || match[2];
811
+ if (domain &&
812
+ (!options.allowRelative || matched) &&
813
+ !isDomainValid(matched, domain)) {
814
+
815
+ return helpers.error('string.domain', { value: matched });
816
+ }
817
+
818
+ return value;
819
+ }
820
+
821
+ if (options.relativeOnly) {
822
+ return helpers.error('string.uriRelativeOnly');
823
+ }
824
+
825
+ if (options.scheme) {
826
+ return helpers.error('string.uriCustomScheme', { scheme, value });
827
+ }
828
+
829
+ return helpers.error('string.uri');
830
+ },
831
+ jsonSchema(rule, res) {
832
+
833
+ res.format = 'uri';
834
+ return res;
835
+ }
836
+ }
837
+ },
838
+
839
+ manifest: {
840
+
841
+ build(obj, desc) {
842
+
843
+ if (desc.replacements) {
844
+ for (const { pattern, replacement } of desc.replacements) {
845
+ obj = obj.replace(pattern, replacement);
846
+ }
847
+ }
848
+
849
+ return obj;
850
+ }
851
+ },
852
+
853
+ messages: {
854
+ 'string.alphanum': '{{#label}} must only contain alpha-numeric characters',
855
+ 'string.base': '{{#label}} must be a string',
856
+ 'string.base64': '{{#label}} must be a valid base64 string',
857
+ 'string.creditCard': '{{#label}} must be a credit card',
858
+ 'string.dataUri': '{{#label}} must be a valid dataUri string',
859
+ 'string.domain': '{{#label}} must contain a valid domain name',
860
+ 'string.email': '{{#label}} must be a valid email',
861
+ 'string.empty': '{{#label}} is not allowed to be empty',
862
+ 'string.guid': '{{#label}} must be a valid GUID',
863
+ 'string.hex': '{{#label}} must only contain hexadecimal characters',
864
+ 'string.hexAlign': '{{#label}} hex decoded representation must be byte aligned',
865
+ 'string.hostname': '{{#label}} must be a valid hostname',
866
+ 'string.ip': '{{#label}} must be a valid ip address with a {{#cidr}} CIDR',
867
+ 'string.ipVersion': '{{#label}} must be a valid ip address of one of the following versions {{#version}} with a {{#cidr}} CIDR',
868
+ 'string.isoDate': '{{#label}} must be in iso format',
869
+ 'string.isoDuration': '{{#label}} must be a valid ISO 8601 duration',
870
+ 'string.length': '{{#label}} length must be {{#limit}} characters long',
871
+ 'string.lowercase': '{{#label}} must only contain lowercase characters',
872
+ 'string.max': '{{#label}} length must be less than or equal to {{#limit}} characters long',
873
+ 'string.min': '{{#label}} length must be at least {{#limit}} characters long',
874
+ 'string.normalize': '{{#label}} must be unicode normalized in the {{#form}} form',
875
+ 'string.token': '{{#label}} must only contain alpha-numeric and underscore characters',
876
+ 'string.pattern.base': '{{#label}} with value {:[.]} fails to match the required pattern: {{#regex}}',
877
+ 'string.pattern.name': '{{#label}} with value {:[.]} fails to match the {{#name}} pattern',
878
+ 'string.pattern.invert.base': '{{#label}} with value {:[.]} matches the inverted pattern: {{#regex}}',
879
+ 'string.pattern.invert.name': '{{#label}} with value {:[.]} matches the inverted {{#name}} pattern',
880
+ 'string.trim': '{{#label}} must not have leading or trailing whitespace',
881
+ 'string.uri': '{{#label}} must be a valid uri',
882
+ 'string.uriCustomScheme': '{{#label}} must be a valid uri with a scheme matching the {{#scheme}} pattern',
883
+ 'string.uriRelativeOnly': '{{#label}} must be a valid relative uri',
884
+ 'string.uppercase': '{{#label}} must only contain uppercase characters'
885
+ }
886
+ });
887
+
888
+
889
+ // Helpers
890
+
891
+ internals.addressOptions = function (options) {
892
+
893
+ if (!options) {
894
+ return internals.tlds || options; // $lab:coverage:ignore$
895
+ }
896
+
897
+ // minDomainSegments
898
+
899
+ assert(options.minDomainSegments === undefined ||
900
+ Number.isSafeInteger(options.minDomainSegments) && options.minDomainSegments > 0, 'minDomainSegments must be a positive integer');
901
+
902
+ // maxDomainSegments
903
+
904
+ assert(options.maxDomainSegments === undefined ||
905
+ Number.isSafeInteger(options.maxDomainSegments) && options.maxDomainSegments > 0, 'maxDomainSegments must be a positive integer');
906
+
907
+ // tlds
908
+
909
+ if (options.tlds === false) {
910
+ return options;
911
+ }
912
+
913
+ if (options.tlds === true ||
914
+ options.tlds === undefined) {
915
+
916
+ assert(internals.tlds, 'Built-in TLD list disabled');
917
+ return Object.assign({}, options, internals.tlds);
918
+ }
919
+
920
+ assert(typeof options.tlds === 'object', 'tlds must be true, false, or an object');
921
+
922
+ const deny = options.tlds.deny;
923
+ if (deny) {
924
+ if (Array.isArray(deny)) {
925
+ options = Object.assign({}, options, { tlds: { deny: new Set(deny) } });
926
+ }
927
+
928
+ assert(options.tlds.deny instanceof Set, 'tlds.deny must be an array, Set, or boolean');
929
+ assert(!options.tlds.allow, 'Cannot specify both tlds.allow and tlds.deny lists');
930
+ internals.validateTlds(options.tlds.deny, 'tlds.deny');
931
+ return options;
932
+ }
933
+
934
+ const allow = options.tlds.allow;
935
+ if (!allow) {
936
+ return { ...options, tlds: false };
937
+ }
938
+
939
+ if (allow === true) {
940
+ assert(internals.tlds, 'Built-in TLD list disabled');
941
+ return Object.assign({}, options, internals.tlds);
942
+ }
943
+
944
+ if (Array.isArray(allow)) {
945
+ options = Object.assign({}, options, { tlds: { allow: new Set(allow) } });
946
+ }
947
+
948
+ assert(options.tlds.allow instanceof Set, 'tlds.allow must be an array, Set, or boolean');
949
+ internals.validateTlds(options.tlds.allow, 'tlds.allow');
950
+ return options;
951
+ };
952
+
953
+
954
+ internals.validateTlds = function (set, source) {
955
+
956
+ for (const tld of set) {
957
+ assert(isDomainValid(tld, { minDomainSegments: 1, maxDomainSegments: 1 }), `${source} must contain valid top level domain names`);
958
+ }
959
+ };
960
+
961
+
962
+ internals.isoDate = function (value) {
963
+
964
+ if (!Common.isIsoDate(value)) {
965
+ return null;
966
+ }
967
+
968
+ if (/.*T.*[+-]\d\d$/.test(value)) { // Add missing trailing zeros to timeshift
969
+ value += '00';
970
+ }
971
+
972
+ const date = new Date(value);
973
+ if (isNaN(date.getTime())) {
974
+ return null;
975
+ }
976
+
977
+ return date.toISOString();
978
+ };
979
+
980
+
981
+ internals.length = function (schema, name, limit, operator, encoding) {
982
+
983
+ assert(!encoding || Buffer && Buffer.isEncoding(encoding), 'Invalid encoding:', encoding); // $lab:coverage:ignore$
984
+
985
+ return schema.$_addRule({ name, method: 'length', args: { limit, encoding }, operator });
986
+ };