You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
7.8 KiB
JavaScript
303 lines
7.8 KiB
JavaScript
"use strict";
|
|
|
|
const createError = require("create-error");
|
|
const assureArray = require("assure-array");
|
|
const defaultValue = require("default-value");
|
|
const util = require("util");
|
|
|
|
let ValidationError = createError("ValidationError", { path: [] });
|
|
|
|
function allowExtraProperties (rules) {
|
|
return function (value) {
|
|
return validateObject(value, rules, true);
|
|
};
|
|
}
|
|
|
|
function validateValue(value, argRules) {
|
|
let argRules_ = assureArray(argRules);
|
|
let isRequired = argRules_.some((rule) => rule.___validationMarker === "required");
|
|
|
|
if (isRequired && value == null) {
|
|
return [ new ValidationError(`Required value is missing`) ];
|
|
} else if (value != null) {
|
|
let actualRules = argRules_.filter((rule) => {
|
|
return (rule.___validationMarker == null);
|
|
});
|
|
|
|
let errors = [];
|
|
|
|
for (let rule of actualRules) {
|
|
if (typeof rule === "object") {
|
|
/* FIXME: Check that this isn't an array, Date, Buffer, ... */
|
|
errors = validateObject(value, rule);
|
|
} else {
|
|
try {
|
|
errors = defaultValue(rule(value), []);
|
|
} catch (error) {
|
|
if (error instanceof ValidationError) {
|
|
errors = [ error ];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
}
|
|
|
|
function validateObject(object, rules, allowExtra = false) {
|
|
let errors = [];
|
|
|
|
if (!allowExtra) {
|
|
errors = Object.keys(object).map((propertyName) => {
|
|
if (rules[propertyName] == null) {
|
|
return new ValidationError(`Encountered an unexpected property '${propertyName}'`);
|
|
} else {
|
|
return null;
|
|
}
|
|
}).filter((error) => {
|
|
return (error != null);
|
|
});
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
return errors;
|
|
} else {
|
|
return Object.keys(rules).map((key) => {
|
|
let errors = validateValue(object[key], rules[key]);
|
|
|
|
return annotateErrors(key, object[key], assureArray(errors));
|
|
}).reduce((allErrors, errors) => {
|
|
return allErrors.concat(errors);
|
|
}, []);
|
|
}
|
|
}
|
|
|
|
function validateArgumentList(args, rules) {
|
|
if (args.length > rules.length) {
|
|
return [ new ValidationError(`Got ${args.length} arguments, but only expected ${rules.length}`) ];
|
|
} else {
|
|
return rules.map((item, i) => {
|
|
let arg = args[i];
|
|
let argName = item[0];
|
|
let argRules = item.slice(1);
|
|
|
|
if (typeof argName !== "string") {
|
|
throw new Error("First item in the argument rules list must be the argument name");
|
|
} else {
|
|
let errors = validateValue(arg, argRules);
|
|
|
|
return annotateErrors(argName, arg, assureArray(errors));
|
|
}
|
|
}).reduce((allErrors, errors) => {
|
|
return allErrors.concat(errors);
|
|
}, []);
|
|
}
|
|
}
|
|
|
|
function annotateErrors(pathSegment, value, errors) {
|
|
return errors.map((error) => {
|
|
error.path = assureArray(pathSegment).concat(error.path);
|
|
|
|
if (error.value == null) {
|
|
error.value = value;
|
|
}
|
|
|
|
return error;
|
|
});
|
|
}
|
|
|
|
function aggregrateErrors(errors) {
|
|
let rephrasedErrors = errors.map((error) => {
|
|
/* TODO: Make immutable */
|
|
let path = (error.path.length > 0)
|
|
? error.path.join(" -> ")
|
|
: "(root)";
|
|
|
|
error.message = `At ${path}: ${error.message}`;
|
|
return error;
|
|
});
|
|
|
|
let detailLines = rephrasedErrors.map((error) => {
|
|
return ` - ${error.message}`;
|
|
}).join("\n");
|
|
|
|
if (errors.length > 0) {
|
|
throw new ValidationError(`One or more validation errors occurred:\n${detailLines}`, {
|
|
errors: rephrasedErrors
|
|
});
|
|
}
|
|
}
|
|
|
|
function validateArguments(args, rules) {
|
|
let errors = validateArgumentList(args, rules);
|
|
|
|
aggregrateErrors(errors);
|
|
}
|
|
|
|
module.exports = {
|
|
validateArguments: validateArguments,
|
|
validateValue: function (value, rules) {
|
|
let errors = validateValue(value, assureArray(rules));
|
|
|
|
aggregrateErrors(errors);
|
|
},
|
|
validateOptions: function (args, optionsRules) {
|
|
return validateArguments(args, [
|
|
["options", optionsRules]
|
|
]);
|
|
},
|
|
ValidationError: ValidationError,
|
|
required: {___validationMarker: "required"},
|
|
isFunction: function (value) {
|
|
if (typeof value !== "function") {
|
|
throw new ValidationError("Must be a function");
|
|
}
|
|
},
|
|
isString: function (value) {
|
|
if (typeof value !== "string") {
|
|
throw new ValidationError("Must be a string");
|
|
}
|
|
},
|
|
isNumber: function (value) {
|
|
if (typeof value !== "number") {
|
|
throw new ValidationError("Must be a number");
|
|
}
|
|
},
|
|
isBoolean: function (value) {
|
|
if (typeof value !== "boolean") {
|
|
throw new ValidationError("Must be a boolean");
|
|
}
|
|
},
|
|
isDate: function (value) {
|
|
if (!(value instanceof Date)) {
|
|
throw new ValidationError("Must be a Date object");
|
|
}
|
|
},
|
|
isBuffer: function (value) {
|
|
if (!(value instanceof Buffer)) {
|
|
throw new ValidationError("Must be a Buffer object");
|
|
}
|
|
},
|
|
either: function (...alternatives) {
|
|
if (alternatives.length === 0) {
|
|
throw new Error("Must specify at least one alternative");
|
|
} else {
|
|
return function (value) {
|
|
let errors = [];
|
|
|
|
for (let alternative of alternatives) {
|
|
let result = validateValue(value, alternative);
|
|
|
|
if (result.length === 0) {
|
|
return;
|
|
} else {
|
|
errors = errors.concat(result);
|
|
}
|
|
}
|
|
|
|
let errorList = errors.map((error) => {
|
|
return `"${error.message}"`;
|
|
}).join(", ");
|
|
|
|
return new ValidationError(`Must satisfy at least one of: ${errorList}`, { errors: errors });
|
|
};
|
|
}
|
|
},
|
|
when: function (predicate, rules) {
|
|
if (rules == null) {
|
|
throw new Error("No rules specified for a `when` validation clause; did you misplace a parenthese?");
|
|
} else {
|
|
let rules_ = assureArray(rules).map((rule) => {
|
|
if (typeof rule === "object") {
|
|
/* We automatically allow extraneous properties in a `when` clause, because it'll generally be used as a partial addition to an otherwise-fully-specified object structure. */
|
|
return allowExtraProperties(rule);
|
|
} else {
|
|
return rule;
|
|
}
|
|
});
|
|
|
|
return function (value) {
|
|
let matches = predicate(value);
|
|
|
|
if (matches) {
|
|
return validateValue(value, rules_);
|
|
} else {
|
|
return [];
|
|
}
|
|
};
|
|
}
|
|
},
|
|
matchesFormat: function (regex) {
|
|
return function (value) {
|
|
if (!regex.test(value)) {
|
|
throw new ValidationError(`Must match format: ${regex}`);
|
|
}
|
|
};
|
|
},
|
|
oneOf: function (validValues) {
|
|
if (Array.isArray(validValues)) {
|
|
let validValueSet = new Set(validValues);
|
|
|
|
return function (value) {
|
|
if (!validValueSet.has(value)) {
|
|
throw new ValidationError(`Must be one of: ${validValues.map((item) => util.inspect(item)).join(", ")}`);
|
|
}
|
|
}
|
|
} else {
|
|
throw new Error("Argument to `oneOf` must be an array of values");
|
|
}
|
|
},
|
|
arrayOf: function (rules) {
|
|
let rules_ = assureArray(rules);
|
|
|
|
return function (value) {
|
|
if (!Array.isArray(value)) {
|
|
throw new ValidationError("Must be an array");
|
|
} else {
|
|
return value.map((item, i) => {
|
|
let errors = validateValue(item, rules_);
|
|
|
|
return annotateErrors(i, item, assureArray(errors));
|
|
}).reduce((allErrors, errors) => {
|
|
return allErrors.concat(errors);
|
|
}, []);
|
|
}
|
|
};
|
|
},
|
|
anyProperty: function (rules) {
|
|
let keyRules = assureArray(defaultValue(rules.key, []));
|
|
let valueRules = assureArray(defaultValue(rules.value, []));
|
|
|
|
return function (object) {
|
|
if (typeof object !== "object" || Array.isArray(object)) {
|
|
throw new ValidationError("Must be an object");
|
|
} else {
|
|
return Object.keys(object).map((key) => {
|
|
let value = object[key];
|
|
|
|
let keyErrors = validateValue(key, keyRules);
|
|
let valueErrors = validateValue(value, valueRules);
|
|
|
|
let annotatedKeyErrors = annotateErrors([key, "(key)"], key, assureArray(keyErrors));
|
|
let annotatedValueErrors = annotateErrors([key, "(value)"], value, assureArray(valueErrors));
|
|
|
|
return annotatedKeyErrors.concat(annotatedValueErrors);
|
|
}).reduce((allErrors, errors) => {
|
|
return allErrors.concat(errors);
|
|
}, []);
|
|
}
|
|
};
|
|
},
|
|
allowExtraProperties: allowExtraProperties,
|
|
forbidden: function (value) {
|
|
if (value != null) {
|
|
throw new ValidationError("Value exists in a place that should be empty");
|
|
}
|
|
}
|
|
};
|