isvalid
Version:
Async JSON validation library for node.js.
376 lines (301 loc) • 11.7 kB
JavaScript
//
// formalize.js
//
// Created by Kristian Trenskow on 2014-06-09
//
// See license in LICENSE
//
import merge from '@trenskow/merge';
import SchemaError from './errors/schema.js';
import { instanceTypeName, typeName, isSameType } from './utils.js';
import { formalize as _formalizeRange, testFormalizedRange } from './ranges.js';
import { all as allPlugins } from './plugins.js';
const finalize = (formalizedSchema, nonFormalizedSchema) => {
// Add the old non-formalized schema - for preventing
// redundant formalization and for usage by the
// validator when sending validation errors.
//
// Make the property innumerable.
Object.defineProperty(formalizedSchema, '_nonFormalizedSchema', {
value: nonFormalizedSchema,
enumerable: false,
writable: false
});
return formalizedSchema;
};
const formalizeObject = (formalizedSchema, nonFormalizedSchema, options) => {
formalizedSchema.schema = formalizedSchema.schema || {};
if (instanceTypeName(formalizedSchema.schema) !== 'object') {
throw new SchemaError(formalizedSchema.schema, 'Object schemas must be an object.');
}
// Build new formalized schema into this.
let formalizedSubSchema = {};
// We iterate through all keys.
Object.keys(formalizedSchema.schema)
.forEach((key) => {
formalizedSubSchema[key] = formalizeAny(formalizedSchema.schema[key], options);
});
formalizedSchema.schema = finalize(formalizedSubSchema, nonFormalizedSchema.schema);
if (typeof formalizedSchema.required === 'undefined') {
if (Object.keys(formalizedSubSchema).some((key) => formalizedSubSchema[key].required === true || formalizedSubSchema[key].required === 'implicit')) {
formalizedSchema.required = 'implicit';
}
}
return formalizedSchema;
};
const formalizeArray = (formalizedSchema, options) => {
// formalizedSchema has been pre-processed by formalizeAny, so
// we only need to formalize the sub-schema.
// If no sub-schema is provided we consider the schema final.
if (typeof formalizedSchema.schema === 'undefined') return formalizedSchema;
formalizedSchema.schema = formalizeAny(formalizedSchema.schema, options);
// Apply implicit required if sub-schema has required data.
if (typeof(formalizedSchema.required) === 'undefined' || formalizedSchema.required === 'implicit') {
if (formalizedSchema.schema.required === true) formalizedSchema.required = true;
}
return formalizedSchema;
};
const formalizeAny = (schema, options = {}) => {
// If schema is already formalized we just call back.
if (typeof schema._nonFormalizedSchema !== 'undefined') {
if (options.force !== true) return schema;
schema = merge.strategy({
objectKeyLookupMethod: 'keys'
})({}, schema);
}
if (!schema.type && !schema.post && !schema.pre && !schema.equal) {
if ('object' == instanceTypeName(schema)) {
return formalizeAny({ type: Object, schema: schema }, options);
}
if ('array' == instanceTypeName(schema)) {
if (schema.length === 0) return formalizeAny({ type: Array }, options);
return formalizeAny({ type: Array, schema: schema[0] }, options);
}
if ((typeof schema === 'string' && schema.length) || (typeof schema === 'function' && typeName(schema) !== undefined)) {
return formalizeAny({ type: schema }, options);
}
throw new SchemaError(schema, 'Schemas must have at least on validator of `type`, `post`/`pre` and/or `equal`.');
}
const convenienceNames = {
'autowrap': 'autoWrap' // Fix younger Kristian
};
// Copy schema.
let formalizedSchema = {};
for (let key in schema) {
formalizedSchema[convenienceNames[key] || key] = schema[key];
}
// Validators common to all types.
let validators = {
'type': ['function', 'string'],
'equal': 'any',
'required': ['boolean', 'string'],
'default': 'any',
'null': ['string'],
'errors': ['object'],
'pre': ['function', 'array', 'asyncfunction'],
'post': ['function', 'array', 'asyncfunction'],
'priority': 'number',
'_plugins': 'any'
};
// Validators specific to type.
const type = Array.isArray(formalizedSchema.type) ? formalizedSchema.type[0] : formalizedSchema.type;
if (type !== undefined) {
if (isSameType('object', typeName(type))) validators = merge(validators, {
'schema': 'any',
'unknownKeys': [ 'string' ]
});
if (isSameType('array', typeName(type))) validators = merge(validators, {
'schema': 'any',
'len': [ 'string', 'number', testFormalizedRange ],
'unique': [ 'boolean' ],
'autoWrap': [ 'boolean' ]
});
if (isSameType('string', typeName(type))) validators = merge(validators, {
'len': [ 'string', 'number' ],
'match': [ 'regexp' ],
'trim': [ 'boolean' ],
'enum': [ 'array', 'object' ]
});
if (isSameType('number', typeName(type))) validators = merge(validators, {
'range': [ 'string', 'number', testFormalizedRange ],
'float': [ 'string' ]
});
}
// If post validator is provided allow for options.
if (formalizedSchema.pre !== undefined || formalizedSchema.post !== undefined) {
validators = merge(validators, { 'options': 'any' });
}
// Copy validators to formalizedSchema - checking
// for non-supported validators at the same time.
for (let key in formalizedSchema) {
key = convenienceNames[key] || key;
let validator = validators[key];
if (typeof validator === 'undefined') {
let plugin;
[plugin, validator] = options.plugins
.filter((plugin) => plugin.supportsType(type))
.map((plugin) => [plugin, plugin.validatorsForType(type)])
.reduce((plugins, [plugin, validators]) => {
if (Object.keys(validators).includes(key)) {
validators = validators[key];
if (!Array.isArray(validators)) validators = [validators];
return [plugin, validators.map((validator) => typeName(validator))];
}
return plugins;
}, []);
if (typeof plugin === 'undefined') throw new SchemaError(schema, `Validator \`${key}\` is unknown in this context.`);
Object.defineProperty(formalizedSchema, '_plugins', {
value: formalizedSchema._plugins || {},
enumerable: false,
writable: true
});
formalizedSchema._plugins[key] = {
phase: plugin.phase || 'post',
validator: plugin.validate,
formalize: plugin.formalize
};
}
let test = formalizedSchema[key];
// Test for - and transform - errors in validator.
if (Array.isArray(test) &&
test.length === 2 &&
validator.includes(instanceTypeName(test[0]))) {
formalizedSchema.errors = formalizedSchema.errors || {};
formalizedSchema.errors[key] = test[1];
formalizedSchema[key] = test = test[0];
}
if (typeof (options.transform || {}).pre === 'function') {
const transformed = options.transform.pre(key, formalizedSchema[key], { schema: formalizedSchema });
test = formalizedSchema[key] = typeof transformed !== 'undefined' ? transformed : formalizedSchema[key];
}
// Ensure validator is of correct type.
if (typeof validator !== 'undefined' && validator !== 'any' && Array.isArray(validator) && !validator.some((validator) => typeof validator === 'function' ? validator(test) : validator === instanceTypeName(test))) {
throw new SchemaError(
schema,
`Validator '${key}' must be of type(s) ${validators[key].filter((validator) => typeof validator === 'string').join(', ')}.`
);
}
if (typeof (formalizedSchema._plugins || {})[key] !== 'undefined') {
try {
formalizedSchema[key] = formalizedSchema._plugins[key].formalize(formalizedSchema[key], key, type) || formalizedSchema[key];
} catch (error) {
throw new SchemaError(schema, error.message);
}
}
if (typeof (options.transform || {}).post === 'function') {
const transformed = options.transform.post(key, formalizedSchema[key], { schema: formalizedSchema }) || formalizedSchema[key];
test = formalizedSchema[key] = typeof transformed !== 'undefined' ? transformed : formalizedSchema[key];
}
}
// Check for supported type.
if (typeof formalizedSchema.type !== 'undefined' && typeName(formalizedSchema.type) === undefined) {
throw new SchemaError(formalizedSchema, `Cannot validate schema of type ${formalizedSchema.type}.`);
}
// Convert pre function to array.
if (typeof formalizedSchema.pre === 'function') {
formalizedSchema.pre = [formalizedSchema.pre];
}
// Convert post function to array
if (typeof formalizedSchema.post === 'function') {
formalizedSchema.post = [formalizedSchema.post];
}
// Throw error if required is invalid value
if (typeof formalizedSchema.required === 'string' && formalizedSchema.required !== 'implicit') {
throw new SchemaError(
schema,
'Validator `required` must be a Boolean or String of value `implicit`.'
);
}
// Check object unknownKeys
if (typeof formalizedSchema.unknownKeys === 'string' &&
['allow','deny','remove'].indexOf(formalizedSchema.unknownKeys) == -1) {
throw new SchemaError(
schema,
'Validator `unknownKeys` must have value `\'allow\'`, `\'deny\'` or `\'remove\'`.'
);
}
// Check null
if (typeof formalizedSchema.null === 'string' &&
['allow', 'deny', 'undefine'].indexOf(formalizedSchema.null) == -1) {
throw new SchemaError(
schema,
'Validator `null` must have value `\'allow\'`, `\'deny\'`, `\'undefined\'`.'
);
}
// Check number float
if (typeof formalizedSchema.float === 'string' &&
['allow', 'deny', 'round', 'floor', 'ceil'].indexOf(formalizedSchema.float) == -1) {
throw new SchemaError(
schema,
'Validator `float` must have value `\'allow\'`, `\'deny\'`, `\'round\'`, `\'floor\'` or `\'ceil\'`.'
);
}
// Check len
if (typeof formalizedSchema.len !== 'undefined') {
formalizedSchema.len = _formalizeRange(formalizedSchema.len, {
allowNegative: false,
allowNonIntegers: false
});
}
// Check range
if (typeof formalizedSchema.range !== 'undefined') {
formalizedSchema.range = _formalizeRange(formalizedSchema.range, {
allowNegative: true,
allowNonIntegers: true
});
}
// Check string enums
if (typeof formalizedSchema.enum !== 'undefined') {
let wasArray = false;
if (Array.isArray(formalizedSchema.enum)) {
wasArray = true;
for (let value of formalizedSchema.enum) {
if (typeof value !== 'string') {
throw new SchemaError(
schema,
wasArray ? 'Validator `enum` must be an array of strings.' : 'Validator `enum` must be an object with string keys.'
);
}
}
formalizedSchema.enum = Object.fromEntries(formalizedSchema.enum.map((value) => [value, value]));
}
if (typeof formalizedSchema.enum !== 'object' || formalizedSchema.enum === null) {
throw new SchemaError(
schema,
'Validator `enum` must be an array or object.'
);
}
const keys = Object.keys(formalizedSchema.enum);
if (keys.length < 1) {
throw new SchemaError(
schema,
'Validator `enum` must have at least one item.'
);
}
}
// Add priority if not added.
formalizedSchema.priority = formalizedSchema.priority || 10;
// Finalize objects and arrays if necessary.
if (formalizedSchema.type) {
if (isSameType('object', typeName(formalizedSchema.type))) {
formalizedSchema = formalizeObject(formalizedSchema, schema, options);
}
else if (isSameType('array', typeName(formalizedSchema.type))) {
formalizedSchema = formalizeArray(formalizedSchema, options);
}
}
return finalize(formalizedSchema, schema);
};
export default (schema, options = {}) => {
return formalizeAny(schema, Object.assign({}, options, {
plugins: allPlugins().concat((options.plugins || []).map((plugin) => plugin({ instanceTypeName, typeName, isSameType })))
}));
};
export function strip(schema) {
return Object.keys(schema)
.filter((key) => key !== '_nonFormalizedSchema')
.reduce((result, key) => {
result[key] = schema[key];
return result;
}, {});
}