slack5-1770541397/services/preferences-api/internal/domain/preferences.go
rdev-worker e3e19a3fa8
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /implement-feature user-preferences
2026-02-08 09:29:22 +00:00

144 lines
3.3 KiB
Go

package domain
import (
"encoding/json"
"time"
"unicode/utf8"
)
// UserID is a strongly-typed identifier for users.
type UserID string
// String returns the string representation of the ID.
func (id UserID) String() string {
return string(id)
}
// ValidThemes lists the allowed theme values.
var ValidThemes = []string{"light", "dark", "system"}
// NotificationPreferences controls notification delivery channels.
type NotificationPreferences struct {
Email bool `json:"email"`
Push bool `json:"push"`
SMS bool `json:"sms"`
}
// Preferences holds a user's preference settings.
// Known fields are typed; unknown keys are preserved in Extra for extensibility.
type Preferences struct {
Theme string `json:"theme"`
Language string `json:"language"`
Notifications NotificationPreferences `json:"notifications"`
Extra map[string]any `json:"-"`
}
// MarshalJSON merges known fields and Extra into a single JSON object.
func (p Preferences) MarshalJSON() ([]byte, error) {
// Build a map with known fields
m := make(map[string]any)
// Copy extra fields first (so known fields override if there's overlap)
for k, v := range p.Extra {
m[k] = v
}
m["theme"] = p.Theme
m["language"] = p.Language
m["notifications"] = p.Notifications
return json.Marshal(m)
}
// UnmarshalJSON decodes known fields into struct fields and captures unknown keys in Extra.
func (p *Preferences) UnmarshalJSON(data []byte) error {
// Decode known fields via an alias to avoid recursion
type Alias Preferences
var alias Alias
if err := json.Unmarshal(data, &alias); err != nil {
return err
}
// Decode all fields into a map to find unknown keys
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
// Copy known fields
p.Theme = alias.Theme
p.Language = alias.Language
p.Notifications = alias.Notifications
// Capture unknown keys
knownKeys := map[string]bool{
"theme": true,
"language": true,
"notifications": true,
}
p.Extra = nil
for k, v := range raw {
if knownKeys[k] {
continue
}
if p.Extra == nil {
p.Extra = make(map[string]any)
}
var val any
if err := json.Unmarshal(v, &val); err != nil {
return err
}
p.Extra[k] = val
}
return nil
}
// MaxLanguageLen is the maximum rune length for a language tag.
const MaxLanguageLen = 10
// Validate checks the known preference fields.
// Returns ErrInvalidTheme if theme is not a valid value.
// Returns ErrInvalidLanguage if language exceeds MaxLanguageLen runes.
func (p *Preferences) Validate() error {
if p.Theme != "" {
valid := false
for _, t := range ValidThemes {
if p.Theme == t {
valid = true
break
}
}
if !valid {
return ErrInvalidTheme
}
}
if utf8.RuneCountInString(p.Language) > MaxLanguageLen {
return ErrInvalidLanguage
}
return nil
}
// DefaultPreferences returns the default preference values.
func DefaultPreferences() Preferences {
return Preferences{
Theme: "system",
Language: "en",
Notifications: NotificationPreferences{
Email: true,
Push: true,
SMS: false,
},
}
}
// UserPreferences is the aggregate associating preferences with a user.
type UserPreferences struct {
UserID UserID
Preferences Preferences
UpdatedAt time.Time
}