joi 17.1.1 → 17.4.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/manifest.js CHANGED
@@ -256,7 +256,7 @@ internals.Builder = class {
256
256
 
257
257
  // Type
258
258
 
259
- let schema = this.joi[desc.type]();
259
+ let schema = this.joi[desc.type]()._bare();
260
260
  const def = schema._definition;
261
261
 
262
262
  // Flags
package/lib/ref.js CHANGED
@@ -25,7 +25,7 @@ const internals = {
25
25
  exports.create = function (key, options = {}) {
26
26
 
27
27
  Assert(typeof key === 'string', 'Invalid reference key:', key);
28
- Common.assertOptions(options, ['adjust', 'ancestor', 'in', 'iterables', 'map', 'prefix', 'separator']);
28
+ Common.assertOptions(options, ['adjust', 'ancestor', 'in', 'iterables', 'map', 'prefix', 'render', 'separator']);
29
29
  Assert(!options.prefix || typeof options.prefix === 'object', 'options.prefix must be of type object');
30
30
 
31
31
  const ref = Object.assign({}, internals.defaults, options);
@@ -77,7 +77,7 @@ exports.create = function (key, options = {}) {
77
77
 
78
78
  exports.in = function (key, options = {}) {
79
79
 
80
- return exports.create(key, Object.assign({}, options, { in: true }));
80
+ return exports.create(key, { ...options, in: true });
81
81
  };
82
82
 
83
83
 
@@ -93,8 +93,8 @@ internals.Ref = class {
93
93
 
94
94
  Assert(typeof options === 'object', 'Invalid reference construction');
95
95
  Common.assertOptions(options, [
96
- 'adjust', 'ancestor', 'in', 'iterables', 'map', 'path', 'separator', 'type', // Copied
97
- 'depth', 'key', 'root', 'display' // Overridden
96
+ 'adjust', 'ancestor', 'in', 'iterables', 'map', 'path', 'render', 'separator', 'type', // Copied
97
+ 'depth', 'key', 'root', 'display' // Overridden
98
98
  ]);
99
99
 
100
100
  Assert([false, undefined].includes(options.separator) || typeof options.separator === 'string' && options.separator.length === 1, 'Invalid separator');
@@ -211,8 +211,10 @@ internals.Ref = class {
211
211
  ref.map = [...this.map];
212
212
  }
213
213
 
214
- for (const key of ['adjust', 'iterables']) {
215
- if (this[key] !== null) {
214
+ for (const key of ['adjust', 'iterables', 'render']) {
215
+ if (this[key] !== null &&
216
+ this[key] !== undefined) {
217
+
216
218
  ref[key] = this[key];
217
219
  }
218
220
  }
package/lib/schemas.js CHANGED
@@ -17,6 +17,7 @@ internals.wrap = Joi.string()
17
17
  exports.preferences = Joi.object({
18
18
  allowUnknown: Joi.boolean(),
19
19
  abortEarly: Joi.boolean(),
20
+ artifacts: Joi.boolean(),
20
21
  cache: Joi.boolean(),
21
22
  context: Joi.object(),
22
23
  convert: Joi.boolean(),
@@ -89,6 +90,10 @@ exports.extension = Joi.object({
89
90
  ])
90
91
  .required(),
91
92
  args: Joi.function(),
93
+ cast: Joi.object().pattern(internals.nameRx, Joi.object({
94
+ from: Joi.function().maxArity(1).required(),
95
+ to: Joi.function().minArity(1).maxArity(2).required()
96
+ })),
92
97
  base: Joi.object().schema()
93
98
  .when('type', { is: Joi.object().regex(), then: Joi.forbidden() }),
94
99
  coerce: [
@@ -157,7 +162,8 @@ internals.desc = {
157
162
  map: Joi.array().items(Joi.array().length(2)).min(1),
158
163
  adjust: Joi.function(),
159
164
  iterables: Joi.boolean(),
160
- in: Joi.boolean()
165
+ in: Joi.boolean(),
166
+ render: Joi.boolean()
161
167
  })
162
168
  .required()
163
169
  }),
@@ -244,6 +250,7 @@ exports.description = Joi.object({
244
250
  preferences: {
245
251
  allowUnknown: Joi.boolean(),
246
252
  abortEarly: Joi.boolean(),
253
+ artifacts: Joi.boolean(),
247
254
  cache: Joi.boolean(),
248
255
  convert: Joi.boolean(),
249
256
  dateFormat: ['date', 'iso', 'string', 'time', 'utc'],
package/lib/template.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const Assert = require('@hapi/hoek/lib/assert');
4
4
  const Clone = require('@hapi/hoek/lib/clone');
5
5
  const EscapeHtml = require('@hapi/hoek/lib/escapeHtml');
6
- const Formula = require('@hapi/formula');
6
+ const Formula = require('@sideway/formula');
7
7
 
8
8
  const Common = require('./common');
9
9
  const Errors = require('./errors');
@@ -78,8 +78,13 @@ module.exports = exports = internals.Template = class {
78
78
  continue;
79
79
  }
80
80
 
81
- const variable = part.slice(raw ? 0 : 1, end);
82
- const dynamic = this._ref(internals.decode(variable), raw);
81
+ let variable = part.slice(raw ? 0 : 1, end);
82
+ const wrapped = variable[0] === ':';
83
+ if (wrapped) {
84
+ variable = variable.slice(1);
85
+ }
86
+
87
+ const dynamic = this._ref(internals.decode(variable), { raw, wrapped });
83
88
  processed.push(dynamic);
84
89
  if (typeof dynamic !== 'string') {
85
90
  refs = true;
@@ -184,11 +189,10 @@ module.exports = exports = internals.Template = class {
184
189
  }
185
190
  else {
186
191
  const rendered = this._part(part, /* context -> [*/ value, state, prefs, local, options /*] */);
187
- const string = internals.stringify(rendered, prefs, options.errors);
192
+ const string = internals.stringify(rendered, value, state, prefs, local, options);
188
193
  if (string !== undefined) {
189
194
  const result = part.raw || (options.errors && options.errors.escapeHtml) === false ? string : EscapeHtml(string);
190
- const ends = part.ref && part.ref.type === 'local' && part.ref.key === 'label' && prefs.errors.wrap.label;
191
- parts.push(internals.wrap(result, ends));
195
+ parts.push(internals.wrap(result, part.wrapped && prefs.errors.wrap.label));
192
196
  }
193
197
  }
194
198
  }
@@ -196,7 +200,7 @@ module.exports = exports = internals.Template = class {
196
200
  return parts.join('');
197
201
  }
198
202
 
199
- _ref(content, raw) {
203
+ _ref(content, { raw, wrapped }) {
200
204
 
201
205
  const refs = [];
202
206
  const reference = (variable) => {
@@ -216,7 +220,8 @@ module.exports = exports = internals.Template = class {
216
220
 
217
221
  if (formula.single) {
218
222
  if (formula.single.type === 'reference') {
219
- return { ref: refs[0], raw, refs };
223
+ const ref = refs[0];
224
+ return { ref, raw, refs, wrapped: wrapped || ref.type === 'local' && ref.key === 'label' };
220
225
  }
221
226
 
222
227
  return internals.stringify(formula.single.value);
@@ -302,10 +307,18 @@ internals.wrap = function (value, ends) {
302
307
  };
303
308
 
304
309
 
305
- internals.stringify = function (value, prefs, options) {
310
+ internals.stringify = function (value, original, state, prefs, local, options) {
306
311
 
307
312
  const type = typeof value;
308
313
 
314
+ let skipWrap = false;
315
+ if (Ref.isRef(value) &&
316
+ value.render) {
317
+
318
+ skipWrap = value.in;
319
+ value = value.resolve(original, state, prefs, local, { in: value.in, ...options });
320
+ }
321
+
309
322
  if (value === null) {
310
323
  return 'null';
311
324
  }
@@ -344,7 +357,11 @@ internals.stringify = function (value, prefs, options) {
344
357
 
345
358
  let partial = '';
346
359
  for (const item of value) {
347
- partial = partial + (partial.length ? ', ' : '') + internals.stringify(item, prefs, options);
360
+ partial = partial + (partial.length ? ', ' : '') + internals.stringify(item, original, state, prefs, local, options);
361
+ }
362
+
363
+ if (skipWrap) {
364
+ return partial;
348
365
  }
349
366
 
350
367
  return internals.wrap(partial, prefs.errors.wrap.array);
package/lib/trace.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const DeepEqual = require('@hapi/hoek/lib/deepEqual');
4
- const Pinpoint = require('@hapi/pinpoint');
4
+ const Pinpoint = require('@sideway/pinpoint');
5
5
 
6
6
  const Errors = require('./errors');
7
7
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Assert = require('@hapi/hoek/lib/assert');
4
+ const Merge = require('@hapi/hoek/lib/merge');
4
5
 
5
6
  const Any = require('./any');
6
7
  const Common = require('../common');
@@ -44,8 +45,7 @@ module.exports = Any.extend({
44
45
  // Match all or one
45
46
 
46
47
  if (schema._flags.match) {
47
- let hits = 0;
48
- let matched;
48
+ const matched = [];
49
49
 
50
50
  for (let i = 0; i < schema.$_terms.matches.length; ++i) {
51
51
  const item = schema.$_terms.matches[i];
@@ -54,23 +54,27 @@ module.exports = Any.extend({
54
54
 
55
55
  const result = item.schema.$_validate(value, localState, prefs);
56
56
  if (!result.errors) {
57
- ++hits;
58
- matched = result.value;
57
+ matched.push(result.value);
59
58
  }
60
59
  else {
61
60
  localState.restore();
62
61
  }
63
62
  }
64
63
 
65
- if (!hits) {
64
+ if (matched.length === 0) {
66
65
  return { errors: error('alternatives.any') };
67
66
  }
68
67
 
69
68
  if (schema._flags.match === 'one') {
70
- return hits === 1 ? { value: matched } : { errors: error('alternatives.one') };
69
+ return matched.length === 1 ? { value: matched[0] } : { errors: error('alternatives.one') };
71
70
  }
72
71
 
73
- return hits === schema.$_terms.matches.length ? { value } : { errors: error('alternatives.all') };
72
+ if (matched.length !== schema.$_terms.matches.length) {
73
+ return { errors: error('alternatives.all') };
74
+ }
75
+
76
+ const allobj = schema.$_terms.matches.reduce((acc, v) => acc && v.schema.type === 'object', true);
77
+ return allobj ? { value: matched.reduce((acc, v) => Merge(acc, v, { mergeArrays: false })) } : { value: matched[matched.length - 1] };
74
78
  }
75
79
 
76
80
  // Match any
@@ -183,7 +187,7 @@ module.exports = Any.extend({
183
187
 
184
188
  label(name) {
185
189
 
186
- const obj = this.$_super.label(name);
190
+ const obj = this.$_parent('label', name);
187
191
  const each = (item, source) => (source.path[0] !== 'is' ? item.label(name) : undefined);
188
192
  return obj.$_modify({ each, ref: false });
189
193
  }
package/lib/types/any.js CHANGED
@@ -167,7 +167,7 @@ module.exports = Base.extend({
167
167
  'any.failover': '{{#label}} threw an error when running failover method',
168
168
  'any.invalid': '{{#label}} contains an invalid value',
169
169
  'any.only': '{{#label}} must be {if(#valids.length == 1, "", "one of ")}{{#valids}}',
170
- 'any.ref': '{{#label}} {{#arg}} references "{{#ref}}" which {{#reason}}',
170
+ 'any.ref': '{{#label}} {{#arg}} references {{:#ref}} which {{#reason}}',
171
171
  'any.required': '{{#label}} is required',
172
172
  'any.unknown': '{{#label}} is not allowed'
173
173
  }
@@ -114,7 +114,7 @@ module.exports = Any.extend({
114
114
 
115
115
  return obj.$_mutateRebuild();
116
116
  },
117
- validate(value, { schema, error, state, prefs }) {
117
+ validate(value, { schema, error, state, prefs, errorsArray }) {
118
118
 
119
119
  const requireds = schema.$_terms._requireds.slice();
120
120
  const ordereds = schema.$_terms.ordered.slice();
@@ -123,7 +123,8 @@ module.exports = Any.extend({
123
123
  const wasArray = !value[Common.symbols.arraySingle];
124
124
  delete value[Common.symbols.arraySingle];
125
125
 
126
- const errors = [];
126
+ const errors = errorsArray();
127
+
127
128
  let il = value.length;
128
129
  for (let i = 0; i < il; ++i) {
129
130
  const item = value[i];
@@ -341,6 +342,10 @@ module.exports = Any.extend({
341
342
 
342
343
  if (ordereds.length) {
343
344
  internals.fillOrderedErrors(schema, errors, ordereds, value, state, prefs);
345
+
346
+ if (!errors.length) {
347
+ internals.fillDefault(ordereds, value, state, prefs);
348
+ }
344
349
  }
345
350
 
346
351
  return errors.length ? errors : value;
@@ -611,7 +616,7 @@ module.exports = Any.extend({
611
616
  messages: {
612
617
  'array.base': '{{#label}} must be an array',
613
618
  'array.excludes': '{{#label}} contains an excluded value',
614
- 'array.hasKnown': '{{#label}} does not contain at least one required match for type "{#patternLabel}"',
619
+ 'array.hasKnown': '{{#label}} does not contain at least one required match for type {:#patternLabel}',
615
620
  'array.hasUnknown': '{{#label}} does not contain at least one required match',
616
621
  'array.includes': '{{#label}} does not match any of the allowed types',
617
622
  'array.includesRequiredBoth': '{{#label}} does not contain {{#knownMisses}} and {{#unknownMisses}} other required value(s)',
@@ -676,6 +681,33 @@ internals.fillOrderedErrors = function (schema, errors, ordereds, value, state,
676
681
  };
677
682
 
678
683
 
684
+ internals.fillDefault = function (ordereds, value, state, prefs) {
685
+
686
+ const overrides = [];
687
+ let trailingUndefined = true;
688
+
689
+ for (let i = ordereds.length - 1; i >= 0; --i) {
690
+ const ordered = ordereds[i];
691
+ const ancestors = [value, ...state.ancestors];
692
+ const override = ordered.$_validate(undefined, state.localize(state.path, ancestors, ordered), prefs).value;
693
+
694
+ if (trailingUndefined) {
695
+ if (override === undefined) {
696
+ continue;
697
+ }
698
+
699
+ trailingUndefined = false;
700
+ }
701
+
702
+ overrides.unshift(override);
703
+ }
704
+
705
+ if (overrides.length) {
706
+ value.push(...overrides);
707
+ }
708
+ };
709
+
710
+
679
711
  internals.fastSplice = function (arr, i) {
680
712
 
681
713
  let pos = i;
package/lib/types/date.js CHANGED
@@ -149,10 +149,10 @@ module.exports = Any.extend({
149
149
  messages: {
150
150
  'date.base': '{{#label}} must be a valid date',
151
151
  'date.format': '{{#label}} must be in {msg("date.format." + #format) || #format} format',
152
- 'date.greater': '{{#label}} must be greater than "{{#limit}}"',
153
- 'date.less': '{{#label}} must be less than "{{#limit}}"',
154
- 'date.max': '{{#label}} must be less than or equal to "{{#limit}}"',
155
- 'date.min': '{{#label}} must be larger than or equal to "{{#limit}}"',
152
+ 'date.greater': '{{#label}} must be greater than {{:#limit}}',
153
+ 'date.less': '{{#label}} must be less than {{:#limit}}',
154
+ 'date.max': '{{#label}} must be less than or equal to {{:#limit}}',
155
+ 'date.min': '{{#label}} must be greater than or equal to {{:#limit}}',
156
156
 
157
157
  // Messages used in date.format
158
158
 
package/lib/types/keys.js CHANGED
@@ -27,6 +27,7 @@ module.exports = Any.extend({
27
27
  type: '_keys',
28
28
 
29
29
  properties: {
30
+
30
31
  typeof: 'object'
31
32
  },
32
33
 
@@ -111,6 +112,10 @@ module.exports = Any.extend({
111
112
  return { value, errors: result.errors };
112
113
  }
113
114
 
115
+ if (result.value !== undefined) {
116
+ value[key] = result.value;
117
+ }
118
+
114
119
  errors.push(...result.errors);
115
120
  }
116
121
  else if (child.schema._flags.result === 'strip' ||
@@ -478,7 +483,7 @@ module.exports = Any.extend({
478
483
  value = Common.symbols.deepDefault;
479
484
  }
480
485
 
481
- return this.$_super.default(value, options);
486
+ return this.$_parent('default', value, options);
482
487
  }
483
488
  },
484
489
 
@@ -528,22 +533,22 @@ module.exports = Any.extend({
528
533
  'object.and': '{{#label}} contains {{#presentWithLabels}} without its required peers {{#missingWithLabels}}',
529
534
  'object.assert': '{{#label}} is invalid because {if(#subject.key, `"` + #subject.key + `" failed to ` + (#message || "pass the assertion test"), #message || "the assertion failed")}',
530
535
  'object.base': '{{#label}} must be of type {{#type}}',
531
- 'object.instance': '{{#label}} must be an instance of "{{#type}}"',
536
+ 'object.instance': '{{#label}} must be an instance of {{:#type}}',
532
537
  'object.length': '{{#label}} must have {{#limit}} key{if(#limit == 1, "", "s")}',
533
538
  'object.max': '{{#label}} must have less than or equal to {{#limit}} key{if(#limit == 1, "", "s")}',
534
539
  'object.min': '{{#label}} must have at least {{#limit}} key{if(#limit == 1, "", "s")}',
535
540
  'object.missing': '{{#label}} must contain at least one of {{#peersWithLabels}}',
536
- 'object.nand': '"{{#mainWithLabel}}" must not exist simultaneously with {{#peersWithLabels}}',
541
+ 'object.nand': '{{:#mainWithLabel}} must not exist simultaneously with {{#peersWithLabels}}',
537
542
  'object.oxor': '{{#label}} contains a conflict between optional exclusive peers {{#peersWithLabels}}',
538
543
  'object.pattern.match': '{{#label}} keys failed to match pattern requirements',
539
544
  'object.refType': '{{#label}} must be a Joi reference',
540
545
  'object.regex': '{{#label}} must be a RegExp object',
541
- 'object.rename.multiple': '{{#label}} cannot rename "{{#from}}" because multiple renames are disabled and another key was already renamed to "{{#to}}"',
542
- 'object.rename.override': '{{#label}} cannot rename "{{#from}}" because override is disabled and target "{{#to}}" exists',
546
+ 'object.rename.multiple': '{{#label}} cannot rename {{:#from}} because multiple renames are disabled and another key was already renamed to {{:#to}}',
547
+ 'object.rename.override': '{{#label}} cannot rename {{:#from}} because override is disabled and target {{:#to}} exists',
543
548
  'object.schema': '{{#label}} must be a Joi schema of {{#type}} type',
544
549
  'object.unknown': '{{#label}} is not allowed',
545
- 'object.with': '"{{#mainWithLabel}}" missing required peer "{{#peerWithLabel}}"',
546
- 'object.without': '"{{#mainWithLabel}}" conflict with forbidden peer "{{#peerWithLabel}}"',
550
+ 'object.with': '{{:#mainWithLabel}} missing required peer {{:#peerWithLabel}}',
551
+ 'object.without': '{{:#mainWithLabel}} conflict with forbidden peer {{:#peerWithLabel}}',
547
552
  'object.xor': '{{#label}} contains a conflict between exclusive peers {{#peersWithLabels}}'
548
553
  }
549
554
  });
@@ -599,7 +604,7 @@ internals.dependency = function (schema, rel, key, peers, options) {
599
604
  const separator = Common.default(options.separator, '.');
600
605
  const paths = [];
601
606
  for (const peer of peers) {
602
- Assert(typeof peer === 'string', rel, 'peers must be a string or a reference');
607
+ Assert(typeof peer === 'string', rel, 'peers must be strings');
603
608
  paths.push(Compile.ref(peer, { separator, ancestor: 0, prefix: false }));
604
609
  }
605
610
 
@@ -284,7 +284,7 @@ module.exports = Any.extend({
284
284
  'number.integer': '{{#label}} must be an integer',
285
285
  'number.less': '{{#label}} must be less than {{#limit}}',
286
286
  'number.max': '{{#label}} must be less than or equal to {{#limit}}',
287
- 'number.min': '{{#label}} must be larger than or equal to {{#limit}}',
287
+ 'number.min': '{{#label}} must be greater than or equal to {{#limit}}',
288
288
  'number.multiple': '{{#label}} must be a multiple of {{#multiple}}',
289
289
  'number.negative': '{{#label}} must be a negative number',
290
290
  'number.port': '{{#label}} must be a valid port',
@@ -312,10 +312,14 @@ internals.normalizeExponent = function (str) {
312
312
  internals.normalizeDecimal = function (str) {
313
313
 
314
314
  str = str
315
+ // Remove leading plus signs
315
316
  .replace(/^\+/, '')
316
- .replace(/\.0+$/, '')
317
+ // Remove trailing zeros if there is a decimal point and unecessary decimal points
318
+ .replace(/\.0*$/, '')
319
+ // Add a integer 0 if the numbers starts with a decimal point
317
320
  .replace(/^(-?)\.([^\.]*)$/, '$10.$2')
318
- .replace(/^(-?)0+([1-9])/, '$1$2');
321
+ // Remove leading zeros
322
+ .replace(/^(-?)0+([0-9])/, '$1$2');
319
323
 
320
324
  if (str.includes('.') &&
321
325
  str.endsWith('0')) {
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  const Assert = require('@hapi/hoek/lib/assert');
4
- const Domain = require('@hapi/address/lib/domain');
5
- const Email = require('@hapi/address/lib/email');
6
- const Ip = require('@hapi/address/lib/ip');
4
+ const Domain = require('@sideway/address/lib/domain');
5
+ const Email = require('@sideway/address/lib/email');
6
+ const Ip = require('@sideway/address/lib/ip');
7
7
  const EscapeRegex = require('@hapi/hoek/lib/escapeRegex');
8
- const Tlds = require('@hapi/address/lib/tlds');
9
- const Uri = require('@hapi/address/lib/uri');
8
+ const Tlds = require('@sideway/address/lib/tlds');
9
+ const Uri = require('@sideway/address/lib/uri');
10
10
 
11
11
  const Any = require('./any');
12
12
  const Common = require('../common');
@@ -41,8 +41,8 @@ const internals = {
41
41
  uuidv4: '4',
42
42
  uuidv5: '5'
43
43
  },
44
+ guidSeparators: new Set([undefined, true, false, '-', ':']),
44
45
 
45
- cidrPresences: ['required', 'optional', 'forbidden'],
46
46
  normalizationForms: ['NFC', 'NFD', 'NFKC', 'NFKD']
47
47
  };
48
48
 
@@ -257,7 +257,7 @@ module.exports = Any.extend({
257
257
  method(options) {
258
258
 
259
259
  if (options) {
260
- Common.assertOptions(options, ['allowUnicode', 'minDomainSegments', 'tlds']);
260
+ Common.assertOptions(options, ['allowUnicode', 'maxDomainSegments', 'minDomainSegments', 'tlds']);
261
261
  }
262
262
 
263
263
  const address = internals.addressOptions(options);
@@ -276,7 +276,7 @@ module.exports = Any.extend({
276
276
  email: {
277
277
  method(options = {}) {
278
278
 
279
- Common.assertOptions(options, ['allowUnicode', 'ignoreLength', 'minDomainSegments', 'multiple', 'separator', 'tlds']);
279
+ Common.assertOptions(options, ['allowUnicode', 'ignoreLength', 'maxDomainSegments', 'minDomainSegments', 'multiple', 'separator', 'tlds']);
280
280
  Assert(options.multiple === undefined || typeof options.multiple === 'boolean', 'multiple option must be an boolean');
281
281
 
282
282
  const address = internals.addressOptions(options);
@@ -306,7 +306,7 @@ module.exports = Any.extend({
306
306
  alias: 'uuid',
307
307
  method(options = {}) {
308
308
 
309
- Common.assertOptions(options, ['version']);
309
+ Common.assertOptions(options, ['version', 'separator']);
310
310
 
311
311
  let versionNumbers = '';
312
312
 
@@ -328,7 +328,12 @@ module.exports = Any.extend({
328
328
  }
329
329
  }
330
330
 
331
- const regex = new RegExp(`^([\\[{\\(]?)[0-9A-F]{8}([:-]?)[0-9A-F]{4}\\2?[${versionNumbers || '0-9A-F'}][0-9A-F]{3}\\2?[${versionNumbers ? '89AB' : '0-9A-F'}][0-9A-F]{3}\\2?[0-9A-F]{12}([\\]}\\)]?)$`, 'i');
331
+ Assert(internals.guidSeparators.has(options.separator), 'separator must be one of true, false, "-", or ":"');
332
+ const separator = options.separator === undefined ? '[:-]?' :
333
+ options.separator === true ? '[:-]' :
334
+ options.separator === false ? '[]?' : `\\${options.separator}`;
335
+
336
+ const regex = new RegExp(`^([\\[{\\(]?)[0-9A-F]{8}(${separator})[0-9A-F]{4}\\2?[${versionNumbers || '0-9A-F'}][0-9A-F]{3}\\2?[${versionNumbers ? '89AB' : '0-9A-F'}][0-9A-F]{3}\\2?[0-9A-F]{12}([\\]}\\)]?)$`, 'i');
332
337
 
333
338
  return this.$_addRule({ name: 'guid', args: { options }, regex });
334
339
  },
@@ -627,7 +632,7 @@ module.exports = Any.extend({
627
632
  Common.assertOptions(options, ['allowRelative', 'allowQuerySquareBrackets', 'domain', 'relativeOnly', 'scheme']);
628
633
 
629
634
  if (options.domain) {
630
- Common.assertOptions(options.domain, ['allowUnicode', 'minDomainSegments', 'tlds']);
635
+ Common.assertOptions(options.domain, ['allowUnicode', 'maxDomainSegments', 'minDomainSegments', 'tlds']);
631
636
  }
632
637
 
633
638
  const { regex, scheme } = Uri.regex(options);
@@ -642,11 +647,12 @@ module.exports = Any.extend({
642
647
 
643
648
  const match = regex.exec(value);
644
649
  if (match) {
645
- if (domain) {
646
- const matched = match[1] || match[2];
647
- if (!Domain.isValid(matched, domain)) {
648
- return helpers.error('string.domain', { value: matched });
649
- }
650
+ const matched = match[1] || match[2];
651
+ if (domain &&
652
+ (!options.allowRelative || matched) &&
653
+ !Domain.isValid(matched, domain)) {
654
+
655
+ return helpers.error('string.domain', { value: matched });
650
656
  }
651
657
 
652
658
  return value;
@@ -702,10 +708,10 @@ module.exports = Any.extend({
702
708
  'string.min': '{{#label}} length must be at least {{#limit}} characters long',
703
709
  'string.normalize': '{{#label}} must be unicode normalized in the {{#form}} form',
704
710
  'string.token': '{{#label}} must only contain alpha-numeric and underscore characters',
705
- 'string.pattern.base': '{{#label}} with value "{[.]}" fails to match the required pattern: {{#regex}}',
706
- 'string.pattern.name': '{{#label}} with value "{[.]}" fails to match the {{#name}} pattern',
707
- 'string.pattern.invert.base': '{{#label}} with value "{[.]}" matches the inverted pattern: {{#regex}}',
708
- 'string.pattern.invert.name': '{{#label}} with value "{[.]}" matches the inverted {{#name}} pattern',
711
+ 'string.pattern.base': '{{#label}} with value {:[.]} fails to match the required pattern: {{#regex}}',
712
+ 'string.pattern.name': '{{#label}} with value {:[.]} fails to match the {{#name}} pattern',
713
+ 'string.pattern.invert.base': '{{#label}} with value {:[.]} matches the inverted pattern: {{#regex}}',
714
+ 'string.pattern.invert.name': '{{#label}} with value {:[.]} matches the inverted {{#name}} pattern',
709
715
  'string.trim': '{{#label}} must not have leading or trailing whitespace',
710
716
  'string.uri': '{{#label}} must be a valid uri',
711
717
  'string.uriCustomScheme': '{{#label}} must be a valid uri with a scheme matching the {{#scheme}} pattern',
@@ -728,6 +734,11 @@ internals.addressOptions = function (options) {
728
734
  Assert(options.minDomainSegments === undefined ||
729
735
  Number.isSafeInteger(options.minDomainSegments) && options.minDomainSegments > 0, 'minDomainSegments must be a positive integer');
730
736
 
737
+ // maxDomainSegments
738
+
739
+ Assert(options.maxDomainSegments === undefined ||
740
+ Number.isSafeInteger(options.maxDomainSegments) && options.maxDomainSegments > 0, 'maxDomainSegments must be a positive integer');
741
+
731
742
  // tlds
732
743
 
733
744
  if (options.tlds === false) {
@@ -751,6 +762,7 @@ internals.addressOptions = function (options) {
751
762
 
752
763
  Assert(options.tlds.deny instanceof Set, 'tlds.deny must be an array, Set, or boolean');
753
764
  Assert(!options.tlds.allow, 'Cannot specify both tlds.allow and tlds.deny lists');
765
+ internals.validateTlds(options.tlds.deny, 'tlds.deny');
754
766
  return options;
755
767
  }
756
768
 
@@ -769,16 +781,29 @@ internals.addressOptions = function (options) {
769
781
  }
770
782
 
771
783
  Assert(options.tlds.allow instanceof Set, 'tlds.allow must be an array, Set, or boolean');
784
+ internals.validateTlds(options.tlds.allow, 'tlds.allow');
772
785
  return options;
773
786
  };
774
787
 
775
788
 
789
+ internals.validateTlds = function (set, source) {
790
+
791
+ for (const tld of set) {
792
+ Assert(Domain.isValid(tld, { minDomainSegments: 1, maxDomainSegments: 1 }), `${source} must contain valid top level domain names`);
793
+ }
794
+ };
795
+
796
+
776
797
  internals.isoDate = function (value) {
777
798
 
778
799
  if (!Common.isIsoDate(value)) {
779
800
  return null;
780
801
  }
781
802
 
803
+ if (/.*T.*[+-]\d\d$/.test(value)) { // Add missing trailing zeros to timeshift
804
+ value += '00';
805
+ }
806
+
782
807
  const date = new Date(value);
783
808
  if (isNaN(date.getTime())) {
784
809
  return null;