@webf/rule
Version:
Business rule validation library
80 lines (60 loc) • 1.86 kB
text/typescript
import { RuleError } from './error.js';
export interface IRule<T> {
key: string;
apply(value: T): boolean | Promise<boolean>;
}
export type RuleClassFn<T> = new () => IRule<T>;
export type RuleType<T> = IRule<T> | RuleClassFn<T>;
export type Collector = {
/** Collects the errors against the failed rules and combine them into one while throwing. */
check: <T>(value: T, ...rules: Array<RuleType<T>>) => Promise<void>;
/** Throws if there are some errors */
rejectIfError: () => void;
};
export abstract class Rule {
key: string;
abstract apply(value: any): boolean | Promise<boolean>;
constructor() {
this.key = this.constructor.name;
}
}
/**
* Creates a rule validator function which throws if any of the validators fail.
*/
export async function test<T>(value: T, ...rules: Array<RuleType<T>>): Promise<void> {
const errors: Set<Error> = new Set();
for (const item of rules) {
// If item is constructor, create instance of validator.
// If item is already an instance, use it as is.
const rule = typeof item === 'function' ? new item() : item;
const isPass = await rule.apply(value);
if (!isPass) {
errors.add(new RuleError(rule.key));
}
}
if (errors.size > 0) {
throw new AggregateError(errors);
}
}
export function withCatch(): Collector {
const errors: Error[] = [];
const check = async <T>(value: T, ...rules: Array<RuleType<T>>) => {
try {
await test(value, ...rules);
} catch (err) {
if (err instanceof AggregateError) {
errors.push(...err.errors);
return;
}
// If it is not an aggregate error, then re-throw it.
throw err;
}
};
const rejectIfError = () => {
if (errors.length > 0) {
throw new AggregateError(errors);
}
};
return { check, rejectIfError };
}
export { RuleError };