ricecoder_storage/
project_store.rs

1//! Project storage implementation for RiceCoder
2//!
3//! Manages the project-local knowledge base stored in ./.agent/
4
5use crate::error::{IoOperation, StorageError, StorageResult};
6use crate::types::ResourceType;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10/// Project store for managing project-local knowledge base
11pub struct ProjectStore {
12    /// Path to the project storage directory (./.agent/)
13    base_path: PathBuf,
14}
15
16impl ProjectStore {
17    /// Create a new project store
18    pub fn new(base_path: PathBuf) -> Self {
19        ProjectStore { base_path }
20    }
21
22    /// Create a new project store with default path (./.agent/)
23    pub fn with_default_path() -> Self {
24        ProjectStore {
25            base_path: PathBuf::from(".agent"),
26        }
27    }
28
29    /// Get the base path
30    pub fn base_path(&self) -> &PathBuf {
31        &self.base_path
32    }
33
34    /// Initialize the project store directory structure
35    ///
36    /// Creates the base directory and all resource subdirectories:
37    /// - templates/
38    /// - standards/
39    /// - specs/
40    /// - steering/
41    /// - boilerplates/
42    /// - rules/
43    /// - history/
44    /// - cache/
45    pub fn initialize(&self) -> StorageResult<()> {
46        // Create base directory
47        self.create_dir_if_not_exists(&self.base_path)?;
48
49        // Create resource directories
50        for resource_type in &[
51            ResourceType::Template,
52            ResourceType::Standard,
53            ResourceType::Spec,
54            ResourceType::Steering,
55            ResourceType::Boilerplate,
56            ResourceType::Rule,
57        ] {
58            let resource_path = self.resource_path(*resource_type);
59            self.create_dir_if_not_exists(&resource_path)?;
60        }
61
62        // Create history directory
63        let history_path = self.base_path.join("history");
64        self.create_dir_if_not_exists(&history_path)?;
65
66        // Create cache directory
67        let cache_path = self.base_path.join("cache");
68        self.create_dir_if_not_exists(&cache_path)?;
69
70        Ok(())
71    }
72
73    /// Get the path for a resource type
74    pub fn resource_path(&self, resource_type: ResourceType) -> PathBuf {
75        self.base_path.join(resource_type.dir_name())
76    }
77
78    /// Store a resource file
79    pub fn store_resource(
80        &self,
81        resource_type: ResourceType,
82        name: &str,
83        content: &[u8],
84    ) -> StorageResult<PathBuf> {
85        let resource_dir = self.resource_path(resource_type);
86        let file_path = resource_dir.join(name);
87
88        // Ensure directory exists
89        self.create_dir_if_not_exists(&resource_dir)?;
90
91        // Write file
92        fs::write(&file_path, content)
93            .map_err(|e| StorageError::io_error(file_path.clone(), IoOperation::Write, e))?;
94
95        Ok(file_path)
96    }
97
98    /// Retrieve a resource file
99    pub fn retrieve_resource(
100        &self,
101        resource_type: ResourceType,
102        name: &str,
103    ) -> StorageResult<Vec<u8>> {
104        let resource_dir = self.resource_path(resource_type);
105        let file_path = resource_dir.join(name);
106
107        fs::read(&file_path).map_err(|e| StorageError::io_error(file_path, IoOperation::Read, e))
108    }
109
110    /// List all resources of a type
111    pub fn list_resources(&self, resource_type: ResourceType) -> StorageResult<Vec<String>> {
112        let resource_dir = self.resource_path(resource_type);
113
114        if !resource_dir.exists() {
115            return Ok(Vec::new());
116        }
117
118        let mut resources = Vec::new();
119        let entries = fs::read_dir(&resource_dir)
120            .map_err(|e| StorageError::io_error(resource_dir.clone(), IoOperation::Read, e))?;
121
122        for entry in entries {
123            let entry = entry
124                .map_err(|e| StorageError::io_error(resource_dir.clone(), IoOperation::Read, e))?;
125
126            let path = entry.path();
127            if path.is_file() {
128                if let Some(file_name) = path.file_name() {
129                    if let Some(name_str) = file_name.to_str() {
130                        resources.push(name_str.to_string());
131                    }
132                }
133            }
134        }
135
136        Ok(resources)
137    }
138
139    /// Delete a resource file
140    pub fn delete_resource(&self, resource_type: ResourceType, name: &str) -> StorageResult<()> {
141        let resource_dir = self.resource_path(resource_type);
142        let file_path = resource_dir.join(name);
143
144        if file_path.exists() {
145            fs::remove_file(&file_path)
146                .map_err(|e| StorageError::io_error(file_path, IoOperation::Delete, e))?;
147        }
148
149        Ok(())
150    }
151
152    /// Check if a resource exists
153    pub fn resource_exists(&self, resource_type: ResourceType, name: &str) -> bool {
154        let resource_dir = self.resource_path(resource_type);
155        let file_path = resource_dir.join(name);
156        file_path.exists()
157    }
158
159    /// Create a folder on-demand
160    ///
161    /// Creates a folder in the project store if it doesn't exist.
162    /// This allows projects to create custom folders as needed.
163    pub fn create_folder(&self, folder_name: &str) -> StorageResult<PathBuf> {
164        let folder_path = self.base_path.join(folder_name);
165        self.create_dir_if_not_exists(&folder_path)?;
166        Ok(folder_path)
167    }
168
169    /// Check if a folder exists
170    pub fn folder_exists(&self, folder_name: &str) -> bool {
171        let folder_path = self.base_path.join(folder_name);
172        folder_path.is_dir()
173    }
174
175    /// Create a directory if it doesn't exist
176    fn create_dir_if_not_exists(&self, path: &Path) -> StorageResult<()> {
177        if !path.exists() {
178            fs::create_dir_all(path)
179                .map_err(|e| StorageError::directory_creation_failed(path.to_path_buf(), e))?;
180        }
181        Ok(())
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use tempfile::TempDir;
189
190    #[test]
191    fn test_project_store_initialization() {
192        let temp_dir = TempDir::new().expect("Failed to create temp dir");
193        let store = ProjectStore::new(temp_dir.path().to_path_buf());
194
195        store.initialize().expect("Failed to initialize store");
196
197        // Verify all directories were created
198        assert!(store.resource_path(ResourceType::Template).exists());
199        assert!(store.resource_path(ResourceType::Standard).exists());
200        assert!(store.resource_path(ResourceType::Spec).exists());
201        assert!(store.resource_path(ResourceType::Steering).exists());
202        assert!(store.resource_path(ResourceType::Boilerplate).exists());
203        assert!(store.resource_path(ResourceType::Rule).exists());
204        assert!(temp_dir.path().join("history").exists());
205        assert!(temp_dir.path().join("cache").exists());
206    }
207
208    #[test]
209    fn test_store_and_retrieve_resource() {
210        let temp_dir = TempDir::new().expect("Failed to create temp dir");
211        let store = ProjectStore::new(temp_dir.path().to_path_buf());
212        store.initialize().expect("Failed to initialize store");
213
214        let content = b"test content";
215        let name = "test.txt";
216
217        // Store resource
218        let path = store
219            .store_resource(ResourceType::Template, name, content)
220            .expect("Failed to store resource");
221
222        assert!(path.exists());
223
224        // Retrieve resource
225        let retrieved = store
226            .retrieve_resource(ResourceType::Template, name)
227            .expect("Failed to retrieve resource");
228
229        assert_eq!(retrieved, content);
230    }
231
232    #[test]
233    fn test_create_folder_on_demand() {
234        let temp_dir = TempDir::new().expect("Failed to create temp dir");
235        let store = ProjectStore::new(temp_dir.path().to_path_buf());
236        store.initialize().expect("Failed to initialize store");
237
238        let folder_name = "custom_folder";
239        assert!(!store.folder_exists(folder_name));
240
241        let folder_path = store
242            .create_folder(folder_name)
243            .expect("Failed to create folder");
244
245        assert!(folder_path.exists());
246        assert!(store.folder_exists(folder_name));
247    }
248
249    #[test]
250    fn test_list_resources() {
251        let temp_dir = TempDir::new().expect("Failed to create temp dir");
252        let store = ProjectStore::new(temp_dir.path().to_path_buf());
253        store.initialize().expect("Failed to initialize store");
254
255        // Store multiple resources
256        store
257            .store_resource(ResourceType::Template, "template1.txt", b"content1")
258            .expect("Failed to store");
259        store
260            .store_resource(ResourceType::Template, "template2.txt", b"content2")
261            .expect("Failed to store");
262
263        // List resources
264        let resources = store
265            .list_resources(ResourceType::Template)
266            .expect("Failed to list resources");
267
268        assert_eq!(resources.len(), 2);
269        assert!(resources.contains(&"template1.txt".to_string()));
270        assert!(resources.contains(&"template2.txt".to_string()));
271    }
272
273    #[test]
274    fn test_delete_resource() {
275        let temp_dir = TempDir::new().expect("Failed to create temp dir");
276        let store = ProjectStore::new(temp_dir.path().to_path_buf());
277        store.initialize().expect("Failed to initialize store");
278
279        let name = "test.txt";
280        store
281            .store_resource(ResourceType::Template, name, b"content")
282            .expect("Failed to store");
283
284        assert!(store.resource_exists(ResourceType::Template, name));
285
286        store
287            .delete_resource(ResourceType::Template, name)
288            .expect("Failed to delete");
289
290        assert!(!store.resource_exists(ResourceType::Template, name));
291    }
292
293    #[test]
294    fn test_resource_exists() {
295        let temp_dir = TempDir::new().expect("Failed to create temp dir");
296        let store = ProjectStore::new(temp_dir.path().to_path_buf());
297        store.initialize().expect("Failed to initialize store");
298
299        let name = "test.txt";
300        assert!(!store.resource_exists(ResourceType::Template, name));
301
302        store
303            .store_resource(ResourceType::Template, name, b"content")
304            .expect("Failed to store");
305
306        assert!(store.resource_exists(ResourceType::Template, name));
307    }
308}