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
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
|
|
}
|