'use strict';
const exceptions = require('../util/errors.js'),
helpers = require('../util/helpers.js');
/**
* The module that does validation of behavior configuration
* @module
*/
/**
* Creates the validator
* @returns {{validate: validate}}
*/
function create () {
function hasExactlyOneKey (obj) {
const keys = Object.keys(obj);
return keys.length === 1;
}
function navigate (config, path) {
if (path === '') {
return config;
}
else {
return path.split('.').reduce(function (field, fieldName) {
return field[fieldName];
}, config);
}
}
function typeErrorMessageFor (allowedTypes, additionalContext) {
const spellings = { number: 'a', object: 'an', string: 'a' };
let message = `must be ${spellings[allowedTypes[0]]} ${allowedTypes[0]}`;
for (let i = 1; i < allowedTypes.length; i += 1) {
message += ` or ${spellings[allowedTypes[i]]} ${allowedTypes[i]}`;
}
if (additionalContext) {
message += `, representing ${additionalContext}`;
}
return message;
}
function pathFor (pathPrefix, fieldName) {
if (pathPrefix === '') {
return fieldName;
}
else {
return `${pathPrefix}.${fieldName}`;
}
}
function nonMetadata (fieldName) {
return fieldName.indexOf('_') !== 0;
}
function isTopLevelSpec (spec) {
// True of copy and lookup behaviors that define the metadata below the top level keys
return helpers.isObject(spec)
&& Object.keys(spec).filter(nonMetadata).length === Object.keys(spec).length;
}
function enumFieldFor (field) {
const isObject = helpers.isObject;
// Can be the string value or the object key
if (isObject(field) && Object.keys(field).length > 0) {
return Object.keys(field)[0];
}
else {
return field;
}
}
function matchesEnum (field, enumSpec) {
return enumSpec.indexOf(enumFieldFor(field)) >= 0;
}
function addMissingFieldError (fieldSpec, path, addErrorFn) {
// eslint-disable-next-line no-underscore-dangle
if (fieldSpec._required) {
addErrorFn(path, 'required');
}
}
function addTypeErrors (fieldSpec, path, field, config, addErrorFn) {
/* eslint complexity: 0 */
const fieldType = typeof field,
allowedTypes = Object.keys(fieldSpec._allowedTypes), // eslint-disable-line no-underscore-dangle
typeSpec = fieldSpec._allowedTypes[fieldType]; // eslint-disable-line no-underscore-dangle
if (!helpers.defined(typeSpec)) {
addErrorFn(path, typeErrorMessageFor(allowedTypes, fieldSpec._additionalContext)); // eslint-disable-line no-underscore-dangle
}
else {
if (typeSpec.singleKeyOnly && !hasExactlyOneKey(field)) {
addErrorFn(path, 'must have exactly one key');
}
else if (typeSpec.enum && !matchesEnum(field, typeSpec.enum)) {
addErrorFn(path, `must be one of [${typeSpec.enum.join(', ')}]`);
}
else if (typeSpec.nonNegativeInteger && field < 0) {
addErrorFn(path, 'must be an integer greater than or equal to 0');
}
else if (typeSpec.positiveInteger && field <= 0) {
addErrorFn(path, 'must be an integer greater than 0');
}
addErrorsFor(config, path, fieldSpec, addErrorFn);
}
}
function addErrorsFor (config, pathPrefix, spec, addErrorFn) {
Object.keys(spec).filter(nonMetadata).forEach(fieldName => {
const fieldSpec = spec[fieldName],
path = pathFor(pathPrefix, fieldName),
field = navigate(config, path);
if (!helpers.defined(field)) {
addMissingFieldError(fieldSpec, path, addErrorFn);
}
else if (isTopLevelSpec(fieldSpec)) {
// Recurse but reset pathPrefix so error message is cleaner
// e.g. 'copy behavior "from" field required' instead of 'copy behavior "copy.from" field required'
addErrorsFor(field, '', fieldSpec, addErrorFn);
}
else {
addTypeErrors(fieldSpec, path, field, config, addErrorFn);
}
});
}
/**
* Validates the behavior configuration and returns all errors
* @memberOf module:models/behaviorsValidator#
* @param {Object} behaviors - The behaviors list
* @param {Object} validationSpec - the specification to validate against
* @returns {Object} The array of errors
*/
function validate (behaviors, validationSpec) {
const errors = [];
(behaviors || []).forEach(config => {
const validBehaviors = [],
unrecognizedKeys = [];
Object.keys(config).forEach(key => {
const addError = function (field, message, subConfig) {
errors.push(exceptions.ValidationError(`${key} behavior "${field}" field ${message}`,
{ source: subConfig || config }));
},
spec = {};
if (validationSpec[key]) {
validBehaviors.push(key);
spec[key] = validationSpec[key];
addErrorsFor(config, '', spec, addError);
}
else {
unrecognizedKeys.push({ key: key, source: config });
}
});
// Allow adding additional custom fields to valid behaviors but ensure there is a valid behavior
if (validBehaviors.length === 0 && unrecognizedKeys.length > 0) {
errors.push(exceptions.ValidationError(`Unrecognized behavior: "${unrecognizedKeys[0].key}"`,
{ source: unrecognizedKeys[0].source }));
}
if (validBehaviors.length > 1) {
errors.push(exceptions.ValidationError('Each behavior object must have only one behavior type',
{ source: config }));
}
});
return errors;
}
return {
validate
};
}
module.exports = {
create
};