'use strict'; // Load modules const Hoek = require('hoek'); const Ref = require('./ref'); const Errors = require('./errors'); let Alternatives = null; // Delay-loaded to prevent circular dependencies let Cast = null; // Declare internals const internals = {}; internals.defaults = { abortEarly: true, convert: true, allowUnknown: false, skipFunctions: false, stripUnknown: false, language: {}, presence: 'optional', strip: false, noDefaults: false // context: null }; module.exports = internals.Any = class { constructor() { Cast = Cast || require('./cast'); 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 empty: undefined, func: false, raw: false */ }; this._description = null; this._unit = null; this._notes = []; this._tags = []; this._examples = []; this._meta = []; this._inner = {}; // Hash of arrays of immutable objects } createError(type, context, state, options) { return Errors.create(type, context, state, options, this._flags); } checkOptions(options) { const Schemas = require('./schemas'); const result = Schemas.options.validate(options); if (result.error) { throw new Error(result.error.details[0].message); } } clone() { const obj = Object.create(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 = {}; const inners = Object.keys(this._inner); for (let i = 0; i < inners.length; ++i) { const key = inners[i]; obj._inner[key] = this._inner[key] ? this._inner[key].slice() : null; } return obj; } concat(schema) { Hoek.assert(schema instanceof internals.Any, 'Invalid schema object'); Hoek.assert(this._type === 'any' || schema._type === 'any' || schema._type === this._type, 'Cannot merge type', this._type, 'with another type:', schema._type); let obj = this.clone(); if (this._type === 'any' && schema._type !== 'any') { // Reset values as if we were "this" const tmpObj = schema.clone(); const keysToRestore = ['_settings', '_valids', '_invalids', '_tests', '_refs', '_flags', '_description', '_unit', '_notes', '_tags', '_examples', '_meta', '_inner']; for (let i = 0; i < keysToRestore.length; ++i) { tmpObj[keysToRestore[i]] = obj[keysToRestore[i]]; } obj = tmpObj; } 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); const inners = Object.keys(schema._inner); const isObject = obj._type === 'object'; for (let i = 0; i < inners.length; ++i) { const key = inners[i]; const source = schema._inner[key]; if (source) { const target = obj._inner[key]; if (target) { if (isObject && key === 'children') { const keys = {}; for (let j = 0; j < target.length; ++j) { keys[target[j].key] = j; } for (let j = 0; j < source.length; ++j) { const sourceKey = source[j].key; if (keys[sourceKey] >= 0) { target[keys[sourceKey]] = { key: sourceKey, schema: target[keys[sourceKey]].schema.concat(source[j].schema) }; } else { target.push(source[j]); } } } else { obj._inner[key] = obj._inner[key].concat(source); } } else { obj._inner[key] = source.slice(); } } } return obj; } _test(name, arg, func, options) { const obj = this.clone(); obj._tests.push({ func, name, arg, options }); return obj; } options(options) { Hoek.assert(!options.context, 'Cannot override context'); this.checkOptions(options); const obj = this.clone(); obj._settings = internals.concatSettings(obj._settings, options); return obj; } strict(isStrict) { const obj = this.clone(); obj._settings = obj._settings || {}; obj._settings.convert = isStrict === undefined ? false : !isStrict; return obj; } raw(isRaw) { const obj = this.clone(); obj._flags.raw = isRaw === undefined ? true : isRaw; return obj; } error(err) { Hoek.assert(err && err instanceof Error, 'Must provide a valid Error object'); const obj = this.clone(); obj._flags.error = err; return obj; } _allow() { const values = Hoek.flatten(Array.prototype.slice.call(arguments)); for (let i = 0; i < values.length; ++i) { const value = values[i]; Hoek.assert(value !== undefined, 'Cannot call allow/valid/invalid with undefined'); this._invalids.remove(value); this._valids.add(value, this._refs); } } allow() { const obj = this.clone(); obj._allow.apply(obj, arguments); return obj; } valid() { const obj = this.allow.apply(this, arguments); obj._flags.allowOnly = true; return obj; } invalid(value) { const obj = this.clone(); const values = Hoek.flatten(Array.prototype.slice.call(arguments)); for (let i = 0; i < values.length; ++i) { value = values[i]; Hoek.assert(value !== undefined, 'Cannot call allow/valid/invalid with undefined'); obj._valids.remove(value); obj._invalids.add(value, this._refs); } return obj; } required() { const obj = this.clone(); obj._flags.presence = 'required'; return obj; } optional() { const obj = this.clone(); obj._flags.presence = 'optional'; return obj; } forbidden() { const obj = this.clone(); obj._flags.presence = 'forbidden'; return obj; } strip() { const obj = this.clone(); obj._flags.strip = true; return obj; } applyFunctionToChildren(children, fn, args, root) { children = [].concat(children); if (children.length !== 1 || children[0] !== '') { root = root ? (root + '.') : ''; const extraChildren = (children[0] === '' ? children.slice(1) : children).map((child) => { return root + child; }); throw new Error('unknown key(s) ' + extraChildren.join(', ')); } return this[fn].apply(this, args); } default(value, description) { if (typeof value === 'function' && !Ref.isRef(value)) { if (!value.description && description) { value.description = description; } if (!this._flags.func) { Hoek.assert(typeof value.description === 'string' && value.description.length > 0, 'description must be provided when default value is a function'); } } const obj = this.clone(); obj._flags.default = value; Ref.push(obj._refs, value); return obj; } empty(schema) { const obj = this.clone(); obj._flags.empty = schema === undefined ? undefined : Cast.schema(schema); return obj; } when(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"'); const then = options.hasOwnProperty('then') ? this.concat(Cast.schema(options.then)) : undefined; const otherwise = options.hasOwnProperty('otherwise') ? this.concat(Cast.schema(options.otherwise)) : undefined; Alternatives = Alternatives || require('./alternatives'); const obj = Alternatives.when(ref, { is: options.is, then, otherwise }); obj._flags.presence = 'ignore'; obj._settings = internals.concatSettings(obj._settings, { baseType: this }); return obj; } description(desc) { Hoek.assert(desc && typeof desc === 'string', 'Description must be a non-empty string'); const obj = this.clone(); obj._description = desc; return obj; } notes(notes) { Hoek.assert(notes && (typeof notes === 'string' || Array.isArray(notes)), 'Notes must be a non-empty string or array'); const obj = this.clone(); obj._notes = obj._notes.concat(notes); return obj; } tags(tags) { Hoek.assert(tags && (typeof tags === 'string' || Array.isArray(tags)), 'Tags must be a non-empty string or array'); const obj = this.clone(); obj._tags = obj._tags.concat(tags); return obj; } meta(meta) { Hoek.assert(meta !== undefined, 'Meta cannot be undefined'); const obj = this.clone(); obj._meta = obj._meta.concat(meta); return obj; } example(value) { Hoek.assert(arguments.length, 'Missing example'); const result = this._validate(value, null, internals.defaults); Hoek.assert(!result.errors, 'Bad example:', result.errors && Errors.process(result.errors, value)); const obj = this.clone(); obj._examples.push(value); return obj; } unit(name) { Hoek.assert(name && typeof name === 'string', 'Unit name must be a non-empty string'); const obj = this.clone(); obj._unit = name; return obj; } _validate(value, state, options, reference) { const originalValue = value; // Setup state and settings state = state || { key: '', path: '', parent: null, reference }; if (this._settings) { options = internals.concatSettings(options, this._settings); } let errors = []; const finish = () => { let finalValue; if (!this._flags.strip) { if (value !== undefined) { finalValue = this._flags.raw ? originalValue : value; } else if (options.noDefaults) { finalValue = originalValue; } else if (Ref.isRef(this._flags.default)) { finalValue = this._flags.default(state.parent, options); } else if (typeof this._flags.default === 'function' && !(this._flags.func && !this._flags.default.description)) { let args; if (state.parent !== null && this._flags.default.length > 0) { args = [Hoek.clone(state.parent), options]; } const defaultValue = internals._try(this._flags.default, args); finalValue = defaultValue.value; if (defaultValue.error) { errors.push(this.createError('any.default', defaultValue.error, state, options)); } } else { finalValue = Hoek.clone(this._flags.default); } } return { value: finalValue, errors: errors.length ? errors : null }; }; if (this._coerce) { const coerced = this._coerce.call(this, value, state, options); if (coerced.errors) { value = coerced.value; errors = errors.concat(coerced.errors); return finish(); // Coerced error always aborts early } value = coerced.value; } if (this._flags.empty && !this._flags.empty._validate(value, null, internals.defaults).errors) { value = undefined; } // Check presence requirements const presence = this._flags.presence || options.presence; if (presence === 'optional') { if (value === undefined) { const isDeepDefault = this._flags.hasOwnProperty('default') && this._flags.default === undefined; if (isDeepDefault && this._type === 'object') { value = {}; } else { return finish(); } } } else if (presence === 'required' && value === undefined) { errors.push(this.createError('any.required', null, state, options)); return finish(); } else if (presence === 'forbidden') { if (value === undefined) { return finish(); } errors.push(this.createError('any.unknown', null, state, options)); 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(this.createError(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) { const 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(this.createError(value === '' ? 'any.empty' : 'any.invalid', null, state, options)); if (options.abortEarly) { return finish(); } } } } // Required values did not match if (this._flags.allowOnly) { errors.push(this.createError('any.allowOnly', { valids: this._valids.values({ stripUndefined: true }) }, state, options)); if (options.abortEarly) { return finish(); } } // Helper.validate tests for (let i = 0; i < this._tests.length; ++i) { const test = this._tests[i]; const ret = test.func.call(this, value, state, options); if (ret instanceof Errors.Err) { errors.push(ret); if (options.abortEarly) { return finish(); } } else { value = ret; } } return finish(); } _validateWithOptions(value, options, callback) { if (options) { this.checkOptions(options); } const settings = internals.concatSettings(internals.defaults, options); const result = this._validate(value, null, settings); const errors = Errors.process(result.errors, value); if (callback) { return callback(errors, result.value); } return { error: errors, value: result.value }; } validate(value, options, callback) { if (typeof options === 'function') { return this._validateWithOptions(value, null, options); } return this._validateWithOptions(value, options, callback); } describe() { const description = { type: this._type }; const flags = Object.keys(this._flags); if (flags.length) { if (['empty', 'default', 'lazy', 'label'].some((flag) => this._flags.hasOwnProperty(flag))) { description.flags = {}; for (let i = 0; i < flags.length; ++i) { const flag = flags[i]; if (flag === 'empty') { description.flags[flag] = this._flags[flag].describe(); } else if (flag === 'default') { if (Ref.isRef(this._flags[flag])) { description.flags[flag] = this._flags[flag].toString(); } else if (typeof this._flags[flag] === 'function') { description.flags[flag] = this._flags[flag].description; } else { description.flags[flag] = this._flags[flag]; } } else if (flag === 'lazy' || flag === 'label') { // We don't want it in the description } else { description.flags[flag] = this._flags[flag]; } } } else { 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; } const valids = this._valids.values(); if (valids.length) { description.valids = valids.map((v) => { return Ref.isRef(v) ? v.toString() : v; }); } const invalids = this._invalids.values(); if (invalids.length) { description.invalids = invalids.map((v) => { return Ref.isRef(v) ? v.toString() : v; }); } description.rules = []; for (let i = 0; i < this._tests.length; ++i) { const validator = this._tests[i]; const item = { name: validator.name }; if (validator.arg !== void 0) { item.arg = Ref.isRef(validator.arg) ? validator.arg.toString() : validator.arg; } const options = validator.options; if (options) { if (options.hasRef) { item.arg = {}; const keys = Object.keys(validator.arg); for (let j = 0; j < keys.length; ++j) { const key = keys[j]; const value = validator.arg[key]; item.arg[key] = Ref.isRef(value) ? value.toString() : value; } } if (typeof options.description === 'string') { item.description = options.description; } else if (typeof options.description === 'function') { item.description = options.description(item.arg); } } description.rules.push(item); } if (!description.rules.length) { delete description.rules; } const label = this._getLabel(); if (label) { description.label = label; } return description; } label(name) { Hoek.assert(name && typeof name === 'string', 'Label name must be a non-empty string'); const obj = this.clone(); obj._flags.label = name; return obj; } _getLabel(def) { return this._flags.label || def; } }; internals.Any.prototype.isImmutable = true; // Prevents Hoek from deep cloning schema objects // Aliases internals.Any.prototype.only = internals.Any.prototype.equal = internals.Any.prototype.valid; internals.Any.prototype.disallow = internals.Any.prototype.not = internals.Any.prototype.invalid; internals.Any.prototype.exist = internals.Any.prototype.required; internals._try = function (fn, args) { let err; let result; try { result = fn.apply(null, args); } catch (e) { err = e; } return { value: result, error: err }; }; internals.Set = class { constructor() { this._set = []; } add(value, refs) { if (!Ref.isRef(value) && this.has(value, null, null, false)) { return; } if (refs !== undefined) { // If it's a merge, we don't have any refs Ref.push(refs, value); } this._set.push(value); } merge(add, remove) { for (let i = 0; i < add._set.length; ++i) { this.add(add._set[i]); } for (let i = 0; i < remove._set.length; ++i) { this.remove(remove._set[i]); } } remove(value) { this._set = this._set.filter((item) => value !== item); } has(value, state, options, insensitive) { for (let i = 0; i < this._set.length; ++i) { let items = this._set[i]; if (state && Ref.isRef(items)) { // Only resolve references if there is a state, otherwise it's a merge items = items(state.reference || state.parent, options); } if (!Array.isArray(items)) { items = [items]; } for (let j = 0; j < items.length; ++j) { const item = items[j]; 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; } values(options) { if (options && options.stripUndefined) { const values = []; for (let i = 0; i < this._set.length; ++i) { const item = this._set[i]; if (item !== undefined) { values.push(item); } } return values; } return this._set.slice(); } }; internals.concatSettings = function (target, source) { // Used to avoid cloning context if (!target && !source) { return null; } const obj = {}; if (target) { Object.assign(obj, target); } if (source) { const sKeys = Object.keys(source); for (let i = 0; i < sKeys.length; ++i) { const key = sKeys[i]; if (key !== 'language' || !obj.hasOwnProperty(key)) { obj[key] = source[key]; } else { obj[key] = Hoek.applyToDefaults(obj[key], source[key]); } } } return obj; };