gtg2json/node_modules/joi/lib/types/array/index.js

670 lines
21 KiB
JavaScript

'use strict';
// Load modules
const Any = require('../any');
const Cast = require('../../cast');
const Ref = require('../../ref');
const Hoek = require('hoek');
// Declare internals
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) {
internals.safeParse(value, result);
}
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.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.
const previousErrors = result.errors;
result.value = [result.value];
result.errors = this._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;
}
_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.concat(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.concat(i) : state.path;
const localState = { key, path, parent: state.parent, reference: 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 === true ? true : !!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.call(this, errors, requireds, state, options);
}
if (ordereds.length) {
this._fillOrderedErrors.call(this, 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());
}
}
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._test('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.key }, 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._test('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._test('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);
});
}
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 = {
key: state.key,
path: state.path.concat(i),
parent: state.parent,
reference: 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 = {
key: state.key,
path: state.path.concat(i),
parent: state.parent,
reference: 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.call(this, errors, requiredOrdereds, state, options);
}
}
};
internals.safeParse = function (value, result) {
try {
const converted = JSON.parse(value);
if (Array.isArray(converted)) {
result.value = converted;
}
}
catch (e) { }
};
module.exports = new internals.Array();