From 06362c6fa33d03e1de07d3afe2e92c83a9b9f1da Mon Sep 17 00:00:00 2001 From: Vlad Rindevich Date: Mon, 5 Aug 2024 13:35:21 +0300 Subject: [PATCH] refactor(model): use classes for Validators --- packages/ts/models/src/validators.ts | 288 ++++++++++++++++----------- 1 file changed, 172 insertions(+), 116 deletions(-) diff --git a/packages/ts/models/src/validators.ts b/packages/ts/models/src/validators.ts index 6cfa652642..e091798f1d 100644 --- a/packages/ts/models/src/validators.ts +++ b/packages/ts/models/src/validators.ts @@ -1,5 +1,3 @@ -const { create, entries, getPrototypeOf, getOwnPropertyDescriptors } = Object; - export class ValidationError extends Error { readonly value: unknown; @@ -24,127 +22,185 @@ export type ValidatableHTMLElement = HTMLElement & | 'validity' >; -export interface Validator { - name: string; - super: Validator; - error(value: unknown): ValidationError; - validate(value: unknown, state?: ValidityState): boolean; - bind(element: ValidatableHTMLElement): void; -} +export class Validator { + readonly name: string = 'Validator'; + + // eslint-disable-next-line @typescript-eslint/class-methods-use-this + bind(_element: ValidatableHTMLElement): void { + // do nothing + } + + error(value: unknown, message = 'Invalid value'): ValidationError { + return new ValidationError(this.name, value, message); + } -export type ValidatorProps = Partial & { error: string }>; - -export function createValidator(name: string, base: Validator, props: ValidatorProps): Validator { - return create( - base, - entries(getOwnPropertyDescriptors(props)).reduce>( - (acc, [key, { value, get, set }]) => { - acc[key] = - key === 'error' ? { value: (v: unknown) => new ValidationError(name, v, value) } : { value, get, set }; - return acc; - }, - { name: { value: name } }, - ), - ); + // eslint-disable-next-line @typescript-eslint/class-methods-use-this + validate(_value: unknown, _state: ValidityState | undefined): boolean { + return true; + } } -export const Validator = create(null, { - name: { - value: 'Validator', - }, - bind: { - value: () => {}, - }, - error: { - value(this: Validator) { - return new ValidationError(this.name, undefined, 'Invalid value'); - }, - }, - validate: { - value: () => true, - }, - super: { - get(this: Validator) { - return getPrototypeOf(this); - }, - }, - [Symbol.hasInstance]: { - value(this: Validator, o: unknown) { - return typeof o === 'object' && o != null && (this === o || Object.prototype.isPrototypeOf.call(this, o)); - }, - }, -}); - -export const Required = createValidator('Required', Validator, { - bind(element) { +export class Required extends Validator { + override readonly name: string = 'Required'; + + override bind(element: ValidatableHTMLElement): void { element.required = true; - }, - validate: (value, state) => !state?.valueMissing && value != null, -}); - -export function Pattern(pattern: RegExp | string): Validator { - const _pattern = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'u'); - - return createValidator('Pattern', Validator, { - bind(element) { - element.pattern = _pattern.source; - }, - validate: (value, state) => !state?.patternMismatch && _pattern.test(String(value)), - }); + } + + override validate(value: unknown, state: ValidityState | undefined): boolean { + return !state?.valueMissing && value != null; + } + + override error(value: unknown, message = 'Must present'): ValidationError { + return super.error(value, message); + } +} + +export class Pattern extends Validator { + override readonly name: string = 'Pattern'; + + readonly #pattern: RegExp; + + constructor(pattern: RegExp | string) { + super(); + this.#pattern = typeof pattern === 'string' ? new RegExp(pattern, 'u') : pattern; + } + + override bind(element: ValidatableHTMLElement): void { + element.pattern = this.#pattern.source; + } + + override validate(value: unknown, state: ValidityState | undefined): boolean { + return !state?.patternMismatch && this.#pattern.test(String(value)); + } + + override error(value: unknown, message = `Must comply the pattern ${this.#pattern}`): ValidationError { + return super.error(value, message); + } } -export const IsNumber = createValidator('IsNumber', Pattern(/^[0-9]*$/u), { - bind(element) { +export class IsNumber extends Pattern { + override readonly name: string = 'IsNumber'; + + constructor() { + super(/^[0-9.,]*$/u); + } + + override bind(element: ValidatableHTMLElement): void { + super.bind(element); element.type = 'number'; - }, - validate(this: Validator, value, state) { - return !state?.typeMismatch && this.super.validate(value) && isFinite(Number(value)); - }, -}); - -export const Email = createValidator('Email', Pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/u), { - error: 'Must be a well-formed email address', -}); - -export const Null = createValidator('Null', Validator, { - validate: (value) => value == null, - error: 'Must be null', -}); - -export const NotNull = createValidator('NotNull', Required, { - error: 'Must not be null', -}); - -export const NotEmpty = createValidator('NotEmpty', Required, { - validate(this: Validator, value) { - return this.super.validate(value) && (typeof value === 'string' || Array.isArray(value)) && value.length > 0; - }, - error: 'Must not be empty', -}); - -export const NotBlank = createValidator('NotBlank', Required, { - validate(this: Validator, value) { - return this.super.validate(value) && typeof value === 'string' && /\S/u.test(value); - }, - error: 'Must not be blank', -}); - -export function Min(min: number): Validator { - return createValidator('Min', IsNumber, { - validate: (value, state) => !state?.rangeUnderflow && IsNumber.validate(value) && Number(value) >= min, - bind(element) { - element.min = String(min); - }, - error: `Must be greater than or equal to ${min}`, - }); + } + + override validate(value: unknown, state: ValidityState | undefined): boolean { + return super.validate(value, state) && isFinite(Number(value)); + } + + override error(value: unknown, message = 'Must be a number'): ValidationError { + return super.error(value, message); + } +} + +export class Email extends Pattern { + override readonly name: string = 'Email'; + + constructor() { + super(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/u); + } + + override bind(element: ValidatableHTMLElement): void { + super.bind(element); + element.type = 'email'; + } + + override error(value: unknown): ValidationError { + return super.error(value, 'Must be a well-formed email address'); + } } -export function Max(max: number): Validator { - return createValidator('Max', IsNumber, { - validate: (value, state) => !state?.rangeOverflow && IsNumber.validate(value) && Number(value) <= max, - bind(element) { - element.max = String(max); - }, - error: `Must be less than or equal to ${max}`, - }); +export class Null extends Validator { + override readonly name: string = 'Null'; + + override validate(value: unknown, _: ValidityState | undefined): boolean { + return value == null; + } + + override error(value: unknown, message = 'Must be null'): ValidationError { + return super.error(value, message); + } +} + +export class NotNull extends Required { + override readonly name: string = 'NotNull'; + + override error(value: unknown, message = 'Must not be null'): ValidationError { + return super.error(value, message); + } +} + +export class NotEmpty extends Required { + override readonly name: string = 'NotEmpty'; + + override validate(value: unknown, state: ValidityState | undefined): boolean { + return super.validate(value, state) && (typeof value === 'string' || Array.isArray(value)) && value.length > 0; + } + + override error(value: unknown, message = 'Must not be empty'): ValidationError { + return super.error(value, message); + } +} + +export class NotBlank extends Required { + override readonly name: string = 'NotBlank'; + + override validate(value: unknown, state: ValidityState | undefined): boolean { + return super.validate(value, state) && typeof value === 'string' && /\S/u.test(value); + } + + override error(value: unknown, message = 'Must not be blank'): ValidationError { + return super.error(value, message); + } +} + +export class Min extends IsNumber { + readonly #min: number; + + constructor(min: number) { + super(); + this.#min = min; + } + + override bind(element: ValidatableHTMLElement): void { + super.bind(element); + element.min = String(this.#min); + } + + override validate(value: unknown, state: ValidityState | undefined): boolean { + return !state?.rangeUnderflow && super.validate(value, state) && Number(value) >= this.#min; + } + + override error(value: unknown, message = `Must be greater than or equal to ${this.#min}`): ValidationError { + return super.error(value, message); + } +} + +export class Max extends IsNumber { + readonly #max: number; + + constructor(max: number) { + super(); + this.#max = max; + } + + override bind(element: ValidatableHTMLElement): void { + super.bind(element); + element.max = String(this.#max); + } + + override validate(value: unknown, state: ValidityState | undefined): boolean { + return !state?.rangeOverflow && super.validate(value, state) && Number(value) <= this.#max; + } + + override error(value: unknown, message = `Must be less than or equal to ${this.#max}`): ValidationError { + return super.error(value, message); + } }