// Load modules var Any = require('./any'); var Cast = require('./cast'); var Errors = require('./errors'); var Hoek = require('hoek'); // Declare internals var internals = {}; internals.fastSplice = function (arr, i) { var il = arr.length; var pos = i; while (pos < il) { arr[pos++] = arr[pos]; } --arr.length; }; internals.Array = function () { Any.call(this); this._type = 'array'; this._inner.items = []; this._inner.ordereds = []; this._inner.inclusions = []; this._inner.exclusions = []; this._inner.requireds = []; this._flags.sparse = false; }; Hoek.inherits(internals.Array, Any); internals.Array.prototype._base = function (value, state, options) { var result = { value: value }; if (typeof value === 'string' && options.convert) { try { var converted = JSON.parse(value); if (Array.isArray(converted)) { result.value = converted; } } catch (e) { } } var isArray = Array.isArray(result.value); var wasArray = isArray; if (options.convert && this._flags.single && !isArray) { result.value = [result.value]; isArray = true; } if (!isArray) { result.errors = Errors.create('array.base', null, state, options); return result; } if (this._inner.inclusions.length || this._inner.exclusions.length || !this._flags.sparse) { // Clone the array so that we don't modify the original if (wasArray) { result.value = result.value.slice(0); } result.errors = internals.checkItems.call(this, result.value, wasArray, state, options); if (result.errors && wasArray && options.convert && this._flags.single) { // Attempt a 2nd pass by putting the array inside one. var previousErrors = result.errors; result.value = [result.value]; result.errors = internals.checkItems.call(this, result.value, wasArray, state, options); if (result.errors) { // Restore previous errors and value since this didn't validate either. result.errors = previousErrors; result.value = result.value[0]; } } } return result; }; internals.checkItems = function (items, wasArray, state, options) { var errors = []; var errored; var requireds = this._inner.requireds.slice(); var ordereds = this._inner.ordereds.slice(); var inclusions = this._inner.inclusions.concat(requireds); for (var v = 0, vl = items.length; v < vl; ++v) { errored = false; var item = items[v]; var isValid = false; var localState = { key: v, path: (state.path ? state.path + '.' : '') + v, parent: items, reference: state.reference }; var res; // Sparse if (!this._flags.sparse && item === undefined) { errors.push(Errors.create('array.sparse', null, { key: state.key, path: localState.path }, options)); if (options.abortEarly) { return errors; } continue; } // Exclusions for (var i = 0, il = this._inner.exclusions.length; i < il; ++i) { res = this._inner.exclusions[i]._validate(item, localState, {}); // Not passing options to use defaults if (!res.errors) { errors.push(Errors.create(wasArray ? 'array.excludes' : 'array.excludesSingle', { pos: v, value: item }, { key: state.key, path: localState.path }, options)); errored = true; if (options.abortEarly) { return errors; } break; } } if (errored) { continue; } // Ordered if (this._inner.ordereds.length) { if (ordereds.length > 0) { var ordered = ordereds.shift(); res = ordered._validate(item, localState, options); if (!res.errors) { if (ordered._flags.strip) { internals.fastSplice(items, v); --v; --vl; } else { items[v] = res.value; } } else { errors.push(Errors.create('array.ordered', { pos: v, reason: res.errors, value: item }, { key: state.key, path: localState.path }, options)); if (options.abortEarly) { return errors; } } continue; } else if (!this._inner.items.length) { errors.push(Errors.create('array.orderedLength', { pos: v, limit: this._inner.ordereds.length }, { key: state.key, path: localState.path }, options)); if (options.abortEarly) { return errors; } continue; } } // Requireds var requiredChecks = []; for (i = 0, il = requireds.length; i < il; ++i) { res = requiredChecks[i] = requireds[i]._validate(item, localState, options); if (!res.errors) { items[v] = res.value; isValid = true; internals.fastSplice(requireds, i); --i; --il; break; } } if (isValid) { continue; } // Inclusions for (i = 0, il = inclusions.length; i < il; ++i) { var inclusion = inclusions[i]; // Avoid re-running requireds that already didn't match in the previous loop var previousCheck = requireds.indexOf(inclusion); if (previousCheck !== -1) { res = requiredChecks[previousCheck]; } else { res = inclusion._validate(item, localState, options); if (!res.errors) { if (inclusion._flags.strip) { internals.fastSplice(items, v); --v; --vl; } else { items[v] = res.value; } isValid = true; break; } } // Return the actual error if only one inclusion defined if (il === 1) { if (options.stripUnknown) { internals.fastSplice(items, v); --v; --vl; isValid = true; break; } errors.push(Errors.create(wasArray ? 'array.includesOne' : 'array.includesOneSingle', { pos: v, reason: res.errors, value: item }, { key: state.key, path: localState.path }, options)); errored = true; if (options.abortEarly) { return errors; } break; } } if (errored) { continue; } if (this._inner.inclusions.length && !isValid) { if (options.stripUnknown) { internals.fastSplice(items, v); --v; --vl; continue; } errors.push(Errors.create(wasArray ? 'array.includes' : 'array.includesSingle', { pos: v, value: item }, { key: state.key, path: localState.path }, options)); if (options.abortEarly) { return errors; } } } if (requireds.length) { internals.fillMissedErrors(errors, requireds, state, options); } if (ordereds.length) { internals.fillOrderedErrors(errors, ordereds, state, options); } return errors.length ? errors : null; }; internals.fillMissedErrors = function (errors, requireds, state, options) { var knownMisses = []; var unknownMisses = 0; for (var i = 0, il = requireds.length; i < il; ++i) { var label = Hoek.reach(requireds[i], '_settings.language.label'); if (label) { knownMisses.push(label); } else { ++unknownMisses; } } if (knownMisses.length) { if (unknownMisses) { errors.push(Errors.create('array.includesRequiredBoth', { knownMisses: knownMisses, unknownMisses: unknownMisses }, { key: state.key, path: state.patk }, options)); } else { errors.push(Errors.create('array.includesRequiredKnowns', { knownMisses: knownMisses }, { key: state.key, path: state.path }, options)); } } else { errors.push(Errors.create('array.includesRequiredUnknowns', { unknownMisses: unknownMisses }, { key: state.key, path: state.path }, options)); } }; internals.fillOrderedErrors = function (errors, ordereds, state, options) { var requiredOrdereds = []; for (var i = 0, il = ordereds.length; i < il; ++i) { var presence = Hoek.reach(ordereds[i], '_flags.presence'); if (presence === 'required') { requiredOrdereds.push(ordereds[i]); } } if (requiredOrdereds.length) { internals.fillMissedErrors(errors, requiredOrdereds, state, options); } }; internals.Array.prototype.describe = function () { var description = Any.prototype.describe.call(this); if (this._inner.ordereds.length) { description.orderedItems = []; for (var o = 0, ol = this._inner.ordereds.length; o < ol; ++o) { description.orderedItems.push(this._inner.ordereds[o].describe()); } } if (this._inner.items.length) { description.items = []; for (var i = 0, il = this._inner.items.length; i < il; ++i) { description.items.push(this._inner.items[i].describe()); } } return description; }; internals.Array.prototype.items = function () { var obj = this.clone(); Hoek.flatten(Array.prototype.slice.call(arguments)).forEach(function (type, index) { try { type = Cast.schema(type); } catch (castErr) { if (castErr.hasOwnProperty('path')) { castErr.path = index + '.' + castErr.path; } else { castErr.path = index; } castErr.message += '(' + castErr.path + ')'; throw castErr; } obj._inner.items.push(type); if (type._flags.presence === 'required') { obj._inner.requireds.push(type); } else if (type._flags.presence === 'forbidden') { obj._inner.exclusions.push(type.optional()); } else { obj._inner.inclusions.push(type); } }); return obj; }; internals.Array.prototype.ordered = function () { var obj = this.clone(); Hoek.flatten(Array.prototype.slice.call(arguments)).forEach(function (type, index) { try { type = Cast.schema(type); } catch (castErr) { if (castErr.hasOwnProperty('path')) { castErr.path = index + '.' + castErr.path; } else { castErr.path = index; } castErr.message += '(' + castErr.path + ')'; throw castErr; } obj._inner.ordereds.push(type); }); return obj; }; internals.Array.prototype.min = function (limit) { Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); return this._test('min', limit, function (value, state, options) { if (value.length >= limit) { return null; } return Errors.create('array.min', { limit: limit, value: value }, state, options); }); }; internals.Array.prototype.max = function (limit) { Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); return this._test('max', limit, function (value, state, options) { if (value.length <= limit) { return null; } return Errors.create('array.max', { limit: limit, value: value }, state, options); }); }; internals.Array.prototype.length = function (limit) { Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); return this._test('length', limit, function (value, state, options) { if (value.length === limit) { return null; } return Errors.create('array.length', { limit: limit, value: value }, state, options); }); }; internals.Array.prototype.unique = function () { return this._test('unique', undefined, function (value, state, options) { var found = { string: {}, number: {}, undefined: {}, boolean: {}, object: [], function: [] }; for (var i = 0, il = value.length; i < il; ++i) { var item = value[i]; var type = typeof item; var records = found[type]; // All available types are supported, so it's not possible to reach 100% coverage without ignoring this line. // I still want to keep the test for future js versions with new types (eg. Symbol). if (/* $lab:coverage:off$ */ records /* $lab:coverage:on$ */) { if (Array.isArray(records)) { for (var r = 0, rl = records.length; r < rl; ++r) { if (Hoek.deepEqual(records[r], item)) { return Errors.create('array.unique', { pos: i, value: item }, state, options); } } records.push(item); } else { if (records[item]) { return Errors.create('array.unique', { pos: i, value: item }, state, options); } records[item] = true; } } } }); }; internals.Array.prototype.sparse = function (enabled) { var obj = this.clone(); obj._flags.sparse = enabled === undefined ? true : !!enabled; return obj; }; internals.Array.prototype.single = function (enabled) { var obj = this.clone(); obj._flags.single = enabled === undefined ? true : !!enabled; return obj; }; module.exports = new internals.Array();