slate-v3-1770514618/services/preferences-api/internal/domain/preferences.go
rdev-worker 1afe983cd6
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature user-preferences
2026-02-08 02:02:18 +00:00

113 lines
2.6 KiB
Go

package domain
import (
"regexp"
"time"
)
// UserID is a strongly-typed identifier for users.
type UserID string
func (id UserID) String() string { return string(id) }
func (id UserID) IsZero() bool { return id == "" }
// Allowed theme values.
var allowedThemes = map[string]bool{
"light": true,
"dark": true,
"system": true,
}
// Allowed digest values.
var allowedDigests = map[string]bool{
"daily": true,
"weekly": true,
"never": true,
}
var languageRegex = regexp.MustCompile(`^[a-z]{2}$`)
// NotificationSettings holds notification preferences.
type NotificationSettings struct {
Email bool
Push bool
Digest string
}
// Preferences holds all user preferences.
type Preferences struct {
UserID UserID
Theme string
Language string
Notifications NotificationSettings
UpdatedAt time.Time
}
// NewDefaultPreferences returns preferences with all defaults applied.
func NewDefaultPreferences(userID UserID) *Preferences {
return &Preferences{
UserID: userID,
Theme: "system",
Language: "en",
Notifications: NotificationSettings{
Email: true,
Push: true,
Digest: "weekly",
},
UpdatedAt: time.Now().UTC(),
}
}
// Validate checks that all field values are within allowed sets.
func (p *Preferences) Validate() error {
if !allowedThemes[p.Theme] {
return ErrInvalidTheme
}
if !languageRegex.MatchString(p.Language) {
return ErrInvalidLanguage
}
if !allowedDigests[p.Notifications.Digest] {
return ErrInvalidDigest
}
return nil
}
// NotificationSettingsUpdate uses pointer fields to distinguish provided vs absent.
type NotificationSettingsUpdate struct {
Email *bool
Push *bool
Digest *string
}
// PreferencesUpdate uses pointer fields to distinguish provided vs absent.
type PreferencesUpdate struct {
Theme *string
Language *string
Notifications *NotificationSettingsUpdate
}
// MergeFrom applies a shallow merge: only overwrites fields where the update pointer is non-nil.
// For Notifications, individual sub-fields are merged when provided.
func (p *Preferences) MergeFrom(incoming *PreferencesUpdate) {
if incoming == nil {
return
}
if incoming.Theme != nil {
p.Theme = *incoming.Theme
}
if incoming.Language != nil {
p.Language = *incoming.Language
}
if incoming.Notifications != nil {
if incoming.Notifications.Email != nil {
p.Notifications.Email = *incoming.Notifications.Email
}
if incoming.Notifications.Push != nil {
p.Notifications.Push = *incoming.Notifications.Push
}
if incoming.Notifications.Digest != nil {
p.Notifications.Digest = *incoming.Notifications.Digest
}
}
}