Created
August 4, 2025 15:46
-
-
Save b2whats/d7d11a9f76abde43d419dda271cb22d3 to your computer and use it in GitHub Desktop.
Revisions
-
b2whats created this gist
Aug 4, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,472 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ type FormMetadata = { fields: FieldsRules options?: { errorFieldName: string | symbol } & Required<ErrorField> touchedFields?: WeakMap<any, Set<string | symbol>> } type FieldsRules = Map<string | symbol, FieldsRule> type FieldsRule = { name: string | symbol value: any clonable: boolean size?: number validator: Validator } type ErrorField = { multiple?: boolean format?: 'object' | 'array' fieldsObserver?: (instance: any, callback: (key: string | symbol, value: any) => void) => void } const unknownValue = Symbol('unknownValue') // @ts-ignore Symbol.metadata ??= Symbol('Symbol.metadata') function Field<This, Value>(rules: ((v: Validator<Value, This>) => Validator) | ErrorField) { return (_: undefined, context: ClassFieldDecoratorContext<This, Value>) => { const { kind, name, metadata } = context if (kind !== 'field') throw new Error('Only fields can be decorated') const fields = (metadata.fields ??= new Map()) as FieldsRules const touchedFields = (metadata.touchedFields ??= new WeakMap()) as WeakMap<any, Set<string | symbol>> if (typeof rules === 'object') { const options = (metadata.options = { errorFieldName: name, multiple: rules.multiple ?? false, format: rules.format ?? 'object', }) if (rules.fieldsObserver !== undefined) { context.addInitializer(function (this: any) { const { multiple, format } = options const onChangeFields = new Map<string | symbol, Validator>() for (const [key, { validator }] of fields) { if (validator.when === 'change') { onChangeFields.set(key, validator) } for (const fieldName of validator.dependent) { fields.get(fieldName)?.validator?.dependencies.add(key) } } if (onChangeFields.size === 0) return const touched = new Set<string | symbol>() touchedFields.set(this, touched) const setError = (key: string | symbol, error: FieldError | undefined) => { if (format === 'object') { this[name][key] = error && errorsToObject([error], multiple)[key] } else { this[name] = this[name].filter((error: FieldError) => error.property !== key) if (error) this[name].push(error) } } rules.fieldsObserver?.(this, (key, value) => { touched.add(key) const validator = onChangeFields.get(key) if (validator === undefined) return setError(key, validator.validate(value, this)) for (const key of validator.dependencies) { const value = this[key] const validator = fields.get(key)?.validator if (validator === undefined || touched.has(key) === false) continue setError(key, validator.validate(value, this)) } }) }) } return } const descriptor: FieldsRule = { name, value: unknownValue, clonable: true, validator: rules(new Validator(name)), } fields.set(name, descriptor) return function (value: any) { if (descriptor.value !== unknownValue) return value descriptor.size = value instanceof Map || value instanceof Set ? value.size : Array.isArray(value) ? value.length : undefined try { const clone = structuredClone(value) descriptor.value = clone descriptor.clonable = true } catch { descriptor.value = value descriptor.clonable = false } return value } } } type ValidateInstance = { [key: string | symbol]: any } type FormObjectErrors = Record<string | symbol, any> type FormErrors = FieldError[] function validate(instance: ValidateInstance): FormErrors | undefined { const metadata = instance?.constructor?.[Symbol.metadata] as FormMetadata | undefined const fields = metadata?.fields if (fields === undefined) return undefined let errors: FormErrors | undefined for (const [key, { validator }] of fields) { const value = instance[key] let error = validator.validate(value, instance) if (error !== undefined && validator.isNested === false) (errors ??= []).push(error) if (validator.isNested === false) continue if (error === undefined) error = { property: key, children: [] } if (value instanceof Set || Array.isArray(value)) { Array.from(value).forEach((item, index) => { const nestedError = validate(item) if (nestedError !== undefined) { ;(error.children ??= []).push({ property: index, children: nestedError, }) } }) } if (value instanceof Map) { Array.from(value.entries()).forEach(([key, value]) => { const nestedError = validate(value) if (nestedError !== undefined) { ;(error.children ??= []).push({ property: key, children: nestedError, }) } }) } if (value instanceof Object) { const nestedError = validate(value) if (nestedError !== undefined) { error.children = nestedError } } if (error.children?.length !== 0) (errors ??= []).push(error) } if (metadata?.options !== undefined) { const { errorFieldName, multiple, format } = metadata.options instance[errorFieldName] = format === 'object' ? errorsToObject(errors, multiple) : errors } return errors } function reset(instance: ValidateInstance): void { const metadata = instance?.constructor?.[Symbol.metadata] as FormMetadata | undefined const fields = metadata?.fields const options = metadata?.options const touchedFields = metadata?.touchedFields?.get(instance) if (fields === undefined) return for (const [key, { value, clonable, size }] of fields) { instance[key] = value if (clonable) continue if (Array.isArray(instance[key])) { instance[key] = instance[key].slice(0, size) for (const item of instance[key]) reset(item) } else if (instance[key] instanceof Set) { instance[key] = new Set(Array.from(instance[key].values()).slice(0, size)) for (const item of instance[key]) reset(item) } else if (instance[key] instanceof Map) { const keysToRemove = Array.from(instance[key].keys()).slice(size) for (const key of keysToRemove) instance[key].delete(key) for (const [_, value] of instance[key]) reset(value) } else { reset(instance[key]) } } if (options?.errorFieldName) instance[options.errorFieldName] = options.format === 'object' ? {} : [] touchedFields?.clear() } function errorsToObject(errors: FormErrors | undefined, multiple: boolean): FormObjectErrors { if (errors === undefined) return {} const result: FormObjectErrors = {} const isArray = typeof errors[0]?.property === 'number' if (isArray) { const arrayResult: any[] = [] for (const error of errors) { const index = error.property as number if (Array.isArray(error.children)) { arrayResult[index] = errorsToObject(error.children, multiple) } } return arrayResult } for (const error of errors) { if (Array.isArray(error.children)) { result[error.property] = errorsToObject(error.children, multiple) } else if (error.errors) { result[error.property] = multiple ? error.errors : error.errors[0] } } return result } type FieldError = { property: string | symbol | number errors?: string[] children?: FieldError[] } type Check<T = any> = (value: any, fields: T) => string | undefined class Validator<Value = any, Fields = any> { private property: string | symbol private checks: Check[] = [] private isRequired = true private isCondition = false public when: 'change' | 'submit' = 'submit' public dependencies: Set<string | symbol> = new Set() public dependent: Set<string | symbol> = new Set() public isNested = false constructor(property: string | symbol) { this.property = property } optional(): this { this.isRequired = false return this } trigger(when: 'change' | 'submit', deps?: string[]): this { this.when = when if (deps !== undefined) { for (const dep of deps) { this.dependencies.add(dep) } } return this } nested(): this { this.isNested = true return this } string(message = 'Value must be a string'): this { this.checks.push((value) => (typeof value === 'string' ? undefined : message)) return this } number(message = 'Value must be a number'): this { this.checks.push((value) => (typeof value === 'number' ? undefined : message)) return this } array(message = 'Value must be an array'): this { this.checks.push((value) => (Array.isArray(value) ? undefined : message)) return this } object(message = 'Value must be an object'): this { this.checks.push((value) => (typeof value === 'object' && value !== null ? undefined : message)) return this } moreThan(limit: number, message?: string): this { this.checks.push((value) => { if (typeof value === 'number') { return value > limit ? undefined : (message ?? `Value must be greater than ${limit}`) } return undefined }) return this } lessThan(limit: number, message?: string): this { this.checks.push((value) => { if (typeof value === 'number') { return value < limit ? undefined : (message ?? `Value must be less than ${limit}`) } return undefined }) return this } lessOrEqualThanField(field: keyof Fields, message?: string): this { this.dependent.add(field as string | symbol) this.checks.push((value, fields) => { const fieldValue = fields[field] if (typeof value === 'number' && typeof fieldValue === 'number') { return value <= fieldValue ? undefined : (message ?? `Value must be less or equal than ${fieldValue}`) } return undefined }) return this } min(limit: number, message?: string): this { this.checks.push((value) => { if (typeof value === 'number') { return value >= limit ? undefined : (message ?? `Value must be at least ${limit}`) } if (typeof value === 'string' || Array.isArray(value)) { return value.length >= limit ? undefined : (message ?? `Length must be at least ${limit}`) } if (value instanceof Set) { return value.size >= limit ? undefined : (message ?? `Size must be at least ${limit}`) } return undefined }) return this } max(limit: number, message?: string): this { this.checks.push((value) => { if (typeof value === 'number') { return value <= limit ? undefined : (message ?? `Value must be at most ${limit}`) } if (typeof value === 'string' || Array.isArray(value)) { return value.length <= limit ? undefined : (message ?? `Length must be at most ${limit}`) } if (value instanceof Set) { return value.size <= limit ? undefined : (message ?? `Size must be at most ${limit}`) } return undefined }) return this } oneOf(values: any[], message?: string): this { this.checks.push((value) => { if (values.includes(value)) return undefined return message ?? `Value must be one of ${values.join(', ')}` }) return this } if( test: (fields: Fields, value: Value) => boolean, consequent: (v: Validator<Value, Fields>) => void, alternate?: (v: Validator<Value, Fields>) => void, ): this { this.isCondition = true const originalChecks = this.checks const originalNested = this.isNested const originalRequired = this.isRequired this.checks = [] consequent(this) const thenChecks = this.checks const thenNested = this.isNested const thenRequired = this.isRequired let elseChecks: Check[] = [] let elseRequired = (this.isRequired = originalRequired) let elseNested = (this.isNested = originalNested) if (alternate !== undefined) { this.checks = [] alternate(this) elseChecks = this.checks elseRequired = this.isRequired elseNested = this.isNested } this.checks = originalChecks this.isRequired = originalRequired this.isNested = originalNested this.checks.push((value, fields) => { let checks: Check[] = [] let required = originalRequired this.isNested = originalNested if (test(fields, value)) { checks = thenChecks required = thenRequired this.isNested = thenNested } else { checks = elseChecks required = elseRequired this.isNested = elseNested } if ([null, undefined].includes(value) && checks.length > 0) { if (required) return 'Required' return undefined } for (const check of checks) { const error = check(value, fields) if (error !== undefined) return error } return undefined }) return this } custom(fn: (value: Value, fields: Fields) => string | undefined): this { this.checks.push(fn) return this } validate(value: any, fields: Fields): FieldError | undefined { if ([null, undefined].includes(value) && this.isCondition === false) { if (this.isRequired) return { property: this.property, errors: ['Required'] } return undefined } const fieldError: FieldError = { property: this.property, } for (const check of this.checks) { const error = check(value, fields) if (error === undefined) continue ;(fieldError.errors ??= []).push(error) } return fieldError.errors && fieldError } } export { Field, validate, reset }