Last active
July 1, 2020 00:28
-
-
Save aarongeorge/bbdd7ea97841817f90fb797d613c8285 to your computer and use it in GitHub Desktop.
A runtime property validator for JS objects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Property Validator | |
* | |
* @desc A runtime property validator for JS objects | |
* @usage: | |
* const model = new PropertyValidator({ | |
* 'propName': { | |
* 'type': 'any'|'array'|'boolean'|'custom'|'date'|'element'|'function'|'number'|'object'|'regExp'|'string' | |
* 'required': true|false, | |
* 'validateFn': a function that takes one param and returns true or false. Only works if `type` is `custom` | |
* }, | |
* ... | |
* }) | |
* model({ | |
* 'propName': propertyValue, | |
* ... | |
* }) | |
* @notes: | |
* - Any property passed to an instance of PropertyValidator that isn't defined in `model` will be removed from output | |
* - `validateFn` must only be passed if `type` is `custom` | |
*/ | |
const toTitleCase = str => str.charAt(0).toUpperCase() + str.slice(1) | |
const isArray = arr => Object.prototype.toString.call(arr) === '[object Array]' | |
const isBoolean = bool => typeof bool === 'boolean' | |
const isDate = date => Object.prototype.toString.call(date) === '[object Date]' | |
const isElement = el => el instanceof HTMLElement | |
const isFunction = fn => typeof fn === 'function' | |
const isNumber = num => typeof num === 'number' | |
const isObject = obj => Object.prototype.toString.call(obj) === '[object Object]' | |
const isRegExp = regexp => Object.prototype.toString.call(regexp) === '[object RegExp]' | |
const isString = str => typeof str === 'string' | |
const isUndefined = prop => typeof prop === 'undefined' | |
const isWindow = obj => obj === window | |
const typeValues = ['any', 'array', 'boolean', 'custom', 'date', 'element', 'function', 'number', 'object', 'regExp', 'string'] | |
const requiredValues = [!!0, !!1] | |
class PropertyValidator { | |
constructor (model) { | |
if (isUndefined(model)) throw new Error('`model` was not passed') | |
if (!isObject(model)) throw new Error('`model` was not an object') | |
Object.entries(model).forEach(([propName, propOpts]) => { | |
if (isUndefined(propOpts.type)) throw new Error(`\`type\` must be passed for property \`${propName}\`.`) | |
else if (!typeValues.includes(propOpts.type)) throw new Error(`\`type\` of \`${propOpts.type}\` is not valid. Please use one of the following: ${typeValues.join(', ')}.`) | |
if (isUndefined(propOpts.required)) throw new Error(`\`required\` must be passed for property \`${propName}\`.`) | |
else if (!requiredValues.includes(propOpts.required)) throw new Error(`\`required\` of \`${propOpts.required}\` is not valid. Please use one of the following: ${requiredValues.join(', ')}.`) | |
if (propOpts.type === 'custom') { | |
if (isUndefined(propOpts.validateFn)) throw new Error(`\`type\` of \`${propOpts.type}\` requires that \`validateFn\` is passed`) | |
if (!isFunction(propOpts.validateFn)) throw new Error(`\`validateFn\` of type \`${typeof propOpts.validateFn}\` is not a function.`) | |
} | |
else if (!isUndefined(propOpts.validateFn)) throw new Error(`\`validateFn\` should only be passed if \`type\` is \`custom\`. \`type\` is currently ${propOpts.type}.`) | |
}) | |
return props => this.validateProperties(props, model) | |
} | |
validateProperties (props, model) { | |
if (!isObject(props)) throw new Error('`props` was not an object') | |
const validatedProperties = {} | |
Object.entries(model).forEach(([propName, propOpts]) => { | |
if (propOpts.required && isUndefined(props[propName])) throw new Error(`\`${propName}\` is required.`) | |
if (!propOpts.required && isUndefined(props[propName])) return | |
switch (propOpts.type) { | |
case 'custom': { | |
const err = propOpts.validateFn(props[propName], props) | |
if (err) throw new Error(err) | |
else validatedProperties[propName] = props[propName] | |
break | |
} | |
case 'any': { | |
if (!isUndefined(props[propName])) validatedProperties[propName] = props[propName] | |
break | |
} | |
default: { | |
if (!isFunction(eval(`is${toTitleCase(propOpts.type)}`))) throw new Error(`Validation for \`is${toTitleCase(propertyRulesObject.type)}\` does not exist natively. Please check your spelling or create a custom validation function for this property.`) | |
else { | |
if (!eval(`is${toTitleCase(propOpts.type)}`)(props[propName])) throw new Error(`Property \`${propName}\` of value \`${props[propName]}\` is not of type \`${propOpts.type}\`.`) | |
validatedProperties[propName] = props[propName] | |
} | |
} | |
} | |
}) | |
return validatedProperties | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment