isvalid
Version:
Async JSON validation library for node.js.
509 lines (407 loc) • 14.4 kB
JavaScript
//
// validate.js
//
// Created by Kristian Trenskow on 2014-06-06
//
// See license in LICENSE
//
import ValidationError from './errors/validation.js';
import AggregatedError from './errors/aggregated.js';
import { testIndex } from './ranges.js';
import unique from './unique.js';
import formalize from './formalize.js';
import { isSameType, instanceTypeName, typeName } from './utils.js';
import equals from './equals.js';
const checkBoolValue = (name, schema, defaults) => {
if (schema[name] === undefined) return defaults[name] === true;
return schema[name] === true;
};
const customErrorMessage = (str, ...args) => {
if (typeof str === 'function') return str(...args);
return str;
};
const validateObject = async (data, schema, options, keyPath, validatedData) => {
if (data) {
if (typeof data !== 'object') {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'type',
(schema.errors || {}).type || customErrorMessage(((options.errorMessages || {}).object || {}).type || 'Is not of type object.')
);
}
// If there is no schema we just return the object.
if (typeof schema.schema === 'undefined') return data;
// Find unknown keys
for (let key in data) {
if (schema.schema[key] === undefined) {
switch (schema.unknownKeys || options.defaults.unknownKeys) {
case 'allow':
break;
case 'remove':
delete data[key];
break;
default:
throw new ValidationError(
keyPath.concat([key]),
schema._nonFormalizedSchema,
'unknownKeys',
(schema.errors || {}).unknownKeys || customErrorMessage(((options.errorMessages || {}).object || {}).unknownKeys || 'Unknown key.')
);
}
}
}
// Get keys and sort by priority
let keys = Object.keys(schema.schema).sort((key1, key2) => {
return schema.schema[key1].priority - schema.schema[key2].priority;
});
let errors = [];
for (let key of keys) {
try {
let value = await validateAny(data[key], schema.schema[key], options, keyPath.concat([key]), validatedData);
if (typeof value !== 'undefined') {
data[key] = value;
}
} catch (error) {
switch (options.aggregatedErrors || 'none') {
case 'none': throw error;
case 'flatten':
errors = errors.concat(error.errors || [error]);
break;
default:
errors.push(error);
break;
}
}
}
if (errors.length) {
if (errors.length === 1) throw errors[0];
throw new AggregatedError(
keyPath,
schema._nonFormalizedSchema,
'object',
(schema.errors || {}).object || customErrorMessage(((options.errorMessages || {}).object || {}).object || 'Multiple errors occurred.'),
errors
);
}
}
return data;
};
const validateArray = async (data, schema, options, keyPath, validatedData) => {
if (data) {
if (!Array.isArray(data)) {
if (checkBoolValue('autoWrap', schema, options.defaults)) {
return await validateArray([data], schema, options, keyPath, validatedData);
} else {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'type',
(schema.errors || {}).type || customErrorMessage(((options.errorMessages || {}).array || {}).type || 'Is not of type array.')
);
}
}
if (typeof schema.schema !== 'undefined') {
let errors = [];
for (let idx in data) {
try {
data[idx] = await validateAny(data[idx], schema.schema, options, keyPath.concat([idx]), validatedData);
} catch (error) {
switch (options.aggregatedErrors || 'none') {
case 'none': throw error;
case 'flatten':
errors = errors.concat(error.errors || [error]);
break;
default:
errors.push(error);
break;
}
}
}
if (errors.length) {
if (errors.length === 1) throw errors[0];
throw new AggregatedError(
keyPath,
schema._nonFormalizedSchema,
'array',
(schema.errors || {}).array || customErrorMessage(((options.errorMessages || {}).array || {}).array || 'Multiple errors occurred.'),
errors
);
}
}
if ((schema.len || options.defaults.len) && !testIndex((schema.len || options.defaults.len), data.length)) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'len',
(schema.errors || {}).len || customErrorMessage(((options.errorMessages || {}).array || {}).len || ((len) => `Array length is not within range of '${len}'.`), schema._nonFormalizedSchema.len)
);
}
if ((schema.unique || options.defaults.unique) && !await unique(data)) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'unique',
(schema.errors || {}).unique || customErrorMessage(((options.errorMessages || {}).array || {}).unique || 'Array is not unique.')
);
}
}
return data;
};
const validateString = async (data, schema, options, keyPath) => {
if (typeof data !== 'string') {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'type',
(schema.errors || {}).type || customErrorMessage(((options.errorMessages || {}).string || {}).type || 'Is not of type string.')
);
}
if (checkBoolValue('trim', schema, options.defaults)) {
data = data.replace(/^\s+|\s+$/g,'');
}
if (schema.len || options.defaults.len) {
if (!testIndex(schema.len || options.defaults.len, data.length)) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'len',
(schema.errors || {}).len || customErrorMessage(((options.errorMessages || {}).string || {}).len || ((len) => `String length is not within range of ${len}`), schema._nonFormalizedSchema.len)
);
}
}
if (schema.match || options.defaults.match) {
// We are guaranteed that match is a RegExp because the formalizer has tested it.
if (!(schema.match || options.defaults.match).test(data)) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'match',
(schema.errors || {}).match || customErrorMessage(((options.errorMessages || {}).string || {}).match || ((source) => `Does not match expression ${source}.`), schema.match.source)
);
}
}
// Validate enums
if ((schema.enum || options.defaults.enum) && Object.keys(schema.enum || options.defaults.enum).indexOf(data) == -1) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'enum',
(schema.errors || {}).enum || customErrorMessage(((options.errorMessages || {}).string || {}).enum || ((values) => {
return `Possible values are ${Object.keys(values).map(function(val) {
return '"' + val + '"';
}).reduce(function(prev, cur, idx, arr) {
return prev + (idx == arr.length - 1 ? ' and ' : ', ') + cur;
})}.`;
}), schema.enum)
);
}
return data;
};
const validateNumber = async (data, schema, options, keyPath) => {
if (typeof data === 'string' && /^-?[0-9]+(?:\.[0-9]+)?(?:[eE](?:-|\+)?[0-9]+)?$/.test(data)) {
data = parseFloat(data);
}
if (typeof data !== 'number' || isNaN(data)) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'type',
(schema.errors || {}).type || customErrorMessage(((options.errorMessages || {}).number || {}).type || 'Is not of type number.')
);
}
if (schema.range || options.defaults.range) {
if (!testIndex(schema.range || options.defaults.range, data)) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'range',
(schema.errors || {}).range || customErrorMessage(((options.errorMessages || {}).number || {}).range || ((range) => `Not within range of ${range}.`), schema._nonFormalizedSchema.range)
);
}
}
if (schema.float || options.defaults.float) {
if (!Number.isInteger(data)) {
switch (schema.float) {
case 'deny':
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'float',
(schema.errors || {}).float || customErrorMessage(((options.errorMessages || {}).number || {}).float || 'Number must be an integer.'));
default:
data = Math[schema.float](data);
break;
}
}
}
return data;
};
const validateBoolean = async (data, schema, options, keyPath) => {
if (typeof data === 'string' && /^true|false$/i.test(data)) {
data = /^true$/i.test(data);
}
if (typeof data !== 'boolean') {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'type',
(schema.errors || {}).type || customErrorMessage(((options.errorMessages || {}).boolean || {}).type || 'Is not of type boolean.')
);
}
return data;
};
const validateDate = async (data, schema, options, keyPath) => {
if (typeof data === 'string') {
data = new Date(data);
if (isNaN(data.getDate())) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'type',
(schema.errors || {}).type || customErrorMessage(((options.errorMessages || {}).date || {}).type || 'Is not of type date.')
);
}
}
if (!isSameType('date', instanceTypeName(data))) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'type',
(schema.errors || {}).type || customErrorMessage(((options.errorMessages || {}).date || {}).type || 'Is not of type date.')
);
}
return data;
};
const validateOther = async (data, schema, options, keyPath) => {
if (!isSameType(typeName(schema.type), instanceTypeName(data))) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'type',
(schema.errors || {}).type || customErrorMessage(((options.errorMessages || {}).other || {}).type || ((type) => `Is not of type ${type}.`), typeName(schema.type))
);
}
return data;
};
const validateCustom = async (phase, data, schema, options, keyPath, validatedData) => {
if (!schema[phase]) return data;
for (let idx = 0 ; idx < schema[phase].length ; idx++) {
try {
let result = await Promise.resolve(schema[phase][idx](data, schema, { options, keyPath, data: validatedData }));
if (typeof result !== 'undefined') data = result;
} catch (error) {
throw ValidationError.fromError(keyPath, schema._nonFormalizedSchema, phase, error);
}
}
return data;
};
const validatePlugins = async (phase, data, schema, options, keyPath) => {
const plugins = Object.keys(schema._plugins || {})
.filter((key) => schema._plugins[key].phase === phase)
.map((key) => [key, schema._plugins[key].validator]);
for (let idx = 0 ; idx < plugins.length ; idx++) {
const [key, validator] = plugins[idx];
try {
data = await validator(data, schema[key], key, schema.type);
} catch (error) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
key,
(schema.errors || {})[key] || customErrorMessage((options.errorMessages || {})[key] || error.message));
}
}
return data;
};
const validatePre = async (data, schema, options, keyPath, validatedData) => {
data = await validateCustom('pre', data, schema, options, keyPath, validatedData);
return await validatePlugins('pre', data, schema, options, keyPath);
};
const validatePost = async (data, schema, options, keyPath, validatedData) => {
data = await validatePlugins('post', data, schema, options, keyPath);
return await validateCustom('post', data, schema, options, keyPath, validatedData);
};
const validateAny = async (data, schema, options, keyPath, validatedData) => {
// If schema is not yet formalized - formalize it and come back.
if (schema._nonFormalizedSchema === undefined) {
return await validateAny(data, formalize(schema, options), options, keyPath, validatedData);
}
data = await validatePre(data, schema, options, keyPath, validatedData);
if (typeof data === 'undefined' || data === null) {
if (data === null) {
let nul = schema.null || options.defaults.null || 'deny';
if (nul === 'undefine') data = undefined;
else if (nul === 'allow') return await validatePost(data, schema, options, keyPath, validatedData);
else throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'null',
(schema.errors || {}).null || customErrorMessage((options.errorMessages || {}).null || 'Cannot be null.')
);
}
if (typeof schema.default !== 'undefined') {
let data = schema.default;
if (typeof data === 'function') {
data = data(options, validatedData);
}
data = await Promise.resolve(data);
return await validatePost(data, schema, options, keyPath, validatedData);
}
if (schema.required === 'implicit') {
data = {};
} else if (checkBoolValue('required', schema, options.defaults)) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'required',
(schema.errors || {}).required || customErrorMessage((options.errorMessages || {}).required || 'Data is required.')
);
} else {
return await validatePost(data, schema, options, keyPath, validatedData);
}
}
if (typeof schema.equal !== 'undefined' || typeof options.defaults.equal !== 'undefined') {
if (!await equals(schema.equal || options.defaults.equal, data)) {
throw new ValidationError(
keyPath,
schema._nonFormalizedSchema,
'equal',
(schema.errors || {}).equal || customErrorMessage((options.errorMessages || {}).equal || ((value) => `Data does not equal ${value}.`), schema.equal)
);
}
}
if (schema.type !== undefined) {
switch (typeName(schema.type).toLowerCase()) {
case 'object':
data = await validateObject(data, schema, options, keyPath, validatedData);
break;
case 'array':
data = await validateArray(data, schema, options, keyPath, validatedData);
break;
case 'string':
data = await validateString(data, schema, options, keyPath);
break;
case 'number':
data = await validateNumber(data, schema, options, keyPath);
break;
case 'boolean':
data = await validateBoolean(data, schema, options, keyPath);
break;
case 'date':
data = await validateDate(data, schema, options, keyPath);
break;
default:
data = await validateOther(data, schema, options, keyPath);
break;
}
}
return await validatePost(data, schema, options, keyPath, validatedData);
};
export default async (data, schema, options = {}, keyPath = '') => {
if (typeof schema === 'undefined') throw new Error('Missing parameter schema.');
options.keyPath = options.keyPath || keyPath;
if (!Array.isArray(options.keyPath)) options.keyPath = options.keyPath.split('.');
options.defaults = options.defaults || {};
return await validateAny(data, schema, options, options.keyPath, data);
};