// Load modules var Path = require('path'); var Hoek = require('hoek'); var Ref = require('./ref'); var Errors = require('./errors'); var Alternatives = null; // Delay-loaded to prevent circular dependencies var Cast = null; // Declare internals var internals = {}; internals.defaults = { abortEarly: true, convert: true, allowUnknown: false, skipFunctions: false, stripUnknown: false, language: {} // context: null }; module.exports = internals.Any = function () { this.isJoi = true; this._type = 'any'; this._settings = null; this._valids = new internals.Set(); this._invalids = new internals.Set(); this._tests = []; this._refs = []; this._flags = { /* presence: 'optional', // optional, required, forbidden, ignore allowOnly: false, allowUnknown: undefined, default: undefined, forbidden: false, encoding: undefined, insensitive: false, trim: false, case: undefined // upper, lower */}; this._description = null; this._unit = null; this._notes = []; this._tags = []; this._examples = []; this._meta = []; this._inner = {}; // Hash of arrays of immutable objects }; internals.Any.prototype.isImmutable = true; // Prevents Hoek from deep cloning schema objects internals.Any.prototype.clone = function () { var obj = {}; obj.__proto__ = Object.getPrototypeOf(this); obj.isJoi = true; obj._type = this._type; obj._settings = internals.concatSettings(this._settings); obj._valids = Hoek.clone(this._valids); obj._invalids = Hoek.clone(this._invalids); obj._tests = this._tests.slice(); obj._refs = this._refs.slice(); obj._flags = Hoek.clone(this._flags); obj._description = this._description; obj._unit = this._unit; obj._notes = this._notes.slice(); obj._tags = this._tags.slice(); obj._examples = this._examples.slice(); obj._meta = this._meta.slice(); obj._inner = {}; var inners = Object.keys(this._inner); for (var i = 0, il = inners.length; i < il; ++i) { var key = inners[i]; obj._inner[key] = this._inner[key] ? this._inner[key].slice() : null; } return obj; }; internals.Any.prototype.concat = function (schema) { Hoek.assert(schema && schema.isJoi, 'Invalid schema object'); Hoek.assert(schema._type === 'any' || schema._type === this._type, 'Cannot merge with another type:', schema._type); var obj = this.clone(); obj._settings = obj._settings ? internals.concatSettings(obj._settings, schema._settings) : schema._settings; obj._valids.merge(schema._valids, schema._invalids); obj._invalids.merge(schema._invalids, schema._valids); obj._tests = obj._tests.concat(schema._tests); obj._refs = obj._refs.concat(schema._refs); Hoek.merge(obj._flags, schema._flags); obj._description = schema._description || obj._description; obj._unit = schema._unit || obj._unit; obj._notes = obj._notes.concat(schema._notes); obj._tags = obj._tags.concat(schema._tags); obj._examples = obj._examples.concat(schema._examples); obj._meta = obj._meta.concat(schema._meta); var inners = Object.keys(schema._inner); for (var i = 0, il = inners.length; i < il; ++i) { var key = inners[i]; if (schema._inner[key]) { obj._inner[key] = (obj._inner[key] ? obj._inner[key].concat(schema._inner[key]) : schema._inner[key].slice()); } } return obj; }; internals.Any.prototype._test = function (name, arg, func) { Hoek.assert(!this._flags.allowOnly, 'Cannot define rules when valid values specified'); var obj = this.clone(); obj._tests.push({ func: func, name: name, arg: arg }); return obj; }; internals.Any.prototype.options = function (options) { Hoek.assert(!options.context, 'Cannot override context'); var obj = this.clone(); obj._settings = internals.concatSettings(obj._settings, options); return obj; }; internals.Any.prototype.strict = function () { var obj = this.clone(); obj._settings = obj._settings || {}; obj._settings.convert = false; return obj; }; internals.Any.prototype._allow = function () { var values = Hoek.flatten(Array.prototype.slice.call(arguments)); for (var i = 0, il = values.length; i < il; ++i) { var value = values[i]; this._invalids.remove(value); this._valids.add(value, this._refs); } }; internals.Any.prototype.allow = function () { var obj = this.clone(); obj._allow.apply(obj, arguments); return obj; }; internals.Any.prototype.valid = internals.Any.prototype.equal = function () { Hoek.assert(!this._tests.length, 'Cannot set valid values when rules specified'); var obj = this.allow.apply(this, arguments); obj._flags.allowOnly = true; return obj; }; internals.Any.prototype.invalid = internals.Any.prototype.not = function (value) { var obj = this.clone(); var values = Hoek.flatten(Array.prototype.slice.call(arguments)); for (var i = 0, il = values.length; i < il; ++i) { var value = values[i]; obj._valids.remove(value); obj._invalids.add(value, this._refs); } return obj; }; internals.Any.prototype.required = internals.Any.prototype.exist = function () { var obj = this.clone(); obj._flags.presence = 'required'; return obj; }; internals.Any.prototype.optional = function () { var obj = this.clone(); delete obj._flags.presence; // Defaults to 'optional' return obj; }; internals.Any.prototype.default = function (value) { var obj = this.clone(); obj._flags.default = value; Ref.push(obj._refs, value); return obj; }; internals.Any.prototype.forbidden = function () { var obj = this.clone(); obj._flags.presence = 'forbidden'; return obj; }; internals.Any.prototype.when = function (ref, options) { Hoek.assert(options && typeof options === 'object', 'Invalid options'); Hoek.assert(options.then !== undefined || options.otherwise !== undefined, 'options must have at least one of "then" or "otherwise"'); Cast = Cast || require('./cast'); var then = options.then ? this.concat(Cast.schema(options.then)) : this; var otherwise = options.otherwise ? this.concat(Cast.schema(options.otherwise)) : this; Alternatives = Alternatives || require('./alternatives'); var obj = Alternatives.when(ref, { is: options.is, then: then, otherwise: otherwise }); obj._flags.presence = 'ignore'; return obj; }; internals.Any.prototype.description = function (desc) { Hoek.assert(desc && typeof desc === 'string', 'Description must be a non-empty string'); var obj = this.clone(); obj._description = desc; return obj; }; internals.Any.prototype.notes = function (notes) { Hoek.assert(notes && (typeof notes === 'string' || Array.isArray(notes)), 'Notes must be a non-empty string or array'); var obj = this.clone(); obj._notes = obj._notes.concat(notes); return obj; }; internals.Any.prototype.tags = function (tags) { Hoek.assert(tags && (typeof tags === 'string' || Array.isArray(tags)), 'Tags must be a non-empty string or array'); var obj = this.clone(); obj._tags = obj._tags.concat(tags); return obj; }; internals.Any.prototype.meta = function (meta) { Hoek.assert(meta !== undefined, 'Meta cannot be undefined'); var obj = this.clone(); obj._meta = obj._meta.concat(meta); return obj; }; internals.Any.prototype.example = function (value) { Hoek.assert(arguments.length, 'Missing example'); var result = this._validate(value, null, internals.defaults); Hoek.assert(!result.errors, 'Bad example:', result.errors && Errors.process(result.errors, value)); var obj = this.clone(); obj._examples = obj._examples.concat(value); return obj; }; internals.Any.prototype.unit = function (name) { Hoek.assert(name && typeof name === 'string', 'Unit name must be a non-empty string'); var obj = this.clone(); obj._unit = name; return obj; }; internals.Any.prototype._validate = function (value, state, options, reference) { var self = this; // Setup state and settings state = state || { key: '', path: '', parent: null, reference: reference }; if (this._settings) { options = internals.concatSettings(options, this._settings); } var errors = []; var finish = function () { return { value: (value !== undefined) ? value : (Ref.isRef(self._flags.default) ? self._flags.default(state.parent, options) : self._flags.default), errors: errors.length ? errors : null }; }; // Check presence requirements if (this._flags.presence) { // 'required', 'forbidden', or 'ignore' if (this._flags.presence === 'required' && value === undefined) { errors.push(Errors.create('any.required', null, state, options)); return finish(); } else if (this._flags.presence === 'forbidden') { if (value === undefined) { return finish(); } errors.push(Errors.create('any.unknown', null, state, options)); return finish(); } } else { // 'optional' if (value === undefined) { return finish(); } } // Check allowed and denied values using the original value if (this._valids.has(value, state, options, this._flags.insensitive)) { return finish(); } if (this._invalids.has(value, state, options, this._flags.insensitive)) { errors.push(Errors.create(value === '' ? 'any.empty' : 'any.invalid', null, state, options)); if (options.abortEarly || value === undefined) { // No reason to keep validating missing value return finish(); } } // Convert value and validate type if (this._base) { var base = this._base.call(this, value, state, options); if (base.errors) { value = base.value; errors = errors.concat(base.errors); return finish(); // Base error always aborts early } if (base.value !== value) { value = base.value; // Check allowed and denied values using the converted value if (this._valids.has(value, state, options, this._flags.insensitive)) { return finish(); } if (this._invalids.has(value, state, options, this._flags.insensitive)) { errors.push(Errors.create('any.invalid', null, state, options)); if (options.abortEarly) { return finish(); } } } } // Required values did not match if (this._flags.allowOnly) { errors.push(Errors.create('any.allowOnly', { valids: this._valids.toString(false) }, state, options)); if (options.abortEarly) { return finish(); } } // Helper.validate tests for (var i = 0, il = this._tests.length; i < il; ++i) { var test = this._tests[i]; var err = test.func.call(this, value, state, options); if (err) { errors.push(err); if (options.abortEarly) { return finish(); } } } return finish(); }; internals.Any.prototype._validateWithOptions = function (value, options, callback) { var settings = internals.concatSettings(internals.defaults, options); var result = this._validate(value, null, settings); var errors = Errors.process(result.errors, value); if (callback) { return callback(errors, result.value); } return { error: errors, value: result.value }; }; internals.Any.prototype.validate = function (value, callback) { var result = this._validate(value, null, internals.defaults); var errors = Errors.process(result.errors, value); if (callback) { return callback(errors, result.value); } return { error: errors, value: result.value }; }; internals.Any.prototype.describe = function () { var description = { type: this._type }; if (Object.keys(this._flags).length) { description.flags = this._flags; } if (this._description) { description.description = this._description; } if (this._notes.length) { description.notes = this._notes; } if (this._tags.length) { description.tags = this._tags; } if (this._meta.length) { description.meta = this._meta; } if (this._examples.length) { description.examples = this._examples; } if (this._unit) { description.unit = this._unit; } var valids = this._valids.values(); if (valids.length) { description.valids = valids; } var invalids = this._invalids.values(); if (invalids.length) { description.invalids = invalids; } description.rules = []; for (var i = 0, il = this._tests.length; i < il; ++i) { var validator = this._tests[i]; var item = { name: validator.name }; if (validator.arg) { item.arg = validator.arg; } description.rules.push(item); } if (!description.rules.length) { delete description.rules; } return description; }; // Set internals.Set = function () { this._set = []; }; internals.Set.prototype.add = function (value, refs) { Hoek.assert(value === null || value === undefined || value instanceof Date || Buffer.isBuffer(value) || Ref.isRef(value) || (typeof value !== 'function' && typeof value !== 'object'), 'Value cannot be an object or function'); if (typeof value !== 'function' && this.has(value, null, null, false)) { return; } Ref.push(refs, value); this._set.push(value); }; internals.Set.prototype.merge = function (add, remove) { for (var i = 0, il = add._set.length; i < il; ++i) { this.add(add._set[i]); } for (i = 0, il = remove._set.length; i < il; ++i) { this.remove(remove._set[i]); } }; internals.Set.prototype.remove = function (value) { this._set = this._set.filter(function (item) { return value !== item; }); }; internals.Set.prototype.has = function (value, state, options, insensitive) { for (var i = 0, il = this._set.length; i < il; ++i) { var item = this._set[i]; if (Ref.isRef(item)) { item = item(state.reference || state.parent, options); } if (typeof value !== typeof item) { continue; } if (value === item || (value instanceof Date && item instanceof Date && value.getTime() === item.getTime()) || (insensitive && typeof value === 'string' && value.toLowerCase() === item.toLowerCase()) || (Buffer.isBuffer(value) && Buffer.isBuffer(item) && value.length === item.length && value.toString('binary') === item.toString('binary'))) { return true; } } return false; }; internals.Set.prototype.values = function () { return this._set.slice(); }; internals.Set.prototype.toString = function (includeUndefined) { var list = ''; for (var i = 0, il = this._set.length; i < il; ++i) { var item = this._set[i]; if (item !== undefined || includeUndefined) { list += (list ? ', ' : '') + internals.stringify(item); } } return list; }; internals.stringify = function (value) { if (value === undefined) { return 'undefined'; } if (value === null) { return 'null'; } if (typeof value === 'string') { return value; } return value.toString(); }; internals.concatSettings = function (target, source) { // Used to avoid cloning context if (!target && !source) { return null; } var obj = {}; if (target) { var tKeys = Object.keys(target); for (var i = 0, il = tKeys.length; i < il; ++i) { var key = tKeys[i]; obj[key] = target[key]; } } if (source) { var sKeys = Object.keys(source); for (var j = 0, jl = sKeys.length; j < jl; ++j) { var key = sKeys[j]; if (key !== 'language' || !obj.hasOwnProperty(key)) { obj[key] = source[key]; } else { obj[key] = Hoek.applyToDefaults(obj[key], source[key]); } } } return obj; };