144 lines
3.3 KiB
Go
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
|
|
}
|