"use strict"; const createError = require("create-error"); const assureArray = require("assure-array"); const defaultValue = require("default-value"); let ValidationError = createError("ValidationError", { path: [] }); 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") { 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 }); } } module.exports = { validateArguments: function (args, rules) { let errors = validateArgumentList(args, rules); aggregrateErrors(errors); }, validateValue: function (value, rules) { let errors = validateValue(value, assureArray(rules)); aggregrateErrors(errors); }, 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"); } }, 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) { 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}`); } }; }, 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: function (rules) { return function (value) { return validateObject(value, rules, true); }; } };