subcog/storage/index/
domain.rs1use crate::storage::index::SqliteBackend;
9use crate::{Error, Result};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::sync::Mutex;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum DomainScope {
17 Project,
19 User,
21 Org,
23}
24
25impl DomainScope {
26 #[must_use]
28 pub const fn as_str(&self) -> &'static str {
29 match self {
30 Self::Project => "project",
31 Self::User => "user",
32 Self::Org => "org",
33 }
34 }
35
36 #[must_use]
43 pub fn default_for_context() -> Self {
44 if is_in_git_repo() {
45 Self::Project
46 } else {
47 Self::User
48 }
49 }
50
51 #[must_use]
56 pub fn default_for_path(path: &Path) -> Self {
57 if is_path_in_git_repo(path) {
58 Self::Project
59 } else {
60 Self::User
61 }
62 }
63}
64
65#[derive(Debug, Clone, Default)]
67pub struct DomainIndexConfig {
68 pub repo_path: Option<PathBuf>,
70 pub org_config: Option<OrgIndexConfig>,
72 pub user_data_dir: Option<PathBuf>,
77}
78
79#[derive(Debug, Clone)]
81pub enum OrgIndexConfig {
82 SqlitePath(PathBuf),
84 PostgresUrl(String),
86 RedisUrl(String),
88}
89
90pub struct DomainIndexManager {
92 indices: HashMap<DomainScope, Mutex<SqliteBackend>>,
94 config: DomainIndexConfig,
96 user_data_dir: PathBuf,
98}
99
100impl DomainIndexManager {
101 pub fn new(config: DomainIndexConfig) -> Result<Self> {
111 let user_data_dir = config
114 .user_data_dir
115 .clone()
116 .map_or_else(get_user_data_dir, Ok)?;
117
118 Ok(Self {
119 indices: HashMap::new(),
120 config,
121 user_data_dir,
122 })
123 }
124
125 pub fn get_or_create(&mut self, scope: DomainScope) -> Result<&Mutex<SqliteBackend>> {
131 if !self.indices.contains_key(&scope) {
132 let backend = self.create_index(scope)?;
133 self.indices.insert(scope, Mutex::new(backend));
134 }
135
136 self.indices
137 .get(&scope)
138 .ok_or_else(|| Error::OperationFailed {
139 operation: "get_index".to_string(),
140 cause: format!("Index for scope {scope:?} not found"),
141 })
142 }
143
144 fn create_index(&self, scope: DomainScope) -> Result<SqliteBackend> {
146 let db_path = self.get_index_path(scope)?;
147
148 if let Some(parent) = db_path.parent() {
150 std::fs::create_dir_all(parent).map_err(|e| Error::OperationFailed {
151 operation: "create_index_dir".to_string(),
152 cause: e.to_string(),
153 })?;
154 }
155
156 SqliteBackend::new(&db_path)
157 }
158
159 pub fn get_index_path(&self, scope: DomainScope) -> Result<PathBuf> {
165 match scope {
166 DomainScope::Project => Ok(self.get_project_index_path()),
167 DomainScope::User => Ok(self.get_user_index_path()),
168 DomainScope::Org => self.get_org_index_path(),
169 }
170 }
171
172 fn get_project_index_path(&self) -> PathBuf {
174 self.user_data_dir.join("index.db")
175 }
176
177 fn get_user_index_path(&self) -> PathBuf {
181 self.user_data_dir.join("index.db")
182 }
183
184 fn get_org_index_path(&self) -> Result<PathBuf> {
186 match &self.config.org_config {
187 Some(OrgIndexConfig::SqlitePath(path)) => Ok(path.clone()),
188 Some(OrgIndexConfig::PostgresUrl(_) | OrgIndexConfig::RedisUrl(_)) => {
189 Err(Error::NotImplemented(
190 "PostgreSQL and Redis org indices not yet implemented".to_string(),
191 ))
192 },
193 None => {
194 Ok(self.user_data_dir.join("org").join("index.db"))
196 },
197 }
198 }
199
200 #[must_use]
202 pub fn available_scopes(&self) -> Vec<DomainScope> {
203 self.indices.keys().copied().collect()
204 }
205
206 #[must_use]
208 pub fn has_scope(&self, scope: DomainScope) -> bool {
209 self.indices.contains_key(&scope)
210 }
211
212 pub fn create_backend(&self, scope: DomainScope) -> Result<SqliteBackend> {
242 self.create_index(scope)
243 }
244
245 pub fn create_backend_with_path(&self, scope: DomainScope) -> Result<(SqliteBackend, PathBuf)> {
259 let path = self.get_index_path(scope)?;
260
261 if let Some(parent) = path.parent() {
263 std::fs::create_dir_all(parent).map_err(|e| Error::OperationFailed {
264 operation: "create_index_dir".to_string(),
265 cause: e.to_string(),
266 })?;
267 }
268
269 let backend = SqliteBackend::new(&path)?;
270 Ok((backend, path))
271 }
272}
273
274pub fn get_user_data_dir() -> Result<PathBuf> {
285 directories::BaseDirs::new()
286 .map(|b| b.data_local_dir().join("subcog"))
287 .ok_or_else(|| Error::OperationFailed {
288 operation: "get_user_data_dir".to_string(),
289 cause: "Could not determine user data directory".to_string(),
290 })
291}
292
293#[must_use]
298pub fn is_in_git_repo() -> bool {
299 std::env::current_dir()
300 .map(|cwd| is_path_in_git_repo(&cwd))
301 .unwrap_or(false)
302}
303
304#[must_use]
308pub fn is_path_in_git_repo(path: &Path) -> bool {
309 find_repo_root(path).is_ok()
310}
311
312pub fn find_repo_root(start: &Path) -> Result<PathBuf> {
320 let mut current = start.to_path_buf();
321
322 if let Ok(canonical) = current.canonicalize() {
324 current = canonical;
325 }
326
327 loop {
328 if current.join(".git").exists() {
329 return Ok(current);
330 }
331
332 if !current.pop() {
333 break;
334 }
335 }
336
337 Err(Error::InvalidInput(format!(
338 "No git repository found starting from: {}",
339 start.display()
340 )))
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use tempfile::TempDir;
347
348 #[test]
349 fn test_domain_scope_as_str() {
350 assert_eq!(DomainScope::Project.as_str(), "project");
351 assert_eq!(DomainScope::User.as_str(), "user");
352 assert_eq!(DomainScope::Org.as_str(), "org");
353 }
354
355 #[test]
356 fn test_find_repo_root() {
357 let dir = TempDir::new().unwrap();
358 let repo_root = dir.path();
359
360 std::fs::create_dir(repo_root.join(".git")).unwrap();
362
363 let nested = repo_root.join("src").join("lib");
365 std::fs::create_dir_all(&nested).unwrap();
366
367 let found = find_repo_root(&nested).unwrap();
369 assert_eq!(found, repo_root.canonicalize().unwrap());
370 }
371
372 #[test]
373 fn test_find_repo_root_not_found() {
374 let dir = TempDir::new().unwrap();
375 let result = find_repo_root(dir.path());
377 assert!(result.is_err());
378 }
379
380 #[test]
381 fn test_project_index_path() {
382 let config = DomainIndexConfig {
383 repo_path: Some(PathBuf::from("/path/to/repo")),
384 org_config: None,
385 user_data_dir: None,
386 };
387 let manager = DomainIndexManager::new(config).unwrap();
388
389 let path = manager.get_index_path(DomainScope::Project).unwrap();
390 let expected = get_user_data_dir().unwrap().join("index.db");
391 assert_eq!(path, expected);
392 }
393
394 #[test]
395 fn test_user_index_path() {
396 let config = DomainIndexConfig {
397 repo_path: Some(PathBuf::from("/path/to/repo")),
398 org_config: None,
399 user_data_dir: None,
400 };
401 let manager = DomainIndexManager::new(config).unwrap();
402
403 let path = manager.get_index_path(DomainScope::User).unwrap();
404 let expected = get_user_data_dir().unwrap().join("index.db");
405 assert_eq!(path, expected);
406 }
407
408 #[test]
409 fn test_org_index_path_configured() {
410 let config = DomainIndexConfig {
411 repo_path: Some(PathBuf::from("/path/to/repo")),
412 org_config: Some(OrgIndexConfig::SqlitePath(PathBuf::from(
413 "/shared/org/index.db",
414 ))),
415 user_data_dir: None,
416 };
417 let manager = DomainIndexManager::new(config).unwrap();
418
419 let path = manager.get_index_path(DomainScope::Org).unwrap();
420 assert_eq!(path, PathBuf::from("/shared/org/index.db"));
421 }
422
423 #[test]
424 fn test_is_path_in_git_repo_with_git() {
425 let dir = TempDir::new().unwrap();
426 let repo_root = dir.path();
427
428 std::fs::create_dir(repo_root.join(".git")).unwrap();
430
431 assert!(is_path_in_git_repo(repo_root));
433
434 let nested = repo_root.join("src");
436 std::fs::create_dir_all(&nested).unwrap();
437 assert!(is_path_in_git_repo(&nested));
438 }
439
440 #[test]
441 fn test_is_path_in_git_repo_without_git() {
442 let dir = TempDir::new().unwrap();
443 assert!(!is_path_in_git_repo(dir.path()));
445 }
446
447 #[test]
448 fn test_default_for_path_in_git_repo() {
449 let dir = TempDir::new().unwrap();
450 let repo_root = dir.path();
451
452 std::fs::create_dir(repo_root.join(".git")).unwrap();
454
455 assert_eq!(
457 DomainScope::default_for_path(repo_root),
458 DomainScope::Project
459 );
460 }
461
462 #[test]
463 fn test_default_for_path_not_in_git_repo() {
464 let dir = TempDir::new().unwrap();
465 assert_eq!(DomainScope::default_for_path(dir.path()), DomainScope::User);
467 }
468}