forked from jashkenas/backbone
-
Notifications
You must be signed in to change notification settings - Fork 2
Open
Description
withValidation and withComputed has types that does not work declaring descendants
This is the sonnet proposed code for a class mixin:
Prompt
You are a coding assistant
The language is typescript
Create a withValidation class mixin that extends a Model class adding support to a static property validation
// Base Model class
class Model {
constructor(public data: Record<string, any> = {}) {}
toJSON() {
return { ...this.data };
}
}
// Type definitions for validation
type ValidationRule = {
validator: (value: any) => boolean;
message: string;
};
type ValidationRules = {
[field: string]: ValidationRule[];
};
type ValidationError = {
field: string;
messages: string[];
};
// Validation mixin constructor type
type ModelConstructor = new (...args: any[]) => Model;
// Define a interface for the class with validation
interface WithValidation {
validation: ValidationRules;
}
/**
* Mixin that adds validation capabilities to a Model class
*/
function withValidation<T extends ModelConstructor>(BaseClass: T) {
// Create a class that includes validation
return class extends BaseClass {
// Static validation property to be overridden by child classes
static validation: ValidationRules = {};
/**
* Validates the model data against defined validation rules
* @returns An array of validation errors or null if valid
*/
validate(): ValidationError[] | null {
// Access the constructor and safely cast it to include the validation property
const validationRules = (this.constructor as unknown as WithValidation).validation;
const errors: ValidationError[] = [];
// Loop through each validation rule
for (const [field, rules] of Object.entries(validationRules)) {
const value = this.data[field];
const fieldErrors: string[] = [];
// Apply each rule to the field
for (const rule of rules) {
if (!rule.validator(value)) {
fieldErrors.push(rule.message);
}
}
if (fieldErrors.length > 0) {
errors.push({
field,
messages: fieldErrors
});
}
}
return errors.length > 0 ? errors : null;
}
/**
* Checks if the model is valid
*/
isValid(): boolean {
return this.validate() === null;
}
};
}
// Common validators
const validators = {
required: (value: any) => value !== undefined && value !== null && value !== '',
minLength: (length: number) => (value: string) => typeof value === 'string' && value.length >= length,
maxLength: (length: number) => (value: string) => typeof value === 'string' && value.length <= length,
isEmail: (value: string) => typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
isNumber: (value: any) => typeof value === 'number' && !isNaN(value)
};
// Example User model with validation
class User extends withValidation(Model) {
static override validation: ValidationRules = {
name: [
{ validator: validators.required, message: 'Name is required' },
{ validator: validators.minLength(2), message: 'Name must be at least 2 characters' }
],
email: [
{ validator: validators.required, message: 'Email is required' },
{ validator: validators.isEmail, message: 'Email format is invalid' }
],
age: [
{ validator: validators.isNumber, message: 'Age must be a number' }
]
};
constructor(data: Record<string, any> = {}) {
super(data);
}
}
// Example usage
const validUser = new User({
name: 'John Doe',
email: 'john@example.com',
age: 30
});
const invalidUser = new User({
name: 'J',
email: 'not-an-email'
});
console.log('Valid user is valid:', validUser.isValid()); // true
console.log('Invalid user is valid:', invalidUser.isValid()); // false
console.log('Invalid user validation errors:', invalidUser.validate());Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels