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.

468 lines
18 KiB
Go

package errors
import (
"fmt"
"knowfoolery/backend/shared/types"
)
// DomainError represents a base error type for all domain errors
type DomainError struct {
Code string
Message string
Cause error
}
// Error implements the error interface
func (e *DomainError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s: %s (caused by: %v)", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
// Unwrap returns the underlying cause
func (e *DomainError) Unwrap() error {
return e.Cause
}
// NewDomainError creates a new domain error
func NewDomainError(code, message string, cause error) *DomainError {
return &DomainError{
Code: code,
Message: message,
Cause: cause,
}
}
// Error codes for different domain errors
const (
// User errors
ErrCodeInvalidPlayerName = "INVALID_PLAYER_NAME"
ErrCodeUserNotFound = "USER_NOT_FOUND"
ErrCodeUserAlreadyExists = "USER_ALREADY_EXISTS"
// Game Session errors
ErrCodeSessionNotFound = "SESSION_NOT_FOUND"
ErrCodeSessionAlreadyExists = "SESSION_ALREADY_EXISTS"
ErrCodeSessionExpired = "SESSION_EXPIRED"
ErrCodeSessionNotActive = "SESSION_NOT_ACTIVE"
ErrCodeInvalidSessionTransition = "INVALID_SESSION_TRANSITION"
ErrCodeSessionTimedOut = "SESSION_TIMED_OUT"
ErrCodeUserAlreadyHasActiveSession = "USER_ALREADY_HAS_ACTIVE_SESSION"
// Attempt errors
ErrCodeMaxAttemptsExceeded = "MAX_ATTEMPTS_EXCEEDED"
ErrCodeInvalidAttempt = "INVALID_ATTEMPT"
ErrCodeAttemptTooFast = "ATTEMPT_TOO_FAST"
// Question errors
ErrCodeQuestionNotFound = "QUESTION_NOT_FOUND"
ErrCodeQuestionAlreadyAnswered = "QUESTION_ALREADY_ANSWERED"
ErrCodeInvalidQuestion = "INVALID_QUESTION"
ErrCodeNoQuestionsAvailable = "NO_QUESTIONS_AVAILABLE"
// Theme errors
ErrCodeThemeNotFound = "THEME_NOT_FOUND"
ErrCodeInvalidTheme = "INVALID_THEME"
// Answer errors
ErrCodeInvalidAnswer = "INVALID_ANSWER"
ErrCodeEmptyAnswer = "EMPTY_ANSWER"
// Hint errors
ErrCodeHintNotAvailable = "HINT_NOT_AVAILABLE"
ErrCodeHintAlreadyUsed = "HINT_ALREADY_USED"
// Scoring errors
ErrCodeInvalidScore = "INVALID_SCORE"
ErrCodeScoringFailed = "SCORING_FAILED"
// Validation errors
ErrCodeValidationFailed = "VALIDATION_FAILED"
ErrCodeBusinessRuleViolation = "BUSINESS_RULE_VIOLATION"
// General errors
ErrCodeOperationNotAllowed = "OPERATION_NOT_ALLOWED"
ErrCodeInternalError = "INTERNAL_ERROR"
// Leaderboard errors
ErrCodeInvalidLeaderboardID = "INVALID_LEADERBOARD_ID"
ErrCodeLeaderboardNotFound = "LEADERBOARD_NOT_FOUND"
ErrCodeLeaderboardAlreadyExists = "LEADERBOARD_ALREADY_EXISTS"
ErrCodeLeaderboardFrozen = "LEADERBOARD_FROZEN"
ErrCodeLeaderboardArchived = "LEADERBOARD_ARCHIVED"
ErrCodeInvalidRank = "INVALID_RANK"
ErrCodePlayerNotFound = "PLAYER_NOT_FOUND"
ErrCodeInvalidResetFrequency = "INVALID_RESET_FREQUENCY"
ErrCodeInvalidResetAction = "INVALID_RESET_ACTION"
ErrCodeInvalidResetInterval = "INVALID_RESET_INTERVAL"
ErrCodeInvalidTimezone = "INVALID_TIMEZONE"
ErrCodeInvalidPreserveCount = "INVALID_PRESERVE_COUNT"
ErrCodeInvalidResetTime = "INVALID_RESET_TIME"
ErrCodeInvalidNotificationTime = "INVALID_NOTIFICATION_TIME"
ErrCodeInvalidDisplayFormat = "INVALID_DISPLAY_FORMAT"
ErrCodeInvalidSortOrder = "INVALID_SORT_ORDER"
ErrCodeInvalidRankDisplay = "INVALID_RANK_DISPLAY"
ErrCodeInvalidEntriesPerPage = "INVALID_ENTRIES_PER_PAGE"
ErrCodeInvalidHighlightCount = "INVALID_HIGHLIGHT_COUNT"
ErrCodeInvalidColorKey = "INVALID_COLOR_KEY"
ErrCodeInvalidColorValue = "INVALID_COLOR_VALUE"
ErrCodeInvalidLabelKey = "INVALID_LABEL_KEY"
ErrCodeInvalidLabelValue = "INVALID_LABEL_VALUE"
ErrCodeInvalidMetadata = "INVALID_METADATA"
ErrCodeInvalidGameCount = "INVALID_GAME_COUNT"
ErrCodeInvalidPlayerCount = "INVALID_PLAYER_COUNT"
ErrCodeInvalidStreak = "INVALID_STREAK"
ErrCodeInvalidPlayerID = "INVALID_PLAYER_ID"
ErrCodeInvalidSessionID = "INVALID_SESSION_ID"
ErrCodeInvalidRankRange = "INVALID_RANK_RANGE"
ErrCodeInvalidTopCount = "INVALID_TOP_COUNT"
ErrCodeInvalidContext = "INVALID_CONTEXT"
ErrCodeInvalidCompetitionName = "INVALID_COMPETITION_NAME"
ErrCodeInvalidCompetitionStatus = "INVALID_COMPETITION_STATUS"
ErrCodeCompetitionNotYetStarted = "COMPETITION_NOT_YET_STARTED"
ErrCodeCompetitionNotActive = "COMPETITION_NOT_ACTIVE"
ErrCodeCompetitionAlreadyCompleted = "COMPETITION_ALREADY_COMPLETED"
ErrCodePlayerNotQualified = "PLAYER_NOT_QUALIFIED"
ErrCodeCompetitionFull = "COMPETITION_FULL"
ErrCodeScoreBelowMinimum = "SCORE_BELOW_MINIMUM"
ErrCodeTooManyEntriesPerPlayer = "TOO_MANY_ENTRIES_PER_PLAYER"
ErrCodeEntryNotFound = "ENTRY_NOT_FOUND"
ErrCodeInvalidStartTime = "INVALID_START_TIME"
ErrCodeInvalidEndTime = "INVALID_END_TIME"
)
// User-related errors
// ErrInvalidPlayerName indicates the player name doesn't meet validation requirements
func ErrInvalidPlayerName(name string, reason string) *DomainError {
return NewDomainError(
ErrCodeInvalidPlayerName,
fmt.Sprintf("invalid player name '%s': %s", name, reason),
nil,
)
}
// ErrUserNotFound indicates a user was not found
func ErrUserNotFound(userID types.UserID) *DomainError {
return NewDomainError(
ErrCodeUserNotFound,
fmt.Sprintf("user not found: %s", userID),
nil,
)
}
// ErrUserAlreadyExists indicates a user already exists
func ErrUserAlreadyExists(name string) *DomainError {
return NewDomainError(
ErrCodeUserAlreadyExists,
fmt.Sprintf("user already exists: %s", name),
nil,
)
}
// Game Session-related errors
// ErrSessionNotFound indicates a game session was not found
func ErrSessionNotFound(sessionID types.GameSessionID) *DomainError {
return NewDomainError(
ErrCodeSessionNotFound,
fmt.Sprintf("game session not found: %s", sessionID),
nil,
)
}
// ErrSessionExpired indicates a game session has expired
func ErrSessionExpired(sessionID types.GameSessionID) *DomainError {
return NewDomainError(
ErrCodeSessionExpired,
fmt.Sprintf("game session expired: %s", sessionID),
nil,
)
}
// ErrSessionNotActive indicates operation requires an active session
func ErrSessionNotActive(sessionID types.GameSessionID, status types.SessionStatus) *DomainError {
return NewDomainError(
ErrCodeSessionNotActive,
fmt.Sprintf("session %s is not active (current status: %s)", sessionID, status),
nil,
)
}
// ErrInvalidSessionTransition indicates an invalid session status transition
func ErrInvalidSessionTransition(from, to types.SessionStatus) *DomainError {
return NewDomainError(
ErrCodeInvalidSessionTransition,
fmt.Sprintf("invalid session transition from %s to %s", from, to),
nil,
)
}
// ErrUserAlreadyHasActiveSession indicates user already has an active session
func ErrUserAlreadyHasActiveSession(userID types.UserID, existingSessionID types.GameSessionID) *DomainError {
return NewDomainError(
ErrCodeUserAlreadyHasActiveSession,
fmt.Sprintf("user %s already has active session: %s", userID, existingSessionID),
nil,
)
}
// Attempt-related errors
// ErrMaxAttemptsExceeded indicates maximum attempts for a question have been exceeded
func ErrMaxAttemptsExceeded(questionID types.QuestionID, maxAttempts int) *DomainError {
return NewDomainError(
ErrCodeMaxAttemptsExceeded,
fmt.Sprintf("maximum attempts (%d) exceeded for question: %s", maxAttempts, questionID),
nil,
)
}
// ErrAttemptTooFast indicates an attempt was made too quickly (anti-cheat)
func ErrAttemptTooFast(minInterval string) *DomainError {
return NewDomainError(
ErrCodeAttemptTooFast,
fmt.Sprintf("attempt made too quickly, minimum interval: %s", minInterval),
nil,
)
}
// Question-related errors
// ErrQuestionNotFound indicates a question was not found
func ErrQuestionNotFound(questionID types.QuestionID) *DomainError {
return NewDomainError(
ErrCodeQuestionNotFound,
fmt.Sprintf("question not found: %s", questionID),
nil,
)
}
// ErrQuestionAlreadyAnswered indicates question was already answered in this session
func ErrQuestionAlreadyAnswered(questionID types.QuestionID, sessionID types.GameSessionID) *DomainError {
return NewDomainError(
ErrCodeQuestionAlreadyAnswered,
fmt.Sprintf("question %s already answered in session %s", questionID, sessionID),
nil,
)
}
// ErrNoQuestionsAvailable indicates no questions are available for selection
func ErrNoQuestionsAvailable() *DomainError {
return NewDomainError(
ErrCodeNoQuestionsAvailable,
"no questions available for selection",
nil,
)
}
// Theme-related errors
// ErrThemeNotFound indicates a theme was not found
func ErrThemeNotFound(themeID types.ThemeID) *DomainError {
return NewDomainError(
ErrCodeThemeNotFound,
fmt.Sprintf("theme not found: %s", themeID),
nil,
)
}
// ErrInvalidTheme indicates an invalid theme
func ErrInvalidTheme(reason string) *DomainError {
return NewDomainError(
ErrCodeInvalidTheme,
fmt.Sprintf("invalid theme: %s", reason),
nil,
)
}
// Answer-related errors
// ErrInvalidAnswer indicates an answer is invalid
func ErrInvalidAnswer(reason string) *DomainError {
return NewDomainError(
ErrCodeInvalidAnswer,
fmt.Sprintf("invalid answer: %s", reason),
nil,
)
}
// ErrEmptyAnswer indicates an empty answer was provided
func ErrEmptyAnswer() *DomainError {
return NewDomainError(
ErrCodeEmptyAnswer,
"answer cannot be empty",
nil,
)
}
// ErrInvalidQuestion indicates an invalid question
func ErrInvalidQuestion(reason string) *DomainError {
return NewDomainError(
ErrCodeInvalidQuestion,
fmt.Sprintf("invalid question: %s", reason),
nil,
)
}
// Hint-related errors
// ErrHintNotAvailable indicates no hint is available for the question
func ErrHintNotAvailable(questionID types.QuestionID) *DomainError {
return NewDomainError(
ErrCodeHintNotAvailable,
fmt.Sprintf("no hint available for question: %s", questionID),
nil,
)
}
// ErrHintAlreadyUsed indicates hint was already used for this question
func ErrHintAlreadyUsed(questionID types.QuestionID) *DomainError {
return NewDomainError(
ErrCodeHintAlreadyUsed,
fmt.Sprintf("hint already used for question: %s", questionID),
nil,
)
}
// Scoring-related errors
// ErrScoringFailed indicates a scoring operation failed
func ErrScoringFailed(reason string) *DomainError {
return NewDomainError(
ErrCodeScoringFailed,
fmt.Sprintf("scoring failed: %s", reason),
nil,
)
}
// Validation-related errors
// ErrValidationFailed indicates domain validation failed
func ErrValidationFailed(field string, reason string) *DomainError {
return NewDomainError(
ErrCodeValidationFailed,
fmt.Sprintf("validation failed for %s: %s", field, reason),
nil,
)
}
// ErrBusinessRuleViolation indicates a business rule was violated
func ErrBusinessRuleViolation(rule string, details string) *DomainError {
return NewDomainError(
ErrCodeBusinessRuleViolation,
fmt.Sprintf("business rule violation - %s: %s", rule, details),
nil,
)
}
// General errors
// ErrOperationNotAllowed indicates the requested operation is not allowed
func ErrOperationNotAllowed(operation string, reason string) *DomainError {
return NewDomainError(
ErrCodeOperationNotAllowed,
fmt.Sprintf("operation '%s' not allowed: %s", operation, reason),
nil,
)
}
// ErrInternalError indicates an internal domain error occurred
func ErrInternalError(reason string, cause error) *DomainError {
return NewDomainError(
ErrCodeInternalError,
fmt.Sprintf("internal error: %s", reason),
cause,
)
}
// Error checking functions
// IsErrorOfType checks if an error is of a specific domain error type
func IsErrorOfType(err error, code string) bool {
if domainErr, ok := err.(*DomainError); ok {
return domainErr.Code == code
}
return false
}
// IsUserError checks if error is user-related
func IsUserError(err error) bool {
return IsErrorOfType(err, ErrCodeInvalidPlayerName) ||
IsErrorOfType(err, ErrCodeUserNotFound) ||
IsErrorOfType(err, ErrCodeUserAlreadyExists)
}
// IsSessionError checks if error is session-related
func IsSessionError(err error) bool {
return IsErrorOfType(err, ErrCodeSessionNotFound) ||
IsErrorOfType(err, ErrCodeSessionExpired) ||
IsErrorOfType(err, ErrCodeSessionNotActive) ||
IsErrorOfType(err, ErrCodeInvalidSessionTransition) ||
IsErrorOfType(err, ErrCodeUserAlreadyHasActiveSession)
}
// IsValidationError checks if error is validation-related
func IsValidationError(err error) bool {
return IsErrorOfType(err, ErrCodeValidationFailed) ||
IsErrorOfType(err, ErrCodeInvalidPlayerName) ||
IsErrorOfType(err, ErrCodeInvalidAnswer)
}
// IsBusinessRuleError checks if error is business rule violation
func IsBusinessRuleError(err error) bool {
return IsErrorOfType(err, ErrCodeBusinessRuleViolation) ||
IsErrorOfType(err, ErrCodeMaxAttemptsExceeded) ||
IsErrorOfType(err, ErrCodeAttemptTooFast)
}
// Leaderboard-related error variables
var (
ErrInvalidLeaderboardID = NewDomainError(ErrCodeInvalidLeaderboardID, "invalid leaderboard ID", nil)
ErrLeaderboardNotFound = NewDomainError(ErrCodeLeaderboardNotFound, "leaderboard not found", nil)
ErrLeaderboardAlreadyExists = NewDomainError(ErrCodeLeaderboardAlreadyExists, "leaderboard already exists", nil)
ErrLeaderboardFrozen = NewDomainError(ErrCodeLeaderboardFrozen, "leaderboard is frozen", nil)
ErrLeaderboardArchived = NewDomainError(ErrCodeLeaderboardArchived, "leaderboard is archived", nil)
ErrInvalidRank = NewDomainError(ErrCodeInvalidRank, "invalid rank", nil)
ErrPlayerNotFound = NewDomainError(ErrCodePlayerNotFound, "player not found", nil)
ErrInvalidResetFrequency = NewDomainError(ErrCodeInvalidResetFrequency, "invalid reset frequency", nil)
ErrInvalidResetAction = NewDomainError(ErrCodeInvalidResetAction, "invalid reset action", nil)
ErrInvalidResetInterval = NewDomainError(ErrCodeInvalidResetInterval, "invalid reset interval", nil)
ErrInvalidTimezone = NewDomainError(ErrCodeInvalidTimezone, "invalid timezone", nil)
ErrInvalidPreserveCount = NewDomainError(ErrCodeInvalidPreserveCount, "invalid preserve count", nil)
ErrInvalidResetTime = NewDomainError(ErrCodeInvalidResetTime, "invalid reset time", nil)
ErrInvalidNotificationTime = NewDomainError(ErrCodeInvalidNotificationTime, "invalid notification time", nil)
ErrInvalidDisplayFormat = NewDomainError(ErrCodeInvalidDisplayFormat, "invalid display format", nil)
ErrInvalidSortOrder = NewDomainError(ErrCodeInvalidSortOrder, "invalid sort order", nil)
ErrInvalidRankDisplay = NewDomainError(ErrCodeInvalidRankDisplay, "invalid rank display", nil)
ErrInvalidEntriesPerPage = NewDomainError(ErrCodeInvalidEntriesPerPage, "invalid entries per page", nil)
ErrInvalidHighlightCount = NewDomainError(ErrCodeInvalidHighlightCount, "invalid highlight count", nil)
ErrInvalidColorKey = NewDomainError(ErrCodeInvalidColorKey, "invalid color key", nil)
ErrInvalidColorValue = NewDomainError(ErrCodeInvalidColorValue, "invalid color value", nil)
ErrInvalidLabelKey = NewDomainError(ErrCodeInvalidLabelKey, "invalid label key", nil)
ErrInvalidLabelValue = NewDomainError(ErrCodeInvalidLabelValue, "invalid label value", nil)
ErrInvalidMetadata = NewDomainError(ErrCodeInvalidMetadata, "invalid metadata", nil)
ErrInvalidGameCount = NewDomainError(ErrCodeInvalidGameCount, "invalid game count", nil)
ErrInvalidPlayerCount = NewDomainError(ErrCodeInvalidPlayerCount, "invalid player count", nil)
ErrInvalidStreak = NewDomainError(ErrCodeInvalidStreak, "invalid streak", nil)
ErrInvalidPlayerID = NewDomainError(ErrCodeInvalidPlayerID, "invalid player ID", nil)
ErrInvalidSessionID = NewDomainError(ErrCodeInvalidSessionID, "invalid session ID", nil)
ErrInvalidScore = NewDomainError(ErrCodeInvalidScore, "invalid score", nil)
ErrInvalidRankRange = NewDomainError(ErrCodeInvalidRankRange, "invalid rank range", nil)
ErrInvalidTopCount = NewDomainError(ErrCodeInvalidTopCount, "invalid top count", nil)
ErrInvalidContext = NewDomainError(ErrCodeInvalidContext, "invalid context", nil)
ErrInvalidCompetitionName = NewDomainError(ErrCodeInvalidCompetitionName, "invalid competition name", nil)
ErrInvalidCompetitionStatus = NewDomainError(ErrCodeInvalidCompetitionStatus, "invalid competition status", nil)
ErrCompetitionNotYetStarted = NewDomainError(ErrCodeCompetitionNotYetStarted, "competition not yet started", nil)
ErrCompetitionNotActive = NewDomainError(ErrCodeCompetitionNotActive, "competition not active", nil)
ErrCompetitionAlreadyCompleted = NewDomainError(ErrCodeCompetitionAlreadyCompleted, "competition already completed", nil)
ErrPlayerNotQualified = NewDomainError(ErrCodePlayerNotQualified, "player not qualified", nil)
ErrCompetitionFull = NewDomainError(ErrCodeCompetitionFull, "competition full", nil)
ErrScoreBelowMinimum = NewDomainError(ErrCodeScoreBelowMinimum, "score below minimum", nil)
ErrTooManyEntriesPerPlayer = NewDomainError(ErrCodeTooManyEntriesPerPlayer, "too many entries per player", nil)
ErrEntryNotFound = NewDomainError(ErrCodeEntryNotFound, "entry not found", nil)
ErrInvalidStartTime = NewDomainError(ErrCodeInvalidStartTime, "invalid start time", nil)
ErrInvalidEndTime = NewDomainError(ErrCodeInvalidEndTime, "invalid end time", nil)
)