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.

264 lines
6.7 KiB
JavaScript

"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);
};
}
};