parameter-validator
Version:
Parameter validator makes it easy to verify that an object contains required, valid parameters.
286 lines (241 loc) • 12.1 kB
JavaScript
/**
* Indicates that one or more parameter validation rules failed.
*
* @class
*/
export class ParameterValidationError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
this.message = message;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor.name);
}
}
}
/**
* Performs validation on parameters contained in an object.
* @class
*/
export default class ParameterValidator {
/**
* @param {object} [options]
* @param {function} [options.defaultValidation] - An optional alternate function to use as the default validation
* function instead of isDefined(). The function must accept a parameter
* value as an input and return a boolean indicating its validity.
*/
constructor(options) {
if (options) {
let {defaultValidation} = options;
if (!(defaultValidation === undefined || typeof defaultValidation === 'function')) {
throw new ParameterValidationError(`The optional defaultValidation parameter provided is not a function.`);
}
this._defaultValidation = options.defaultValidation;
}
}
/**
* @param {Object} paramsProvided - The names and values of provided parameters
* @param {Array} paramRequirements - Each item in this array is interpretted in order as a validation rule.
* - If an item is a string, it's interpretted as the name of a parameter that must be in paramsProvided.
* - If an item is an Array, it's interpretted as an array of parameter names where at least one of the
* parameters in the Array must be in paramsProvided.
* - If an item is an Object, it's assumed that the object's only key is the name of a parameter to be validated
* and its corresponding value is a function that returns true if that parameter's value in paramsProvided is
* valid.
* @param {Object|null} [extractedParams] - This method returns an object containing the names and values of the validated parameters extracted.
* By default, it creates a new object and assigns the extracted parameters to it, but if you want this
* method to add the extracted params to an existing object (such as the class instance that internally
* invokes this method), you can supply that object as the extractedParams parameter.
*
* @param {Object} [options] - Additional options
* @param {string} [options.addPrefix] - Specifies a prefix that will be added to each param name before it's assigned to the
* extractedParams object. This is useful, for example, for prefixing property names with an underscore
* to indicate that they're private properties.
* @param {class} [options.errorClass] - Specifies a specific `Error` subclass to throw instead of the default `ParameterValidationError
* when invalid parameters are detected.
* @returns {Object} extractedParams - The names and values of the validated parameters extracted.
*
* @throws {ParameterValidationError} Indicates that one or more parameter validation rules failed.
*
* @example
* let parameterValidator = new ParameterValidator();
* parameterValidator.validate(params, ['requiredParam0', 'requiredParam1', ['eitherNeedThis', 'orThat'], {param3: (val) => val > 30}]);
*/
validate(paramsProvided, paramRequirements, extractedParams, options = {}) {
extractedParams = this._getExtractedParamsObject(extractedParams);
const ValidationErrorSubclass = this._getValidationErrorSubclass(options);
if (!paramsProvided) {
// If only I could use the ParameterValidator here...
throw new ValidationErrorSubclass(`A params object is required.`);
}
if (!Array.isArray(paramRequirements)) {
throw new Error('paramRequirements must be an array.');
}
let prefix = options.addPrefix || ''; // Optional prefix to be added to each parameter name.
if (typeof prefix !== 'string') {
throw new Error('addPrefix option must be a string if provided.');
}
let errors = [];
for (let paramRequirement of paramRequirements) {
if (Array.isArray(paramRequirement) && paramRequirement.length) {
let validationResult = this._performLogicalOrParamValidation(paramsProvided, paramRequirement);
this._assignProperties(extractedParams, validationResult.params, prefix);
errors.push(...validationResult.errors);
} else if (typeof paramRequirement === 'object') {
// paramRequirement is an object with one or more keys where each key is a parameter's name
// and its value is a validation function that returns true if the value is valid.
for (let paramName in paramRequirement) {
let validationFunction = paramRequirement[paramName],
validationResult = this._executeValidationFunction(paramsProvided, paramName, validationFunction);
this._assignProperties(extractedParams, validationResult.params, prefix);
errors.push(...validationResult.errors);
}
} else if ((typeof paramRequirement === 'string') && paramRequirement) {
// paramRequirement is a string specifying the name of a required parameter,
// So use the default validation function for validation.
let validationResult = this._executeValidationFunction(paramsProvided, paramRequirement, this.defaultValidation);
this._assignProperties(extractedParams, validationResult.params, prefix);
errors.push(...validationResult.errors);
}
}
if (errors.length) {
let errorMessage = '';
for (let error of errors) {
errorMessage += error + ' ';
}
errorMessage = errorMessage.slice(0, -1);
throw new ValidationErrorSubclass(errorMessage);
}
return extractedParams;
}
/**
* Same as `validate()`, but wrapped in a promise. This is handy for use in methods that need to be
* async, because it guarantees that errors bubble up the promise chain as a rejected promise.
*
* @example
* let parameterValidator = new ParameterValidator();
* return parameterValidator.validateAsync(params, [ 'requiredParam0', 'requiredParam1', [ 'eitherNeedThis', 'orThat' ], { param3: (val) => val > 30 }])
* then(({ requiredParam0, requiredParam1, param3 }) => {
* // do stuff
* });
*/
validateAsync(...args) {
return Promise.resolve()
.then(() => this.validate(...args));
}
/**
* Returns isDefined() as the defaultValidation if a custom one was not provided.
*/
get defaultValidation() {
return this._defaultValidation || this.isDefined;
}
/**
* Like Object.assign(), but with the ability to add an optional prefix to the property names.
*
* @param {Object} targetObject
* @param {Object} propertiesToAdd
* @param {string} [prefix]
*/
_assignProperties(targetObject, propertiesToAdd, prefix = '') {
for (let propertyName in propertiesToAdd) {
targetObject[prefix + propertyName] = propertiesToAdd[propertyName];
}
}
/**
* Determines the correct object to use for extractedParams based on what the client provided.
*
* @param {Object|Function|null|undefined} - extractedParams argument provided to `validate()`
* @returns {Object|Function}
* @private
*/
_getExtractedParamsObject(extractedParams) {
if ([ null, undefined ].includes(extractedParams)) {
// The client either didn't provide an extractedParams argument or explicitly set
// it to null, so just use a new object.
return {};
}
if ([ 'object', 'function' ].includes(typeof extractedParams)) {
// The client provided an existing object or function on which we'll set the extracted params.
return extractedParams;
}
throw new Error(`Invalid value of '${extractedParams}' was provided for the extractedParams parameter.`);
}
/**
* Determines whether to use the default ParameterValidationError or a custom
* error subclass passed to `validate()`.
*
* @param {options} [options] - options that were passed to `validate()`
* @param {class} [options.errorClass]
* @returns {class}
* @private
*/
_getValidationErrorSubclass(options) {
if ((typeof options !== 'object') || (options.errorClass === undefined)) {
return ParameterValidationError;
}
let { errorClass } = options;
// Verify that errorClass is Error or an Error subclass.
if (errorClass === Error || errorClass.prototype instanceof Error) {
return errorClass;
}
throw new Error(`The errorClass provided was of type ${typeof errorClass} and was not an Error subclass.`);
}
/*
* @param {Object} paramsProvided - The names and values of provided parameters
* @param {Array} paramNames - Names of parameters, only one of which is required.
* @returns {Array} errors - Error message strings
* @returns {Object} params - Extracted parameter names & values.
*/
_performLogicalOrParamValidation(paramsProvided, paramNames) {
let extractedParams = {},
errors = [],
isValid = this.defaultValidation;
for (var paramName of paramNames) {
if (isValid(paramsProvided[paramName])) {
extractedParams[paramName] = paramsProvided[paramName];
}
}
if (!Object.keys(extractedParams).length) {
var errorMessage = 'One of the following parameters must be included: ';
for (paramName of paramNames) {
errorMessage += `'${paramName}', `;
}
errorMessage = errorMessage.slice(0, -2) + '.';
errors.push(errorMessage);
}
return {
errors: errors,
params: extractedParams
};
}
/*
* @param {Object} paramsProvided - The names and values of provided parameters
* @param {Object} paramRequirement - object with one key where the key is the parameter's name
* and the value is a validation function that returns true if the value is valid.
* @returns {Array} errors - Error message strings
* @returns {Object} params - Extracted parameter names & values
*/
_executeValidationFunction(paramsProvided, paramName, validationFunction) {
var errors = [];
var extractedParams = {};
if (typeof validationFunction !== 'function') {
throw new Error(`A paramRequirement value provided for the parameter ${paramName} is not a function.`);
}
if (validationFunction(paramsProvided[paramName]) === true) {
extractedParams[paramName] = paramsProvided[paramName];
} else {
errors.push(`Invalid value of '${paramsProvided[paramName]}' was provided for parameter '${paramName}'.`);
}
return {
errors: errors,
params: extractedParams
};
}
isDefined(value) {
return value !== undefined;
}
}
// Also export `validate()` and `validateAsync` as standalone functions by creating a singleton instance.
const parameterValidator = new ParameterValidator();
export const validate = parameterValidator.validate.bind(parameterValidator);
export const validateAsync = parameterValidator.validateAsync.bind(parameterValidator);