1070 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1070 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| const Assert = require('@hapi/hoek/lib/assert');
 | |
| const Clone = require('@hapi/hoek/lib/clone');
 | |
| const DeepEqual = require('@hapi/hoek/lib/deepEqual');
 | |
| const Merge = require('@hapi/hoek/lib/merge');
 | |
| 
 | |
| const Cache = require('./cache');
 | |
| const Common = require('./common');
 | |
| const Compile = require('./compile');
 | |
| const Errors = require('./errors');
 | |
| const Extend = require('./extend');
 | |
| const Manifest = require('./manifest');
 | |
| const Messages = require('./messages');
 | |
| const Modify = require('./modify');
 | |
| const Ref = require('./ref');
 | |
| const Trace = require('./trace');
 | |
| const Validator = require('./validator');
 | |
| const Values = require('./values');
 | |
| 
 | |
| 
 | |
| const internals = {};
 | |
| 
 | |
| 
 | |
| internals.Base = class {
 | |
| 
 | |
|     constructor(type) {
 | |
| 
 | |
|         // Naming: public, _private, $_extension, $_mutate{action}
 | |
| 
 | |
|         this.type = type;
 | |
| 
 | |
|         this.$_root = null;
 | |
|         this._definition = {};
 | |
|         this._reset();
 | |
|     }
 | |
| 
 | |
|     _reset() {
 | |
| 
 | |
|         this._ids = new Modify.Ids();
 | |
|         this._preferences = null;
 | |
|         this._refs = new Ref.Manager();
 | |
|         this._cache = null;
 | |
| 
 | |
|         this._valids = null;
 | |
|         this._invalids = null;
 | |
| 
 | |
|         this._flags = {};
 | |
|         this._rules = [];
 | |
|         this._singleRules = new Map();              // The rule options passed for non-multi rules
 | |
| 
 | |
|         this.$_terms = {};                          // Hash of arrays of immutable objects (extended by other types)
 | |
| 
 | |
|         this.$_temp = {                             // Runtime state (not cloned)
 | |
|             ruleset: null,                          // null: use last, false: error, number: start position
 | |
|             whens: {}                               // Runtime cache of generated whens
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     // Manifest
 | |
| 
 | |
|     describe() {
 | |
| 
 | |
|         Assert(typeof Manifest.describe === 'function', 'Manifest functionality disabled');
 | |
|         return Manifest.describe(this);
 | |
|     }
 | |
| 
 | |
|     // Rules
 | |
| 
 | |
|     allow(...values) {
 | |
| 
 | |
|         Common.verifyFlat(values, 'allow');
 | |
|         return this._values(values, '_valids');
 | |
|     }
 | |
| 
 | |
|     alter(targets) {
 | |
| 
 | |
|         Assert(targets && typeof targets === 'object' && !Array.isArray(targets), 'Invalid targets argument');
 | |
|         Assert(!this._inRuleset(), 'Cannot set alterations inside a ruleset');
 | |
| 
 | |
|         const obj = this.clone();
 | |
|         obj.$_terms.alterations = obj.$_terms.alterations || [];
 | |
|         for (const target in targets) {
 | |
|             const adjuster = targets[target];
 | |
|             Assert(typeof adjuster === 'function', 'Alteration adjuster for', target, 'must be a function');
 | |
|             obj.$_terms.alterations.push({ target, adjuster });
 | |
|         }
 | |
| 
 | |
|         obj.$_temp.ruleset = false;
 | |
|         return obj;
 | |
|     }
 | |
| 
 | |
|     artifact(id) {
 | |
| 
 | |
|         Assert(id !== undefined, 'Artifact cannot be undefined');
 | |
|         Assert(!this._cache, 'Cannot set an artifact with a rule cache');
 | |
| 
 | |
|         return this.$_setFlag('artifact', id);
 | |
|     }
 | |
| 
 | |
|     cast(to) {
 | |
| 
 | |
|         Assert(to === false || typeof to === 'string', 'Invalid to value');
 | |
|         Assert(to === false || this._definition.cast[to], 'Type', this.type, 'does not support casting to', to);
 | |
| 
 | |
|         return this.$_setFlag('cast', to === false ? undefined : to);
 | |
|     }
 | |
| 
 | |
|     default(value, options) {
 | |
| 
 | |
|         return this._default('default', value, options);
 | |
|     }
 | |
| 
 | |
|     description(desc) {
 | |
| 
 | |
|         Assert(desc && typeof desc === 'string', 'Description must be a non-empty string');
 | |
| 
 | |
|         return this.$_setFlag('description', desc);
 | |
|     }
 | |
| 
 | |
|     empty(schema) {
 | |
| 
 | |
|         const obj = this.clone();
 | |
| 
 | |
|         if (schema !== undefined) {
 | |
|             schema = obj.$_compile(schema, { override: false });
 | |
|         }
 | |
| 
 | |
|         return obj.$_setFlag('empty', schema, { clone: false });
 | |
|     }
 | |
| 
 | |
|     error(err) {
 | |
| 
 | |
|         Assert(err, 'Missing error');
 | |
|         Assert(err instanceof Error || typeof err === 'function', 'Must provide a valid Error object or a function');
 | |
| 
 | |
|         return this.$_setFlag('error', err);
 | |
|     }
 | |
| 
 | |
|     example(example, options = {}) {
 | |
| 
 | |
|         Assert(example !== undefined, 'Missing example');
 | |
|         Common.assertOptions(options, ['override']);
 | |
| 
 | |
|         return this._inner('examples', example, { single: true, override: options.override });
 | |
|     }
 | |
| 
 | |
|     external(method, description) {
 | |
| 
 | |
|         if (typeof method === 'object') {
 | |
|             Assert(!description, 'Cannot combine options with description');
 | |
|             description = method.description;
 | |
|             method = method.method;
 | |
|         }
 | |
| 
 | |
|         Assert(typeof method === 'function', 'Method must be a function');
 | |
|         Assert(description === undefined || description && typeof description === 'string', 'Description must be a non-empty string');
 | |
| 
 | |
|         return this._inner('externals', { method, description }, { single: true });
 | |
|     }
 | |
| 
 | |
|     failover(value, options) {
 | |
| 
 | |
|         return this._default('failover', value, options);
 | |
|     }
 | |
| 
 | |
|     forbidden() {
 | |
| 
 | |
|         return this.presence('forbidden');
 | |
|     }
 | |
| 
 | |
|     id(id) {
 | |
| 
 | |
|         if (!id) {
 | |
|             return this.$_setFlag('id', undefined);
 | |
|         }
 | |
| 
 | |
|         Assert(typeof id === 'string', 'id must be a non-empty string');
 | |
|         Assert(/^[^\.]+$/.test(id), 'id cannot contain period character');
 | |
| 
 | |
|         return this.$_setFlag('id', id);
 | |
|     }
 | |
| 
 | |
|     invalid(...values) {
 | |
| 
 | |
|         return this._values(values, '_invalids');
 | |
|     }
 | |
| 
 | |
|     label(name) {
 | |
| 
 | |
|         Assert(name && typeof name === 'string', 'Label name must be a non-empty string');
 | |
| 
 | |
|         return this.$_setFlag('label', name);
 | |
|     }
 | |
| 
 | |
|     meta(meta) {
 | |
| 
 | |
|         Assert(meta !== undefined, 'Meta cannot be undefined');
 | |
| 
 | |
|         return this._inner('metas', meta, { single: true });
 | |
|     }
 | |
| 
 | |
|     note(...notes) {
 | |
| 
 | |
|         Assert(notes.length, 'Missing notes');
 | |
|         for (const note of notes) {
 | |
|             Assert(note && typeof note === 'string', 'Notes must be non-empty strings');
 | |
|         }
 | |
| 
 | |
|         return this._inner('notes', notes);
 | |
|     }
 | |
| 
 | |
|     only(mode = true) {
 | |
| 
 | |
|         Assert(typeof mode === 'boolean', 'Invalid mode:', mode);
 | |
| 
 | |
|         return this.$_setFlag('only', mode);
 | |
|     }
 | |
| 
 | |
|     optional() {
 | |
| 
 | |
|         return this.presence('optional');
 | |
|     }
 | |
| 
 | |
|     prefs(prefs) {
 | |
| 
 | |
|         Assert(prefs, 'Missing preferences');
 | |
|         Assert(prefs.context === undefined, 'Cannot override context');
 | |
|         Assert(prefs.externals === undefined, 'Cannot override externals');
 | |
|         Assert(prefs.warnings === undefined, 'Cannot override warnings');
 | |
|         Assert(prefs.debug === undefined, 'Cannot override debug');
 | |
| 
 | |
|         Common.checkPreferences(prefs);
 | |
| 
 | |
|         const obj = this.clone();
 | |
|         obj._preferences = Common.preferences(obj._preferences, prefs);
 | |
|         return obj;
 | |
|     }
 | |
| 
 | |
|     presence(mode) {
 | |
| 
 | |
|         Assert(['optional', 'required', 'forbidden'].includes(mode), 'Unknown presence mode', mode);
 | |
| 
 | |
|         return this.$_setFlag('presence', mode);
 | |
|     }
 | |
| 
 | |
|     raw(enabled = true) {
 | |
| 
 | |
|         return this.$_setFlag('result', enabled ? 'raw' : undefined);
 | |
|     }
 | |
| 
 | |
|     result(mode) {
 | |
| 
 | |
|         Assert(['raw', 'strip'].includes(mode), 'Unknown result mode', mode);
 | |
| 
 | |
|         return this.$_setFlag('result', mode);
 | |
|     }
 | |
| 
 | |
|     required() {
 | |
| 
 | |
|         return this.presence('required');
 | |
|     }
 | |
| 
 | |
|     strict(enabled) {
 | |
| 
 | |
|         const obj = this.clone();
 | |
| 
 | |
|         const convert = enabled === undefined ? false : !enabled;
 | |
|         obj._preferences = Common.preferences(obj._preferences, { convert });
 | |
|         return obj;
 | |
|     }
 | |
| 
 | |
|     strip(enabled = true) {
 | |
| 
 | |
|         return this.$_setFlag('result', enabled ? 'strip' : undefined);
 | |
|     }
 | |
| 
 | |
|     tag(...tags) {
 | |
| 
 | |
|         Assert(tags.length, 'Missing tags');
 | |
|         for (const tag of tags) {
 | |
|             Assert(tag && typeof tag === 'string', 'Tags must be non-empty strings');
 | |
|         }
 | |
| 
 | |
|         return this._inner('tags', tags);
 | |
|     }
 | |
| 
 | |
|     unit(name) {
 | |
| 
 | |
|         Assert(name && typeof name === 'string', 'Unit name must be a non-empty string');
 | |
| 
 | |
|         return this.$_setFlag('unit', name);
 | |
|     }
 | |
| 
 | |
|     valid(...values) {
 | |
| 
 | |
|         Common.verifyFlat(values, 'valid');
 | |
| 
 | |
|         const obj = this.allow(...values);
 | |
|         obj.$_setFlag('only', !!obj._valids, { clone: false });
 | |
|         return obj;
 | |
|     }
 | |
| 
 | |
|     when(condition, options) {
 | |
| 
 | |
|         const obj = this.clone();
 | |
| 
 | |
|         if (!obj.$_terms.whens) {
 | |
|             obj.$_terms.whens = [];
 | |
|         }
 | |
| 
 | |
|         const when = Compile.when(obj, condition, options);
 | |
|         if (!['any', 'link'].includes(obj.type)) {
 | |
|             const conditions = when.is ? [when] : when.switch;
 | |
|             for (const item of conditions) {
 | |
|                 Assert(!item.then || item.then.type === 'any' || item.then.type === obj.type, 'Cannot combine', obj.type, 'with', item.then && item.then.type);
 | |
|                 Assert(!item.otherwise || item.otherwise.type === 'any' || item.otherwise.type === obj.type, 'Cannot combine', obj.type, 'with', item.otherwise && item.otherwise.type);
 | |
| 
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         obj.$_terms.whens.push(when);
 | |
|         return obj.$_mutateRebuild();
 | |
|     }
 | |
| 
 | |
|     // Helpers
 | |
| 
 | |
|     cache(cache) {
 | |
| 
 | |
|         Assert(!this._inRuleset(), 'Cannot set caching inside a ruleset');
 | |
|         Assert(!this._cache, 'Cannot override schema cache');
 | |
|         Assert(this._flags.artifact === undefined, 'Cannot cache a rule with an artifact');
 | |
| 
 | |
|         const obj = this.clone();
 | |
|         obj._cache = cache || Cache.provider.provision();
 | |
|         obj.$_temp.ruleset = false;
 | |
|         return obj;
 | |
|     }
 | |
| 
 | |
|     clone() {
 | |
| 
 | |
|         const obj = Object.create(Object.getPrototypeOf(this));
 | |
|         return this._assign(obj);
 | |
|     }
 | |
| 
 | |
|     concat(source) {
 | |
| 
 | |
|         Assert(Common.isSchema(source), 'Invalid schema object');
 | |
|         Assert(this.type === 'any' || source.type === 'any' || source.type === this.type, 'Cannot merge type', this.type, 'with another type:', source.type);
 | |
|         Assert(!this._inRuleset(), 'Cannot concatenate onto a schema with open ruleset');
 | |
|         Assert(!source._inRuleset(), 'Cannot concatenate a schema with open ruleset');
 | |
| 
 | |
|         let obj = this.clone();
 | |
| 
 | |
|         if (this.type === 'any' &&
 | |
|             source.type !== 'any') {
 | |
| 
 | |
|             // Change obj to match source type
 | |
| 
 | |
|             const tmpObj = source.clone();
 | |
|             for (const key of Object.keys(obj)) {
 | |
|                 if (key !== 'type') {
 | |
|                     tmpObj[key] = obj[key];
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             obj = tmpObj;
 | |
|         }
 | |
| 
 | |
|         obj._ids.concat(source._ids);
 | |
|         obj._refs.register(source, Ref.toSibling);
 | |
| 
 | |
|         obj._preferences = obj._preferences ? Common.preferences(obj._preferences, source._preferences) : source._preferences;
 | |
|         obj._valids = Values.merge(obj._valids, source._valids, source._invalids);
 | |
|         obj._invalids = Values.merge(obj._invalids, source._invalids, source._valids);
 | |
| 
 | |
|         // Remove unique rules present in source
 | |
| 
 | |
|         for (const name of source._singleRules.keys()) {
 | |
|             if (obj._singleRules.has(name)) {
 | |
|                 obj._rules = obj._rules.filter((target) => target.keep || target.name !== name);
 | |
|                 obj._singleRules.delete(name);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Rules
 | |
| 
 | |
|         for (const test of source._rules) {
 | |
|             if (!source._definition.rules[test.method].multi) {
 | |
|                 obj._singleRules.set(test.name, test);
 | |
|             }
 | |
| 
 | |
|             obj._rules.push(test);
 | |
|         }
 | |
| 
 | |
|         // Flags
 | |
| 
 | |
|         if (obj._flags.empty &&
 | |
|             source._flags.empty) {
 | |
| 
 | |
|             obj._flags.empty = obj._flags.empty.concat(source._flags.empty);
 | |
|             const flags = Object.assign({}, source._flags);
 | |
|             delete flags.empty;
 | |
|             Merge(obj._flags, flags);
 | |
|         }
 | |
|         else if (source._flags.empty) {
 | |
|             obj._flags.empty = source._flags.empty;
 | |
|             const flags = Object.assign({}, source._flags);
 | |
|             delete flags.empty;
 | |
|             Merge(obj._flags, flags);
 | |
|         }
 | |
|         else {
 | |
|             Merge(obj._flags, source._flags);
 | |
|         }
 | |
| 
 | |
|         // Terms
 | |
| 
 | |
|         for (const key in source.$_terms) {
 | |
|             const terms = source.$_terms[key];
 | |
|             if (!terms) {
 | |
|                 if (!obj.$_terms[key]) {
 | |
|                     obj.$_terms[key] = terms;
 | |
|                 }
 | |
| 
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             if (!obj.$_terms[key]) {
 | |
|                 obj.$_terms[key] = terms.slice();
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             obj.$_terms[key] = obj.$_terms[key].concat(terms);
 | |
|         }
 | |
| 
 | |
|         // Tracing
 | |
| 
 | |
|         if (this.$_root._tracer) {
 | |
|             this.$_root._tracer._combine(obj, [this, source]);
 | |
|         }
 | |
| 
 | |
|         // Rebuild
 | |
| 
 | |
|         return obj.$_mutateRebuild();
 | |
|     }
 | |
| 
 | |
|     extend(options) {
 | |
| 
 | |
|         Assert(!options.base, 'Cannot extend type with another base');
 | |
| 
 | |
|         return Extend.type(this, options);
 | |
|     }
 | |
| 
 | |
|     extract(path) {
 | |
| 
 | |
|         path = Array.isArray(path) ? path : path.split('.');
 | |
|         return this._ids.reach(path);
 | |
|     }
 | |
| 
 | |
|     fork(paths, adjuster) {
 | |
| 
 | |
|         Assert(!this._inRuleset(), 'Cannot fork inside a ruleset');
 | |
| 
 | |
|         let obj = this;                                             // eslint-disable-line consistent-this
 | |
|         for (let path of [].concat(paths)) {
 | |
|             path = Array.isArray(path) ? path : path.split('.');
 | |
|             obj = obj._ids.fork(path, adjuster, obj);
 | |
|         }
 | |
| 
 | |
|         obj.$_temp.ruleset = false;
 | |
|         return obj;
 | |
|     }
 | |
| 
 | |
|     rule(options) {
 | |
| 
 | |
|         const def = this._definition;
 | |
|         Common.assertOptions(options, Object.keys(def.modifiers));
 | |
| 
 | |
|         Assert(this.$_temp.ruleset !== false, 'Cannot apply rules to empty ruleset or the last rule added does not support rule properties');
 | |
|         const start = this.$_temp.ruleset === null ? this._rules.length - 1 : this.$_temp.ruleset;
 | |
|         Assert(start >= 0 && start < this._rules.length, 'Cannot apply rules to empty ruleset');
 | |
| 
 | |
|         const obj = this.clone();
 | |
| 
 | |
|         for (let i = start; i < obj._rules.length; ++i) {
 | |
|             const original = obj._rules[i];
 | |
|             const rule = Clone(original);
 | |
| 
 | |
|             for (const name in options) {
 | |
|                 def.modifiers[name](rule, options[name]);
 | |
|                 Assert(rule.name === original.name, 'Cannot change rule name');
 | |
|             }
 | |
| 
 | |
|             obj._rules[i] = rule;
 | |
| 
 | |
|             if (obj._singleRules.get(rule.name) === original) {
 | |
|                 obj._singleRules.set(rule.name, rule);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         obj.$_temp.ruleset = false;
 | |
|         return obj.$_mutateRebuild();
 | |
|     }
 | |
| 
 | |
|     get ruleset() {
 | |
| 
 | |
|         Assert(!this._inRuleset(), 'Cannot start a new ruleset without closing the previous one');
 | |
| 
 | |
|         const obj = this.clone();
 | |
|         obj.$_temp.ruleset = obj._rules.length;
 | |
|         return obj;
 | |
|     }
 | |
| 
 | |
|     get $() {
 | |
| 
 | |
|         return this.ruleset;
 | |
|     }
 | |
| 
 | |
|     tailor(targets) {
 | |
| 
 | |
|         targets = [].concat(targets);
 | |
| 
 | |
|         Assert(!this._inRuleset(), 'Cannot tailor inside a ruleset');
 | |
| 
 | |
|         let obj = this;                                                     // eslint-disable-line consistent-this
 | |
| 
 | |
|         if (this.$_terms.alterations) {
 | |
|             for (const { target, adjuster } of this.$_terms.alterations) {
 | |
|                 if (targets.includes(target)) {
 | |
|                     obj = adjuster(obj);
 | |
|                     Assert(Common.isSchema(obj), 'Alteration adjuster for', target, 'failed to return a schema object');
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         obj = obj.$_modify({ each: (item) => item.tailor(targets), ref: false });
 | |
|         obj.$_temp.ruleset = false;
 | |
|         return obj.$_mutateRebuild();
 | |
|     }
 | |
| 
 | |
|     tracer() {
 | |
| 
 | |
|         return Trace.location ? Trace.location(this) : this;                // $lab:coverage:ignore$
 | |
|     }
 | |
| 
 | |
|     validate(value, options) {
 | |
| 
 | |
|         return Validator.entry(value, this, options);
 | |
|     }
 | |
| 
 | |
|     validateAsync(value, options) {
 | |
| 
 | |
|         return Validator.entryAsync(value, this, options);
 | |
|     }
 | |
| 
 | |
|     // Extensions
 | |
| 
 | |
|     $_addRule(options) {
 | |
| 
 | |
|         // Normalize rule
 | |
| 
 | |
|         if (typeof options === 'string') {
 | |
|             options = { name: options };
 | |
|         }
 | |
| 
 | |
|         Assert(options && typeof options === 'object', 'Invalid options');
 | |
|         Assert(options.name && typeof options.name === 'string', 'Invalid rule name');
 | |
| 
 | |
|         for (const key in options) {
 | |
|             Assert(key[0] !== '_', 'Cannot set private rule properties');
 | |
|         }
 | |
| 
 | |
|         const rule = Object.assign({}, options);        // Shallow cloned
 | |
|         rule._resolve = [];
 | |
|         rule.method = rule.method || rule.name;
 | |
| 
 | |
|         const definition = this._definition.rules[rule.method];
 | |
|         const args = rule.args;
 | |
| 
 | |
|         Assert(definition, 'Unknown rule', rule.method);
 | |
| 
 | |
|         // Args
 | |
| 
 | |
|         const obj = this.clone();
 | |
| 
 | |
|         if (args) {
 | |
|             Assert(Object.keys(args).length === 1 || Object.keys(args).length === this._definition.rules[rule.name].args.length, 'Invalid rule definition for', this.type, rule.name);
 | |
| 
 | |
|             for (const key in args) {
 | |
|                 let arg = args[key];
 | |
| 
 | |
|                 if (definition.argsByName) {
 | |
|                     const resolver = definition.argsByName.get(key);
 | |
| 
 | |
|                     if (resolver.ref &&
 | |
|                         Common.isResolvable(arg)) {
 | |
| 
 | |
|                         rule._resolve.push(key);
 | |
|                         obj.$_mutateRegister(arg);
 | |
|                     }
 | |
|                     else {
 | |
|                         if (resolver.normalize) {
 | |
|                             arg = resolver.normalize(arg);
 | |
|                             args[key] = arg;
 | |
|                         }
 | |
| 
 | |
|                         if (resolver.assert) {
 | |
|                             const error = Common.validateArg(arg, key, resolver);
 | |
|                             Assert(!error, error, 'or reference');
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
| 
 | |
|                 if (arg === undefined) {
 | |
|                     delete args[key];
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 args[key] = arg;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Unique rules
 | |
| 
 | |
|         if (!definition.multi) {
 | |
|             obj._ruleRemove(rule.name, { clone: false });
 | |
|             obj._singleRules.set(rule.name, rule);
 | |
|         }
 | |
| 
 | |
|         if (obj.$_temp.ruleset === false) {
 | |
|             obj.$_temp.ruleset = null;
 | |
|         }
 | |
| 
 | |
|         if (definition.priority) {
 | |
|             obj._rules.unshift(rule);
 | |
|         }
 | |
|         else {
 | |
|             obj._rules.push(rule);
 | |
|         }
 | |
| 
 | |
|         return obj;
 | |
|     }
 | |
| 
 | |
|     $_compile(schema, options) {
 | |
| 
 | |
|         return Compile.schema(this.$_root, schema, options);
 | |
|     }
 | |
| 
 | |
|     $_createError(code, value, local, state, prefs, options = {}) {
 | |
| 
 | |
|         const flags = options.flags !== false ? this._flags : {};
 | |
|         const messages = options.messages ? Messages.merge(this._definition.messages, options.messages) : this._definition.messages;
 | |
|         return new Errors.Report(code, value, local, flags, messages, state, prefs);
 | |
|     }
 | |
| 
 | |
|     $_getFlag(name) {
 | |
| 
 | |
|         return this._flags[name];
 | |
|     }
 | |
| 
 | |
|     $_getRule(name) {
 | |
| 
 | |
|         return this._singleRules.get(name);
 | |
|     }
 | |
| 
 | |
|     $_mapLabels(path) {
 | |
| 
 | |
|         path = Array.isArray(path) ? path : path.split('.');
 | |
|         return this._ids.labels(path);
 | |
|     }
 | |
| 
 | |
|     $_match(value, state, prefs, overrides) {
 | |
| 
 | |
|         prefs = Object.assign({}, prefs);       // Shallow cloned
 | |
|         prefs.abortEarly = true;
 | |
|         prefs._externals = false;
 | |
| 
 | |
|         state.snapshot();
 | |
|         const result = !Validator.validate(value, this, state, prefs, overrides).errors;
 | |
|         state.restore();
 | |
| 
 | |
|         return result;
 | |
|     }
 | |
| 
 | |
|     $_modify(options) {
 | |
| 
 | |
|         Common.assertOptions(options, ['each', 'once', 'ref', 'schema']);
 | |
|         return Modify.schema(this, options) || this;
 | |
|     }
 | |
| 
 | |
|     $_mutateRebuild() {
 | |
| 
 | |
|         Assert(!this._inRuleset(), 'Cannot add this rule inside a ruleset');
 | |
| 
 | |
|         this._refs.reset();
 | |
|         this._ids.reset();
 | |
| 
 | |
|         const each = (item, { source, name, path, key }) => {
 | |
| 
 | |
|             const family = this._definition[source][name] && this._definition[source][name].register;
 | |
|             if (family !== false) {
 | |
|                 this.$_mutateRegister(item, { family, key });
 | |
|             }
 | |
|         };
 | |
| 
 | |
|         this.$_modify({ each });
 | |
| 
 | |
|         if (this._definition.rebuild) {
 | |
|             this._definition.rebuild(this);
 | |
|         }
 | |
| 
 | |
|         this.$_temp.ruleset = false;
 | |
|         return this;
 | |
|     }
 | |
| 
 | |
|     $_mutateRegister(schema, { family, key } = {}) {
 | |
| 
 | |
|         this._refs.register(schema, family);
 | |
|         this._ids.register(schema, { key });
 | |
|     }
 | |
| 
 | |
|     $_property(name) {
 | |
| 
 | |
|         return this._definition.properties[name];
 | |
|     }
 | |
| 
 | |
|     $_reach(path) {
 | |
| 
 | |
|         return this._ids.reach(path);
 | |
|     }
 | |
| 
 | |
|     $_rootReferences() {
 | |
| 
 | |
|         return this._refs.roots();
 | |
|     }
 | |
| 
 | |
|     $_setFlag(name, value, options = {}) {
 | |
| 
 | |
|         Assert(name[0] === '_' || !this._inRuleset(), 'Cannot set flag inside a ruleset');
 | |
| 
 | |
|         const flag = this._definition.flags[name] || {};
 | |
|         if (DeepEqual(value, flag.default)) {
 | |
|             value = undefined;
 | |
|         }
 | |
| 
 | |
|         if (DeepEqual(value, this._flags[name])) {
 | |
|             return this;
 | |
|         }
 | |
| 
 | |
|         const obj = options.clone !== false ? this.clone() : this;
 | |
| 
 | |
|         if (value !== undefined) {
 | |
|             obj._flags[name] = value;
 | |
|             obj.$_mutateRegister(value);
 | |
|         }
 | |
|         else {
 | |
|             delete obj._flags[name];
 | |
|         }
 | |
| 
 | |
|         if (name[0] !== '_') {
 | |
|             obj.$_temp.ruleset = false;
 | |
|         }
 | |
| 
 | |
|         return obj;
 | |
|     }
 | |
| 
 | |
|     $_parent(method, ...args) {
 | |
| 
 | |
|         return this[method][Common.symbols.parent].call(this, ...args);
 | |
|     }
 | |
| 
 | |
|     $_validate(value, state, prefs) {
 | |
| 
 | |
|         return Validator.validate(value, this, state, prefs);
 | |
|     }
 | |
| 
 | |
|     // Internals
 | |
| 
 | |
|     _assign(target) {
 | |
| 
 | |
|         target.type = this.type;
 | |
| 
 | |
|         target.$_root = this.$_root;
 | |
| 
 | |
|         target.$_temp = Object.assign({}, this.$_temp);
 | |
|         target.$_temp.whens = {};
 | |
| 
 | |
|         target._ids = this._ids.clone();
 | |
|         target._preferences = this._preferences;
 | |
|         target._valids = this._valids && this._valids.clone();
 | |
|         target._invalids = this._invalids && this._invalids.clone();
 | |
|         target._rules = this._rules.slice();
 | |
|         target._singleRules = Clone(this._singleRules, { shallow: true });
 | |
|         target._refs = this._refs.clone();
 | |
|         target._flags = Object.assign({}, this._flags);
 | |
|         target._cache = null;
 | |
| 
 | |
|         target.$_terms = {};
 | |
|         for (const key in this.$_terms) {
 | |
|             target.$_terms[key] = this.$_terms[key] ? this.$_terms[key].slice() : null;
 | |
|         }
 | |
| 
 | |
|         // Backwards compatibility
 | |
| 
 | |
|         target.$_super = {};
 | |
|         for (const override in this.$_super) {
 | |
|             target.$_super[override] = this._super[override].bind(target);
 | |
|         }
 | |
| 
 | |
|         return target;
 | |
|     }
 | |
| 
 | |
|     _bare() {
 | |
| 
 | |
|         const obj = this.clone();
 | |
|         obj._reset();
 | |
| 
 | |
|         const terms = obj._definition.terms;
 | |
|         for (const name in terms) {
 | |
|             const term = terms[name];
 | |
|             obj.$_terms[name] = term.init;
 | |
|         }
 | |
| 
 | |
|         return obj.$_mutateRebuild();
 | |
|     }
 | |
| 
 | |
|     _default(flag, value, options = {}) {
 | |
| 
 | |
|         Common.assertOptions(options, 'literal');
 | |
| 
 | |
|         Assert(value !== undefined, 'Missing', flag, 'value');
 | |
|         Assert(typeof value === 'function' || !options.literal, 'Only function value supports literal option');
 | |
| 
 | |
|         if (typeof value === 'function' &&
 | |
|             options.literal) {
 | |
| 
 | |
|             value = {
 | |
|                 [Common.symbols.literal]: true,
 | |
|                 literal: value
 | |
|             };
 | |
|         }
 | |
| 
 | |
|         const obj = this.$_setFlag(flag, value);
 | |
|         return obj;
 | |
|     }
 | |
| 
 | |
|     _generate(value, state, prefs) {
 | |
| 
 | |
|         if (!this.$_terms.whens) {
 | |
|             return { schema: this };
 | |
|         }
 | |
| 
 | |
|         // Collect matching whens
 | |
| 
 | |
|         const whens = [];
 | |
|         const ids = [];
 | |
|         for (let i = 0; i < this.$_terms.whens.length; ++i) {
 | |
|             const when = this.$_terms.whens[i];
 | |
| 
 | |
|             if (when.concat) {
 | |
|                 whens.push(when.concat);
 | |
|                 ids.push(`${i}.concat`);
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             const input = when.ref ? when.ref.resolve(value, state, prefs) : value;
 | |
|             const tests = when.is ? [when] : when.switch;
 | |
|             const before = ids.length;
 | |
| 
 | |
|             for (let j = 0; j < tests.length; ++j) {
 | |
|                 const { is, then, otherwise } = tests[j];
 | |
| 
 | |
|                 const baseId = `${i}${when.switch ? '.' + j : ''}`;
 | |
|                 if (is.$_match(input, state.nest(is, `${baseId}.is`), prefs)) {
 | |
|                     if (then) {
 | |
|                         const localState = state.localize([...state.path, `${baseId}.then`], state.ancestors, state.schemas);
 | |
|                         const { schema: generated, id } = then._generate(value, localState, prefs);
 | |
|                         whens.push(generated);
 | |
|                         ids.push(`${baseId}.then${id ? `(${id})` : ''}`);
 | |
|                         break;
 | |
|                     }
 | |
|                 }
 | |
|                 else if (otherwise) {
 | |
|                     const localState = state.localize([...state.path, `${baseId}.otherwise`], state.ancestors, state.schemas);
 | |
|                     const { schema: generated, id } = otherwise._generate(value, localState, prefs);
 | |
|                     whens.push(generated);
 | |
|                     ids.push(`${baseId}.otherwise${id ? `(${id})` : ''}`);
 | |
|                     break;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (when.break &&
 | |
|                 ids.length > before) {          // Something matched
 | |
| 
 | |
|                 break;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Check cache
 | |
| 
 | |
|         const id = ids.join(', ');
 | |
|         state.mainstay.tracer.debug(state, 'rule', 'when', id);
 | |
| 
 | |
|         if (!id) {
 | |
|             return { schema: this };
 | |
|         }
 | |
| 
 | |
|         if (!state.mainstay.tracer.active &&
 | |
|             this.$_temp.whens[id]) {
 | |
| 
 | |
|             return { schema: this.$_temp.whens[id], id };
 | |
|         }
 | |
| 
 | |
|         // Generate dynamic schema
 | |
| 
 | |
|         let obj = this;                                             // eslint-disable-line consistent-this
 | |
|         if (this._definition.generate) {
 | |
|             obj = this._definition.generate(this, value, state, prefs);
 | |
|         }
 | |
| 
 | |
|         // Apply whens
 | |
| 
 | |
|         for (const when of whens) {
 | |
|             obj = obj.concat(when);
 | |
|         }
 | |
| 
 | |
|         // Tracing
 | |
| 
 | |
|         if (this.$_root._tracer) {
 | |
|             this.$_root._tracer._combine(obj, [this, ...whens]);
 | |
|         }
 | |
| 
 | |
|         // Cache result
 | |
| 
 | |
|         this.$_temp.whens[id] = obj;
 | |
|         return { schema: obj, id };
 | |
|     }
 | |
| 
 | |
|     _inner(type, values, options = {}) {
 | |
| 
 | |
|         Assert(!this._inRuleset(), `Cannot set ${type} inside a ruleset`);
 | |
| 
 | |
|         const obj = this.clone();
 | |
|         if (!obj.$_terms[type] ||
 | |
|             options.override) {
 | |
| 
 | |
|             obj.$_terms[type] = [];
 | |
|         }
 | |
| 
 | |
|         if (options.single) {
 | |
|             obj.$_terms[type].push(values);
 | |
|         }
 | |
|         else {
 | |
|             obj.$_terms[type].push(...values);
 | |
|         }
 | |
| 
 | |
|         obj.$_temp.ruleset = false;
 | |
|         return obj;
 | |
|     }
 | |
| 
 | |
|     _inRuleset() {
 | |
| 
 | |
|         return this.$_temp.ruleset !== null && this.$_temp.ruleset !== false;
 | |
|     }
 | |
| 
 | |
|     _ruleRemove(name, options = {}) {
 | |
| 
 | |
|         if (!this._singleRules.has(name)) {
 | |
|             return this;
 | |
|         }
 | |
| 
 | |
|         const obj = options.clone !== false ? this.clone() : this;
 | |
| 
 | |
|         obj._singleRules.delete(name);
 | |
| 
 | |
|         const filtered = [];
 | |
|         for (let i = 0; i < obj._rules.length; ++i) {
 | |
|             const test = obj._rules[i];
 | |
|             if (test.name === name &&
 | |
|                 !test.keep) {
 | |
| 
 | |
|                 if (obj._inRuleset() &&
 | |
|                     i < obj.$_temp.ruleset) {
 | |
| 
 | |
|                     --obj.$_temp.ruleset;
 | |
|                 }
 | |
| 
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             filtered.push(test);
 | |
|         }
 | |
| 
 | |
|         obj._rules = filtered;
 | |
|         return obj;
 | |
|     }
 | |
| 
 | |
|     _values(values, key) {
 | |
| 
 | |
|         Common.verifyFlat(values, key.slice(1, -1));
 | |
| 
 | |
|         const obj = this.clone();
 | |
| 
 | |
|         const override = values[0] === Common.symbols.override;
 | |
|         if (override) {
 | |
|             values = values.slice(1);
 | |
|         }
 | |
| 
 | |
|         if (!obj[key] &&
 | |
|             values.length) {
 | |
| 
 | |
|             obj[key] = new Values();
 | |
|         }
 | |
|         else if (override) {
 | |
|             obj[key] = values.length ? new Values() : null;
 | |
|             obj.$_mutateRebuild();
 | |
|         }
 | |
| 
 | |
|         if (!obj[key]) {
 | |
|             return obj;
 | |
|         }
 | |
| 
 | |
|         if (override) {
 | |
|             obj[key].override();
 | |
|         }
 | |
| 
 | |
|         for (const value of values) {
 | |
|             Assert(value !== undefined, 'Cannot call allow/valid/invalid with undefined');
 | |
|             Assert(value !== Common.symbols.override, 'Override must be the first value');
 | |
| 
 | |
|             const other = key === '_invalids' ? '_valids' : '_invalids';
 | |
|             if (obj[other]) {
 | |
|                 obj[other].remove(value);
 | |
|                 if (!obj[other].length) {
 | |
|                     Assert(key === '_valids' || !obj._flags.only, 'Setting invalid value', value, 'leaves schema rejecting all values due to previous valid rule');
 | |
|                     obj[other] = null;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             obj[key].add(value, obj._refs);
 | |
|         }
 | |
| 
 | |
|         return obj;
 | |
|     }
 | |
| };
 | |
| 
 | |
| 
 | |
| internals.Base.prototype[Common.symbols.any] = {
 | |
|     version: Common.version,
 | |
|     compile: Compile.compile,
 | |
|     root: '$_root'
 | |
| };
 | |
| 
 | |
| 
 | |
| internals.Base.prototype.isImmutable = true;                // Prevents Hoek from deep cloning schema objects (must be on prototype)
 | |
| 
 | |
| 
 | |
| // Aliases
 | |
| 
 | |
| internals.Base.prototype.deny = internals.Base.prototype.invalid;
 | |
| internals.Base.prototype.disallow = internals.Base.prototype.invalid;
 | |
| internals.Base.prototype.equal = internals.Base.prototype.valid;
 | |
| internals.Base.prototype.exist = internals.Base.prototype.required;
 | |
| internals.Base.prototype.not = internals.Base.prototype.invalid;
 | |
| internals.Base.prototype.options = internals.Base.prototype.prefs;
 | |
| internals.Base.prototype.preferences = internals.Base.prototype.prefs;
 | |
| 
 | |
| 
 | |
| module.exports = new internals.Base();
 |