use std::collections::HashMap;
use async_trait::async_trait;
use chrono::Utc;
use xz_memory_core::{Entry, EntryStore, IndexSearcher, StoreError};
use super::traits::UserProfileMemory;
impl<S: EntryStore, I: IndexSearcher> super::default::LayeredMemory<S, I> {
fn user_profile_partition(user_id: &str) -> String {
format!("user:{}:profile", user_id)
}
}
#[async_trait]
impl<S: EntryStore, I: IndexSearcher> UserProfileMemory for super::default::LayeredMemory<S, I> {
async fn get_preferences(&self, user_id: &str) -> Result<HashMap<String, String>, StoreError> {
let partition = Self::user_profile_partition(user_id);
let prefix = format!("{}:", user_id);
let opts = xz_memory_core::QueryOptions {
limit: usize::MAX,
sort: xz_memory_core::SortOrder::Ascending,
};
let range = xz_memory_core::TimeRange { start: None, end: None };
let entries = self.store.query(&partition, &range, &opts).await?;
let mut map = HashMap::new();
for e in entries {
if let Some(key) = e.id.strip_prefix(&prefix) {
map.insert(key.to_string(), e.body);
}
}
Ok(map)
}
async fn set_preference(
&self,
user_id: &str,
key: &str,
value: &str,
) -> Result<(), StoreError> {
let partition = Self::user_profile_partition(user_id);
let entry_id = format!("{}:{}", user_id, key);
let _ = self.store.delete(&entry_id).await;
let entry = Entry {
id: entry_id,
partition,
body: value.to_string(),
recorded_at: Utc::now().timestamp_millis() as u64,
};
self.store.append(entry).await
}
async fn remove_preference(&self, user_id: &str, key: &str) -> Result<(), StoreError> {
let entry_id = format!("{}:{}", user_id, key);
let _ = self.store.delete(&entry_id).await;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use xz_memory_core::{ScoredEntry, SearchOptions};
use crate::backends::InMemoryEntryStore;
struct MockSearcher;
#[async_trait]
impl IndexSearcher for MockSearcher {
async fn search(
&self,
_partitions: &[String],
_query: &str,
_opts: &SearchOptions,
) -> Result<Vec<ScoredEntry>, StoreError> {
Ok(vec![])
}
}
fn setup() -> Arc<super::super::default::LayeredMemory<InMemoryEntryStore, MockSearcher>> {
Arc::new(super::super::default::LayeredMemory::new(
Arc::new(InMemoryEntryStore::new()),
Arc::new(MockSearcher),
))
}
#[tokio::test]
async fn test_set_and_get_preferences() {
let memory = setup();
memory.set_preference("u1", "lang", "zh-CN").await.unwrap();
memory.set_preference("u1", "theme", "dark").await.unwrap();
let prefs = memory.get_preferences("u1").await.unwrap();
assert_eq!(prefs.get("lang").map(String::as_str), Some("zh-CN"));
assert_eq!(prefs.get("theme").map(String::as_str), Some("dark"));
}
#[tokio::test]
async fn test_overwrite_preference() {
let memory = setup();
memory.set_preference("u1", "lang", "en").await.unwrap();
memory.set_preference("u1", "lang", "ja").await.unwrap();
let prefs = memory.get_preferences("u1").await.unwrap();
assert_eq!(prefs.len(), 1);
assert_eq!(prefs.get("lang").map(String::as_str), Some("ja"));
}
#[tokio::test]
async fn test_remove_preference() {
let memory = setup();
memory.set_preference("u1", "lang", "en").await.unwrap();
memory.set_preference("u1", "theme", "dark").await.unwrap();
memory.remove_preference("u1", "lang").await.unwrap();
let prefs = memory.get_preferences("u1").await.unwrap();
assert_eq!(prefs.len(), 1);
assert!(!prefs.contains_key("lang"));
assert_eq!(prefs.get("theme").map(String::as_str), Some("dark"));
}
#[tokio::test]
async fn test_remove_nonexistent_is_noop() {
let memory = setup();
let result = memory.remove_preference("u1", "nonexistent").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_empty_preferences() {
let memory = setup();
let prefs = memory.get_preferences("nonexistent").await.unwrap();
assert!(prefs.is_empty());
}
#[tokio::test]
async fn test_isolated_users() {
let memory = setup();
memory.set_preference("u1", "lang", "en").await.unwrap();
memory.set_preference("u2", "lang", "fr").await.unwrap();
let prefs_u1 = memory.get_preferences("u1").await.unwrap();
let prefs_u2 = memory.get_preferences("u2").await.unwrap();
assert_eq!(prefs_u1.get("lang").map(String::as_str), Some("en"));
assert_eq!(prefs_u2.get("lang").map(String::as_str), Some("fr"));
}
}