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.

512 lines
12 KiB
Go

package valueobjects
import (
"fmt"
"strings"
"knowfoolery/backend/shared/types"
"knowfoolery/backend/shared/errors"
)
// Theme represents a question theme/category
type Theme struct {
id types.ThemeID
name string
description string
color string // Hex color code for UI
icon string // Icon identifier
parentID *types.ThemeID // For hierarchical themes
isActive bool
sortOrder int
createdAt types.Timestamp
updatedAt types.Timestamp
}
// NewTheme creates a new theme
func NewTheme(
name string,
description string,
) (*Theme, error) {
// Validate inputs
if err := validateThemeName(name); err != nil {
return nil, fmt.Errorf("invalid theme name: %w", err)
}
if err := validateThemeDescription(description); err != nil {
return nil, fmt.Errorf("invalid theme description: %w", err)
}
now := types.NewTimestamp()
theme := &Theme{
id: types.NewThemeID(),
name: strings.TrimSpace(name),
description: strings.TrimSpace(description),
color: types.DefaultThemeColor,
icon: types.DefaultThemeIcon,
parentID: nil,
isActive: true,
sortOrder: 0,
createdAt: now,
updatedAt: now,
}
return theme, nil
}
// NewThemeWithID creates a theme with a specific ID (for loading from persistence)
func NewThemeWithID(
id types.ThemeID,
name string,
description string,
createdAt types.Timestamp,
) (*Theme, error) {
if id.IsEmpty() {
return nil, errors.ErrValidationFailed("id", "theme ID cannot be empty")
}
theme, err := NewTheme(name, description)
if err != nil {
return nil, err
}
theme.id = id
theme.createdAt = createdAt
theme.updatedAt = createdAt
return theme, nil
}
// Predefined themes factory functions
// NewGeographyTheme creates a geography theme
func NewGeographyTheme() *Theme {
theme, _ := NewTheme("Geography", "Questions about countries, capitals, landmarks, and physical geography")
theme.color = "#4CAF50"
theme.icon = "globe"
return theme
}
// NewHistoryTheme creates a history theme
func NewHistoryTheme() *Theme {
theme, _ := NewTheme("History", "Questions about historical events, figures, and periods")
theme.color = "#795548"
theme.icon = "history"
return theme
}
// NewScienceTheme creates a science theme
func NewScienceTheme() *Theme {
theme, _ := NewTheme("Science", "Questions about biology, chemistry, physics, and general science")
theme.color = "#2196F3"
theme.icon = "science"
return theme
}
// NewSportsTheme creates a sports theme
func NewSportsTheme() *Theme {
theme, _ := NewTheme("Sports", "Questions about sports, athletes, and sporting events")
theme.color = "#FF5722"
theme.icon = "sports"
return theme
}
// NewEntertainmentTheme creates an entertainment theme
func NewEntertainmentTheme() *Theme {
theme, _ := NewTheme("Entertainment", "Questions about movies, music, TV shows, and celebrities")
theme.color = "#E91E63"
theme.icon = "movie"
return theme
}
// NewLiteratureTheme creates a literature theme
func NewLiteratureTheme() *Theme {
theme, _ := NewTheme("Literature", "Questions about books, authors, and literary works")
theme.color = "#9C27B0"
theme.icon = "book"
return theme
}
// NewGeneralKnowledgeTheme creates a general knowledge theme
func NewGeneralKnowledgeTheme() *Theme {
theme, _ := NewTheme("General Knowledge", "Miscellaneous questions covering various topics")
theme.color = "#607D8B"
theme.icon = "lightbulb"
return theme
}
// Getters
// ID returns the theme's unique identifier
func (t *Theme) ID() types.ThemeID {
return t.id
}
// Name returns the theme name
func (t *Theme) Name() string {
return t.name
}
// Description returns the theme description
func (t *Theme) Description() string {
return t.description
}
// Color returns the theme color
func (t *Theme) Color() string {
return t.color
}
// Icon returns the theme icon
func (t *Theme) Icon() string {
return t.icon
}
// ParentID returns the parent theme ID if this is a sub-theme
func (t *Theme) ParentID() *types.ThemeID {
return t.parentID
}
// IsActive returns true if the theme is active
func (t *Theme) IsActive() bool {
return t.isActive
}
// SortOrder returns the sort order
func (t *Theme) SortOrder() int {
return t.sortOrder
}
// CreatedAt returns the creation timestamp
func (t *Theme) CreatedAt() types.Timestamp {
return t.createdAt
}
// UpdatedAt returns the last update timestamp
func (t *Theme) UpdatedAt() types.Timestamp {
return t.updatedAt
}
// Business methods
// UpdateName updates the theme name
func (t *Theme) UpdateName(newName string) error {
if err := validateThemeName(newName); err != nil {
return fmt.Errorf("invalid theme name: %w", err)
}
t.name = strings.TrimSpace(newName)
t.markUpdated()
return nil
}
// UpdateDescription updates the theme description
func (t *Theme) UpdateDescription(newDescription string) error {
if err := validateThemeDescription(newDescription); err != nil {
return fmt.Errorf("invalid theme description: %w", err)
}
t.description = strings.TrimSpace(newDescription)
t.markUpdated()
return nil
}
// SetColor sets the theme color
func (t *Theme) SetColor(color string) error {
color = strings.TrimSpace(color)
if color == "" {
t.color = types.DefaultThemeColor
} else {
if err := validateHexColor(color); err != nil {
return fmt.Errorf("invalid color: %w", err)
}
t.color = color
}
t.markUpdated()
return nil
}
// SetIcon sets the theme icon
func (t *Theme) SetIcon(icon string) error {
icon = strings.TrimSpace(icon)
if icon == "" {
t.icon = types.DefaultThemeIcon
} else {
if len(icon) > types.MaxIconNameLength {
return errors.ErrValidationFailed("icon", fmt.Sprintf("icon name too long (max %d characters)", types.MaxIconNameLength))
}
t.icon = icon
}
t.markUpdated()
return nil
}
// SetParent sets the parent theme
func (t *Theme) SetParent(parentID *types.ThemeID) error {
// Prevent circular references
if parentID != nil && *parentID == t.id {
return errors.ErrValidationFailed("parent_id", "theme cannot be its own parent")
}
t.parentID = parentID
t.markUpdated()
return nil
}
// Activate activates the theme
func (t *Theme) Activate() {
if !t.isActive {
t.isActive = true
t.markUpdated()
}
}
// Deactivate deactivates the theme
func (t *Theme) Deactivate() {
if t.isActive {
t.isActive = false
t.markUpdated()
}
}
// SetSortOrder sets the sort order
func (t *Theme) SetSortOrder(order int) {
if order < 0 {
order = 0
}
if t.sortOrder != order {
t.sortOrder = order
t.markUpdated()
}
}
// Query methods
// IsRootTheme returns true if this is a root theme (no parent)
func (t *Theme) IsRootTheme() bool {
return t.parentID == nil
}
// IsSubTheme returns true if this is a sub-theme (has parent)
func (t *Theme) IsSubTheme() bool {
return t.parentID != nil
}
// HasParent checks if this theme has the specified parent
func (t *Theme) HasParent(parentID types.ThemeID) bool {
return t.parentID != nil && *t.parentID == parentID
}
// Equals checks if two themes are equal
func (t *Theme) Equals(other *Theme) bool {
if other == nil {
return false
}
return t.id == other.id
}
// Validation methods
// Validate performs comprehensive validation
func (t *Theme) Validate() *types.ValidationResult {
result := types.NewValidationResult()
// Validate ID
if t.id.IsEmpty() {
result.AddError("theme ID cannot be empty")
}
// Validate name
if err := validateThemeName(t.name); err != nil {
result.AddErrorf("invalid name: %v", err)
}
// Validate description
if err := validateThemeDescription(t.description); err != nil {
result.AddErrorf("invalid description: %v", err)
}
// Validate color
if err := validateHexColor(t.color); err != nil {
result.AddErrorf("invalid color: %v", err)
}
// Validate icon
if len(t.icon) > types.MaxIconNameLength {
result.AddErrorf("icon name too long (max %d characters)", types.MaxIconNameLength)
}
// Validate sort order
if t.sortOrder < 0 {
result.AddError("sort order cannot be negative")
}
// Validate parent relationship
if t.parentID != nil && *t.parentID == t.id {
result.AddError("theme cannot be its own parent")
}
return result
}
// Helper methods
// markUpdated updates the timestamp
func (t *Theme) markUpdated() {
t.updatedAt = types.NewTimestamp()
}
// ToSnapshot creates a read-only snapshot of the theme
func (t *Theme) ToSnapshot() *ThemeSnapshot {
return &ThemeSnapshot{
ID: t.id,
Name: t.name,
Description: t.description,
Color: t.color,
Icon: t.icon,
ParentID: t.parentID,
IsActive: t.isActive,
SortOrder: t.sortOrder,
CreatedAt: t.createdAt,
UpdatedAt: t.updatedAt,
}
}
// ToDisplayName returns a formatted display name
func (t *Theme) ToDisplayName() string {
return t.name
}
// String returns a string representation of the theme
func (t *Theme) String() string {
if t.IsSubTheme() {
return fmt.Sprintf("%s (sub-theme)", t.name)
}
return t.name
}
// ThemeSnapshot represents a read-only view of theme data
type ThemeSnapshot struct {
ID types.ThemeID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Color string `json:"color"`
Icon string `json:"icon"`
ParentID *types.ThemeID `json:"parent_id,omitempty"`
IsActive bool `json:"is_active"`
SortOrder int `json:"sort_order"`
CreatedAt types.Timestamp `json:"created_at"`
UpdatedAt types.Timestamp `json:"updated_at"`
}
// GetDisplayName returns the display name
func (ts *ThemeSnapshot) GetDisplayName() string {
return ts.Name
}
// IsSubTheme returns true if this is a sub-theme
func (ts *ThemeSnapshot) IsSubTheme() bool {
return ts.ParentID != nil
}
// Validation helper functions
func validateThemeName(name string) error {
name = strings.TrimSpace(name)
if name == "" {
return errors.ErrValidationFailed("name", "theme name cannot be empty")
}
if len(name) < types.MinThemeNameLength {
return errors.ErrValidationFailed("name", fmt.Sprintf("theme name too short (min %d characters)", types.MinThemeNameLength))
}
if len(name) > types.MaxThemeNameLength {
return errors.ErrValidationFailed("name", fmt.Sprintf("theme name too long (max %d characters)", types.MaxThemeNameLength))
}
return nil
}
func validateThemeDescription(description string) error {
description = strings.TrimSpace(description)
if description == "" {
return errors.ErrValidationFailed("description", "theme description cannot be empty")
}
if len(description) < types.MinThemeDescriptionLength {
return errors.ErrValidationFailed("description", fmt.Sprintf("theme description too short (min %d characters)", types.MinThemeDescriptionLength))
}
if len(description) > types.MaxThemeDescriptionLength {
return errors.ErrValidationFailed("description", fmt.Sprintf("theme description too long (max %d characters)", types.MaxThemeDescriptionLength))
}
return nil
}
func validateHexColor(color string) error {
if color == "" {
return nil // Empty is allowed, will use default
}
if !strings.HasPrefix(color, "#") {
return errors.ErrValidationFailed("color", "color must start with #")
}
if len(color) != 7 {
return errors.ErrValidationFailed("color", "color must be in format #RRGGBB")
}
// Check if all characters after # are valid hex
for i, char := range color[1:] {
if !isHexChar(char) {
return errors.ErrValidationFailed("color", fmt.Sprintf("invalid hex character at position %d", i+1))
}
}
return nil
}
func isHexChar(char rune) bool {
return (char >= '0' && char <= '9') ||
(char >= 'A' && char <= 'F') ||
(char >= 'a' && char <= 'f')
}
// Theme collections for easy access
// GetDefaultThemes returns a list of default themes
func GetDefaultThemes() []*Theme {
return []*Theme{
NewGeographyTheme(),
NewHistoryTheme(),
NewScienceTheme(),
NewSportsTheme(),
NewEntertainmentTheme(),
NewLiteratureTheme(),
NewGeneralKnowledgeTheme(),
}
}
// GetThemeByName returns a default theme by name
func GetThemeByName(name string) *Theme {
themes := GetDefaultThemes()
name = strings.TrimSpace(strings.ToLower(name))
for _, theme := range themes {
if strings.ToLower(theme.Name()) == name {
return theme
}
}
return nil
}