ricecoder_refactoring/config/
storage_loader.rs

1//! Configuration loading from ricecoder-storage with hierarchy support
2
3use crate::error::Result;
4use crate::types::RefactoringConfig;
5use ricecoder_storage::manager::StorageManager;
6use ricecoder_storage::types::ResourceType;
7use std::sync::Arc;
8
9/// Loads refactoring configurations from storage with hierarchy support
10pub struct StorageConfigLoader {
11    storage: Arc<dyn StorageManager>,
12}
13
14impl StorageConfigLoader {
15    /// Create a new storage configuration loader
16    pub fn new(storage: Arc<dyn StorageManager>) -> Self {
17        Self { storage }
18    }
19
20    /// Load language configuration with hierarchy support
21    ///
22    /// Priority (highest to lowest):
23    /// 1. Project-level config (./.agent/refactoring/languages/{language}.yaml)
24    /// 2. User-level config (~/.ricecoder/refactoring/languages/{language}.yaml)
25    /// 3. Built-in config (fallback)
26    pub fn load_language_config(&self, language: &str) -> Result<RefactoringConfig> {
27        // Try project-level first
28        if let Some(project_path) = self
29            .storage
30            .project_resource_path(ResourceType::RefactoringLanguageConfig)
31        {
32            let config_path = project_path.join(format!("{}.yaml", language));
33            if config_path.exists() {
34                tracing::debug!(
35                    "Loading refactoring config from project: {}",
36                    config_path.display()
37                );
38                let config = self.load_from_file(&config_path)?;
39                config.validate()?;
40                return Ok(config);
41            }
42        }
43
44        // Try user-level
45        let global_path = self
46            .storage
47            .global_resource_path(ResourceType::RefactoringLanguageConfig);
48        let config_path = global_path.join(format!("{}.yaml", language));
49        if config_path.exists() {
50            tracing::debug!(
51                "Loading refactoring config from user: {}",
52                config_path.display()
53            );
54            let config = self.load_from_file(&config_path)?;
55            config.validate()?;
56            return Ok(config);
57        }
58
59        // Fall back to built-in (generic refactoring)
60        tracing::debug!(
61            "No configuration found for language '{}', using generic refactoring",
62            language
63        );
64        Ok(RefactoringConfig::generic_fallback(language))
65    }
66
67    /// Load configuration from a file
68    fn load_from_file(&self, path: &std::path::Path) -> Result<RefactoringConfig> {
69        use crate::config::loader::ConfigLoader;
70        ConfigLoader::load(path)
71    }
72
73    /// Get all available language configurations
74    pub fn list_available_languages(&self) -> Result<Vec<String>> {
75        let mut languages = Vec::new();
76
77        // Check project-level
78        if let Some(project_path) = self
79            .storage
80            .project_resource_path(ResourceType::RefactoringLanguageConfig)
81        {
82            if project_path.exists() {
83                if let Ok(entries) = std::fs::read_dir(&project_path) {
84                    for entry in entries.flatten() {
85                        if let Some(name) = entry.file_name().to_str() {
86                            if name.ends_with(".yaml") || name.ends_with(".yml") {
87                                let lang = name.trim_end_matches(".yaml").trim_end_matches(".yml");
88                                if !languages.contains(&lang.to_string()) {
89                                    languages.push(lang.to_string());
90                                }
91                            }
92                        }
93                    }
94                }
95            }
96        }
97
98        // Check user-level
99        let global_path = self
100            .storage
101            .global_resource_path(ResourceType::RefactoringLanguageConfig);
102        if global_path.exists() {
103            if let Ok(entries) = std::fs::read_dir(&global_path) {
104                for entry in entries.flatten() {
105                    if let Some(name) = entry.file_name().to_str() {
106                        if name.ends_with(".yaml") || name.ends_with(".yml") {
107                            let lang = name.trim_end_matches(".yaml").trim_end_matches(".yml");
108                            if !languages.contains(&lang.to_string()) {
109                                languages.push(lang.to_string());
110                            }
111                        }
112                    }
113                }
114            }
115        }
116
117        Ok(languages)
118    }
119
120    /// Check if a language configuration exists
121    pub fn has_language_config(&self, language: &str) -> Result<bool> {
122        // Check project-level
123        if let Some(project_path) = self
124            .storage
125            .project_resource_path(ResourceType::RefactoringLanguageConfig)
126        {
127            let config_path = project_path.join(format!("{}.yaml", language));
128            if config_path.exists() {
129                return Ok(true);
130            }
131        }
132
133        // Check user-level
134        let global_path = self
135            .storage
136            .global_resource_path(ResourceType::RefactoringLanguageConfig);
137        let config_path = global_path.join(format!("{}.yaml", language));
138        Ok(config_path.exists())
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use ricecoder_storage::types::StorageMode;
146    use std::path::PathBuf;
147
148    /// Mock storage manager for testing
149    struct MockStorageManager {
150        global_path: PathBuf,
151        project_path: Option<PathBuf>,
152    }
153
154    impl StorageManager for MockStorageManager {
155        fn global_path(&self) -> &PathBuf {
156            &self.global_path
157        }
158
159        fn project_path(&self) -> Option<&PathBuf> {
160            self.project_path.as_ref()
161        }
162
163        fn mode(&self) -> StorageMode {
164            StorageMode::Merged
165        }
166
167        fn global_resource_path(&self, resource_type: ResourceType) -> PathBuf {
168            self.global_path.join(resource_type.dir_name())
169        }
170
171        fn project_resource_path(&self, resource_type: ResourceType) -> Option<PathBuf> {
172            self.project_path
173                .as_ref()
174                .map(|p| p.join(resource_type.dir_name()))
175        }
176
177        fn is_first_run(&self) -> bool {
178            false
179        }
180    }
181
182    #[test]
183    fn test_load_language_config_fallback() -> Result<()> {
184        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
185        let storage = Arc::new(MockStorageManager {
186            global_path: temp_dir.path().to_path_buf(),
187            project_path: None,
188        });
189
190        let loader = StorageConfigLoader::new(storage);
191        let config = loader.load_language_config("unknown_language")?;
192
193        // Should return generic fallback
194        assert_eq!(config.language, "unknown_language");
195        assert!(config.rules.is_empty());
196
197        Ok(())
198    }
199
200    #[test]
201    fn test_list_available_languages() -> Result<()> {
202        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
203        let refactoring_dir = temp_dir.path().join("refactoring/languages");
204        std::fs::create_dir_all(&refactoring_dir).expect("Failed to create dir");
205
206        // Create some dummy config files
207        std::fs::write(refactoring_dir.join("rust.yaml"), "language: rust").ok();
208        std::fs::write(refactoring_dir.join("python.yaml"), "language: python").ok();
209
210        let storage = Arc::new(MockStorageManager {
211            global_path: temp_dir.path().to_path_buf(),
212            project_path: None,
213        });
214
215        let loader = StorageConfigLoader::new(storage);
216        let languages = loader.list_available_languages()?;
217
218        assert!(languages.contains(&"rust".to_string()));
219        assert!(languages.contains(&"python".to_string()));
220
221        Ok(())
222    }
223
224    #[test]
225    fn test_has_language_config() -> Result<()> {
226        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
227        let refactoring_dir = temp_dir.path().join("refactoring/languages");
228        std::fs::create_dir_all(&refactoring_dir).expect("Failed to create dir");
229
230        std::fs::write(refactoring_dir.join("rust.yaml"), "language: rust").ok();
231
232        let storage = Arc::new(MockStorageManager {
233            global_path: temp_dir.path().to_path_buf(),
234            project_path: None,
235        });
236
237        let loader = StorageConfigLoader::new(storage);
238
239        assert!(loader.has_language_config("rust")?);
240        assert!(!loader.has_language_config("unknown")?);
241
242        Ok(())
243    }
244}