134 lines
3.6 KiB
Go
134 lines
3.6 KiB
Go
package domain
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
)
|
|
|
|
// PreferenceKey is a typed key for a known preference.
|
|
type PreferenceKey string
|
|
|
|
const (
|
|
KeyTheme PreferenceKey = "theme"
|
|
KeyLanguage PreferenceKey = "language"
|
|
KeyNotificationsEnabled PreferenceKey = "notifications_enabled"
|
|
)
|
|
|
|
// PreferenceDefinition describes a known preference key with its default and validator.
|
|
type PreferenceDefinition struct {
|
|
Key PreferenceKey
|
|
DefaultValue string
|
|
Validate func(value string) error
|
|
}
|
|
|
|
// UserPreferences is the aggregate representing all preferences for a user.
|
|
type UserPreferences struct {
|
|
UserID string
|
|
Preferences map[PreferenceKey]string
|
|
}
|
|
|
|
// uuidRegex validates UUID format.
|
|
var uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
|
|
|
|
// bcp47Regex validates BCP 47 language tags.
|
|
var bcp47Regex = regexp.MustCompile(`^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{1,8})*$`)
|
|
|
|
// validThemes is the set of allowed theme values.
|
|
var validThemes = map[string]bool{
|
|
"light": true,
|
|
"dark": true,
|
|
"system": true,
|
|
}
|
|
|
|
// registry holds the definitions for all known preference keys.
|
|
var registry = map[PreferenceKey]PreferenceDefinition{
|
|
KeyTheme: {
|
|
Key: KeyTheme,
|
|
DefaultValue: "system",
|
|
Validate: func(value string) error {
|
|
if !validThemes[value] {
|
|
return fmt.Errorf("%w: theme must be one of: light, dark, system", ErrInvalidPreferenceValue)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
KeyLanguage: {
|
|
Key: KeyLanguage,
|
|
DefaultValue: "en",
|
|
Validate: func(value string) error {
|
|
if !bcp47Regex.MatchString(value) {
|
|
return fmt.Errorf("%w: language must be a valid BCP 47 tag", ErrInvalidPreferenceValue)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
KeyNotificationsEnabled: {
|
|
Key: KeyNotificationsEnabled,
|
|
DefaultValue: "true",
|
|
Validate: func(value string) error {
|
|
if value != "true" && value != "false" {
|
|
return fmt.Errorf("%w: notifications_enabled must be true or false", ErrInvalidPreferenceValue)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
// ValidateUserID checks that userID is a valid UUID.
|
|
func ValidateUserID(userID string) error {
|
|
if !uuidRegex.MatchString(userID) {
|
|
return ErrInvalidUserID
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateKey checks that key is a known preference key.
|
|
func ValidateKey(key string) error {
|
|
if _, ok := registry[PreferenceKey(key)]; !ok {
|
|
return fmt.Errorf("%w: %s", ErrUnknownPreferenceKey, key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateValue validates a value for the given preference key.
|
|
func ValidateValue(key PreferenceKey, value string) error {
|
|
def, ok := registry[key]
|
|
if !ok {
|
|
return fmt.Errorf("%w: %s", ErrUnknownPreferenceKey, key)
|
|
}
|
|
return def.Validate(value)
|
|
}
|
|
|
|
// DefaultPreferences returns all known keys with their default values.
|
|
func DefaultPreferences() map[PreferenceKey]string {
|
|
defaults := make(map[PreferenceKey]string, len(registry))
|
|
for k, def := range registry {
|
|
defaults[k] = def.DefaultValue
|
|
}
|
|
return defaults
|
|
}
|
|
|
|
// MergeWithDefaults fills in missing keys from defaults, preserving stored values.
|
|
func MergeWithDefaults(stored map[PreferenceKey]string) map[PreferenceKey]string {
|
|
merged := DefaultPreferences()
|
|
for k, v := range stored {
|
|
merged[k] = v
|
|
}
|
|
return merged
|
|
}
|
|
|
|
// SerializeForResponse converts stored string values to typed values for JSON.
|
|
// notifications_enabled "true"/"false" becomes boolean true/false.
|
|
func SerializeForResponse(prefs map[PreferenceKey]string) map[string]any {
|
|
result := make(map[string]any, len(prefs))
|
|
for k, v := range prefs {
|
|
switch k {
|
|
case KeyNotificationsEnabled:
|
|
result[string(k)] = v == "true"
|
|
default:
|
|
result[string(k)] = v
|
|
}
|
|
}
|
|
return result
|
|
}
|