joi 13.1.0 → 13.3.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/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012-2017, Project contributors
1
+ Copyright (c) 2012-2018, Project contributors
2
2
  Copyright (c) 2012-2014, Walmart
3
3
  All rights reserved.
4
4
 
package/README.md CHANGED
@@ -3,20 +3,10 @@
3
3
  Object schema description language and validator for JavaScript objects.
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/joi.svg)](http://badge.fury.io/js/joi)
6
- [![Build Status](https://secure.travis-ci.org/hapijs/joi.svg?branch=master)](http://travis-ci.org/hapijs/joi)
7
- <!--
8
-
9
- Remove those badges until they work properly on semver.
10
-
11
- [![Dependencies Status](https://david-dm.org/hapijs/joi.svg)](https://david-dm.org/hapijs/joi)
12
- [![DevDependencies Status](https://david-dm.org/hapijs/joi/dev-status.svg)](https://david-dm.org/hapijs/joi#info=devDependencies)
13
-
14
- -->
6
+ [![Build Status](https://travis-ci.org/hapijs/joi.svg?branch=master)](https://travis-ci.org/hapijs/joi)
15
7
  [![NSP Status](https://nodesecurity.io/orgs/hapijs/projects/0394bf83-b5bc-410b-878c-e8cf1b92033e/badge)](https://nodesecurity.io/orgs/hapijs/projects/0394bf83-b5bc-410b-878c-e8cf1b92033e)
16
8
  [![Known Vulnerabilities](https://snyk.io/test/github/hapijs/joi/badge.svg)](https://snyk.io/test/github/hapijs/joi)
17
9
 
18
- [![Join the chat at https://gitter.im/hapijs/joi](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hapijs/joi?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
19
-
20
10
  Lead Maintainer: [Nicolas Morel](https://github.com/marsup)
21
11
 
22
12
  # Introduction
@@ -26,7 +16,7 @@ Imagine you run facebook and you want visitors to sign up on the website with re
26
16
  This is joi, joi allows you to create *blueprints* or *schemas* for JavaScript objects (an object that stores information) to ensure *validation* of key information.
27
17
 
28
18
  # API
29
- See the detailed [API Reference](https://github.com/hapijs/joi/blob/v13.1.0/API.md).
19
+ See the detailed [API Reference](https://github.com/hapijs/joi/blob/v13.3.0/API.md).
30
20
 
31
21
  # Example
32
22
 
@@ -129,3 +119,9 @@ When validating a schema:
129
119
  # Browsers
130
120
 
131
121
  Joi doesn't directly support browsers, but you could use [joi-browser](https://github.com/jeffbski/joi-browser) for an ES5 build of Joi that works in browsers, or as a source of inspiration for your own builds.
122
+
123
+ ## Acknowledgements
124
+
125
+ This project is kindly sponsored by:
126
+
127
+ [![nearForm logo](https://www.nearform.com/img/nearform-logotype.svg)](https://nearform.com)
package/lib/index.js CHANGED
@@ -8,6 +8,7 @@ const Cast = require('./cast');
8
8
  const Errors = require('./errors');
9
9
  const Lazy = require('./types/lazy');
10
10
  const Ref = require('./ref');
11
+ const Settings = require('./types/any/settings');
11
12
 
12
13
 
13
14
  // Declare internals
@@ -191,25 +192,31 @@ internals.root = function () {
191
192
  root.reach = function (schema, path) {
192
193
 
193
194
  Hoek.assert(schema && schema instanceof Any, 'you must provide a joi schema');
194
- Hoek.assert(typeof path === 'string', 'path must be a string');
195
+ Hoek.assert(Array.isArray(path) || typeof path === 'string', 'path must be a string or an array of strings');
195
196
 
196
- if (path === '') {
197
- return schema;
198
- }
197
+ const reach = (sourceSchema, schemaPath) => {
199
198
 
200
- const parts = path.split('.');
201
- const children = schema._inner.children;
202
- if (!children) {
203
- return;
204
- }
199
+ if (!schemaPath.length) {
200
+ return sourceSchema;
201
+ }
205
202
 
206
- const key = parts[0];
207
- for (let i = 0; i < children.length; ++i) {
208
- const child = children[i];
209
- if (child.key === key) {
210
- return this.reach(child.schema, path.substr(key.length + 1));
203
+ const children = sourceSchema._inner.children;
204
+ if (!children) {
205
+ return;
211
206
  }
212
- }
207
+
208
+ const key = schemaPath.shift();
209
+ for (let i = 0; i < children.length; ++i) {
210
+ const child = children[i];
211
+ if (child.key === key) {
212
+ return reach(child.schema, schemaPath);
213
+ }
214
+ }
215
+ };
216
+
217
+ const schemaPath = typeof path === 'string' ? path.split('.') : path.slice();
218
+
219
+ return reach(schema, schemaPath);
213
220
  };
214
221
 
215
222
  root.lazy = function (fn) {
@@ -276,9 +283,10 @@ internals.root = function () {
276
283
  this._type = extension.name;
277
284
 
278
285
  if (extension.language) {
279
- this._settings = this._settings || { language: {} };
280
- this._settings.language = Hoek.applyToDefaults(this._settings.language, {
281
- [extension.name]: extension.language
286
+ this._settings = Settings.concat(this._settings, {
287
+ language: {
288
+ [extension.name]: extension.language
289
+ }
282
290
  });
283
291
  }
284
292
  }
package/lib/language.js CHANGED
@@ -118,7 +118,8 @@ exports.errors = {
118
118
  positive: 'must be a positive number',
119
119
  precision: 'must have no more than {{limit}} decimal places',
120
120
  ref: 'references "{{ref}}" which is not a number',
121
- multiple: 'must be a multiple of {{multiple}}'
121
+ multiple: 'must be a multiple of {{multiple}}',
122
+ port: 'must be a valid port'
122
123
  },
123
124
  string: {
124
125
  base: 'must be a string',
@@ -142,6 +143,7 @@ exports.errors = {
142
143
  isoDate: 'must be a valid ISO 8601 date',
143
144
  guid: 'must be a valid GUID',
144
145
  hex: 'must only contain hexadecimal characters',
146
+ hexAlign: 'hex decoded representation must be byte aligned',
145
147
  base64: 'must be a valid base64 string',
146
148
  hostname: 'must be a valid hostname',
147
149
  normalize: 'must be unicode normalized in the {{form}} form',
package/lib/set.js CHANGED
@@ -2,36 +2,75 @@
2
2
 
3
3
  const Ref = require('./ref');
4
4
 
5
- module.exports = class Set {
6
5
 
7
- constructor() {
6
+ const internals = {};
8
7
 
9
- this._set = [];
8
+
9
+ internals.extendedCheckForValue = function (value, insensitive) {
10
+
11
+ const valueType = typeof value;
12
+
13
+ if (valueType === 'object') {
14
+ if (value instanceof Date) {
15
+ return (item) => {
16
+
17
+ return item instanceof Date && value.getTime() === item.getTime();
18
+ };
19
+ }
20
+ if (Buffer.isBuffer(value)) {
21
+ return (item) => {
22
+
23
+ return Buffer.isBuffer(item) && value.length === item.length && value.toString('binary') === item.toString('binary');
24
+ };
25
+ }
26
+ }
27
+ else if (insensitive && valueType === 'string') {
28
+ const lowercaseValue = value.toLowerCase();
29
+ return (item) => {
30
+
31
+ return typeof item === 'string' && lowercaseValue === item.toLowerCase();
32
+ };
33
+ }
34
+
35
+ return null;
36
+ };
37
+
38
+
39
+ module.exports = class InternalSet {
40
+
41
+ constructor(from) {
42
+
43
+ this._set = new Set(from);
44
+ this._hasRef = false;
10
45
  }
11
46
 
12
47
  add(value, refs) {
13
48
 
14
- if (!Ref.isRef(value) && this.has(value, null, null, false)) {
49
+ const isRef = Ref.isRef(value);
50
+ if (!isRef && this.has(value, null, null, false)) {
15
51
 
16
- return;
52
+ return this;
17
53
  }
18
54
 
19
55
  if (refs !== undefined) { // If it's a merge, we don't have any refs
20
56
  Ref.push(refs, value);
21
57
  }
22
58
 
23
- this._set.push(value);
59
+ this._set.add(value);
60
+
61
+ this._hasRef |= isRef;
62
+
24
63
  return this;
25
64
  }
26
65
 
27
66
  merge(add, remove) {
28
67
 
29
- for (let i = 0; i < add._set.length; ++i) {
30
- this.add(add._set[i]);
68
+ for (const item of add._set) {
69
+ this.add(item);
31
70
  }
32
71
 
33
- for (let i = 0; i < remove._set.length; ++i) {
34
- this.remove(remove._set[i]);
72
+ for (const item of remove._set) {
73
+ this.remove(item);
35
74
  }
36
75
 
37
76
  return this;
@@ -39,37 +78,68 @@ module.exports = class Set {
39
78
 
40
79
  remove(value) {
41
80
 
42
- this._set = this._set.filter((item) => value !== item);
81
+ this._set.delete(value);
43
82
  return this;
44
83
  }
45
84
 
46
85
  has(value, state, options, insensitive) {
47
86
 
48
- for (let i = 0; i < this._set.length; ++i) {
49
- let items = this._set[i];
87
+ if (!this._set.size) {
88
+ return false;
89
+ }
90
+
91
+ const hasValue = this._set.has(value);
92
+ if (hasValue) {
93
+ return hasValue;
94
+ }
50
95
 
51
- if (state && Ref.isRef(items)) { // Only resolve references if there is a state, otherwise it's a merge
52
- items = items(state.reference || state.parent, options);
96
+ const extendedCheck = internals.extendedCheckForValue(value, insensitive);
97
+ if (!extendedCheck) {
98
+ if (state && this._hasRef) {
99
+ for (let item of this._set) {
100
+ if (Ref.isRef(item)) {
101
+ item = item(state.reference || state.parent, options);
102
+ if (value === item || (Array.isArray(item) && item.includes(value))) {
103
+ return true;
104
+ }
105
+ }
106
+ }
53
107
  }
54
108
 
55
- if (!Array.isArray(items)) {
56
- items = [items];
109
+ return false;
110
+ }
111
+
112
+ return this._has(value, state, options, extendedCheck);
113
+ }
114
+
115
+ _has(value, state, options, check) {
116
+
117
+ const checkRef = !!(state && this._hasRef);
118
+
119
+ const isReallyEqual = function (item) {
120
+
121
+ if (value === item) {
122
+ return true;
57
123
  }
58
124
 
59
- for (let j = 0; j < items.length; ++j) {
60
- const item = items[j];
61
- if (typeof value !== typeof item) {
62
- continue;
63
- }
125
+ return check(item);
126
+ };
64
127
 
65
- if (value === item ||
66
- (value instanceof Date && item instanceof Date && value.getTime() === item.getTime()) ||
67
- (insensitive && typeof value === 'string' && value.toLowerCase() === item.toLowerCase()) ||
68
- (Buffer.isBuffer(value) && Buffer.isBuffer(item) && value.length === item.length && value.toString('binary') === item.toString('binary'))) {
128
+ for (let item of this._set) {
129
+ if (checkRef && Ref.isRef(item)) { // Only resolve references if there is a state, otherwise it's a merge
130
+ item = item(state.reference || state.parent, options);
69
131
 
70
- return true;
132
+ if (Array.isArray(item)) {
133
+ if (item.find(isReallyEqual)) {
134
+ return true;
135
+ }
136
+ continue;
71
137
  }
72
138
  }
139
+
140
+ if (isReallyEqual(item)) {
141
+ return true;
142
+ }
73
143
  }
74
144
 
75
145
  return false;
@@ -80,8 +150,7 @@ module.exports = class Set {
80
150
  if (options && options.stripUndefined) {
81
151
  const values = [];
82
152
 
83
- for (let i = 0; i < this._set.length; ++i) {
84
- const item = this._set[i];
153
+ for (const item of this._set) {
85
154
  if (item !== undefined) {
86
155
  values.push(item);
87
156
  }
@@ -90,22 +159,20 @@ module.exports = class Set {
90
159
  return values;
91
160
  }
92
161
 
93
- return this._set.slice();
162
+ return Array.from(this._set);
94
163
  }
95
164
 
96
165
  slice() {
97
166
 
98
- const newSet = new Set();
99
- newSet._set = this._set.slice();
100
-
101
- return newSet;
167
+ const set = new InternalSet(this._set);
168
+ set._hasRef = this._hasRef;
169
+ return set;
102
170
  }
103
171
 
104
172
  concat(source) {
105
173
 
106
- const newSet = new Set();
107
- newSet._set = this._set.concat(source._set);
108
-
109
- return newSet;
174
+ const set = new InternalSet([...this._set, ...source._set]);
175
+ set._hasRef = !!(this._hasRef | source._hasRef);
176
+ return set;
110
177
  }
111
178
  };
@@ -3,6 +3,7 @@
3
3
  // Load modules
4
4
 
5
5
  const Hoek = require('hoek');
6
+ const Settings = require('./settings');
6
7
  const Ref = require('../../ref');
7
8
  const Errors = require('../../errors');
8
9
  let Alternatives = null; // Delay-loaded to prevent circular dependencies
@@ -104,10 +105,10 @@ module.exports = internals.Any = class {
104
105
  obj.isJoi = true;
105
106
  obj._currentJoi = this._currentJoi;
106
107
  obj._type = this._type;
107
- obj._settings = internals.concatSettings(this._settings);
108
+ obj._settings = this._settings;
108
109
  obj._baseType = this._baseType;
109
- obj._valids = Hoek.clone(this._valids);
110
- obj._invalids = Hoek.clone(this._invalids);
110
+ obj._valids = this._valids.slice();
111
+ obj._invalids = this._invalids.slice();
111
112
  obj._tests = this._tests.slice();
112
113
  obj._refs = this._refs.slice();
113
114
  obj._flags = Hoek.clone(this._flags);
@@ -150,7 +151,7 @@ module.exports = internals.Any = class {
150
151
  obj = tmpObj;
151
152
  }
152
153
 
153
- obj._settings = obj._settings ? internals.concatSettings(obj._settings, schema._settings) : schema._settings;
154
+ obj._settings = obj._settings ? Settings.concat(obj._settings, schema._settings) : schema._settings;
154
155
  obj._valids.merge(schema._valids, schema._invalids);
155
156
  obj._invalids.merge(schema._invalids, schema._valids);
156
157
  obj._tests = obj._tests.concat(schema._tests);
@@ -218,15 +219,16 @@ module.exports = internals.Any = class {
218
219
  this.checkOptions(options);
219
220
 
220
221
  const obj = this.clone();
221
- obj._settings = internals.concatSettings(obj._settings, options);
222
+ obj._settings = Settings.concat(obj._settings, options);
222
223
  return obj;
223
224
  }
224
225
 
225
226
  strict(isStrict) {
226
227
 
227
228
  const obj = this.clone();
228
- obj._settings = obj._settings || {};
229
- obj._settings.convert = isStrict === undefined ? false : !isStrict;
229
+
230
+ const convert = isStrict === undefined ? false : !isStrict;
231
+ obj._settings = Settings.concat(obj._settings, { convert });
230
232
  return obj;
231
233
  }
232
234
 
@@ -480,7 +482,7 @@ module.exports = internals.Any = class {
480
482
  state = state || { key: '', path: [], parent: null, reference };
481
483
 
482
484
  if (this._settings) {
483
- options = internals.concatSettings(options, this._settings);
485
+ options = Settings.concat(options, this._settings);
484
486
  }
485
487
 
486
488
  let errors = [];
@@ -593,7 +595,7 @@ module.exports = internals.Any = class {
593
595
  }
594
596
 
595
597
  if (this._invalids.has(value, state, options, this._flags.insensitive)) {
596
- errors.push(this.createError(value === '' ? 'any.empty' : 'any.invalid', null, state, options));
598
+ errors.push(this.createError(value === '' ? 'any.empty' : 'any.invalid', { value, invalids: this._invalids.values({ stripUndefined: true }) }, state, options));
597
599
  if (options.abortEarly ||
598
600
  value === undefined) { // No reason to keep validating missing value
599
601
 
@@ -621,7 +623,7 @@ module.exports = internals.Any = class {
621
623
  }
622
624
 
623
625
  if (this._invalids.has(value, state, options, this._flags.insensitive)) {
624
- errors.push(this.createError(value === '' ? 'any.empty' : 'any.invalid', null, state, options));
626
+ errors.push(this.createError(value === '' ? 'any.empty' : 'any.invalid', { value, invalids: this._invalids.values({ stripUndefined: true }) }, state, options));
625
627
  if (options.abortEarly) {
626
628
  return finish();
627
629
  }
@@ -632,13 +634,13 @@ module.exports = internals.Any = class {
632
634
  // Required values did not match
633
635
 
634
636
  if (this._flags.allowOnly) {
635
- errors.push(this.createError('any.allowOnly', { valids: this._valids.values({ stripUndefined: true }) }, state, options));
637
+ errors.push(this.createError('any.allowOnly', { value, valids: this._valids.values({ stripUndefined: true }) }, state, options));
636
638
  if (options.abortEarly) {
637
639
  return finish();
638
640
  }
639
641
  }
640
642
 
641
- // Helper.validate tests
643
+ // Validate tests
642
644
 
643
645
  for (let i = 0; i < this._tests.length; ++i) {
644
646
  const test = this._tests[i];
@@ -663,7 +665,7 @@ module.exports = internals.Any = class {
663
665
  this.checkOptions(options);
664
666
  }
665
667
 
666
- const settings = internals.concatSettings(internals.defaults, options);
668
+ const settings = Settings.concat(internals.defaults, options);
667
669
  const result = this._validate(value, null, settings);
668
670
  const errors = Errors.process(result.errors, value);
669
671
 
@@ -880,33 +882,3 @@ internals._try = function (fn, args) {
880
882
  error: err
881
883
  };
882
884
  };
883
-
884
- internals.concatSettings = function (target, source) {
885
-
886
- // Used to avoid cloning context
887
-
888
- if (!target &&
889
- !source) {
890
-
891
- return null;
892
- }
893
-
894
- const obj = Object.assign({}, target);
895
-
896
- if (source) {
897
- const sKeys = Object.keys(source);
898
- for (let i = 0; i < sKeys.length; ++i) {
899
- const key = sKeys[i];
900
- if (key !== 'language' ||
901
- !obj.hasOwnProperty(key)) {
902
-
903
- obj[key] = source[key];
904
- }
905
- else {
906
- obj[key] = Hoek.applyToDefaults(obj[key], source[key]);
907
- }
908
- }
909
- }
910
-
911
- return obj;
912
- };
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ // Load modules
4
+
5
+ const Hoek = require('hoek');
6
+
7
+
8
+ // Declare internals
9
+
10
+ const internals = {};
11
+
12
+
13
+ exports.concat = function (target, source) {
14
+
15
+ if (!source) {
16
+ return target;
17
+ }
18
+
19
+ const obj = Object.assign({}, target);
20
+
21
+ const sKeys = Object.keys(source);
22
+ for (let i = 0; i < sKeys.length; ++i) {
23
+ const key = sKeys[i];
24
+ if (key !== 'language' ||
25
+ !obj.hasOwnProperty(key)) {
26
+
27
+ obj[key] = source[key];
28
+ }
29
+ else {
30
+ obj[key] = Hoek.applyToDefaults(obj[key], source[key]);
31
+ }
32
+ }
33
+
34
+ return obj;
35
+ };
@@ -123,6 +123,8 @@ internals.Array = class extends Any {
123
123
  return errors;
124
124
  }
125
125
 
126
+ ordereds.shift();
127
+
126
128
  continue;
127
129
  }
128
130
 
@@ -139,6 +141,8 @@ internals.Array = class extends Any {
139
141
  return errors;
140
142
  }
141
143
 
144
+ ordereds.shift();
145
+
142
146
  break;
143
147
  }
144
148
  }
@@ -491,10 +495,10 @@ internals.Array = class extends Any {
491
495
  return this._test('unique', settings, function (value, state, options) {
492
496
 
493
497
  const found = {
494
- string: {},
495
- number: {},
496
- undefined: {},
497
- boolean: {},
498
+ string: Object.create(null),
499
+ number: Object.create(null),
500
+ undefined: Object.create(null),
501
+ boolean: Object.create(null),
498
502
  object: new Map(),
499
503
  function: new Map(),
500
504
  custom: new Map()
@@ -29,7 +29,7 @@ internals.Binary = class extends Any {
29
29
  options.convert) {
30
30
 
31
31
  try {
32
- result.value = new Buffer(value, this._flags.encoding);
32
+ result.value = Buffer.from(value, this._flags.encoding);
33
33
  }
34
34
  catch (e) {
35
35
  }
@@ -128,6 +128,18 @@ internals.Number = class extends Any {
128
128
  return obj;
129
129
  }
130
130
 
131
+ port() {
132
+
133
+ return this._test('port', undefined, function (value, state, options) {
134
+
135
+ if (!Number.isSafeInteger(value) || value < 0 || value > 65535) {
136
+ return this.createError('number.port', { value }, state, options);
137
+ }
138
+
139
+ return value;
140
+ });
141
+ }
142
+
131
143
  };
132
144
 
133
145
 
@@ -191,7 +191,7 @@ internals.Object = class extends Any {
191
191
  return finish();
192
192
  }
193
193
 
194
- const unprocessed = Hoek.mapToObject(Object.keys(target));
194
+ const unprocessed = new Set(Object.keys(target));
195
195
 
196
196
  if (this._inner.children) {
197
197
  const stripProps = [];
@@ -201,7 +201,7 @@ internals.Object = class extends Any {
201
201
  const key = child.key;
202
202
  const item = target[key];
203
203
 
204
- delete unprocessed[key];
204
+ unprocessed.delete(key);
205
205
 
206
206
  const localState = { key, path: state.path.concat(key), parent: target, reference: state.reference };
207
207
  const result = child.schema._validate(item, localState, options);
@@ -230,24 +230,30 @@ internals.Object = class extends Any {
230
230
 
231
231
  // Unknown keys
232
232
 
233
- let unprocessedKeys = Object.keys(unprocessed);
234
- if (unprocessedKeys.length &&
235
- this._inner.patterns.length) {
233
+ if (unprocessed.size && this._inner.patterns.length) {
236
234
 
237
- for (let i = 0; i < unprocessedKeys.length; ++i) {
238
- const key = unprocessedKeys[i];
239
- const localState = { key, path: state.path.concat(key), parent: target, reference: state.reference };
235
+ for (const key of unprocessed) {
236
+ const localState = {
237
+ key,
238
+ path: state.path.concat(key),
239
+ parent: target,
240
+ reference: state.reference
241
+ };
240
242
  const item = target[key];
241
243
 
242
- for (let j = 0; j < this._inner.patterns.length; ++j) {
243
- const pattern = this._inner.patterns[j];
244
+ for (let i = 0; i < this._inner.patterns.length; ++i) {
245
+ const pattern = this._inner.patterns[i];
244
246
 
245
247
  if (pattern.regex.test(key)) {
246
- delete unprocessed[key];
248
+ unprocessed.delete(key);
247
249
 
248
250
  const result = pattern.rule._validate(item, localState, options);
249
251
  if (result.errors) {
250
- errors.push(this.createError('object.child', { key, child: pattern.rule._getLabel(key), reason: result.errors }, localState, options));
252
+ errors.push(this.createError('object.child', {
253
+ key,
254
+ child: pattern.rule._getLabel(key),
255
+ reason: result.errors
256
+ }, localState, options));
251
257
 
252
258
  if (options.abortEarly) {
253
259
  return finish();
@@ -258,11 +264,9 @@ internals.Object = class extends Any {
258
264
  }
259
265
  }
260
266
  }
261
-
262
- unprocessedKeys = Object.keys(unprocessed);
263
267
  }
264
268
 
265
- if ((this._inner.children || this._inner.patterns.length) && unprocessedKeys.length) {
269
+ if (unprocessed.size && (this._inner.children || this._inner.patterns.length)) {
266
270
  if ((options.stripUnknown && this._flags.allowUnknown !== true) ||
267
271
  options.skipFunctions) {
268
272
 
@@ -271,27 +275,24 @@ internals.Object = class extends Any {
271
275
  : false;
272
276
 
273
277
 
274
- for (let i = 0; i < unprocessedKeys.length; ++i) {
275
- const key = unprocessedKeys[i];
276
-
278
+ for (const key of unprocessed) {
277
279
  if (stripUnknown) {
278
280
  delete target[key];
279
- delete unprocessed[key];
281
+ unprocessed.delete(key);
280
282
  }
281
283
  else if (typeof target[key] === 'function') {
282
- delete unprocessed[key];
284
+ unprocessed.delete(key);
283
285
  }
284
286
  }
285
-
286
- unprocessedKeys = Object.keys(unprocessed);
287
287
  }
288
288
 
289
- if (unprocessedKeys.length &&
290
- (this._flags.allowUnknown !== undefined ? !this._flags.allowUnknown : !options.allowUnknown)) {
289
+ if ((this._flags.allowUnknown !== undefined ? !this._flags.allowUnknown : !options.allowUnknown)) {
291
290
 
292
- for (let i = 0; i < unprocessedKeys.length; ++i) {
293
- const unprocessedKey = unprocessedKeys[i];
294
- errors.push(this.createError('object.allowUnknown', { child: unprocessedKey }, { key: unprocessedKey, path: state.path.concat(unprocessedKey) }, options, {}));
291
+ for (const unprocessedKey of unprocessed) {
292
+ errors.push(this.createError('object.allowUnknown', { child: unprocessedKey }, {
293
+ key: unprocessedKey,
294
+ path: state.path.concat(unprocessedKey)
295
+ }, options, {}));
295
296
  }
296
297
  }
297
298
  }
@@ -366,6 +367,15 @@ internals.Object = class extends Any {
366
367
  return obj;
367
368
  }
368
369
 
370
+ append(schema) {
371
+ // Skip any changes
372
+ if (schema === null || schema === undefined || Object.keys(schema).length === 0) {
373
+ return this;
374
+ }
375
+
376
+ return this.keys(schema);
377
+ }
378
+
369
379
  unknown(allow) {
370
380
 
371
381
  const value = allow !== false;
@@ -459,11 +469,15 @@ internals.Object = class extends Any {
459
469
 
460
470
  with(key, peers) {
461
471
 
472
+ Hoek.assert(arguments.length === 2, 'Invalid number of arguments, expected 2.');
473
+
462
474
  return this._dependency('with', key, peers);
463
475
  }
464
476
 
465
477
  without(key, peers) {
466
478
 
479
+ Hoek.assert(arguments.length === 2, 'Invalid number of arguments, expected 2.');
480
+
467
481
  return this._dependency('without', key, peers);
468
482
  }
469
483
 
@@ -73,6 +73,10 @@ internals.String = class extends Any {
73
73
  }
74
74
  }
75
75
  }
76
+
77
+ if (this._flags.byteAligned && value.length % 2 !== 0) {
78
+ value = `0${value}`;
79
+ }
76
80
  }
77
81
 
78
82
  return {
@@ -399,18 +403,32 @@ internals.String = class extends Any {
399
403
  });
400
404
  }
401
405
 
402
- hex() {
406
+ hex(hexOptions = {}) {
403
407
 
408
+ Hoek.assert(typeof hexOptions === 'object', 'hex options must be an object');
409
+ Hoek.assert(typeof hexOptions.byteAligned === 'undefined' || typeof hexOptions.byteAligned === 'boolean',
410
+ 'byteAligned must be boolean');
411
+
412
+ const byteAligned = hexOptions.byteAligned === true;
404
413
  const regex = /^[a-f0-9]+$/i;
405
414
 
406
- return this._test('hex', regex, function (value, state, options) {
415
+ const obj = this._test('hex', regex, function (value, state, options) {
407
416
 
408
417
  if (regex.test(value)) {
418
+ if (byteAligned && value.length % 2 !== 0) {
419
+ return this.createError('string.hexAlign', { value }, state, options);
420
+ }
409
421
  return value;
410
422
  }
411
423
 
412
424
  return this.createError('string.hex', { value }, state, options);
413
425
  });
426
+
427
+ if (byteAligned) {
428
+ obj._flags.byteAligned = true;
429
+ }
430
+
431
+ return obj;
414
432
  }
415
433
 
416
434
  base64(base64Options = {}) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "joi",
3
3
  "description": "Object schema validation",
4
- "version": "13.1.0",
4
+ "version": "13.3.0",
5
5
  "homepage": "https://github.com/hapijs/joi",
6
6
  "repository": "git://github.com/hapijs/joi",
7
7
  "main": "lib/index.js",