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/trace.js ADDED
@@ -0,0 +1,346 @@
1
+ 'use strict';
2
+
3
+ const DeepEqual = require('@hapi/hoek/lib/deepEqual');
4
+ const Pinpoint = require('@hapi/pinpoint');
5
+
6
+ const Errors = require('./errors');
7
+
8
+
9
+ const internals = {
10
+ codes: {
11
+ error: 1,
12
+ pass: 2,
13
+ full: 3
14
+ },
15
+ labels: {
16
+ 0: 'never used',
17
+ 1: 'always error',
18
+ 2: 'always pass'
19
+ }
20
+ };
21
+
22
+
23
+ exports.setup = function (root) {
24
+
25
+ const trace = function () {
26
+
27
+ root._tracer = root._tracer || new internals.Tracer();
28
+ return root._tracer;
29
+ };
30
+
31
+ root.trace = trace;
32
+ root[Symbol.for('@hapi/lab/coverage/initialize')] = trace;
33
+
34
+ root.untrace = () => {
35
+
36
+ root._tracer = null;
37
+ };
38
+ };
39
+
40
+
41
+ exports.location = function (schema) {
42
+
43
+ return schema.$_setFlag('_tracerLocation', Pinpoint.location(2)); // base.tracer(), caller
44
+ };
45
+
46
+
47
+ internals.Tracer = class {
48
+
49
+ constructor() {
50
+
51
+ this.name = 'Joi';
52
+ this._schemas = new Map();
53
+ }
54
+
55
+ _register(schema) {
56
+
57
+ const existing = this._schemas.get(schema);
58
+ if (existing) {
59
+ return existing.store;
60
+ }
61
+
62
+ const store = new internals.Store(schema);
63
+ const { filename, line } = schema._flags._tracerLocation || Pinpoint.location(5); // internals.tracer(), internals.entry(), exports.entry(), validate(), caller
64
+ this._schemas.set(schema, { filename, line, store });
65
+ return store;
66
+ }
67
+
68
+ _combine(merged, sources) {
69
+
70
+ for (const { store } of this._schemas.values()) {
71
+ store._combine(merged, sources);
72
+ }
73
+ }
74
+
75
+ report(file) {
76
+
77
+ const coverage = [];
78
+
79
+ // Process each registered schema
80
+
81
+ for (const { filename, line, store } of this._schemas.values()) {
82
+ if (file &&
83
+ file !== filename) {
84
+
85
+ continue;
86
+ }
87
+
88
+ // Process sub schemas of the registered root
89
+
90
+ const missing = [];
91
+ const skipped = [];
92
+
93
+ for (const [schema, log] of store._sources.entries()) {
94
+
95
+ // Check if sub schema parent skipped
96
+
97
+ if (internals.sub(log.paths, skipped)) {
98
+ continue;
99
+ }
100
+
101
+ // Check if sub schema reached
102
+
103
+ if (!log.entry) {
104
+ missing.push({
105
+ status: 'never reached',
106
+ paths: [...log.paths]
107
+ });
108
+
109
+ skipped.push(...log.paths);
110
+ continue;
111
+ }
112
+
113
+ // Check values
114
+
115
+ for (const type of ['valid', 'invalid']) {
116
+ const set = schema[`_${type}s`];
117
+ if (!set) {
118
+ continue;
119
+ }
120
+
121
+ const values = new Set(set._values);
122
+ const refs = new Set(set._refs);
123
+ for (const { value, ref } of log[type]) {
124
+ values.delete(value);
125
+ refs.delete(ref);
126
+ }
127
+
128
+ if (values.size ||
129
+ refs.size) {
130
+
131
+ missing.push({
132
+ status: [...values, ...[...refs].map((ref) => ref.display)],
133
+ rule: `${type}s`
134
+ });
135
+ }
136
+ }
137
+
138
+ // Check rules status
139
+
140
+ const rules = schema._rules.map((rule) => rule.name);
141
+ for (const type of ['default', 'failover']) {
142
+ if (schema._flags[type] !== undefined) {
143
+ rules.push(type);
144
+ }
145
+ }
146
+
147
+ for (const name of rules) {
148
+ const status = internals.labels[log.rule[name] || 0];
149
+ if (status) {
150
+ const report = { rule: name, status };
151
+ if (log.paths.size) {
152
+ report.paths = [...log.paths];
153
+ }
154
+
155
+ missing.push(report);
156
+ }
157
+ }
158
+ }
159
+
160
+ if (missing.length) {
161
+ coverage.push({
162
+ filename,
163
+ line,
164
+ missing,
165
+ severity: 'error',
166
+ message: `Schema missing tests for ${missing.map(internals.message).join(', ')}`
167
+ });
168
+ }
169
+ }
170
+
171
+ return coverage.length ? coverage : null;
172
+ }
173
+ };
174
+
175
+
176
+ internals.Store = class {
177
+
178
+ constructor(schema) {
179
+
180
+ this.active = true;
181
+ this._sources = new Map(); // schema -> { paths, entry, rule, valid, invalid }
182
+ this._combos = new Map(); // merged -> [sources]
183
+ this._scan(schema);
184
+ }
185
+
186
+ debug(state, source, name, result) {
187
+
188
+ state.mainstay.debug && state.mainstay.debug.push({ type: source, name, result, path: state.path });
189
+ }
190
+
191
+ entry(schema, state) {
192
+
193
+ internals.debug(state, { type: 'entry' });
194
+
195
+ this._record(schema, (log) => {
196
+
197
+ log.entry = true;
198
+ });
199
+ }
200
+
201
+ filter(schema, state, source, value) {
202
+
203
+ internals.debug(state, { type: source, ...value });
204
+
205
+ this._record(schema, (log) => {
206
+
207
+ log[source].add(value);
208
+ });
209
+ }
210
+
211
+ log(schema, state, source, name, result) {
212
+
213
+ internals.debug(state, { type: source, name, result: result === 'full' ? 'pass' : result });
214
+
215
+ this._record(schema, (log) => {
216
+
217
+ log[source][name] = log[source][name] || 0;
218
+ log[source][name] |= internals.codes[result];
219
+ });
220
+ }
221
+
222
+ resolve(state, ref, to) {
223
+
224
+ if (!state.mainstay.debug) {
225
+ return;
226
+ }
227
+
228
+ const log = { type: 'resolve', ref: ref.display, to, path: state.path };
229
+ state.mainstay.debug.push(log);
230
+ }
231
+
232
+ value(state, by, from, to, name) {
233
+
234
+ if (!state.mainstay.debug ||
235
+ DeepEqual(from, to)) {
236
+
237
+ return;
238
+ }
239
+
240
+ const log = { type: 'value', by, from, to, path: state.path };
241
+ if (name) {
242
+ log.name = name;
243
+ }
244
+
245
+ state.mainstay.debug.push(log);
246
+ }
247
+
248
+ _record(schema, each) {
249
+
250
+ const log = this._sources.get(schema);
251
+ if (log) {
252
+ each(log);
253
+ return;
254
+ }
255
+
256
+ const sources = this._combos.get(schema);
257
+ for (const source of sources) {
258
+ this._record(source, each);
259
+ }
260
+ }
261
+
262
+ _scan(schema, _path) {
263
+
264
+ const path = _path || [];
265
+
266
+ let log = this._sources.get(schema);
267
+ if (!log) {
268
+ log = {
269
+ paths: new Set(),
270
+ entry: false,
271
+ rule: {},
272
+ valid: new Set(),
273
+ invalid: new Set()
274
+ };
275
+
276
+ this._sources.set(schema, log);
277
+ }
278
+
279
+ if (path.length) {
280
+ log.paths.add(path);
281
+ }
282
+
283
+ const each = (sub, source) => {
284
+
285
+ const subId = internals.id(sub, source);
286
+ this._scan(sub, path.concat(subId));
287
+ };
288
+
289
+ schema.$_modify({ each, ref: false });
290
+ }
291
+
292
+ _combine(merged, sources) {
293
+
294
+ this._combos.set(merged, sources);
295
+ }
296
+ };
297
+
298
+
299
+ internals.message = function (item) {
300
+
301
+ const path = item.paths ? Errors.path(item.paths[0]) + (item.rule ? ':' : '') : '';
302
+ return `${path}${item.rule || ''} (${item.status})`;
303
+ };
304
+
305
+
306
+ internals.id = function (schema, { source, name, path, key }) {
307
+
308
+ if (schema._flags.id) {
309
+ return schema._flags.id;
310
+ }
311
+
312
+ if (key) {
313
+ return key;
314
+ }
315
+
316
+ name = `@${name}`;
317
+
318
+ if (source === 'terms') {
319
+ return [name, path[Math.min(path.length - 1, 1)]];
320
+ }
321
+
322
+ return name;
323
+ };
324
+
325
+
326
+ internals.sub = function (paths, skipped) {
327
+
328
+ for (const path of paths) {
329
+ for (const skip of skipped) {
330
+ if (DeepEqual(path.slice(0, skip.length), skip)) {
331
+ return true;
332
+ }
333
+ }
334
+ }
335
+
336
+ return false;
337
+ };
338
+
339
+
340
+ internals.debug = function (state, event) {
341
+
342
+ if (state.mainstay.debug) {
343
+ event.path = state.debug ? [...state.path, state.debug] : state.path;
344
+ state.mainstay.debug.push(event);
345
+ }
346
+ };
@@ -0,0 +1,329 @@
1
+ 'use strict';
2
+
3
+ const Assert = require('@hapi/hoek/lib/assert');
4
+
5
+ const Any = require('./any');
6
+ const Common = require('../common');
7
+ const Compile = require('../compile');
8
+ const Errors = require('../errors');
9
+ const Ref = require('../ref');
10
+
11
+
12
+ const internals = {};
13
+
14
+
15
+ module.exports = Any.extend({
16
+
17
+ type: 'alternatives',
18
+
19
+ flags: {
20
+
21
+ match: { default: 'any' } // 'any', 'one', 'all'
22
+ },
23
+
24
+ terms: {
25
+
26
+ matches: { init: [], register: Ref.toSibling }
27
+ },
28
+
29
+ args(schema, ...schemas) {
30
+
31
+ if (schemas.length === 1) {
32
+ if (Array.isArray(schemas[0])) {
33
+ return schema.try(...schemas[0]);
34
+ }
35
+ }
36
+
37
+ return schema.try(...schemas);
38
+ },
39
+
40
+ validate(value, helpers) {
41
+
42
+ const { schema, error, state, prefs } = helpers;
43
+
44
+ // Match all or one
45
+
46
+ if (schema._flags.match) {
47
+ let hits = 0;
48
+ let matched;
49
+
50
+ for (let i = 0; i < schema.$_terms.matches.length; ++i) {
51
+ const item = schema.$_terms.matches[i];
52
+ const localState = state.nest(item.schema, `match.${i}`);
53
+ localState.snapshot();
54
+
55
+ const result = item.schema.$_validate(value, localState, prefs);
56
+ if (!result.errors) {
57
+ ++hits;
58
+ matched = result.value;
59
+ }
60
+ else {
61
+ localState.restore();
62
+ }
63
+ }
64
+
65
+ if (!hits) {
66
+ return { errors: error('alternatives.any') };
67
+ }
68
+
69
+ if (schema._flags.match === 'one') {
70
+ return hits === 1 ? { value: matched } : { errors: error('alternatives.one') };
71
+ }
72
+
73
+ return hits === schema.$_terms.matches.length ? { value } : { errors: error('alternatives.all') };
74
+ }
75
+
76
+ // Match any
77
+
78
+ const errors = [];
79
+ for (let i = 0; i < schema.$_terms.matches.length; ++i) {
80
+ const item = schema.$_terms.matches[i];
81
+
82
+ // Try
83
+
84
+ if (item.schema) {
85
+ const localState = state.nest(item.schema, `match.${i}`);
86
+ localState.snapshot();
87
+
88
+ const result = item.schema.$_validate(value, localState, prefs);
89
+ if (!result.errors) {
90
+ return result;
91
+ }
92
+
93
+ localState.restore();
94
+ errors.push({ schema: item.schema, reports: result.errors });
95
+ continue;
96
+ }
97
+
98
+ // Conditional
99
+
100
+ const input = item.ref ? item.ref.resolve(value, state, prefs) : value;
101
+ const tests = item.is ? [item] : item.switch;
102
+
103
+ for (let j = 0; j < tests.length; ++j) {
104
+ const test = tests[j];
105
+ const { is, then, otherwise } = test;
106
+
107
+ const id = `match.${i}${item.switch ? '.' + j : ''}`;
108
+ if (!is.$_match(input, state.nest(is, `${id}.is`), prefs)) {
109
+ if (otherwise) {
110
+ return otherwise.$_validate(value, state.nest(otherwise, `${id}.otherwise`), prefs);
111
+ }
112
+ }
113
+ else if (then) {
114
+ return then.$_validate(value, state.nest(then, `${id}.then`), prefs);
115
+ }
116
+ }
117
+ }
118
+
119
+ return internals.errors(errors, helpers);
120
+ },
121
+
122
+ rules: {
123
+
124
+ conditional: {
125
+ method(condition, options) {
126
+
127
+ Assert(!this._flags._endedSwitch, 'Unreachable condition');
128
+ Assert(!this._flags.match, 'Cannot combine match mode', this._flags.match, 'with conditional rule');
129
+ Assert(options.break === undefined, 'Cannot use break option with alternatives conditional');
130
+
131
+ const obj = this.clone();
132
+
133
+ const match = Compile.when(obj, condition, options);
134
+ const conditions = match.is ? [match] : match.switch;
135
+ for (const item of conditions) {
136
+ if (item.then &&
137
+ item.otherwise) {
138
+
139
+ obj.$_setFlag('_endedSwitch', true, { clone: false });
140
+ break;
141
+ }
142
+ }
143
+
144
+ obj.$_terms.matches.push(match);
145
+ return obj.$_mutateRebuild();
146
+ }
147
+ },
148
+
149
+ match: {
150
+ method(mode) {
151
+
152
+ Assert(['any', 'one', 'all'].includes(mode), 'Invalid alternatives match mode', mode);
153
+
154
+ if (mode !== 'any') {
155
+ for (const match of this.$_terms.matches) {
156
+ Assert(match.schema, 'Cannot combine match mode', mode, 'with conditional rules');
157
+ }
158
+ }
159
+
160
+ return this.$_setFlag('match', mode);
161
+ }
162
+ },
163
+
164
+ try: {
165
+ method(...schemas) {
166
+
167
+ Assert(schemas.length, 'Missing alternative schemas');
168
+ Common.verifyFlat(schemas, 'try');
169
+
170
+ Assert(!this._flags._endedSwitch, 'Unreachable condition');
171
+
172
+ const obj = this.clone();
173
+ for (const schema of schemas) {
174
+ obj.$_terms.matches.push({ schema: obj.$_compile(schema) });
175
+ }
176
+
177
+ return obj.$_mutateRebuild();
178
+ }
179
+ }
180
+ },
181
+
182
+ overrides: {
183
+
184
+ label(name) {
185
+
186
+ const obj = this.$_parent('label', name);
187
+ const each = (item, source) => (source.path[0] !== 'is' ? item.label(name) : undefined);
188
+ return obj.$_modify({ each, ref: false });
189
+ }
190
+ },
191
+
192
+ rebuild(schema) {
193
+
194
+ // Flag when an alternative type is an array
195
+
196
+ const each = (item) => {
197
+
198
+ if (Common.isSchema(item) &&
199
+ item.type === 'array') {
200
+
201
+ schema.$_setFlag('_arrayItems', true, { clone: false });
202
+ }
203
+ };
204
+
205
+ schema.$_modify({ each });
206
+ },
207
+
208
+ manifest: {
209
+
210
+ build(obj, desc) {
211
+
212
+ if (desc.matches) {
213
+ for (const match of desc.matches) {
214
+ const { schema, ref, is, not, then, otherwise } = match;
215
+ if (schema) {
216
+ obj = obj.try(schema);
217
+ }
218
+ else if (ref) {
219
+ obj = obj.conditional(ref, { is, then, not, otherwise, switch: match.switch });
220
+ }
221
+ else {
222
+ obj = obj.conditional(is, { then, otherwise });
223
+ }
224
+ }
225
+ }
226
+
227
+ return obj;
228
+ }
229
+ },
230
+
231
+ messages: {
232
+ 'alternatives.all': '{{#label}} does not match all of the required types',
233
+ 'alternatives.any': '{{#label}} does not match any of the allowed types',
234
+ 'alternatives.match': '{{#label}} does not match any of the allowed types',
235
+ 'alternatives.one': '{{#label}} matches more than one allowed type',
236
+ 'alternatives.types': '{{#label}} must be one of {{#types}}'
237
+ }
238
+ });
239
+
240
+
241
+ // Helpers
242
+
243
+ internals.errors = function (failures, { error, state }) {
244
+
245
+ // Nothing matched due to type criteria rules
246
+
247
+ if (!failures.length) {
248
+ return { errors: error('alternatives.any') };
249
+ }
250
+
251
+ // Single error
252
+
253
+ if (failures.length === 1) {
254
+ return { errors: failures[0].reports };
255
+ }
256
+
257
+ // Analyze reasons
258
+
259
+ const valids = new Set();
260
+ const complex = [];
261
+
262
+ for (const { reports, schema } of failures) {
263
+
264
+ // Multiple errors (!abortEarly)
265
+
266
+ if (reports.length > 1) {
267
+ return internals.unmatched(failures, error);
268
+ }
269
+
270
+ // Custom error
271
+
272
+ const report = reports[0];
273
+ if (report instanceof Errors.Report === false) {
274
+ return internals.unmatched(failures, error);
275
+ }
276
+
277
+ // Internal object or array error
278
+
279
+ if (report.state.path.length !== state.path.length) {
280
+ complex.push({ type: schema.type, report });
281
+ continue;
282
+ }
283
+
284
+ // Valids
285
+
286
+ if (report.code === 'any.only') {
287
+ for (const valid of report.local.valids) {
288
+ valids.add(valid);
289
+ }
290
+
291
+ continue;
292
+ }
293
+
294
+ // Base type
295
+
296
+ const [type, code] = report.code.split('.');
297
+ if (code !== 'base') {
298
+ complex.push({ type: schema.type, report });
299
+ continue;
300
+ }
301
+
302
+ valids.add(type);
303
+ }
304
+
305
+ // All errors are base types or valids
306
+
307
+ if (!complex.length) {
308
+ return { errors: error('alternatives.types', { types: [...valids] }) };
309
+ }
310
+
311
+ // Single complex error
312
+
313
+ if (complex.length === 1) {
314
+ return { errors: complex[0].report };
315
+ }
316
+
317
+ return internals.unmatched(failures, error);
318
+ };
319
+
320
+
321
+ internals.unmatched = function (failures, error) {
322
+
323
+ const errors = [];
324
+ for (const failure of failures) {
325
+ errors.push(...failure.reports);
326
+ }
327
+
328
+ return { errors: error('alternatives.match', Errors.details(errors, { override: false })) };
329
+ };