0
\$\begingroup\$

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).

\$\endgroup\$
2
  • \$\begingroup\$ Welcome to Code Review! Asking about code that you haven't written yet is specifically outside the scope of Code Review. Can you confirm that the requirements in your final paragraph are fully implemented? \$\endgroup\$ Sep 27, 2021 at 7:43
  • \$\begingroup\$ @TobySpeight Yes, it's implmented and even functional but I chose to leave out some parts which aren't relevant. My question concerns finding a better way to attach the getHash method in a type-safe manner while preventingg children classes from overriding it. \$\endgroup\$
    – Parlor311
    Sep 27, 2021 at 10:56

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.