Skip to main content

bamboo_memory/memory_store/
paths.rs

1use std::path::{Path, PathBuf};
2
3use bamboo_infrastructure::paths::bamboo_dir;
4
5use super::types::MemoryScope;
6
7pub const MEMORY_ROOT_DIR: &str = "memory";
8pub const MEMORY_VERSION_DIR: &str = "v1";
9pub const SESSIONS_DIR: &str = "sessions";
10pub const SCOPES_DIR: &str = "scopes";
11pub const GLOBAL_DIR: &str = "global";
12pub const PROJECTS_DIR: &str = "projects";
13pub const NOTE_DIR: &str = "note";
14pub const STATE_DIR: &str = "state";
15pub const INDEXES_DIR: &str = "indexes";
16pub const VIEWS_DIR: &str = "views";
17pub const LOGS_DIR: &str = "logs";
18pub const LOCKS_DIR: &str = "locks";
19pub const TOPICS_DIR: &str = "topics";
20
21#[derive(Debug, Clone)]
22pub struct MemoryPathResolver {
23    data_dir: PathBuf,
24    root: PathBuf,
25}
26
27impl Default for MemoryPathResolver {
28    fn default() -> Self {
29        Self::from_data_dir(bamboo_dir())
30    }
31}
32
33impl MemoryPathResolver {
34    pub fn new(root: impl Into<PathBuf>) -> Self {
35        let root = root.into();
36        let data_dir = infer_data_dir_from_root(&root);
37        Self { data_dir, root }
38    }
39
40    pub fn from_data_dir(data_dir: impl Into<PathBuf>) -> Self {
41        let data_dir = data_dir.into();
42        let root = data_dir.join(MEMORY_ROOT_DIR).join(MEMORY_VERSION_DIR);
43        Self { data_dir, root }
44    }
45
46    pub fn data_dir(&self) -> PathBuf {
47        self.data_dir.clone()
48    }
49
50    pub fn root(&self) -> PathBuf {
51        self.root.clone()
52    }
53
54    pub fn sessions_root(&self) -> PathBuf {
55        self.root.join(SESSIONS_DIR)
56    }
57
58    pub fn session_root(&self, session_id: &str) -> PathBuf {
59        self.sessions_root().join(session_id)
60    }
61
62    pub fn session_note_dir(&self, session_id: &str) -> PathBuf {
63        self.session_root(session_id).join(NOTE_DIR)
64    }
65
66    pub fn session_topic_path(&self, session_id: &str, topic: &str) -> PathBuf {
67        self.session_note_dir(session_id)
68            .join(format!("{}.md", topic))
69    }
70
71    pub fn session_state_path(&self, session_id: &str) -> PathBuf {
72        self.session_root(session_id).join("state.json")
73    }
74
75    pub fn scopes_root(&self) -> PathBuf {
76        self.root.join(SCOPES_DIR)
77    }
78
79    pub fn global_root(&self) -> PathBuf {
80        self.scopes_root().join(GLOBAL_DIR)
81    }
82
83    pub fn project_root(&self, project_key: &str) -> PathBuf {
84        self.scopes_root().join(PROJECTS_DIR).join(project_key)
85    }
86
87    pub fn scope_root(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
88        match scope {
89            MemoryScope::Global => self.global_root(),
90            MemoryScope::Project => self.project_root(project_key.unwrap_or("unknown")),
91            MemoryScope::Session => self.sessions_root(),
92        }
93    }
94
95    pub fn topic_dir(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
96        match scope {
97            MemoryScope::Global => self.global_root().join(TOPICS_DIR),
98            MemoryScope::Project => self
99                .project_root(project_key.unwrap_or("unknown"))
100                .join(TOPICS_DIR),
101            MemoryScope::Session => self.sessions_root(),
102        }
103    }
104
105    pub fn topic_path(
106        &self,
107        scope: MemoryScope,
108        project_key: Option<&str>,
109        memory_id: &str,
110    ) -> PathBuf {
111        self.topic_dir(scope, project_key)
112            .join(format!("{}.md", memory_id))
113    }
114
115    pub fn indexes_dir(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
116        match scope {
117            MemoryScope::Global => self.global_root().join(INDEXES_DIR),
118            MemoryScope::Project => self
119                .project_root(project_key.unwrap_or("unknown"))
120                .join(INDEXES_DIR),
121            MemoryScope::Session => self.sessions_root(),
122        }
123    }
124
125    pub fn views_dir(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
126        match scope {
127            MemoryScope::Global => self.global_root().join(VIEWS_DIR),
128            MemoryScope::Project => self
129                .project_root(project_key.unwrap_or("unknown"))
130                .join(VIEWS_DIR),
131            MemoryScope::Session => self.sessions_root(),
132        }
133    }
134
135    pub fn logs_dir(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
136        match scope {
137            MemoryScope::Global => self.global_root().join(LOGS_DIR),
138            MemoryScope::Project => self
139                .project_root(project_key.unwrap_or("unknown"))
140                .join(LOGS_DIR),
141            MemoryScope::Session => self.sessions_root(),
142        }
143    }
144
145    pub fn state_dir(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
146        match scope {
147            MemoryScope::Global => self.global_root().join(STATE_DIR),
148            MemoryScope::Project => self
149                .project_root(project_key.unwrap_or("unknown"))
150                .join(STATE_DIR),
151            MemoryScope::Session => self.sessions_root(),
152        }
153    }
154
155    pub fn locks_dir(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
156        self.state_dir(scope, project_key).join(LOCKS_DIR)
157    }
158
159    pub fn legacy_notes_root(&self) -> PathBuf {
160        self.data_dir.join("notes")
161    }
162
163    pub fn legacy_session_file(&self, session_id: &str) -> PathBuf {
164        self.legacy_notes_root().join(format!("{}.md", session_id))
165    }
166
167    pub fn legacy_session_topic_dir(&self, session_id: &str) -> PathBuf {
168        self.legacy_notes_root().join(session_id)
169    }
170}
171
172pub fn memory_root() -> PathBuf {
173    MemoryPathResolver::from_data_dir(bamboo_dir()).root()
174}
175
176fn infer_data_dir_from_root(root: &Path) -> PathBuf {
177    if root
178        .file_name()
179        .and_then(|value| value.to_str())
180        .is_some_and(|value| value == MEMORY_VERSION_DIR)
181    {
182        if let Some(memory_dir) = root.parent() {
183            if memory_dir
184                .file_name()
185                .and_then(|value| value.to_str())
186                .is_some_and(|value| value == MEMORY_ROOT_DIR)
187            {
188                if let Some(data_dir) = memory_dir.parent() {
189                    return data_dir.to_path_buf();
190                }
191            }
192        }
193    }
194    root.to_path_buf()
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn paths_follow_v1_layout() {
203        let resolver = MemoryPathResolver::new("/tmp/memory/v1");
204        assert_eq!(
205            resolver.session_topic_path("session-1", "default"),
206            PathBuf::from("/tmp/memory/v1/sessions/session-1/note/default.md")
207        );
208        assert_eq!(
209            resolver.topic_path(MemoryScope::Project, Some("proj-1"), "mem_1"),
210            PathBuf::from("/tmp/memory/v1/scopes/projects/proj-1/topics/mem_1.md")
211        );
212    }
213}