You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

165 lines
4.2 KiB
Go

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
}