use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use tokio::sync::RwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserPreferences {
pub disabled_types: HashSet<String>,
#[serde(default)]
pub muted: bool,
}
impl UserPreferences {
pub fn new() -> Self {
Self {
disabled_types: HashSet::new(),
muted: false,
}
}
pub fn is_type_enabled(&self, notification_type: &str) -> bool {
if self.muted {
return false;
}
!self.disabled_types.contains(notification_type)
}
pub fn disable_type(&mut self, notification_type: impl Into<String>) {
self.disabled_types.insert(notification_type.into());
}
pub fn enable_type(&mut self, notification_type: &str) {
self.disabled_types.remove(notification_type);
}
}
impl Default for UserPreferences {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct NotificationPreferencesStore {
preferences: RwLock<HashMap<String, UserPreferences>>,
}
impl NotificationPreferencesStore {
pub fn new() -> Self {
Self {
preferences: RwLock::new(HashMap::new()),
}
}
pub async fn get(&self, user_id: &str) -> UserPreferences {
let store = self.preferences.read().await;
store.get(user_id).cloned().unwrap_or_default()
}
pub async fn update(&self, user_id: impl Into<String>, prefs: UserPreferences) {
let mut store = self.preferences.write().await;
store.insert(user_id.into(), prefs);
}
pub async fn is_enabled(&self, user_id: &str, notification_type: &str) -> bool {
let store = self.preferences.read().await;
match store.get(user_id) {
Some(prefs) => prefs.is_type_enabled(notification_type),
None => true, }
}
pub async fn disable_type(&self, user_id: &str, notification_type: &str) {
let mut store = self.preferences.write().await;
let prefs = store
.entry(user_id.to_string())
.or_insert_with(UserPreferences::new);
prefs.disable_type(notification_type);
}
pub async fn enable_type(&self, user_id: &str, notification_type: &str) {
let mut store = self.preferences.write().await;
if let Some(prefs) = store.get_mut(user_id) {
prefs.enable_type(notification_type);
}
}
pub async fn mute(&self, user_id: &str) {
let mut store = self.preferences.write().await;
let prefs = store
.entry(user_id.to_string())
.or_insert_with(UserPreferences::new);
prefs.muted = true;
}
pub async fn unmute(&self, user_id: &str) {
let mut store = self.preferences.write().await;
if let Some(prefs) = store.get_mut(user_id) {
prefs.muted = false;
}
}
}
impl Default for NotificationPreferencesStore {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_prefs_default_all_enabled() {
let prefs = UserPreferences::new();
assert!(prefs.is_type_enabled("new_follower"));
assert!(prefs.is_type_enabled("new_like"));
assert!(prefs.is_type_enabled("anything"));
assert!(!prefs.muted);
}
#[test]
fn test_user_prefs_disable_type() {
let mut prefs = UserPreferences::new();
prefs.disable_type("new_like");
assert!(!prefs.is_type_enabled("new_like"));
assert!(prefs.is_type_enabled("new_follower"));
}
#[test]
fn test_user_prefs_enable_type() {
let mut prefs = UserPreferences::new();
prefs.disable_type("new_like");
assert!(!prefs.is_type_enabled("new_like"));
prefs.enable_type("new_like");
assert!(prefs.is_type_enabled("new_like"));
}
#[test]
fn test_user_prefs_muted() {
let mut prefs = UserPreferences::new();
prefs.muted = true;
assert!(!prefs.is_type_enabled("new_follower"));
assert!(!prefs.is_type_enabled("new_like"));
assert!(!prefs.is_type_enabled("anything"));
}
#[tokio::test]
async fn test_store_default_all_enabled() {
let store = NotificationPreferencesStore::new();
assert!(store.is_enabled("user-A", "new_follower").await);
assert!(store.is_enabled("user-A", "new_like").await);
}
#[tokio::test]
async fn test_store_disable_type() {
let store = NotificationPreferencesStore::new();
store.disable_type("user-A", "new_like").await;
assert!(!store.is_enabled("user-A", "new_like").await);
assert!(store.is_enabled("user-A", "new_follower").await);
assert!(store.is_enabled("user-B", "new_like").await);
}
#[tokio::test]
async fn test_store_enable_type() {
let store = NotificationPreferencesStore::new();
store.disable_type("user-A", "new_like").await;
store.enable_type("user-A", "new_like").await;
assert!(store.is_enabled("user-A", "new_like").await);
}
#[tokio::test]
async fn test_store_mute_unmute() {
let store = NotificationPreferencesStore::new();
store.mute("user-A").await;
assert!(!store.is_enabled("user-A", "new_follower").await);
assert!(!store.is_enabled("user-A", "new_like").await);
store.unmute("user-A").await;
assert!(store.is_enabled("user-A", "new_follower").await);
}
#[tokio::test]
async fn test_store_update_full_preferences() {
let store = NotificationPreferencesStore::new();
let mut prefs = UserPreferences::new();
prefs.disable_type("new_follower");
prefs.disable_type("new_comment");
store.update("user-A", prefs).await;
assert!(!store.is_enabled("user-A", "new_follower").await);
assert!(!store.is_enabled("user-A", "new_comment").await);
assert!(store.is_enabled("user-A", "new_like").await);
}
#[tokio::test]
async fn test_store_get_returns_defaults() {
let store = NotificationPreferencesStore::new();
let prefs = store.get("nonexistent").await;
assert!(prefs.disabled_types.is_empty());
assert!(!prefs.muted);
}
#[tokio::test]
async fn test_store_get_returns_updated() {
let store = NotificationPreferencesStore::new();
store.disable_type("user-A", "new_like").await;
let prefs = store.get("user-A").await;
assert!(prefs.disabled_types.contains("new_like"));
}
}