ricecoder_storage/
manager.rs

1//! Storage manager trait and path resolution
2
3use crate::error::{StorageError, StorageResult};
4use crate::types::{ResourceType, StorageMode};
5use std::path::{Path, PathBuf};
6
7/// Storage manager trait for managing storage operations
8pub trait StorageManager: Send + Sync {
9    /// Get the global storage path
10    fn global_path(&self) -> &PathBuf;
11
12    /// Get the project storage path (if in a project)
13    fn project_path(&self) -> Option<&PathBuf>;
14
15    /// Get the current storage mode
16    fn mode(&self) -> StorageMode;
17
18    /// Get the path for a resource type in global storage
19    fn global_resource_path(&self, resource_type: ResourceType) -> PathBuf;
20
21    /// Get the path for a resource type in project storage
22    fn project_resource_path(&self, resource_type: ResourceType) -> Option<PathBuf>;
23
24    /// Check if this is the first run
25    fn is_first_run(&self) -> bool;
26}
27
28/// Path resolver for cross-platform storage paths
29pub struct PathResolver;
30
31impl PathResolver {
32    /// Resolve the global storage path based on OS and environment
33    ///
34    /// Priority:
35    /// 1. RICECODER_HOME environment variable
36    /// 2. ~/Documents/.ricecoder/ (primary)
37    /// 3. ~/.ricecoder/ (fallback if Documents doesn't exist)
38    pub fn resolve_global_path() -> StorageResult<PathBuf> {
39        // Check for RICECODER_HOME environment variable
40        if let Ok(home_override) = std::env::var("RICECODER_HOME") {
41            let path = PathBuf::from(home_override);
42            return Ok(path);
43        }
44
45        // Try Documents folder first
46        if let Some(docs_dir) = dirs::document_dir() {
47            let ricecoder_path = docs_dir.join(".ricecoder");
48            return Ok(ricecoder_path);
49        }
50
51        // Fallback to home directory
52        if let Some(home_dir) = dirs::home_dir() {
53            let ricecoder_path = home_dir.join(".ricecoder");
54            return Ok(ricecoder_path);
55        }
56
57        Err(StorageError::path_resolution_error(
58            "Could not determine home directory",
59        ))
60    }
61
62    /// Resolve the project storage path (./.agent/)
63    pub fn resolve_project_path() -> PathBuf {
64        PathBuf::from(".agent")
65    }
66
67    /// Expand ~ in paths to home directory
68    pub fn expand_home(path: &Path) -> StorageResult<PathBuf> {
69        let path_str = path
70            .to_str()
71            .ok_or_else(|| StorageError::path_resolution_error("Invalid path encoding"))?;
72
73        if path_str.starts_with("~") {
74            if let Some(home_dir) = dirs::home_dir() {
75                let expanded = if path_str == "~" {
76                    home_dir
77                } else {
78                    home_dir.join(&path_str[2..])
79                };
80                return Ok(expanded);
81            }
82        }
83
84        Ok(path.to_path_buf())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_resolve_global_path_with_env_override() {
94        // Set RICECODER_HOME environment variable
95        std::env::set_var("RICECODER_HOME", "/tmp/ricecoder-test");
96        let path = PathResolver::resolve_global_path().expect("Should resolve path");
97        assert_eq!(path, PathBuf::from("/tmp/ricecoder-test"));
98        std::env::remove_var("RICECODER_HOME");
99    }
100
101    #[test]
102    fn test_resolve_global_path_without_env() {
103        // Ensure RICECODER_HOME is not set
104        std::env::remove_var("RICECODER_HOME");
105        let path = PathResolver::resolve_global_path().expect("Should resolve path");
106        // Should be either Documents/.ricecoder or ~/.ricecoder
107        assert!(path.to_string_lossy().contains(".ricecoder"));
108    }
109
110    #[test]
111    fn test_resolve_project_path() {
112        let path = PathResolver::resolve_project_path();
113        assert_eq!(path, PathBuf::from(".agent"));
114    }
115
116    #[test]
117    fn test_expand_home_with_tilde() {
118        let path = PathBuf::from("~/.ricecoder");
119        let expanded = PathResolver::expand_home(&path).expect("Should expand");
120        assert!(!expanded.to_string_lossy().contains("~"));
121    }
122
123    #[test]
124    fn test_expand_home_without_tilde() {
125        let path = PathBuf::from("/tmp/ricecoder");
126        let expanded = PathResolver::expand_home(&path).expect("Should expand");
127        assert_eq!(expanded, path);
128    }
129}