saorsa_agent/session/
bookmark.rs1use crate::SaorsaAgentError;
4use crate::session::SessionId;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Bookmark {
13 pub name: String,
15 pub session_id: SessionId,
17 pub created: chrono::DateTime<chrono::Utc>,
19}
20
21#[derive(Debug, Clone)]
23pub struct BookmarkManager {
24 bookmarks_path: PathBuf,
25}
26
27impl BookmarkManager {
28 pub fn new() -> Result<Self, SaorsaAgentError> {
30 let base = crate::session::path::sessions_dir()?;
31 let bookmarks_path = base
32 .parent()
33 .ok_or_else(|| SaorsaAgentError::Session("Invalid sessions directory".to_string()))?
34 .join("bookmarks.json");
35 Ok(Self { bookmarks_path })
36 }
37
38 pub fn with_path(path: PathBuf) -> Self {
40 Self {
41 bookmarks_path: path,
42 }
43 }
44
45 fn load_bookmarks(&self) -> Result<HashMap<String, Bookmark>, SaorsaAgentError> {
47 if !self.bookmarks_path.exists() {
48 return Ok(HashMap::new());
49 }
50
51 let json = fs::read_to_string(&self.bookmarks_path).map_err(|e| {
52 SaorsaAgentError::Session(format!("Failed to read bookmarks file: {}", e))
53 })?;
54
55 serde_json::from_str(&json)
56 .map_err(|e| SaorsaAgentError::Session(format!("Failed to parse bookmarks: {}", e)))
57 }
58
59 fn save_bookmarks(
61 &self,
62 bookmarks: &HashMap<String, Bookmark>,
63 ) -> Result<(), SaorsaAgentError> {
64 if let Some(parent) = self.bookmarks_path.parent() {
66 crate::session::path::ensure_dir(parent)?;
67 }
68
69 let json = serde_json::to_string_pretty(bookmarks).map_err(|e| {
70 SaorsaAgentError::Session(format!("Failed to serialize bookmarks: {}", e))
71 })?;
72
73 fs::write(&self.bookmarks_path, json).map_err(|e| {
74 SaorsaAgentError::Session(format!("Failed to write bookmarks file: {}", e))
75 })?;
76
77 Ok(())
78 }
79
80 pub fn add_bookmark(
82 &self,
83 name: String,
84 session_id: SessionId,
85 ) -> Result<(), SaorsaAgentError> {
86 let mut bookmarks = self.load_bookmarks()?;
87
88 bookmarks.insert(
89 name.clone(),
90 Bookmark {
91 name,
92 session_id,
93 created: chrono::Utc::now(),
94 },
95 );
96
97 self.save_bookmarks(&bookmarks)?;
98 Ok(())
99 }
100
101 pub fn remove_bookmark(&self, name: &str) -> Result<bool, SaorsaAgentError> {
103 let mut bookmarks = self.load_bookmarks()?;
104
105 let removed = bookmarks.remove(name).is_some();
106 if removed {
107 self.save_bookmarks(&bookmarks)?;
108 }
109
110 Ok(removed)
111 }
112
113 pub fn rename_bookmark(
115 &self,
116 old_name: &str,
117 new_name: String,
118 ) -> Result<(), SaorsaAgentError> {
119 let mut bookmarks = self.load_bookmarks()?;
120
121 let bookmark = bookmarks.remove(old_name).ok_or_else(|| {
122 SaorsaAgentError::Session(format!("Bookmark '{}' not found", old_name))
123 })?;
124
125 bookmarks.insert(
126 new_name.clone(),
127 Bookmark {
128 name: new_name,
129 ..bookmark
130 },
131 );
132
133 self.save_bookmarks(&bookmarks)?;
134 Ok(())
135 }
136
137 pub fn get_bookmark(&self, name: &str) -> Result<Option<Bookmark>, SaorsaAgentError> {
139 let bookmarks = self.load_bookmarks()?;
140 Ok(bookmarks.get(name).cloned())
141 }
142
143 pub fn list_bookmarks(&self) -> Result<Vec<Bookmark>, SaorsaAgentError> {
145 let bookmarks = self.load_bookmarks()?;
146 let mut list: Vec<Bookmark> = bookmarks.into_values().collect();
147 list.sort_by(|a, b| a.name.cmp(&b.name));
148 Ok(list)
149 }
150
151 pub fn generate_auto_name(&self) -> Result<String, SaorsaAgentError> {
153 let bookmarks = self.load_bookmarks()?;
154 let mut counter = 1;
155
156 loop {
157 let name = format!("bookmark-{}", counter);
158 if !bookmarks.contains_key(&name) {
159 return Ok(name);
160 }
161 counter += 1;
162 if counter > 10000 {
163 return Err(SaorsaAgentError::Session(
164 "Could not generate unique bookmark name".to_string(),
165 ));
166 }
167 }
168 }
169}
170
171impl Default for BookmarkManager {
172 fn default() -> Self {
173 Self::new().unwrap_or_else(|_| Self::with_path(PathBuf::from("/tmp/saorsa-bookmarks.json")))
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use tempfile::TempDir;
181
182 fn test_manager() -> (TempDir, BookmarkManager) {
183 let temp_dir = match TempDir::new() {
184 Ok(dir) => dir,
185 Err(_) => panic!("Failed to create temp dir for test"),
186 };
187 let path = temp_dir.path().join("bookmarks.json");
188 let manager = BookmarkManager::with_path(path);
189 (temp_dir, manager)
190 }
191
192 #[test]
193 fn test_add_and_get_bookmark() {
194 let (_temp, manager) = test_manager();
195 let session_id = SessionId::new();
196
197 assert!(manager.add_bookmark("test".to_string(), session_id).is_ok());
198
199 let result = manager.get_bookmark("test");
200 assert!(result.is_ok());
201 match result {
202 Ok(Some(bookmark)) => {
203 assert!(bookmark.name == "test");
204 assert!(bookmark.session_id == session_id);
205 }
206 Ok(None) => panic!("Expected bookmark to exist"),
207 Err(_) => unreachable!(),
208 }
209 }
210
211 #[test]
212 fn test_remove_bookmark() {
213 let (_temp, manager) = test_manager();
214 let session_id = SessionId::new();
215
216 assert!(
217 manager
218 .add_bookmark("remove-me".to_string(), session_id)
219 .is_ok()
220 );
221
222 let removed = manager.remove_bookmark("remove-me");
223 assert!(removed.is_ok());
224 match removed {
225 Ok(true) => {}
226 Ok(false) => panic!("Expected bookmark to be removed"),
227 Err(_) => unreachable!(),
228 }
229
230 let result = manager.get_bookmark("remove-me");
231 assert!(result.is_ok());
232 match result {
233 Ok(None) => {}
234 Ok(Some(_)) => panic!("Expected bookmark to not exist"),
235 Err(_) => unreachable!(),
236 }
237 }
238
239 #[test]
240 fn test_rename_bookmark() {
241 let (_temp, manager) = test_manager();
242 let session_id = SessionId::new();
243
244 assert!(
245 manager
246 .add_bookmark("old-name".to_string(), session_id)
247 .is_ok()
248 );
249
250 let result = manager.rename_bookmark("old-name", "new-name".to_string());
251 assert!(result.is_ok());
252
253 let old = manager.get_bookmark("old-name");
255 assert!(old.is_ok());
256 match old {
257 Ok(None) => {}
258 Ok(Some(_)) => panic!("Old bookmark should not exist"),
259 Err(_) => unreachable!(),
260 }
261
262 let new = manager.get_bookmark("new-name");
264 assert!(new.is_ok());
265 match new {
266 Ok(Some(bookmark)) => {
267 assert!(bookmark.session_id == session_id);
268 }
269 Ok(None) => panic!("New bookmark should exist"),
270 Err(_) => unreachable!(),
271 }
272 }
273
274 #[test]
275 fn test_list_bookmarks_sorted() {
276 let (_temp, manager) = test_manager();
277
278 let id1 = SessionId::new();
279 let id2 = SessionId::new();
280 let id3 = SessionId::new();
281
282 assert!(manager.add_bookmark("zebra".to_string(), id1).is_ok());
283 assert!(manager.add_bookmark("alpha".to_string(), id2).is_ok());
284 assert!(manager.add_bookmark("beta".to_string(), id3).is_ok());
285
286 let list = manager.list_bookmarks();
287 assert!(list.is_ok());
288 match list {
289 Ok(bookmarks) => {
290 assert!(bookmarks.len() == 3);
291 assert!(bookmarks[0].name == "alpha");
292 assert!(bookmarks[1].name == "beta");
293 assert!(bookmarks[2].name == "zebra");
294 }
295 Err(_) => unreachable!(),
296 }
297 }
298
299 #[test]
300 fn test_generate_auto_name() {
301 let (_temp, manager) = test_manager();
302
303 let name1 = manager.generate_auto_name();
304 assert!(name1.is_ok());
305 match name1 {
306 Ok(name) => {
307 assert!(name == "bookmark-1");
308
309 assert!(manager.add_bookmark(name, SessionId::new()).is_ok());
311
312 let name2 = manager.generate_auto_name();
314 assert!(name2.is_ok());
315 match name2 {
316 Ok(n) => assert!(n == "bookmark-2"),
317 Err(_) => unreachable!(),
318 }
319 }
320 Err(_) => unreachable!(),
321 }
322 }
323}