Last active
November 22, 2022 23:04
-
-
Save emkis/9ade42b8f4550280e41163904922e0a0 to your computer and use it in GitHub Desktop.
JavaScript composable validation
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
import { composeValidation, v } from './validation' | |
const validateRequirement = composeValidation([ | |
v.min(10, 'cannot be empty'), | |
v.hasUpperCase('at least one uppercase letter'), | |
v.hasNumber('at least one number character'), | |
v.hasSpecialCharacter('at least one special character'), | |
v.max(64, 'max character limit (64) reached'), | |
]); | |
validateRequirement('123') |
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
const primitives = { | |
string(value) { | |
return typeof value === 'string'; | |
}, | |
min(value, minValue) { | |
return value?.length >= minValue; | |
}, | |
max(value, maxValue) { | |
return value?.length <= maxValue; | |
}, | |
regex(value, regex) { | |
return regex.test(value); | |
}, | |
email(value) { | |
// from https://stackoverflow.com/a/46181/1550155 | |
// this regex supports emails with unicode | |
const emailRegex = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; | |
return emailRegex.test(value); | |
}, | |
}; | |
const composed = { | |
hasUpperCase(value) { | |
return primitives.regex(value, /((?=.*[A-Z]))/); | |
}, | |
hasLowerCase(value) { | |
return primitives.regex(value, /((?=.*[a-z]))/); | |
}, | |
hasNumber(value) { | |
return primitives.regex(value, /((?=.*\d))/); | |
}, | |
hasSpecialCharacter(value) { | |
return primitives.regex(value, /((?=.*[_\W]))/); | |
}, | |
}; | |
/** | |
* All validation functions you can use with `composeValidation`. | |
*/ | |
export const v = { | |
string: validatorWithoutCriteria(primitives.string), | |
email: validatorWithoutCriteria(primitives.email), | |
min: validatorWithCriteria(primitives.min), | |
max: validatorWithCriteria(primitives.max), | |
regex: validatorWithCriteria(primitives.regex), | |
hasLowerCase: validatorWithoutCriteria(composed.hasLowerCase), | |
hasUpperCase: validatorWithoutCriteria(composed.hasUpperCase), | |
hasNumber: validatorWithoutCriteria(composed.hasNumber), | |
hasSpecialCharacter: validatorWithoutCriteria(composed.hasSpecialCharacter), | |
}; | |
/** | |
* Composes all `v` validator functions into one. | |
* @param validators - Validator functions from the `v` object. | |
* @example | |
* const validateEmail = composeValidation([ | |
* v.string('is not a string'), | |
* v.min(1, 'cannot be empty'), | |
* v.email('is not an email'), | |
* ]); | |
* | |
* validateEmail() // { success: false, error: ["is not a string", "cannot be empty", "is not an email"] } | |
* validateEmail('ddd') // { success: false, error: ["is not an email"] } | |
* validateEmail('d@d.com') // { success: true, error: null } | |
*/ | |
export function composeValidation(validators = []) { | |
return function validate(data) { | |
const results = validators.map((validate) => validate(data)); | |
const isResultsSuccess = results.every(({ success }) => success === true); | |
if (isResultsSuccess) { | |
return parseResult('success'); | |
} | |
const validErrors = results.reduce((errors, result) => { | |
if (!result.error) return errors; | |
return [...errors, result.error]; | |
}, []); | |
return parseResult('error', validErrors); | |
}; | |
} | |
function parseResult(type = 'success', errorValue) { | |
const success = { success: true, error: null }; | |
const error = { success: false, error: errorValue }; | |
return type === 'success' ? success : error; | |
} | |
/** | |
* Creates a lazy validator, for functions that depend on a criteria (second argument). | |
* @param validateFn - Validation function, it returns a boolean. | |
* | |
* @example | |
* const isWithinRange = (value, range = []) => range.includes(value) | |
* const isWithinRangeValidator = validatorWithCriteria(isWithinRange) | |
* const validadeIsWithinRange = isWithinRangeValidator([1, 2, 3], 'it will return this if error') | |
* | |
* validadeIsWithinRange(3) // returns { success: true, error: null } | |
* validadeIsWithinRange(4) // returns { success: false, error: 'it will return this if error' } | |
*/ | |
function validatorWithCriteria(validateFn) { | |
return (criteria, errorValue) => { | |
return (value) => { | |
return validateFn(value, criteria) | |
? parseResult('success') | |
: parseResult('error', errorValue); | |
}; | |
}; | |
} | |
/** | |
* Creates a lazy validator, for functions that just need to validate a value (just one argument). | |
* @param validateFn - Validation function, it returns a boolean. | |
* | |
* @example | |
* const hasGreeting = (value) => value.includes('hi') | |
* const hasGreetingValidator = validatorWithoutCriteria(hasGreeting) | |
* const validateGreeting = hasGreetingValidator(['this value it will return if error']) | |
* | |
* validateGreeting('hi') // returns { success: true, error: null } | |
* validateGreeting('hello') // returns { success: false, error: ['this value it will return if error'] } | |
*/ | |
function validatorWithoutCriteria(validateFn) { | |
return (errorValue) => { | |
return (value) => { | |
return validateFn(value) | |
? parseResult('success') | |
: parseResult('error', errorValue); | |
}; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment