xz_memory_engine/layered/
user_profile.rs1use std::collections::HashMap;
8
9use async_trait::async_trait;
10use chrono::Utc;
11use xz_memory_core::{Entry, EntryStore, IndexSearcher, StoreError};
12
13use super::traits::UserProfileMemory;
14
15impl<S: EntryStore, I: IndexSearcher> super::default::LayeredMemory<S, I> {
16 fn user_profile_partition(user_id: &str) -> String {
17 format!("user:{}:profile", user_id)
18 }
19}
20
21#[async_trait]
22impl<S: EntryStore, I: IndexSearcher> UserProfileMemory for super::default::LayeredMemory<S, I> {
23 async fn get_preferences(&self, user_id: &str) -> Result<HashMap<String, String>, StoreError> {
24 let partition = Self::user_profile_partition(user_id);
25 let prefix = format!("{}:", user_id);
26 let opts = xz_memory_core::QueryOptions {
27 limit: usize::MAX,
28 sort: xz_memory_core::SortOrder::Ascending,
29 };
30 let range = xz_memory_core::TimeRange { start: None, end: None };
31 let entries = self.store.query(&partition, &range, &opts).await?;
32 let mut map = HashMap::new();
33 for e in entries {
34 if let Some(key) = e.id.strip_prefix(&prefix) {
35 map.insert(key.to_string(), e.body);
36 }
37 }
38 Ok(map)
39 }
40
41 async fn set_preference(
42 &self,
43 user_id: &str,
44 key: &str,
45 value: &str,
46 ) -> Result<(), StoreError> {
47 let partition = Self::user_profile_partition(user_id);
48 let entry_id = format!("{}:{}", user_id, key);
49 let _ = self.store.delete(&entry_id).await;
50 let entry = Entry {
51 id: entry_id,
52 partition,
53 body: value.to_string(),
54 recorded_at: Utc::now().timestamp_millis() as u64,
55 };
56 self.store.append(entry).await
57 }
58
59 async fn remove_preference(&self, user_id: &str, key: &str) -> Result<(), StoreError> {
60 let entry_id = format!("{}:{}", user_id, key);
61 let _ = self.store.delete(&entry_id).await;
62 Ok(())
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use std::sync::Arc;
70 use xz_memory_core::{ScoredEntry, SearchOptions};
71
72 use crate::backends::InMemoryEntryStore;
73
74 struct MockSearcher;
75
76 #[async_trait]
77 impl IndexSearcher for MockSearcher {
78 async fn search(
79 &self,
80 _partitions: &[String],
81 _query: &str,
82 _opts: &SearchOptions,
83 ) -> Result<Vec<ScoredEntry>, StoreError> {
84 Ok(vec![])
85 }
86 }
87
88 fn setup() -> Arc<super::super::default::LayeredMemory<InMemoryEntryStore, MockSearcher>> {
89 Arc::new(super::super::default::LayeredMemory::new(
90 Arc::new(InMemoryEntryStore::new()),
91 Arc::new(MockSearcher),
92 ))
93 }
94
95 #[tokio::test]
96 async fn test_set_and_get_preferences() {
97 let memory = setup();
98
99 memory.set_preference("u1", "lang", "zh-CN").await.unwrap();
100 memory.set_preference("u1", "theme", "dark").await.unwrap();
101
102 let prefs = memory.get_preferences("u1").await.unwrap();
103 assert_eq!(prefs.get("lang").map(String::as_str), Some("zh-CN"));
104 assert_eq!(prefs.get("theme").map(String::as_str), Some("dark"));
105 }
106
107 #[tokio::test]
108 async fn test_overwrite_preference() {
109 let memory = setup();
110
111 memory.set_preference("u1", "lang", "en").await.unwrap();
112 memory.set_preference("u1", "lang", "ja").await.unwrap();
113
114 let prefs = memory.get_preferences("u1").await.unwrap();
115 assert_eq!(prefs.len(), 1);
116 assert_eq!(prefs.get("lang").map(String::as_str), Some("ja"));
117 }
118
119 #[tokio::test]
120 async fn test_remove_preference() {
121 let memory = setup();
122
123 memory.set_preference("u1", "lang", "en").await.unwrap();
124 memory.set_preference("u1", "theme", "dark").await.unwrap();
125 memory.remove_preference("u1", "lang").await.unwrap();
126
127 let prefs = memory.get_preferences("u1").await.unwrap();
128 assert_eq!(prefs.len(), 1);
129 assert!(!prefs.contains_key("lang"));
130 assert_eq!(prefs.get("theme").map(String::as_str), Some("dark"));
131 }
132
133 #[tokio::test]
134 async fn test_remove_nonexistent_is_noop() {
135 let memory = setup();
136 let result = memory.remove_preference("u1", "nonexistent").await;
137 assert!(result.is_ok());
138 }
139
140 #[tokio::test]
141 async fn test_empty_preferences() {
142 let memory = setup();
143 let prefs = memory.get_preferences("nonexistent").await.unwrap();
144 assert!(prefs.is_empty());
145 }
146
147 #[tokio::test]
148 async fn test_isolated_users() {
149 let memory = setup();
150
151 memory.set_preference("u1", "lang", "en").await.unwrap();
152 memory.set_preference("u2", "lang", "fr").await.unwrap();
153
154 let prefs_u1 = memory.get_preferences("u1").await.unwrap();
155 let prefs_u2 = memory.get_preferences("u2").await.unwrap();
156
157 assert_eq!(prefs_u1.get("lang").map(String::as_str), Some("en"));
158 assert_eq!(prefs_u2.get("lang").map(String::as_str), Some("fr"));
159 }
160}