123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699 |
- 'use strict';
- const Bourne = require('@hapi/bourne');
- const Hoek = require('@hapi/hoek');
- const Any = require('../any');
- const Cast = require('../../cast');
- const Ref = require('../../ref');
- const State = require('../state');
- const internals = {};
- internals.fastSplice = function (arr, i) {
- let pos = i;
- while (pos < arr.length) {
- arr[pos++] = arr[pos];
- }
- --arr.length;
- };
- internals.Array = class extends Any {
- constructor() {
- super();
- this._type = 'array';
- this._inner.items = [];
- this._inner.ordereds = [];
- this._inner.inclusions = [];
- this._inner.exclusions = [];
- this._inner.requireds = [];
- this._flags.sparse = false;
- }
- _base(value, state, options) {
- const result = {
- value
- };
- if (typeof value === 'string' &&
- options.convert) {
- if (value.length > 1 &&
- (value[0] === '[' || /^\s*\[/.test(value))) {
- try {
- result.value = Bourne.parse(value);
- }
- catch (e) { }
- }
- }
- let isArray = Array.isArray(result.value);
- const wasArray = isArray;
- if (options.convert && this._flags.single && !isArray) {
- result.value = [result.value];
- isArray = true;
- }
- if (!isArray) {
- result.errors = this.createError('array.base', null, state, options);
- return result;
- }
- if (this._inner.inclusions.length ||
- this._inner.exclusions.length ||
- this._inner.requireds.length ||
- this._inner.ordereds.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 = this._checkItems(result.value, wasArray, state, options);
- if (result.errors && wasArray && options.convert && this._flags.single) {
- // Attempt a 2nd pass by putting the array inside one.
- const previousErrors = result.errors;
- result.value = [result.value];
- result.errors = this._checkItems(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;
- }
- _checkItems(items, wasArray, state, options) {
- const errors = [];
- let errored;
- const requireds = this._inner.requireds.slice();
- const ordereds = this._inner.ordereds.slice();
- const inclusions = [...this._inner.inclusions, ...requireds];
- let il = items.length;
- for (let i = 0; i < il; ++i) {
- errored = false;
- const item = items[i];
- let isValid = false;
- const key = wasArray ? i : state.key;
- const path = wasArray ? [...state.path, i] : state.path;
- const localState = new State(key, path, state.parent, state.reference);
- let res;
- // Sparse
- if (!this._flags.sparse && item === undefined) {
- errors.push(this.createError('array.sparse', null, { key: state.key, path: localState.path, pos: i }, options));
- if (options.abortEarly) {
- return errors;
- }
- ordereds.shift();
- continue;
- }
- // Exclusions
- for (let j = 0; j < this._inner.exclusions.length; ++j) {
- res = this._inner.exclusions[j]._validate(item, localState, {}); // Not passing options to use defaults
- if (!res.errors) {
- errors.push(this.createError(wasArray ? 'array.excludes' : 'array.excludesSingle', { pos: i, value: item }, { key: state.key, path: localState.path }, options));
- errored = true;
- if (options.abortEarly) {
- return errors;
- }
- ordereds.shift();
- break;
- }
- }
- if (errored) {
- continue;
- }
- // Ordered
- if (this._inner.ordereds.length) {
- if (ordereds.length > 0) {
- const ordered = ordereds.shift();
- res = ordered._validate(item, localState, options);
- if (!res.errors) {
- if (ordered._flags.strip) {
- internals.fastSplice(items, i);
- --i;
- --il;
- }
- else if (!this._flags.sparse && res.value === undefined) {
- errors.push(this.createError('array.sparse', null, { key: state.key, path: localState.path, pos: i }, options));
- if (options.abortEarly) {
- return errors;
- }
- continue;
- }
- else {
- items[i] = res.value;
- }
- }
- else {
- errors.push(this.createError('array.ordered', { pos: i, 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(this.createError('array.orderedLength', { pos: i, limit: this._inner.ordereds.length }, { key: state.key, path: localState.path }, options));
- if (options.abortEarly) {
- return errors;
- }
- continue;
- }
- }
- // Requireds
- const requiredChecks = [];
- let jl = requireds.length;
- for (let j = 0; j < jl; ++j) {
- res = requiredChecks[j] = requireds[j]._validate(item, localState, options);
- if (!res.errors) {
- items[i] = res.value;
- isValid = true;
- internals.fastSplice(requireds, j);
- --j;
- --jl;
- if (!this._flags.sparse && res.value === undefined) {
- errors.push(this.createError('array.sparse', null, { key: state.key, path: localState.path, pos: i }, options));
- if (options.abortEarly) {
- return errors;
- }
- }
- break;
- }
- }
- if (isValid) {
- continue;
- }
- // Inclusions
- const stripUnknown = options.stripUnknown && !!options.stripUnknown.arrays || false;
- jl = inclusions.length;
- for (let j = 0; j < jl; ++j) {
- const inclusion = inclusions[j];
- // Avoid re-running requireds that already didn't match in the previous loop
- const 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, i);
- --i;
- --il;
- }
- else if (!this._flags.sparse && res.value === undefined) {
- errors.push(this.createError('array.sparse', null, { key: state.key, path: localState.path, pos: i }, options));
- errored = true;
- }
- else {
- items[i] = res.value;
- }
- isValid = true;
- break;
- }
- }
- // Return the actual error if only one inclusion defined
- if (jl === 1) {
- if (stripUnknown) {
- internals.fastSplice(items, i);
- --i;
- --il;
- isValid = true;
- break;
- }
- errors.push(this.createError(wasArray ? 'array.includesOne' : 'array.includesOneSingle', { pos: i, 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 (stripUnknown) {
- internals.fastSplice(items, i);
- --i;
- --il;
- continue;
- }
- errors.push(this.createError(wasArray ? 'array.includes' : 'array.includesSingle', { pos: i, value: item }, { key: state.key, path: localState.path }, options));
- if (options.abortEarly) {
- return errors;
- }
- }
- }
- if (requireds.length) {
- this._fillMissedErrors(errors, requireds, state, options);
- }
- if (ordereds.length) {
- this._fillOrderedErrors(errors, ordereds, state, options);
- }
- return errors.length ? errors : null;
- }
- describe() {
- const description = super.describe();
- if (this._inner.ordereds.length) {
- description.orderedItems = [];
- for (let i = 0; i < this._inner.ordereds.length; ++i) {
- description.orderedItems.push(this._inner.ordereds[i].describe());
- }
- }
- if (this._inner.items.length) {
- description.items = [];
- for (let i = 0; i < this._inner.items.length; ++i) {
- description.items.push(this._inner.items[i].describe());
- }
- }
- if (description.rules) {
- for (let i = 0; i < description.rules.length; ++i) {
- const rule = description.rules[i];
- if (rule.name === 'has') {
- rule.arg = rule.arg.describe();
- }
- }
- }
- return description;
- }
- items(...schemas) {
- const obj = this.clone();
- Hoek.flatten(schemas).forEach((type, index) => {
- try {
- type = Cast.schema(this._currentJoi, type);
- }
- catch (castErr) {
- if (castErr.hasOwnProperty('path')) {
- castErr.path = index + '.' + castErr.path;
- }
- else {
- castErr.path = index;
- }
- castErr.message = `${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;
- }
- ordered(...schemas) {
- const obj = this.clone();
- Hoek.flatten(schemas).forEach((type, index) => {
- try {
- type = Cast.schema(this._currentJoi, type);
- }
- catch (castErr) {
- if (castErr.hasOwnProperty('path')) {
- castErr.path = index + '.' + castErr.path;
- }
- else {
- castErr.path = index;
- }
- castErr.message = `${castErr.message}(${castErr.path})`;
- throw castErr;
- }
- obj._inner.ordereds.push(type);
- });
- return obj;
- }
- min(limit) {
- const isRef = Ref.isRef(limit);
- Hoek.assert((Number.isSafeInteger(limit) && limit >= 0) || isRef, 'limit must be a positive integer or reference');
- return this._testUnique('min', limit, function (value, state, options) {
- let compareTo;
- if (isRef) {
- compareTo = limit(state.reference || state.parent, options);
- if (!(Number.isSafeInteger(compareTo) && compareTo >= 0)) {
- return this.createError('array.ref', { ref: limit, value: compareTo }, state, options);
- }
- }
- else {
- compareTo = limit;
- }
- if (value.length >= compareTo) {
- return value;
- }
- return this.createError('array.min', { limit, value }, state, options);
- });
- }
- max(limit) {
- const isRef = Ref.isRef(limit);
- Hoek.assert((Number.isSafeInteger(limit) && limit >= 0) || isRef, 'limit must be a positive integer or reference');
- return this._testUnique('max', limit, function (value, state, options) {
- let compareTo;
- if (isRef) {
- compareTo = limit(state.reference || state.parent, options);
- if (!(Number.isSafeInteger(compareTo) && compareTo >= 0)) {
- return this.createError('array.ref', { ref: limit.key }, state, options);
- }
- }
- else {
- compareTo = limit;
- }
- if (value.length <= compareTo) {
- return value;
- }
- return this.createError('array.max', { limit, value }, state, options);
- });
- }
- length(limit) {
- const isRef = Ref.isRef(limit);
- Hoek.assert((Number.isSafeInteger(limit) && limit >= 0) || isRef, 'limit must be a positive integer or reference');
- return this._testUnique('length', limit, function (value, state, options) {
- let compareTo;
- if (isRef) {
- compareTo = limit(state.reference || state.parent, options);
- if (!(Number.isSafeInteger(compareTo) && compareTo >= 0)) {
- return this.createError('array.ref', { ref: limit.key }, state, options);
- }
- }
- else {
- compareTo = limit;
- }
- if (value.length === compareTo) {
- return value;
- }
- return this.createError('array.length', { limit, value }, state, options);
- });
- }
- has(schema) {
- try {
- schema = Cast.schema(this._currentJoi, schema);
- }
- catch (castErr) {
- if (castErr.hasOwnProperty('path')) {
- castErr.message = `${castErr.message}(${castErr.path})`;
- }
- throw castErr;
- }
- return this._test('has', schema, function (value, state, options) {
- const isValid = value.some((item, idx) => {
- const localState = new State(idx, [...state.path, idx], state.key, state.reference);
- return !schema._validate(item, localState, options).errors;
- });
- if (isValid) {
- return value;
- }
- const patternLabel = schema._getLabel();
- if (patternLabel) {
- return this.createError('array.hasKnown', { patternLabel }, state, options);
- }
- return this.createError('array.hasUnknown', null, state, options);
- });
- }
- unique(comparator, configs) {
- Hoek.assert(comparator === undefined ||
- typeof comparator === 'function' ||
- typeof comparator === 'string', 'comparator must be a function or a string');
- Hoek.assert(configs === undefined ||
- typeof configs === 'object', 'configs must be an object');
- const settings = {
- ignoreUndefined: (configs && configs.ignoreUndefined) || false
- };
- if (typeof comparator === 'string') {
- settings.path = comparator;
- }
- else if (typeof comparator === 'function') {
- settings.comparator = comparator;
- }
- return this._test('unique', settings, function (value, state, options) {
- const found = {
- string: Object.create(null),
- number: Object.create(null),
- undefined: Object.create(null),
- boolean: Object.create(null),
- object: new Map(),
- function: new Map(),
- custom: new Map()
- };
- const compare = settings.comparator || Hoek.deepEqual;
- const ignoreUndefined = settings.ignoreUndefined;
- for (let i = 0; i < value.length; ++i) {
- const item = settings.path ? Hoek.reach(value[i], settings.path) : value[i];
- const records = settings.comparator ? found.custom : found[typeof item];
- // 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 (records instanceof Map) {
- const entries = records.entries();
- let current;
- while (!(current = entries.next()).done) {
- if (compare(current.value[0], item)) {
- const localState = new State(state.key, [...state.path, i], state.parent, state.reference);
- const context = {
- pos: i,
- value: value[i],
- dupePos: current.value[1],
- dupeValue: value[current.value[1]]
- };
- if (settings.path) {
- context.path = settings.path;
- }
- return this.createError('array.unique', context, localState, options);
- }
- }
- records.set(item, i);
- }
- else {
- if ((!ignoreUndefined || item !== undefined) && records[item] !== undefined) {
- const localState = new State(state.key, [...state.path, i], state.parent, state.reference);
- const context = {
- pos: i,
- value: value[i],
- dupePos: records[item],
- dupeValue: value[records[item]]
- };
- if (settings.path) {
- context.path = settings.path;
- }
- return this.createError('array.unique', context, localState, options);
- }
- records[item] = i;
- }
- }
- }
- return value;
- });
- }
- sparse(enabled) {
- const value = enabled === undefined ? true : !!enabled;
- if (this._flags.sparse === value) {
- return this;
- }
- const obj = this.clone();
- obj._flags.sparse = value;
- return obj;
- }
- single(enabled) {
- const value = enabled === undefined ? true : !!enabled;
- if (this._flags.single === value) {
- return this;
- }
- const obj = this.clone();
- obj._flags.single = value;
- return obj;
- }
- _fillMissedErrors(errors, requireds, state, options) {
- const knownMisses = [];
- let unknownMisses = 0;
- for (let i = 0; i < requireds.length; ++i) {
- const label = requireds[i]._getLabel();
- if (label) {
- knownMisses.push(label);
- }
- else {
- ++unknownMisses;
- }
- }
- if (knownMisses.length) {
- if (unknownMisses) {
- errors.push(this.createError('array.includesRequiredBoth', { knownMisses, unknownMisses }, { key: state.key, path: state.path }, options));
- }
- else {
- errors.push(this.createError('array.includesRequiredKnowns', { knownMisses }, { key: state.key, path: state.path }, options));
- }
- }
- else {
- errors.push(this.createError('array.includesRequiredUnknowns', { unknownMisses }, { key: state.key, path: state.path }, options));
- }
- }
- _fillOrderedErrors(errors, ordereds, state, options) {
- const requiredOrdereds = [];
- for (let i = 0; i < ordereds.length; ++i) {
- const presence = Hoek.reach(ordereds[i], '_flags.presence');
- if (presence === 'required') {
- requiredOrdereds.push(ordereds[i]);
- }
- }
- if (requiredOrdereds.length) {
- this._fillMissedErrors(errors, requiredOrdereds, state, options);
- }
- }
- };
- module.exports = new internals.Array();
|