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.
833 lines
22 KiB
Go
833 lines
22 KiB
Go
package leaderboard
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"knowfoolery/backend/shared/types"
|
|
"knowfoolery/backend/shared/errors"
|
|
"knowfoolery/backend/services/leaderboard-service/internal/domain/valueobjects"
|
|
)
|
|
|
|
// Leaderboard represents a leaderboard aggregate
|
|
type Leaderboard struct {
|
|
// Identity
|
|
id types.LeaderboardID
|
|
name string
|
|
description string
|
|
|
|
// Configuration
|
|
category types.LeaderboardCategory
|
|
metric types.LeaderboardMetric
|
|
timeframe types.TimeFrame
|
|
maxEntries int
|
|
|
|
// Entries
|
|
entries []*valueobjects.LeaderboardEntry
|
|
|
|
// Status and lifecycle
|
|
status types.LeaderboardStatus
|
|
isActive bool
|
|
isFrozen bool
|
|
|
|
// Time boundaries
|
|
startTime types.Timestamp
|
|
endTime *types.Timestamp
|
|
|
|
// Update tracking
|
|
lastUpdated types.Timestamp
|
|
lastReset *types.Timestamp
|
|
|
|
// Competition settings
|
|
allowTies bool
|
|
resetRules *valueobjects.ResetRules
|
|
|
|
// Display settings
|
|
displaySettings *valueobjects.DisplaySettings
|
|
|
|
// Statistics
|
|
statistics *valueobjects.LeaderboardStatistics
|
|
|
|
// Lifecycle
|
|
createdAt types.Timestamp
|
|
updatedAt types.Timestamp
|
|
version int
|
|
}
|
|
|
|
// NewLeaderboard creates a new leaderboard
|
|
func NewLeaderboard(
|
|
name string,
|
|
description string,
|
|
category types.LeaderboardCategory,
|
|
metric types.LeaderboardMetric,
|
|
timeframe types.TimeFrame,
|
|
) (*Leaderboard, error) {
|
|
// Validate inputs
|
|
if err := validateLeaderboardName(name); err != nil {
|
|
return nil, fmt.Errorf("invalid name: %w", err)
|
|
}
|
|
|
|
if description == "" {
|
|
return nil, errors.ErrValidationFailed("description", "description cannot be empty")
|
|
}
|
|
|
|
if len(description) > types.MaxLeaderboardDescriptionLength {
|
|
return nil, errors.ErrValidationFailed("description",
|
|
fmt.Sprintf("description too long (max %d characters)", types.MaxLeaderboardDescriptionLength))
|
|
}
|
|
|
|
now := types.NewTimestamp()
|
|
|
|
// Create default settings
|
|
displaySettings := valueobjects.NewDefaultDisplaySettings()
|
|
resetRules := valueobjects.NewResetRules(timeframe)
|
|
statistics := valueobjects.NewLeaderboardStatistics()
|
|
|
|
leaderboard := &Leaderboard{
|
|
id: types.NewLeaderboardID(),
|
|
name: strings.TrimSpace(name),
|
|
description: strings.TrimSpace(description),
|
|
category: category,
|
|
metric: metric,
|
|
timeframe: timeframe,
|
|
maxEntries: types.DefaultMaxLeaderboardEntries,
|
|
entries: make([]*valueobjects.LeaderboardEntry, 0),
|
|
status: types.LeaderboardStatusActive,
|
|
isActive: true,
|
|
isFrozen: false,
|
|
startTime: now,
|
|
endTime: nil,
|
|
lastUpdated: now,
|
|
lastReset: nil,
|
|
allowTies: true,
|
|
resetRules: resetRules,
|
|
displaySettings: displaySettings,
|
|
statistics: statistics,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
version: 1,
|
|
}
|
|
|
|
return leaderboard, nil
|
|
}
|
|
|
|
// NewLeaderboardWithID creates a leaderboard with specific ID (for loading from persistence)
|
|
func NewLeaderboardWithID(
|
|
id types.LeaderboardID,
|
|
name string,
|
|
description string,
|
|
category types.LeaderboardCategory,
|
|
metric types.LeaderboardMetric,
|
|
timeframe types.TimeFrame,
|
|
createdAt types.Timestamp,
|
|
) (*Leaderboard, error) {
|
|
if id.IsEmpty() {
|
|
return nil, errors.ErrValidationFailed("id", "leaderboard ID cannot be empty")
|
|
}
|
|
|
|
leaderboard, err := NewLeaderboard(name, description, category, metric, timeframe)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
leaderboard.id = id
|
|
leaderboard.createdAt = createdAt
|
|
leaderboard.updatedAt = createdAt
|
|
|
|
return leaderboard, nil
|
|
}
|
|
|
|
// Getters
|
|
|
|
// ID returns the leaderboard's unique identifier
|
|
func (l *Leaderboard) ID() types.LeaderboardID {
|
|
return l.id
|
|
}
|
|
|
|
// Name returns the leaderboard name
|
|
func (l *Leaderboard) Name() string {
|
|
return l.name
|
|
}
|
|
|
|
// Description returns the leaderboard description
|
|
func (l *Leaderboard) Description() string {
|
|
return l.description
|
|
}
|
|
|
|
// Category returns the leaderboard category
|
|
func (l *Leaderboard) Category() types.LeaderboardCategory {
|
|
return l.category
|
|
}
|
|
|
|
// Metric returns the leaderboard metric
|
|
func (l *Leaderboard) Metric() types.LeaderboardMetric {
|
|
return l.metric
|
|
}
|
|
|
|
// Timeframe returns the leaderboard timeframe
|
|
func (l *Leaderboard) Timeframe() types.TimeFrame {
|
|
return l.timeframe
|
|
}
|
|
|
|
// MaxEntries returns the maximum number of entries
|
|
func (l *Leaderboard) MaxEntries() int {
|
|
return l.maxEntries
|
|
}
|
|
|
|
// Entries returns all leaderboard entries
|
|
func (l *Leaderboard) Entries() []*valueobjects.LeaderboardEntry {
|
|
// Return copy to prevent mutation
|
|
entries := make([]*valueobjects.LeaderboardEntry, len(l.entries))
|
|
copy(entries, l.entries)
|
|
return entries
|
|
}
|
|
|
|
// Status returns the leaderboard status
|
|
func (l *Leaderboard) Status() types.LeaderboardStatus {
|
|
return l.status
|
|
}
|
|
|
|
// IsActive returns true if the leaderboard is active
|
|
func (l *Leaderboard) IsActive() bool {
|
|
return l.isActive && l.status == types.LeaderboardStatusActive
|
|
}
|
|
|
|
// IsFrozen returns true if the leaderboard is frozen
|
|
func (l *Leaderboard) IsFrozen() bool {
|
|
return l.isFrozen
|
|
}
|
|
|
|
// StartTime returns when the leaderboard started
|
|
func (l *Leaderboard) StartTime() types.Timestamp {
|
|
return l.startTime
|
|
}
|
|
|
|
// EndTime returns when the leaderboard ended
|
|
func (l *Leaderboard) EndTime() *types.Timestamp {
|
|
return l.endTime
|
|
}
|
|
|
|
// LastUpdated returns when the leaderboard was last updated
|
|
func (l *Leaderboard) LastUpdated() types.Timestamp {
|
|
return l.lastUpdated
|
|
}
|
|
|
|
// LastReset returns when the leaderboard was last reset
|
|
func (l *Leaderboard) LastReset() *types.Timestamp {
|
|
return l.lastReset
|
|
}
|
|
|
|
// AllowTies returns true if ties are allowed
|
|
func (l *Leaderboard) AllowTies() bool {
|
|
return l.allowTies
|
|
}
|
|
|
|
// ResetRules returns the reset rules
|
|
func (l *Leaderboard) ResetRules() *valueobjects.ResetRules {
|
|
return l.resetRules
|
|
}
|
|
|
|
// DisplaySettings returns the display settings
|
|
func (l *Leaderboard) DisplaySettings() *valueobjects.DisplaySettings {
|
|
return l.displaySettings
|
|
}
|
|
|
|
// Statistics returns the leaderboard statistics
|
|
func (l *Leaderboard) Statistics() *valueobjects.LeaderboardStatistics {
|
|
return l.statistics
|
|
}
|
|
|
|
// CreatedAt returns the creation timestamp
|
|
func (l *Leaderboard) CreatedAt() types.Timestamp {
|
|
return l.createdAt
|
|
}
|
|
|
|
// UpdatedAt returns the last update timestamp
|
|
func (l *Leaderboard) UpdatedAt() types.Timestamp {
|
|
return l.updatedAt
|
|
}
|
|
|
|
// Version returns the current version
|
|
func (l *Leaderboard) Version() int {
|
|
return l.version
|
|
}
|
|
|
|
// Business methods
|
|
|
|
// UpdateScore updates or adds a user's score to the leaderboard
|
|
func (l *Leaderboard) UpdateScore(
|
|
userID types.UserID,
|
|
playerName string,
|
|
score int64,
|
|
metadata map[string]interface{},
|
|
) error {
|
|
if !l.IsActive() {
|
|
return errors.ErrOperationNotAllowed("update score", "leaderboard is not active")
|
|
}
|
|
|
|
if l.isFrozen {
|
|
return errors.ErrOperationNotAllowed("update score", "leaderboard is frozen")
|
|
}
|
|
|
|
if userID.IsEmpty() {
|
|
return errors.ErrValidationFailed("user_id", "user ID cannot be empty")
|
|
}
|
|
|
|
// Check if entry already exists
|
|
existingEntry := l.findEntryByUserID(userID)
|
|
if existingEntry != nil {
|
|
// Update existing entry
|
|
oldScore := existingEntry.Score()
|
|
err := existingEntry.UpdateScore(score, metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update entry: %w", err)
|
|
}
|
|
|
|
// Update statistics
|
|
l.statistics.RecordScoreUpdate(oldScore, score)
|
|
} else {
|
|
// Create new entry
|
|
newEntry, err := valueobjects.NewLeaderboardEntry(
|
|
userID, playerName, score, metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create entry: %w", err)
|
|
}
|
|
|
|
l.entries = append(l.entries, newEntry)
|
|
l.statistics.RecordNewEntry(score)
|
|
}
|
|
|
|
// Re-sort entries
|
|
l.sortEntries()
|
|
|
|
// Trim to max entries if needed
|
|
l.trimToMaxEntries()
|
|
|
|
// Update ranks
|
|
l.updateRanks()
|
|
|
|
l.lastUpdated = types.NewTimestamp()
|
|
l.markUpdated()
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveUser removes a user from the leaderboard
|
|
func (l *Leaderboard) RemoveUser(userID types.UserID) error {
|
|
if userID.IsEmpty() {
|
|
return errors.ErrValidationFailed("user_id", "user ID cannot be empty")
|
|
}
|
|
|
|
for i, entry := range l.entries {
|
|
if entry.UserID() == userID {
|
|
// Remove the entry
|
|
removedScore := entry.Score()
|
|
l.entries = append(l.entries[:i], l.entries[i+1:]...)
|
|
|
|
// Update statistics
|
|
l.statistics.RecordEntryRemoval(removedScore)
|
|
|
|
// Update ranks
|
|
l.updateRanks()
|
|
|
|
l.lastUpdated = types.NewTimestamp()
|
|
l.markUpdated()
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return errors.ErrNotFound("user", string(userID))
|
|
}
|
|
|
|
// GetTopEntries returns the top N entries
|
|
func (l *Leaderboard) GetTopEntries(count int) []*valueobjects.LeaderboardEntry {
|
|
if count <= 0 {
|
|
return []*valueobjects.LeaderboardEntry{}
|
|
}
|
|
|
|
if count > len(l.entries) {
|
|
count = len(l.entries)
|
|
}
|
|
|
|
// Return copy to prevent mutation
|
|
result := make([]*valueobjects.LeaderboardEntry, count)
|
|
for i := 0; i < count; i++ {
|
|
result[i] = l.entries[i]
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetUserRank returns the rank of a specific user
|
|
func (l *Leaderboard) GetUserRank(userID types.UserID) (int, *valueobjects.LeaderboardEntry) {
|
|
for _, entry := range l.entries {
|
|
if entry.UserID() == userID {
|
|
return entry.Rank(), entry
|
|
}
|
|
}
|
|
|
|
return -1, nil // User not found
|
|
}
|
|
|
|
// GetEntriesAroundUser returns entries around a specific user's position
|
|
func (l *Leaderboard) GetEntriesAroundUser(
|
|
userID types.UserID,
|
|
contextSize int,
|
|
) []*valueobjects.LeaderboardEntry {
|
|
userRank, _ := l.GetUserRank(userID)
|
|
if userRank == -1 {
|
|
return []*valueobjects.LeaderboardEntry{} // User not found
|
|
}
|
|
|
|
// Calculate range around user (0-based indexing)
|
|
userIndex := userRank - 1
|
|
startIndex := userIndex - contextSize
|
|
endIndex := userIndex + contextSize
|
|
|
|
// Clamp to valid range
|
|
if startIndex < 0 {
|
|
startIndex = 0
|
|
}
|
|
if endIndex >= len(l.entries) {
|
|
endIndex = len(l.entries) - 1
|
|
}
|
|
|
|
// Extract entries
|
|
result := make([]*valueobjects.LeaderboardEntry, 0, endIndex-startIndex+1)
|
|
for i := startIndex; i <= endIndex; i++ {
|
|
result = append(result, l.entries[i])
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Reset resets the leaderboard
|
|
func (l *Leaderboard) Reset() error {
|
|
if !l.IsActive() {
|
|
return errors.ErrOperationNotAllowed("reset", "leaderboard is not active")
|
|
}
|
|
|
|
// Clear entries
|
|
l.entries = make([]*valueobjects.LeaderboardEntry, 0)
|
|
|
|
// Reset statistics
|
|
l.statistics.Reset()
|
|
|
|
// Update timestamps
|
|
now := types.NewTimestamp()
|
|
l.lastReset = &now
|
|
l.lastUpdated = now
|
|
|
|
l.markUpdated()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Freeze freezes the leaderboard to prevent further updates
|
|
func (l *Leaderboard) Freeze() error {
|
|
if !l.IsActive() {
|
|
return errors.ErrOperationNotAllowed("freeze", "leaderboard is not active")
|
|
}
|
|
|
|
if l.isFrozen {
|
|
return errors.ErrOperationNotAllowed("freeze", "leaderboard is already frozen")
|
|
}
|
|
|
|
l.isFrozen = true
|
|
l.markUpdated()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Unfreeze unfreezes the leaderboard
|
|
func (l *Leaderboard) Unfreeze() error {
|
|
if !l.IsActive() {
|
|
return errors.ErrOperationNotAllowed("unfreeze", "leaderboard is not active")
|
|
}
|
|
|
|
if !l.isFrozen {
|
|
return errors.ErrOperationNotAllowed("unfreeze", "leaderboard is not frozen")
|
|
}
|
|
|
|
l.isFrozen = false
|
|
l.markUpdated()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Archive archives the leaderboard
|
|
func (l *Leaderboard) Archive() error {
|
|
if l.status == types.LeaderboardStatusArchived {
|
|
return errors.ErrOperationNotAllowed("archive", "leaderboard is already archived")
|
|
}
|
|
|
|
l.status = types.LeaderboardStatusArchived
|
|
l.isActive = false
|
|
l.isFrozen = true
|
|
|
|
now := types.NewTimestamp()
|
|
l.endTime = &now
|
|
|
|
l.markUpdated()
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateConfiguration updates leaderboard configuration
|
|
func (l *Leaderboard) UpdateConfiguration(
|
|
name string,
|
|
description string,
|
|
maxEntries int,
|
|
allowTies bool,
|
|
) error {
|
|
if err := validateLeaderboardName(name); err != nil {
|
|
return fmt.Errorf("invalid name: %w", err)
|
|
}
|
|
|
|
if description == "" {
|
|
return errors.ErrValidationFailed("description", "description cannot be empty")
|
|
}
|
|
|
|
if len(description) > types.MaxLeaderboardDescriptionLength {
|
|
return errors.ErrValidationFailed("description",
|
|
fmt.Sprintf("description too long (max %d characters)", types.MaxLeaderboardDescriptionLength))
|
|
}
|
|
|
|
if maxEntries < types.MinLeaderboardEntries || maxEntries > types.MaxLeaderboardEntries {
|
|
return errors.ErrValidationFailed("max_entries",
|
|
fmt.Sprintf("max entries must be between %d and %d", types.MinLeaderboardEntries, types.MaxLeaderboardEntries))
|
|
}
|
|
|
|
l.name = strings.TrimSpace(name)
|
|
l.description = strings.TrimSpace(description)
|
|
l.maxEntries = maxEntries
|
|
l.allowTies = allowTies
|
|
|
|
// Trim entries if max entries was reduced
|
|
if len(l.entries) > maxEntries {
|
|
l.trimToMaxEntries()
|
|
l.updateRanks()
|
|
}
|
|
|
|
l.markUpdated()
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateDisplaySettings updates display settings
|
|
func (l *Leaderboard) UpdateDisplaySettings(settings *valueobjects.DisplaySettings) error {
|
|
if settings == nil {
|
|
return errors.ErrValidationFailed("display_settings", "display settings cannot be nil")
|
|
}
|
|
|
|
if err := settings.Validate(); err != nil {
|
|
return fmt.Errorf("invalid display settings: %w", err)
|
|
}
|
|
|
|
l.displaySettings = settings
|
|
l.markUpdated()
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateResetRules updates reset rules
|
|
func (l *Leaderboard) UpdateResetRules(rules *valueobjects.ResetRules) error {
|
|
if rules == nil {
|
|
return errors.ErrValidationFailed("reset_rules", "reset rules cannot be nil")
|
|
}
|
|
|
|
if err := rules.Validate(); err != nil {
|
|
return fmt.Errorf("invalid reset rules: %w", err)
|
|
}
|
|
|
|
l.resetRules = rules
|
|
l.markUpdated()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Query methods
|
|
|
|
// HasUser returns true if a user is on the leaderboard
|
|
func (l *Leaderboard) HasUser(userID types.UserID) bool {
|
|
return l.findEntryByUserID(userID) != nil
|
|
}
|
|
|
|
// GetEntryCount returns the number of entries
|
|
func (l *Leaderboard) GetEntryCount() int {
|
|
return len(l.entries)
|
|
}
|
|
|
|
// IsEmpty returns true if the leaderboard has no entries
|
|
func (l *Leaderboard) IsEmpty() bool {
|
|
return len(l.entries) == 0
|
|
}
|
|
|
|
// IsFull returns true if the leaderboard is at maximum capacity
|
|
func (l *Leaderboard) IsFull() bool {
|
|
return len(l.entries) >= l.maxEntries
|
|
}
|
|
|
|
// GetScoreRange returns the score range (min, max) of the leaderboard
|
|
func (l *Leaderboard) GetScoreRange() (int64, int64) {
|
|
if len(l.entries) == 0 {
|
|
return 0, 0
|
|
}
|
|
|
|
// Entries are sorted by score (descending), so first is highest, last is lowest
|
|
highest := l.entries[0].Score()
|
|
lowest := l.entries[len(l.entries)-1].Score()
|
|
|
|
return lowest, highest
|
|
}
|
|
|
|
// ShouldReset returns true if the leaderboard should be reset based on its rules
|
|
func (l *Leaderboard) ShouldReset() bool {
|
|
if l.resetRules == nil || l.lastReset == nil {
|
|
return false
|
|
}
|
|
|
|
return l.resetRules.ShouldReset(*l.lastReset, types.NewTimestamp())
|
|
}
|
|
|
|
// GetTimeUntilReset returns time until next reset
|
|
func (l *Leaderboard) GetTimeUntilReset() *types.Duration {
|
|
if l.resetRules == nil || l.lastReset == nil {
|
|
return nil
|
|
}
|
|
|
|
return l.resetRules.GetTimeUntilReset(*l.lastReset, types.NewTimestamp())
|
|
}
|
|
|
|
// Internal methods
|
|
|
|
// findEntryByUserID finds an entry by user ID
|
|
func (l *Leaderboard) findEntryByUserID(userID types.UserID) *valueobjects.LeaderboardEntry {
|
|
for _, entry := range l.entries {
|
|
if entry.UserID() == userID {
|
|
return entry
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sortEntries sorts entries by score (descending)
|
|
func (l *Leaderboard) sortEntries() {
|
|
sort.Slice(l.entries, func(i, j int) bool {
|
|
// Sort by score descending
|
|
if l.entries[i].Score() != l.entries[j].Score() {
|
|
return l.entries[i].Score() > l.entries[j].Score()
|
|
}
|
|
|
|
// If scores are equal, sort by update time (earlier first for tie-breaking)
|
|
return l.entries[i].UpdatedAt().Before(l.entries[j].UpdatedAt())
|
|
})
|
|
}
|
|
|
|
// trimToMaxEntries removes excess entries beyond max limit
|
|
func (l *Leaderboard) trimToMaxEntries() {
|
|
if len(l.entries) > l.maxEntries {
|
|
// Remove excess entries from the end (lowest scores)
|
|
removed := l.entries[l.maxEntries:]
|
|
l.entries = l.entries[:l.maxEntries]
|
|
|
|
// Update statistics
|
|
for _, entry := range removed {
|
|
l.statistics.RecordEntryRemoval(entry.Score())
|
|
}
|
|
}
|
|
}
|
|
|
|
// updateRanks updates the rank of all entries
|
|
func (l *Leaderboard) updateRanks() {
|
|
currentRank := 1
|
|
var previousScore *int64
|
|
|
|
for i, entry := range l.entries {
|
|
if previousScore != nil && *previousScore != entry.Score() {
|
|
// Score changed, update rank
|
|
if l.allowTies {
|
|
currentRank = i + 1
|
|
} else {
|
|
currentRank++
|
|
}
|
|
}
|
|
|
|
entry.SetRank(currentRank)
|
|
score := entry.Score()
|
|
previousScore = &score
|
|
|
|
if !l.allowTies {
|
|
currentRank++
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validation
|
|
|
|
// Validate performs comprehensive validation
|
|
func (l *Leaderboard) Validate() *types.ValidationResult {
|
|
result := types.NewValidationResult()
|
|
|
|
// Validate ID
|
|
if l.id.IsEmpty() {
|
|
result.AddError("leaderboard ID cannot be empty")
|
|
}
|
|
|
|
// Validate name
|
|
if err := validateLeaderboardName(l.name); err != nil {
|
|
result.AddErrorf("invalid name: %v", err)
|
|
}
|
|
|
|
// Validate description
|
|
if l.description == "" {
|
|
result.AddError("description cannot be empty")
|
|
}
|
|
|
|
if len(l.description) > types.MaxLeaderboardDescriptionLength {
|
|
result.AddErrorf("description too long (max %d characters)", types.MaxLeaderboardDescriptionLength)
|
|
}
|
|
|
|
// Validate max entries
|
|
if l.maxEntries < types.MinLeaderboardEntries || l.maxEntries > types.MaxLeaderboardEntries {
|
|
result.AddErrorf("max entries must be between %d and %d", types.MinLeaderboardEntries, types.MaxLeaderboardEntries)
|
|
}
|
|
|
|
// Validate entries count
|
|
if len(l.entries) > l.maxEntries {
|
|
result.AddError("entry count exceeds max entries")
|
|
}
|
|
|
|
// Validate display settings
|
|
if l.displaySettings != nil {
|
|
if settingsResult := l.displaySettings.Validate(); !settingsResult.IsValid() {
|
|
for _, error := range settingsResult.Errors() {
|
|
result.AddErrorf("display settings validation failed: %s", error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate reset rules
|
|
if l.resetRules != nil {
|
|
if rulesResult := l.resetRules.Validate(); !rulesResult.IsValid() {
|
|
for _, error := range rulesResult.Errors() {
|
|
result.AddErrorf("reset rules validation failed: %s", error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate statistics
|
|
if l.statistics != nil {
|
|
if statsResult := l.statistics.Validate(); !statsResult.IsValid() {
|
|
for _, error := range statsResult.Errors() {
|
|
result.AddErrorf("statistics validation failed: %s", error)
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
// markUpdated updates the timestamp and version
|
|
func (l *Leaderboard) markUpdated() {
|
|
l.updatedAt = types.NewTimestamp()
|
|
l.version++
|
|
}
|
|
|
|
// ToSnapshot creates a read-only snapshot of the leaderboard
|
|
func (l *Leaderboard) ToSnapshot() *LeaderboardSnapshot {
|
|
// Copy entries
|
|
entriesCopy := make([]*valueobjects.LeaderboardEntrySnapshot, len(l.entries))
|
|
for i, entry := range l.entries {
|
|
entriesCopy[i] = entry.ToSnapshot()
|
|
}
|
|
|
|
return &LeaderboardSnapshot{
|
|
ID: l.id,
|
|
Name: l.name,
|
|
Description: l.description,
|
|
Category: l.category,
|
|
Metric: l.metric,
|
|
Timeframe: l.timeframe,
|
|
MaxEntries: l.maxEntries,
|
|
Entries: entriesCopy,
|
|
Status: l.status,
|
|
IsActive: l.isActive,
|
|
IsFrozen: l.isFrozen,
|
|
StartTime: l.startTime,
|
|
EndTime: l.endTime,
|
|
LastUpdated: l.lastUpdated,
|
|
LastReset: l.lastReset,
|
|
AllowTies: l.allowTies,
|
|
ResetRules: l.resetRules.ToSnapshot(),
|
|
DisplaySettings: l.displaySettings.ToSnapshot(),
|
|
Statistics: l.statistics.ToSnapshot(),
|
|
CreatedAt: l.createdAt,
|
|
UpdatedAt: l.updatedAt,
|
|
Version: l.version,
|
|
}
|
|
}
|
|
|
|
// GetSummary returns a human-readable summary
|
|
func (l *Leaderboard) GetSummary() string {
|
|
return fmt.Sprintf("%s (%s): %d entries, %s timeframe, %s",
|
|
l.name, l.category, len(l.entries), l.timeframe, l.status)
|
|
}
|
|
|
|
// LeaderboardSnapshot represents a read-only view of leaderboard data
|
|
type LeaderboardSnapshot struct {
|
|
ID types.LeaderboardID `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Category types.LeaderboardCategory `json:"category"`
|
|
Metric types.LeaderboardMetric `json:"metric"`
|
|
Timeframe types.TimeFrame `json:"timeframe"`
|
|
MaxEntries int `json:"max_entries"`
|
|
Entries []*valueobjects.LeaderboardEntrySnapshot `json:"entries"`
|
|
Status types.LeaderboardStatus `json:"status"`
|
|
IsActive bool `json:"is_active"`
|
|
IsFrozen bool `json:"is_frozen"`
|
|
StartTime types.Timestamp `json:"start_time"`
|
|
EndTime *types.Timestamp `json:"end_time,omitempty"`
|
|
LastUpdated types.Timestamp `json:"last_updated"`
|
|
LastReset *types.Timestamp `json:"last_reset,omitempty"`
|
|
AllowTies bool `json:"allow_ties"`
|
|
ResetRules *valueobjects.ResetRulesSnapshot `json:"reset_rules"`
|
|
DisplaySettings *valueobjects.DisplaySettingsSnapshot `json:"display_settings"`
|
|
Statistics *valueobjects.LeaderboardStatisticsSnapshot `json:"statistics"`
|
|
CreatedAt types.Timestamp `json:"created_at"`
|
|
UpdatedAt types.Timestamp `json:"updated_at"`
|
|
Version int `json:"version"`
|
|
}
|
|
|
|
// GetSummary returns a summary of the snapshot
|
|
func (ls *LeaderboardSnapshot) GetSummary() string {
|
|
return fmt.Sprintf("%s (%s): %d entries, %s timeframe",
|
|
ls.Name, ls.Category, len(ls.Entries), ls.Timeframe)
|
|
}
|
|
|
|
// Validation helper functions
|
|
|
|
func validateLeaderboardName(name string) error {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return errors.ErrValidationFailed("name", "leaderboard name cannot be empty")
|
|
}
|
|
|
|
if len(name) < types.MinLeaderboardNameLength {
|
|
return errors.ErrValidationFailed("name",
|
|
fmt.Sprintf("leaderboard name too short (min %d characters)", types.MinLeaderboardNameLength))
|
|
}
|
|
|
|
if len(name) > types.MaxLeaderboardNameLength {
|
|
return errors.ErrValidationFailed("name",
|
|
fmt.Sprintf("leaderboard name too long (max %d characters)", types.MaxLeaderboardNameLength))
|
|
}
|
|
|
|
return nil
|
|
} |