package valueobjects import ( "regexp" "strings" "knowfoolery/backend/shared/errors" "knowfoolery/backend/shared/types" ) // PlayerName represents a validated player name value object type PlayerName struct { value string } // NewPlayerName creates a new PlayerName after validation func NewPlayerName(name string) (*PlayerName, error) { if err := validatePlayerName(name); err != nil { return nil, err } normalized := normalizePlayerName(name) return &PlayerName{value: normalized}, nil } // Value returns the underlying string value func (p *PlayerName) Value() string { return p.value } // String returns the string representation func (p *PlayerName) String() string { return p.value } // Equals checks if two PlayerName values are equal func (p *PlayerName) Equals(other *PlayerName) bool { if other == nil { return false } return p.value == other.value } // IsEmpty checks if the player name is empty func (p *PlayerName) IsEmpty() bool { return p.value == "" } // Length returns the length of the player name func (p *PlayerName) Length() int { return len(p.value) } // validatePlayerName validates a player name according to business rules func validatePlayerName(name string) error { // Check if empty if strings.TrimSpace(name) == "" { return errors.ErrInvalidPlayerName(name, "name cannot be empty") } // Check length constraints trimmed := strings.TrimSpace(name) if len(trimmed) < types.MinPlayerNameLength { return errors.ErrInvalidPlayerName(name, "name must be at least 2 characters long") } if len(trimmed) > types.MaxPlayerNameLength { return errors.ErrInvalidPlayerName(name, "name cannot exceed 50 characters") } // Check for valid characters (alphanumeric + spaces) validChars := regexp.MustCompile(`^[a-zA-Z0-9\s]+$`) if !validChars.MatchString(trimmed) { return errors.ErrInvalidPlayerName(name, "name can only contain letters, numbers, and spaces") } // Check for excessive consecutive spaces multipleSpaces := regexp.MustCompile(`\s{2,}`) if multipleSpaces.MatchString(trimmed) { return errors.ErrInvalidPlayerName(name, "name cannot contain multiple consecutive spaces") } // Check that name doesn't start or end with space (after trimming this shouldn't happen, but being explicit) if strings.HasPrefix(trimmed, " ") || strings.HasSuffix(trimmed, " ") { return errors.ErrInvalidPlayerName(name, "name cannot start or end with spaces") } // Check for reserved/inappropriate names (can be extended) if isReservedName(trimmed) { return errors.ErrInvalidPlayerName(name, "name is reserved and cannot be used") } return nil } // normalizePlayerName normalizes a player name (trim spaces, handle capitalization, etc.) func normalizePlayerName(name string) string { // Trim leading and trailing spaces normalized := strings.TrimSpace(name) // Replace multiple spaces with single space multipleSpaces := regexp.MustCompile(`\s+`) normalized = multipleSpaces.ReplaceAllString(normalized, " ") // For consistency, we could apply title case, but let's preserve user's capitalization choice // normalized = strings.Title(strings.ToLower(normalized)) return normalized } // isReservedName checks if a name is in the reserved names list func isReservedName(name string) bool { // Convert to lowercase for case-insensitive comparison lowerName := strings.ToLower(name) reservedNames := []string{ "admin", "administrator", "moderator", "system", "null", "undefined", "anonymous", "guest", "user", "player", "test", "bot", "api", "root", "support", // Add more reserved names as needed } for _, reserved := range reservedNames { if lowerName == reserved { return true } } return false } // PlayerNameFromString creates a PlayerName from a string without validation // This is used when loading from persistence where validation already occurred func PlayerNameFromString(value string) *PlayerName { return &PlayerName{value: value} } // MustCreatePlayerName creates a PlayerName and panics if validation fails // Only use when you're certain the name is valid (e.g., in tests) func MustCreatePlayerName(name string) *PlayerName { playerName, err := NewPlayerName(name) if err != nil { panic(err) } return playerName }