slack5-1770529463/services/preferences-api/internal/domain/preference.go
rdev-worker 73532902e7
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /implement-feature user-preferences
2026-02-08 06:13:10 +00:00

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
}