I'm looking to develop a validation library which can be extended by adding custom validation rules. To write a custom rule, simply write a class and apply the ValidationRule decorator like so:
@ValidationRule('array')
class IsArray {
constructor(a?: number, b?: number, c?: number) { }
evaluate(value: any) {
return Array.isArray(value);
}
getErrorMessage(path: string) {
return `${path} failed the 'IsArray' validation rule`;
}
getErrorMessageForRoot() {
return `The provided object failed the 'IsArray' validation rule`;
}
}
This is how I implemented the decorator:
type Constructor<T> = {
new(...args: any[]): T;
}
interface IValidationRule {
evaluate(value: any): boolean;
getErrorMessageForRoot(): string;
getErrorMessage(path: string): string;
}
function ValidationRule(name: string) {
return <T extends Constructor<IValidationRule>> (rule: T) => {
// Registering the validation rule
ValidationRuleRegistry.set(name, rule);
// Runtime error if the user defines a method called getHash on his class
if (rule.prototype.getHash) {
throw new Error("The method name 'getHash' is reserved");
}
return class extends rule {
hash: string;
constructor(...args: any) {
super(...args);
// I haven't developped the hashing algorithm yet
this.hash = (args.length === 0)
? `${rule.name}-${args}`
: rule.name
;
}
getHash() {
return this.hash;
}
};
}
}
Below is partially how the library is to be consumed (I only left the relevant parts):
function validate(data: any, path: string, rules: string[] | IValidationRule[]) {
const validationRules: IValidationRule[] = [];
for (let rule of rules) {
const ruleToAdd = (typeof(rule) == 'string') ? parseRule(rule) : rule;
if (!validationRules
.map(e => (e as any).getHash())
.includes((ruleToAdd as any).getHash())) {
validationRules.push(ruleToAdd);
}
}
// use the rules to validate the path on the data object
}
validate({ }, 'path', ['array:1,2,3']);
validate({ }, 'path', [new IsArray(1, 2, 3)]);
A class decorated with ValidationRule will be extended by adding the getHash() method which generates a hash based on the name of the validation rule (the 'name' parameter) and the argument list. This is to prevent duplicate validations but I feel this is a really bad way of doing it. Basically, I want the getHash method to be recognized by the typechecker when called inside the validate method (right now I have to cast rules into any), and to raise typecheck errors when the user attempts to define a method getHash or a property hash on his custom rule (instead of the runtime error I'm raising in the decorator).