/** * Domain Value Object: LeagueName * * Represents a valid league name with validation rules. */ import { RacingDomainValidationError } from '../errors/RacingDomainError'; import type { IValueObject } from '@core/shared/domain'; export interface LeagueNameValidationResult { valid: boolean; error?: string; } export const LEAGUE_NAME_CONSTRAINTS = { minLength: 3, maxLength: 64, pattern: /^[a-zA-Z0-9].*$/, // Must start with alphanumeric forbiddenPatterns: [ /^\s/, // No leading whitespace /\s$/, // No trailing whitespace /\s{2,}/, // No multiple consecutive spaces ], } as const; export interface LeagueNameProps { value: string; } export class LeagueName implements IValueObject { readonly value: string; private constructor(value: string) { this.value = value; } /** * Validate a league name without creating the value object */ static validate(value: string): LeagueNameValidationResult { const trimmed = value?.trim() ?? ''; if (!trimmed) { return { valid: false, error: 'League name is required' }; } if (trimmed.length < LEAGUE_NAME_CONSTRAINTS.minLength) { return { valid: false, error: `League name must be at least ${LEAGUE_NAME_CONSTRAINTS.minLength} characters`, }; } if (trimmed.length > LEAGUE_NAME_CONSTRAINTS.maxLength) { return { valid: false, error: `League name must be ${LEAGUE_NAME_CONSTRAINTS.maxLength} characters or less`, }; } if (!LEAGUE_NAME_CONSTRAINTS.pattern.test(trimmed)) { return { valid: false, error: 'League name must start with a letter or number', }; } for (const forbidden of LEAGUE_NAME_CONSTRAINTS.forbiddenPatterns) { if (forbidden.test(value)) { return { valid: false, error: 'League name cannot have leading/trailing spaces or multiple consecutive spaces', }; } } return { valid: true }; } /** * Create a LeagueName from a string value */ static create(value: string): LeagueName { const validation = this.validate(value); if (!validation.valid) { throw new RacingDomainValidationError(validation.error ?? 'Invalid league name'); } return new LeagueName(value.trim()); } get props(): LeagueNameProps { return { value: this.value }; } /** * Try to create a LeagueName, returning null if invalid */ static tryCreate(value: string): LeagueName | null { const validation = this.validate(value); if (!validation.valid) { return null; } return new LeagueName(value.trim()); } toString(): string { return this.value; } equals(other: IValueObject): boolean { return this.props.value === other.props.value; } }