joi 14.2.0 → 17.2.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.
Files changed (59) hide show
  1. package/LICENSE.md +10 -0
  2. package/README.md +9 -118
  3. package/dist/joi-browser.min.js +1 -0
  4. package/lib/annotate.js +175 -0
  5. package/lib/base.js +1068 -0
  6. package/lib/cache.js +143 -0
  7. package/lib/common.js +216 -0
  8. package/lib/compile.js +283 -0
  9. package/lib/errors.js +160 -269
  10. package/lib/extend.js +312 -0
  11. package/lib/index.d.ts +2200 -0
  12. package/lib/index.js +179 -347
  13. package/lib/manifest.js +476 -0
  14. package/lib/messages.js +178 -0
  15. package/lib/modify.js +267 -0
  16. package/lib/ref.js +386 -25
  17. package/lib/schemas.js +291 -15
  18. package/lib/state.js +152 -0
  19. package/lib/template.js +427 -0
  20. package/lib/trace.js +346 -0
  21. package/lib/types/alternatives.js +329 -0
  22. package/lib/types/any.js +174 -0
  23. package/lib/types/array.js +775 -0
  24. package/lib/types/binary.js +98 -0
  25. package/lib/types/boolean.js +150 -0
  26. package/lib/types/date.js +233 -0
  27. package/lib/types/function.js +93 -0
  28. package/lib/types/keys.js +1043 -0
  29. package/lib/types/link.js +168 -0
  30. package/lib/types/number.js +335 -0
  31. package/lib/types/object.js +22 -0
  32. package/lib/types/string.js +820 -0
  33. package/lib/types/symbol.js +102 -0
  34. package/lib/validator.js +650 -0
  35. package/lib/values.js +263 -0
  36. package/package.json +34 -29
  37. package/CHANGELOG.md +0 -3
  38. package/LICENSE +0 -25
  39. package/lib/cast.js +0 -64
  40. package/lib/language.js +0 -166
  41. package/lib/set.js +0 -191
  42. package/lib/types/alternatives/index.js +0 -218
  43. package/lib/types/any/index.js +0 -978
  44. package/lib/types/any/settings.js +0 -36
  45. package/lib/types/array/index.js +0 -707
  46. package/lib/types/binary/index.js +0 -100
  47. package/lib/types/boolean/index.js +0 -100
  48. package/lib/types/date/index.js +0 -182
  49. package/lib/types/func/index.js +0 -90
  50. package/lib/types/lazy/index.js +0 -82
  51. package/lib/types/number/index.js +0 -248
  52. package/lib/types/object/index.js +0 -957
  53. package/lib/types/state.js +0 -11
  54. package/lib/types/string/index.js +0 -703
  55. package/lib/types/string/ip.js +0 -54
  56. package/lib/types/string/rfc3986.js +0 -219
  57. package/lib/types/string/uri.js +0 -46
  58. package/lib/types/symbol/index.js +0 -93
  59. package/lib/types/symbols.js +0 -5
package/lib/errors.js CHANGED
@@ -1,74 +1,56 @@
1
1
  'use strict';
2
2
 
3
- // Load modules
3
+ const Annotate = require('./annotate');
4
+ const Common = require('./common');
5
+ const Template = require('./template');
4
6
 
5
- const Hoek = require('hoek');
6
- const Language = require('./language');
7
7
 
8
+ const internals = {};
8
9
 
9
- // Declare internals
10
10
 
11
- const internals = {
12
- annotations: Symbol('joi-annotations')
13
- };
11
+ exports.Report = class {
14
12
 
15
- internals.stringify = function (value, wrapArrays) {
13
+ constructor(code, value, local, flags, messages, state, prefs) {
16
14
 
17
- const type = typeof value;
15
+ this.code = code;
16
+ this.flags = flags;
17
+ this.messages = messages;
18
+ this.path = state.path;
19
+ this.prefs = prefs;
20
+ this.state = state;
21
+ this.value = value;
18
22
 
19
- if (value === null) {
20
- return 'null';
21
- }
23
+ this.message = null;
24
+ this.template = null;
22
25
 
23
- if (type === 'string') {
24
- return value;
25
- }
26
+ this.local = local || {};
27
+ this.local.label = exports.label(this.flags, this.state, this.prefs, this.messages);
26
28
 
27
- if (value instanceof exports.Err || type === 'function' || type === 'symbol') {
28
- return value.toString();
29
- }
29
+ if (this.value !== undefined &&
30
+ !this.local.hasOwnProperty('value')) {
30
31
 
31
- if (type === 'object') {
32
- if (Array.isArray(value)) {
33
- let partial = '';
32
+ this.local.value = this.value;
33
+ }
34
34
 
35
- for (let i = 0; i < value.length; ++i) {
36
- partial = partial + (partial.length ? ', ' : '') + internals.stringify(value[i], wrapArrays);
35
+ if (this.path.length) {
36
+ const key = this.path[this.path.length - 1];
37
+ if (typeof key !== 'object') {
38
+ this.local.key = key;
37
39
  }
38
-
39
- return wrapArrays ? '[' + partial + ']' : partial;
40
40
  }
41
-
42
- return value.toString();
43
41
  }
44
42
 
45
- return JSON.stringify(value);
46
- };
47
-
48
- exports.Err = class {
43
+ _setTemplate(template) {
49
44
 
50
- constructor(type, context, state, options, flags, message, template) {
51
-
52
- this.isJoi = true;
53
- this.type = type;
54
- this.context = context || {};
55
- this.context.key = state.path[state.path.length - 1];
56
- this.context.label = state.key;
57
- this.path = state.path;
58
- this.options = options;
59
- this.flags = flags;
60
- this.message = message;
61
45
  this.template = template;
62
46
 
63
- const localized = this.options.language;
47
+ if (!this.flags.label &&
48
+ this.path.length === 0) {
64
49
 
65
- if (this.flags.label) {
66
- this.context.label = this.flags.label;
67
- }
68
- else if (localized && // language can be null for arrays exclusion check
69
- (this.context.label === '' ||
70
- this.context.label === null)) {
71
- this.context.label = localized.root || Language.errors.root;
50
+ const localized = this._template(this.template, 'root');
51
+ if (localized) {
52
+ this.local.label = localized;
53
+ }
72
54
  }
73
55
  }
74
56
 
@@ -78,294 +60,203 @@ exports.Err = class {
78
60
  return this.message;
79
61
  }
80
62
 
81
- let format;
63
+ const code = this.code;
82
64
 
83
- if (this.template) {
84
- format = this.template;
65
+ if (!this.prefs.errors.render) {
66
+ return this.code;
85
67
  }
86
68
 
87
- const localized = this.options.language;
69
+ const template = this._template(this.template) ||
70
+ this._template(this.prefs.messages) ||
71
+ this._template(this.messages);
88
72
 
89
- format = format || Hoek.reach(localized, this.type) || Hoek.reach(Language.errors, this.type);
90
-
91
- if (format === undefined) {
92
- return `Error code "${this.type}" is not defined, your custom type is missing the correct language definition`;
73
+ if (template === undefined) {
74
+ return `Error code "${code}" is not defined, your custom type is missing the correct messages definition`;
93
75
  }
94
76
 
95
- let wrapArrays = Hoek.reach(localized, 'messages.wrapArrays');
96
- if (typeof wrapArrays !== 'boolean') {
97
- wrapArrays = Language.errors.messages.wrapArrays;
77
+ // Render and cache result
78
+
79
+ this.message = template.render(this.value, this.state, this.prefs, this.local, { errors: this.prefs.errors, messages: [this.prefs.messages, this.messages] });
80
+ if (!this.prefs.errors.label) {
81
+ this.message = this.message.replace(/^"" /, '').trim();
98
82
  }
99
83
 
100
- if (format === null) {
101
- const childrenString = internals.stringify(this.context.reason, wrapArrays);
102
- if (wrapArrays) {
103
- return childrenString.slice(1, -1);
104
- }
84
+ return this.message;
85
+ }
105
86
 
106
- return childrenString;
107
- }
87
+ _template(messages, code) {
88
+
89
+ return exports.template(this.value, messages, code || this.code, this.state, this.prefs);
90
+ }
91
+ };
108
92
 
109
- const hasKey = /{{!?label}}/.test(format);
110
- const skipKey = format.length > 2 && format[0] === '!' && format[1] === '!';
111
93
 
112
- if (skipKey) {
113
- format = format.slice(2);
94
+ exports.path = function (path) {
95
+
96
+ let label = '';
97
+ for (const segment of path) {
98
+ if (typeof segment === 'object') { // Exclude array single path segment
99
+ continue;
114
100
  }
115
101
 
116
- if (!hasKey && !skipKey) {
117
- const localizedKey = Hoek.reach(localized, 'key');
118
- if (typeof localizedKey === 'string') {
119
- format = localizedKey + format;
120
- }
121
- else {
122
- format = Hoek.reach(Language.errors, 'key') + format;
102
+ if (typeof segment === 'string') {
103
+ if (label) {
104
+ label += '.';
123
105
  }
106
+
107
+ label += segment;
108
+ }
109
+ else {
110
+ label += `[${segment}]`;
124
111
  }
112
+ }
113
+
114
+ return label;
115
+ };
125
116
 
126
- const message = format.replace(/{{(!?)([^}]+)}}/g, ($0, isSecure, name) => {
127
117
 
128
- const value = Hoek.reach(this.context, name);
129
- const normalized = internals.stringify(value, wrapArrays);
130
- return (isSecure && this.options.escapeHtml ? Hoek.escapeHtml(normalized) : normalized);
131
- });
118
+ exports.template = function (value, messages, code, state, prefs) {
132
119
 
133
- this.toString = () => message; // Persist result of last toString call, it won't change
120
+ if (!messages) {
121
+ return;
122
+ }
134
123
 
135
- return message;
124
+ if (Template.isTemplate(messages)) {
125
+ return code !== 'root' ? messages : null;
136
126
  }
137
127
 
138
- };
128
+ let lang = prefs.errors.language;
129
+ if (Common.isResolvable(lang)) {
130
+ lang = lang.resolve(value, state, prefs);
131
+ }
139
132
 
133
+ if (lang &&
134
+ messages[lang] &&
135
+ messages[lang][code] !== undefined) {
140
136
 
141
- exports.create = function (type, context, state, options, flags, message, template) {
137
+ return messages[lang][code];
138
+ }
142
139
 
143
- return new exports.Err(type, context, state, options, flags, message, template);
140
+ return messages[code];
144
141
  };
145
142
 
146
143
 
147
- exports.process = function (errors, object) {
144
+ exports.label = function (flags, state, prefs, messages) {
148
145
 
149
- if (!errors) {
150
- return null;
146
+ if (flags.label) {
147
+ return flags.label;
151
148
  }
152
149
 
153
- // Construct error
154
-
155
- let message = '';
156
- const details = [];
150
+ if (!prefs.errors.label) {
151
+ return '';
152
+ }
157
153
 
158
- const processErrors = function (localErrors, parent, overrideMessage) {
154
+ let path = state.path;
155
+ if (prefs.errors.label === 'key' &&
156
+ state.path.length > 1) {
159
157
 
160
- for (let i = 0; i < localErrors.length; ++i) {
161
- const item = localErrors[i];
158
+ path = state.path.slice(-1);
159
+ }
162
160
 
163
- if (item instanceof Error) {
164
- return item;
165
- }
161
+ const normalized = exports.path(path);
162
+ if (normalized) {
163
+ return normalized;
164
+ }
166
165
 
167
- if (item.flags.error && typeof item.flags.error !== 'function') {
168
- if (!item.flags.selfError || !item.context.reason) {
169
- return item.flags.error;
170
- }
171
- }
166
+ return exports.template(null, prefs.messages, 'root', state, prefs) ||
167
+ messages && exports.template(null, messages, 'root', state, prefs) ||
168
+ 'value';
169
+ };
172
170
 
173
- let itemMessage;
174
- if (parent === undefined) {
175
- itemMessage = item.toString();
176
- message = message + (message ? '. ' : '') + itemMessage;
177
- }
178
171
 
179
- // Do not push intermediate errors, we're only interested in leafs
172
+ exports.process = function (errors, original, prefs) {
180
173
 
181
- if (item.context.reason) {
182
- const override = processErrors(item.context.reason, item.path, item.type === 'override' ? item.message : null);
183
- if (override) {
184
- return override;
185
- }
186
- }
187
- else {
188
- details.push({
189
- message: overrideMessage || itemMessage || item.toString(),
190
- path: item.path,
191
- type: item.type,
192
- context: item.context
193
- });
194
- }
195
- }
196
- };
174
+ if (!errors) {
175
+ return null;
176
+ }
197
177
 
198
- const override = processErrors(errors);
178
+ const { override, message, details } = exports.details(errors);
199
179
  if (override) {
200
180
  return override;
201
181
  }
202
182
 
203
- const error = new Error(message);
204
- error.isJoi = true;
205
- error.name = 'ValidationError';
206
- error.details = details;
207
- error._object = object;
208
- error.annotate = internals.annotate;
209
- return error;
210
- };
211
-
212
-
213
- // Inspired by json-stringify-safe
214
- internals.safeStringify = function (obj, spaces) {
183
+ if (prefs.errors.stack) {
184
+ return new exports.ValidationError(message, details, original);
185
+ }
215
186
 
216
- return JSON.stringify(obj, internals.serializer(), spaces);
187
+ const limit = Error.stackTraceLimit;
188
+ Error.stackTraceLimit = 0;
189
+ const validationError = new exports.ValidationError(message, details, original);
190
+ Error.stackTraceLimit = limit;
191
+ return validationError;
217
192
  };
218
193
 
219
- internals.serializer = function () {
220
-
221
- const keys = [];
222
- const stack = [];
223
194
 
224
- const cycleReplacer = (key, value) => {
195
+ exports.details = function (errors, options = {}) {
225
196
 
226
- if (stack[0] === value) {
227
- return '[Circular ~]';
228
- }
197
+ let messages = [];
198
+ const details = [];
229
199
 
230
- return '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']';
231
- };
200
+ for (const item of errors) {
232
201
 
233
- return function (key, value) {
202
+ // Override
234
203
 
235
- if (stack.length > 0) {
236
- const thisPos = stack.indexOf(this);
237
- if (~thisPos) {
238
- stack.length = thisPos + 1;
239
- keys.length = thisPos + 1;
240
- keys[thisPos] = key;
241
- }
242
- else {
243
- stack.push(this);
244
- keys.push(key);
204
+ if (item instanceof Error) {
205
+ if (options.override !== false) {
206
+ return { override: item };
245
207
  }
246
208
 
247
- if (~stack.indexOf(value)) {
248
- value = cycleReplacer.call(this, key, value);
249
- }
250
- }
251
- else {
252
- stack.push(value);
253
- }
209
+ const message = item.toString();
210
+ messages.push(message);
254
211
 
255
- if (value) {
256
- const annotations = value[internals.annotations];
257
- if (annotations) {
258
- if (Array.isArray(value)) {
259
- const annotated = [];
260
-
261
- for (let i = 0; i < value.length; ++i) {
262
- if (annotations.errors[i]) {
263
- annotated.push(`_$idx$_${annotations.errors[i].sort().join(', ')}_$end$_`);
264
- }
265
-
266
- annotated.push(value[i]);
267
- }
268
-
269
- value = annotated;
270
- }
271
- else {
272
- const errorKeys = Object.keys(annotations.errors);
273
- for (let i = 0; i < errorKeys.length; ++i) {
274
- const errorKey = errorKeys[i];
275
- value[`${errorKey}_$key$_${annotations.errors[errorKey].sort().join(', ')}_$end$_`] = value[errorKey];
276
- value[errorKey] = undefined;
277
- }
278
-
279
- const missingKeys = Object.keys(annotations.missing);
280
- for (let i = 0; i < missingKeys.length; ++i) {
281
- const missingKey = missingKeys[i];
282
- value[`_$miss$_${missingKey}|${annotations.missing[missingKey]}_$end$_`] = '__missing__';
283
- }
284
- }
285
-
286
- return value;
287
- }
288
- }
212
+ details.push({
213
+ message,
214
+ type: 'override',
215
+ context: { error: item }
216
+ });
289
217
 
290
- if (value === Infinity || value === -Infinity || Number.isNaN(value) ||
291
- typeof value === 'function' || typeof value === 'symbol') {
292
- return '[' + value.toString() + ']';
218
+ continue;
293
219
  }
294
220
 
295
- return value;
296
- };
297
- };
221
+ // Report
298
222
 
223
+ const message = item.toString();
224
+ messages.push(message);
299
225
 
300
- internals.annotate = function (stripColorCodes) {
301
-
302
- const redFgEscape = stripColorCodes ? '' : '\u001b[31m';
303
- const redBgEscape = stripColorCodes ? '' : '\u001b[41m';
304
- const endColor = stripColorCodes ? '' : '\u001b[0m';
226
+ details.push({
227
+ message,
228
+ path: item.path.filter((v) => typeof v !== 'object'),
229
+ type: item.code,
230
+ context: item.local
231
+ });
232
+ }
305
233
 
306
- if (typeof this._object !== 'object') {
307
- return this.details[0].message;
234
+ if (messages.length > 1) {
235
+ messages = [...new Set(messages)];
308
236
  }
309
237
 
310
- const obj = Hoek.clone(this._object || {});
238
+ return { message: messages.join('. '), details };
239
+ };
311
240
 
312
- for (let i = this.details.length - 1; i >= 0; --i) { // Reverse order to process deepest child first
313
- const pos = i + 1;
314
- const error = this.details[i];
315
- const path = error.path;
316
- let ref = obj;
317
- for (let j = 0; ; ++j) {
318
- const seg = path[j];
319
241
 
320
- if (ref.isImmutable) {
321
- ref = ref.clone(); // joi schemas are not cloned by hoek, we have to take this extra step
322
- }
242
+ exports.ValidationError = class extends Error {
323
243
 
324
- if (j + 1 < path.length &&
325
- ref[seg] &&
326
- typeof ref[seg] !== 'string') {
244
+ constructor(message, details, original) {
327
245
 
328
- ref = ref[seg];
329
- }
330
- else {
331
- const refAnnotations = ref[internals.annotations] = ref[internals.annotations] || { errors: {}, missing: {} };
332
- const value = ref[seg];
333
- const cacheKey = seg || error.context.label;
334
-
335
- if (value !== undefined) {
336
- refAnnotations.errors[cacheKey] = refAnnotations.errors[cacheKey] || [];
337
- refAnnotations.errors[cacheKey].push(pos);
338
- }
339
- else {
340
- refAnnotations.missing[cacheKey] = pos;
341
- }
342
-
343
- break;
344
- }
345
- }
246
+ super(message);
247
+ this._original = original;
248
+ this.details = details;
346
249
  }
347
250
 
348
- const replacers = {
349
- key: /_\$key\$_([, \d]+)_\$end\$_"/g,
350
- missing: /"_\$miss\$_([^|]+)\|(\d+)_\$end\$_": "__missing__"/g,
351
- arrayIndex: /\s*"_\$idx\$_([, \d]+)_\$end\$_",?\n(.*)/g,
352
- specials: /"\[(NaN|Symbol.*|-?Infinity|function.*|\(.*)]"/g
353
- };
251
+ static isError(err) {
354
252
 
355
- let message = internals.safeStringify(obj, 2)
356
- .replace(replacers.key, ($0, $1) => `" ${redFgEscape}[${$1}]${endColor}`)
357
- .replace(replacers.missing, ($0, $1, $2) => `${redBgEscape}"${$1}"${endColor}${redFgEscape} [${$2}]: -- missing --${endColor}`)
358
- .replace(replacers.arrayIndex, ($0, $1, $2) => `\n${$2} ${redFgEscape}[${$1}]${endColor}`)
359
- .replace(replacers.specials, ($0, $1) => $1);
253
+ return err instanceof exports.ValidationError;
254
+ }
255
+ };
360
256
 
361
- message = `${message}\n${redFgEscape}`;
362
257
 
363
- for (let i = 0; i < this.details.length; ++i) {
364
- const pos = i + 1;
365
- message = `${message}\n[${pos}] ${this.details[i].message}`;
366
- }
258
+ exports.ValidationError.prototype.isJoi = true;
367
259
 
368
- message = message + endColor;
260
+ exports.ValidationError.prototype.name = 'ValidationError';
369
261
 
370
- return message;
371
- };
262
+ exports.ValidationError.prototype.annotate = Annotate.error;