Skip to content

Instantly share code, notes, and snippets.

@emkis
Last active November 22, 2022 23:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emkis/9ade42b8f4550280e41163904922e0a0 to your computer and use it in GitHub Desktop.
Save emkis/9ade42b8f4550280e41163904922e0a0 to your computer and use it in GitHub Desktop.
JavaScript composable validation
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')
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