UNPKG

21 kBJavaScriptView Raw
1'use strict';
2
3const Assert = require('@hapi/hoek/lib/assert');
4const Clone = require('@hapi/hoek/lib/clone');
5const Ignore = require('@hapi/hoek/lib/ignore');
6const Reach = require('@hapi/hoek/lib/reach');
7
8const Common = require('./common');
9const Errors = require('./errors');
10const State = require('./state');
11
12
13const internals = {
14 result: Symbol('result')
15};
16
17
18exports.entry = function (value, schema, prefs) {
19
20 let settings = Common.defaults;
21 if (prefs) {
22 Assert(prefs.warnings === undefined, 'Cannot override warnings preference in synchronous validation');
23 Assert(prefs.artifacts === undefined, 'Cannot override artifacts preference in synchronous validation');
24 settings = Common.preferences(Common.defaults, prefs);
25 }
26
27 const result = internals.entry(value, schema, settings);
28 Assert(!result.mainstay.externals.length, 'Schema with external rules must use validateAsync()');
29 const outcome = { value: result.value };
30
31 if (result.error) {
32 outcome.error = result.error;
33 }
34
35 if (result.mainstay.warnings.length) {
36 outcome.warning = Errors.details(result.mainstay.warnings);
37 }
38
39 if (result.mainstay.debug) {
40 outcome.debug = result.mainstay.debug;
41 }
42
43 if (result.mainstay.artifacts) {
44 outcome.artifacts = result.mainstay.artifacts;
45 }
46
47 return outcome;
48};
49
50
51exports.entryAsync = async function (value, schema, prefs) {
52
53 let settings = Common.defaults;
54 if (prefs) {
55 settings = Common.preferences(Common.defaults, prefs);
56 }
57
58 const result = internals.entry(value, schema, settings);
59 const mainstay = result.mainstay;
60 if (result.error) {
61 if (mainstay.debug) {
62 result.error.debug = mainstay.debug;
63 }
64
65 throw result.error;
66 }
67
68 if (mainstay.externals.length) {
69 let root = result.value;
70 const errors = [];
71 for (const external of mainstay.externals) {
72 const path = external.state.path;
73 const linked = external.schema.type === 'link' ? mainstay.links.get(external.schema) : null;
74 let node = root;
75 let key;
76 let parent;
77
78 const ancestors = path.length ? [root] : [];
79 const original = path.length ? Reach(value, path) : value;
80
81 if (path.length) {
82 key = path[path.length - 1];
83
84 let current = root;
85 for (const segment of path.slice(0, -1)) {
86 current = current[segment];
87 ancestors.unshift(current);
88 }
89
90 parent = ancestors[0];
91 node = parent[key];
92 }
93
94 try {
95 const createError = (code, local) => (linked || external.schema).$_createError(code, node, local, external.state, settings);
96 const output = await external.method(node, {
97 schema: external.schema,
98 linked,
99 state: external.state,
100 prefs,
101 original,
102 error: createError,
103 errorsArray: internals.errorsArray,
104 warn: (code, local) => mainstay.warnings.push((linked || external.schema).$_createError(code, node, local, external.state, settings)),
105 message: (messages, local) => (linked || external.schema).$_createError('external', node, local, external.state, settings, { messages })
106 });
107
108 if (output === undefined ||
109 output === node) {
110
111 continue;
112 }
113
114 if (output instanceof Errors.Report) {
115 mainstay.tracer.log(external.schema, external.state, 'rule', 'external', 'error');
116 errors.push(output);
117
118 if (settings.abortEarly) {
119 break;
120 }
121
122 continue;
123 }
124
125 if (Array.isArray(output) &&
126 output[Common.symbols.errors]) {
127 mainstay.tracer.log(external.schema, external.state, 'rule', 'external', 'error');
128 errors.push(...output);
129
130 if (settings.abortEarly) {
131 break;
132 }
133
134 continue;
135 }
136
137 if (parent) {
138 mainstay.tracer.value(external.state, 'rule', node, output, 'external');
139 parent[key] = output;
140 }
141 else {
142 mainstay.tracer.value(external.state, 'rule', root, output, 'external');
143 root = output;
144 }
145 }
146 catch (err) {
147 if (settings.errors.label) {
148 err.message += ` (${(external.label)})`; // Change message to include path
149 }
150
151 throw err;
152 }
153 }
154
155 result.value = root;
156
157 if (errors.length) {
158 result.error = Errors.process(errors, value, settings);
159
160 if (mainstay.debug) {
161 result.error.debug = mainstay.debug;
162 }
163
164 throw result.error;
165 }
166 }
167
168 if (!settings.warnings &&
169 !settings.debug &&
170 !settings.artifacts) {
171
172 return result.value;
173 }
174
175 const outcome = { value: result.value };
176 if (mainstay.warnings.length) {
177 outcome.warning = Errors.details(mainstay.warnings);
178 }
179
180 if (mainstay.debug) {
181 outcome.debug = mainstay.debug;
182 }
183
184 if (mainstay.artifacts) {
185 outcome.artifacts = mainstay.artifacts;
186 }
187
188 return outcome;
189};
190
191
192internals.Mainstay = class {
193
194 constructor(tracer, debug, links) {
195
196 this.externals = [];
197 this.warnings = [];
198 this.tracer = tracer;
199 this.debug = debug;
200 this.links = links;
201 this.shadow = null;
202 this.artifacts = null;
203
204 this._snapshots = [];
205 }
206
207 snapshot() {
208
209 this._snapshots.push({
210 externals: this.externals.slice(),
211 warnings: this.warnings.slice()
212 });
213 }
214
215 restore() {
216
217 const snapshot = this._snapshots.pop();
218 this.externals = snapshot.externals;
219 this.warnings = snapshot.warnings;
220 }
221
222 commit() {
223
224 this._snapshots.pop();
225 }
226};
227
228
229internals.entry = function (value, schema, prefs) {
230
231 // Prepare state
232
233 const { tracer, cleanup } = internals.tracer(schema, prefs);
234 const debug = prefs.debug ? [] : null;
235 const links = schema._ids._schemaChain ? new Map() : null;
236 const mainstay = new internals.Mainstay(tracer, debug, links);
237 const schemas = schema._ids._schemaChain ? [{ schema }] : null;
238 const state = new State([], [], { mainstay, schemas });
239
240 // Validate value
241
242 const result = exports.validate(value, schema, state, prefs);
243
244 // Process value and errors
245
246 if (cleanup) {
247 schema.$_root.untrace();
248 }
249
250 const error = Errors.process(result.errors, value, prefs);
251 return { value: result.value, error, mainstay };
252};
253
254
255internals.tracer = function (schema, prefs) {
256
257 if (schema.$_root._tracer) {
258 return { tracer: schema.$_root._tracer._register(schema) };
259 }
260
261 if (prefs.debug) {
262 Assert(schema.$_root.trace, 'Debug mode not supported');
263 return { tracer: schema.$_root.trace()._register(schema), cleanup: true };
264 }
265
266 return { tracer: internals.ignore };
267};
268
269
270exports.validate = function (value, schema, state, prefs, overrides = {}) {
271
272 if (schema.$_terms.whens) {
273 schema = schema._generate(value, state, prefs).schema;
274 }
275
276 // Setup state and settings
277
278 if (schema._preferences) {
279 prefs = internals.prefs(schema, prefs);
280 }
281
282 // Cache
283
284 if (schema._cache &&
285 prefs.cache) {
286
287 const result = schema._cache.get(value);
288 state.mainstay.tracer.debug(state, 'validate', 'cached', !!result);
289 if (result) {
290 return result;
291 }
292 }
293
294 // Helpers
295
296 const createError = (code, local, localState) => schema.$_createError(code, value, local, localState || state, prefs);
297 const helpers = {
298 original: value,
299 prefs,
300 schema,
301 state,
302 error: createError,
303 errorsArray: internals.errorsArray,
304 warn: (code, local, localState) => state.mainstay.warnings.push(createError(code, local, localState)),
305 message: (messages, local) => schema.$_createError('custom', value, local, state, prefs, { messages })
306 };
307
308 // Prepare
309
310 state.mainstay.tracer.entry(schema, state);
311
312 const def = schema._definition;
313 if (def.prepare &&
314 value !== undefined &&
315 prefs.convert) {
316
317 const prepared = def.prepare(value, helpers);
318 if (prepared) {
319 state.mainstay.tracer.value(state, 'prepare', value, prepared.value);
320 if (prepared.errors) {
321 return internals.finalize(prepared.value, [].concat(prepared.errors), helpers); // Prepare error always aborts early
322 }
323
324 value = prepared.value;
325 }
326 }
327
328 // Type coercion
329
330 if (def.coerce &&
331 value !== undefined &&
332 prefs.convert &&
333 (!def.coerce.from || def.coerce.from.includes(typeof value))) {
334
335 const coerced = def.coerce.method(value, helpers);
336 if (coerced) {
337 state.mainstay.tracer.value(state, 'coerced', value, coerced.value);
338 if (coerced.errors) {
339 return internals.finalize(coerced.value, [].concat(coerced.errors), helpers); // Coerce error always aborts early
340 }
341
342 value = coerced.value;
343 }
344 }
345
346 // Empty value
347
348 const empty = schema._flags.empty;
349 if (empty &&
350 empty.$_match(internals.trim(value, schema), state.nest(empty), Common.defaults)) {
351
352 state.mainstay.tracer.value(state, 'empty', value, undefined);
353 value = undefined;
354 }
355
356 // Presence requirements (required, optional, forbidden)
357
358 const presence = overrides.presence || schema._flags.presence || (schema._flags._endedSwitch ? null : prefs.presence);
359 if (value === undefined) {
360 if (presence === 'forbidden') {
361 return internals.finalize(value, null, helpers);
362 }
363
364 if (presence === 'required') {
365 return internals.finalize(value, [schema.$_createError('any.required', value, null, state, prefs)], helpers);
366 }
367
368 if (presence === 'optional') {
369 if (schema._flags.default !== Common.symbols.deepDefault) {
370 return internals.finalize(value, null, helpers);
371 }
372
373 state.mainstay.tracer.value(state, 'default', value, {});
374 value = {};
375 }
376 }
377 else if (presence === 'forbidden') {
378 return internals.finalize(value, [schema.$_createError('any.unknown', value, null, state, prefs)], helpers);
379 }
380
381 // Allowed values
382
383 const errors = [];
384
385 if (schema._valids) {
386 const match = schema._valids.get(value, state, prefs, schema._flags.insensitive);
387 if (match) {
388 if (prefs.convert) {
389 state.mainstay.tracer.value(state, 'valids', value, match.value);
390 value = match.value;
391 }
392
393 state.mainstay.tracer.filter(schema, state, 'valid', match);
394 return internals.finalize(value, null, helpers);
395 }
396
397 if (schema._flags.only) {
398 const report = schema.$_createError('any.only', value, { valids: schema._valids.values({ display: true }) }, state, prefs);
399 if (prefs.abortEarly) {
400 return internals.finalize(value, [report], helpers);
401 }
402
403 errors.push(report);
404 }
405 }
406
407 // Denied values
408
409 if (schema._invalids) {
410 const match = schema._invalids.get(value, state, prefs, schema._flags.insensitive);
411 if (match) {
412 state.mainstay.tracer.filter(schema, state, 'invalid', match);
413 const report = schema.$_createError('any.invalid', value, { invalids: schema._invalids.values({ display: true }) }, state, prefs);
414 if (prefs.abortEarly) {
415 return internals.finalize(value, [report], helpers);
416 }
417
418 errors.push(report);
419 }
420 }
421
422 // Base type
423
424 if (def.validate) {
425 const base = def.validate(value, helpers);
426 if (base) {
427 state.mainstay.tracer.value(state, 'base', value, base.value);
428 value = base.value;
429
430 if (base.errors) {
431 if (!Array.isArray(base.errors)) {
432 errors.push(base.errors);
433 return internals.finalize(value, errors, helpers); // Base error always aborts early
434 }
435
436 if (base.errors.length) {
437 errors.push(...base.errors);
438 return internals.finalize(value, errors, helpers); // Base error always aborts early
439 }
440 }
441 }
442 }
443
444 // Validate tests
445
446 if (!schema._rules.length) {
447 return internals.finalize(value, errors, helpers);
448 }
449
450 return internals.rules(value, errors, helpers);
451};
452
453
454internals.rules = function (value, errors, helpers) {
455
456 const { schema, state, prefs } = helpers;
457
458 for (const rule of schema._rules) {
459 const definition = schema._definition.rules[rule.method];
460
461 // Skip rules that are also applied in coerce step
462
463 if (definition.convert &&
464 prefs.convert) {
465
466 state.mainstay.tracer.log(schema, state, 'rule', rule.name, 'full');
467 continue;
468 }
469
470 // Resolve references
471
472 let ret;
473 let args = rule.args;
474 if (rule._resolve.length) {
475 args = Object.assign({}, args); // Shallow copy
476 for (const key of rule._resolve) {
477 const resolver = definition.argsByName.get(key);
478
479 const resolved = args[key].resolve(value, state, prefs);
480 const normalized = resolver.normalize ? resolver.normalize(resolved) : resolved;
481
482 const invalid = Common.validateArg(normalized, null, resolver);
483 if (invalid) {
484 ret = schema.$_createError('any.ref', resolved, { arg: key, ref: args[key], reason: invalid }, state, prefs);
485 break;
486 }
487
488 args[key] = normalized;
489 }
490 }
491
492 // Test rule
493
494 ret = ret || definition.validate(value, helpers, args, rule); // Use ret if already set to reference error
495
496 const result = internals.rule(ret, rule);
497 if (result.errors) {
498 state.mainstay.tracer.log(schema, state, 'rule', rule.name, 'error');
499
500 if (rule.warn) {
501 state.mainstay.warnings.push(...result.errors);
502 continue;
503 }
504
505 if (prefs.abortEarly) {
506 return internals.finalize(value, result.errors, helpers);
507 }
508
509 errors.push(...result.errors);
510 }
511 else {
512 state.mainstay.tracer.log(schema, state, 'rule', rule.name, 'pass');
513 state.mainstay.tracer.value(state, 'rule', value, result.value, rule.name);
514 value = result.value;
515 }
516 }
517
518 return internals.finalize(value, errors, helpers);
519};
520
521
522internals.rule = function (ret, rule) {
523
524 if (ret instanceof Errors.Report) {
525 internals.error(ret, rule);
526 return { errors: [ret], value: null };
527 }
528
529 if (Array.isArray(ret) &&
530 ret[Common.symbols.errors]) {
531
532 ret.forEach((report) => internals.error(report, rule));
533 return { errors: ret, value: null };
534 }
535
536 return { errors: null, value: ret };
537};
538
539
540internals.error = function (report, rule) {
541
542 if (rule.message) {
543 report._setTemplate(rule.message);
544 }
545
546 return report;
547};
548
549
550internals.finalize = function (value, errors, helpers) {
551
552 errors = errors || [];
553 const { schema, state, prefs } = helpers;
554
555 // Failover value
556
557 if (errors.length) {
558 const failover = internals.default('failover', undefined, errors, helpers);
559 if (failover !== undefined) {
560 state.mainstay.tracer.value(state, 'failover', value, failover);
561 value = failover;
562 errors = [];
563 }
564 }
565
566 // Error override
567
568 if (errors.length &&
569 schema._flags.error) {
570
571 if (typeof schema._flags.error === 'function') {
572 errors = schema._flags.error(errors);
573 if (!Array.isArray(errors)) {
574 errors = [errors];
575 }
576
577 for (const error of errors) {
578 Assert(error instanceof Error || error instanceof Errors.Report, 'error() must return an Error object');
579 }
580 }
581 else {
582 errors = [schema._flags.error];
583 }
584 }
585
586 // Default
587
588 if (value === undefined) {
589 const defaulted = internals.default('default', value, errors, helpers);
590 state.mainstay.tracer.value(state, 'default', value, defaulted);
591 value = defaulted;
592 }
593
594 // Cast
595
596 if (schema._flags.cast &&
597 value !== undefined) {
598
599 const caster = schema._definition.cast[schema._flags.cast];
600 if (caster.from(value)) {
601 const casted = caster.to(value, helpers);
602 state.mainstay.tracer.value(state, 'cast', value, casted, schema._flags.cast);
603 value = casted;
604 }
605 }
606
607 // Externals
608
609 if (schema.$_terms.externals &&
610 prefs.externals &&
611 prefs._externals !== false) { // Disabled for matching
612
613 for (const { method } of schema.$_terms.externals) {
614 state.mainstay.externals.push({ method, schema, state, label: Errors.label(schema._flags, state, prefs) });
615 }
616 }
617
618 // Result
619
620 const result = { value, errors: errors.length ? errors : null };
621
622 if (schema._flags.result) {
623 result.value = schema._flags.result === 'strip' ? undefined : /* raw */ helpers.original;
624 state.mainstay.tracer.value(state, schema._flags.result, value, result.value);
625 state.shadow(value, schema._flags.result);
626 }
627
628 // Cache
629
630 if (schema._cache &&
631 prefs.cache !== false &&
632 !schema._refs.length) {
633
634 schema._cache.set(helpers.original, result);
635 }
636
637 // Artifacts
638
639 if (value !== undefined &&
640 !result.errors &&
641 schema._flags.artifact !== undefined) {
642
643 state.mainstay.artifacts = state.mainstay.artifacts || new Map();
644 if (!state.mainstay.artifacts.has(schema._flags.artifact)) {
645 state.mainstay.artifacts.set(schema._flags.artifact, []);
646 }
647
648 state.mainstay.artifacts.get(schema._flags.artifact).push(state.path);
649 }
650
651 return result;
652};
653
654
655internals.prefs = function (schema, prefs) {
656
657 const isDefaultOptions = prefs === Common.defaults;
658 if (isDefaultOptions &&
659 schema._preferences[Common.symbols.prefs]) {
660
661 return schema._preferences[Common.symbols.prefs];
662 }
663
664 prefs = Common.preferences(prefs, schema._preferences);
665 if (isDefaultOptions) {
666 schema._preferences[Common.symbols.prefs] = prefs;
667 }
668
669 return prefs;
670};
671
672
673internals.default = function (flag, value, errors, helpers) {
674
675 const { schema, state, prefs } = helpers;
676 const source = schema._flags[flag];
677 if (prefs.noDefaults ||
678 source === undefined) {
679
680 return value;
681 }
682
683 state.mainstay.tracer.log(schema, state, 'rule', flag, 'full');
684
685 if (!source) {
686 return source;
687 }
688
689 if (typeof source === 'function') {
690 const args = source.length ? [Clone(state.ancestors[0]), helpers] : [];
691
692 try {
693 return source(...args);
694 }
695 catch (err) {
696 errors.push(schema.$_createError(`any.${flag}`, null, { error: err }, state, prefs));
697 return;
698 }
699 }
700
701 if (typeof source !== 'object') {
702 return source;
703 }
704
705 if (source[Common.symbols.literal]) {
706 return source.literal;
707 }
708
709 if (Common.isResolvable(source)) {
710 return source.resolve(value, state, prefs);
711 }
712
713 return Clone(source);
714};
715
716
717internals.trim = function (value, schema) {
718
719 if (typeof value !== 'string') {
720 return value;
721 }
722
723 const trim = schema.$_getRule('trim');
724 if (!trim ||
725 !trim.args.enabled) {
726
727 return value;
728 }
729
730 return value.trim();
731};
732
733
734internals.ignore = {
735 active: false,
736 debug: Ignore,
737 entry: Ignore,
738 filter: Ignore,
739 log: Ignore,
740 resolve: Ignore,
741 value: Ignore
742};
743
744
745internals.errorsArray = function () {
746
747 const errors = [];
748 errors[Common.symbols.errors] = true;
749 return errors;
750};