ricecoder_storage/config/
modes.rs

1//! Storage mode handling
2//!
3//! This module provides handling for different storage modes:
4//! - GlobalOnly: Only use global storage
5//! - ProjectOnly: Only use project storage
6//! - Merged: Use both global and project with project overriding global
7
8use super::Config;
9use crate::types::StorageMode;
10use std::path::Path;
11
12/// Storage mode handler
13pub struct StorageModeHandler;
14
15impl StorageModeHandler {
16    /// Load configuration based on storage mode
17    ///
18    /// - GlobalOnly: Load only from global path
19    /// - ProjectOnly: Load only from project path
20    /// - Merged: Load from both, with project overriding global
21    pub fn load_for_mode(
22        mode: StorageMode,
23        global_path: Option<&Path>,
24        project_path: Option<&Path>,
25    ) -> crate::error::StorageResult<Config> {
26        match mode {
27            StorageMode::GlobalOnly => Self::load_global_only(global_path),
28            StorageMode::ProjectOnly => Self::load_project_only(project_path),
29            StorageMode::Merged => Self::load_merged(global_path, project_path),
30        }
31    }
32
33    /// Load configuration from global storage only
34    fn load_global_only(global_path: Option<&Path>) -> crate::error::StorageResult<Config> {
35        if let Some(path) = global_path {
36            let config_file = path.join("config.yaml");
37            if config_file.exists() {
38                return super::loader::ConfigLoader::load_from_file(&config_file);
39            }
40        }
41        Ok(Config::default())
42    }
43
44    /// Load configuration from project storage only
45    fn load_project_only(project_path: Option<&Path>) -> crate::error::StorageResult<Config> {
46        if let Some(path) = project_path {
47            let config_file = path.join("config.yaml");
48            if config_file.exists() {
49                return super::loader::ConfigLoader::load_from_file(&config_file);
50            }
51        }
52        Ok(Config::default())
53    }
54
55    /// Load configuration from both global and project, with project overriding global
56    fn load_merged(
57        global_path: Option<&Path>,
58        project_path: Option<&Path>,
59    ) -> crate::error::StorageResult<Config> {
60        let global_config = if let Some(path) = global_path {
61            let config_file = path.join("config.yaml");
62            if config_file.exists() {
63                super::loader::ConfigLoader::load_from_file(&config_file).ok()
64            } else {
65                None
66            }
67        } else {
68            None
69        };
70
71        let project_config = if let Some(path) = project_path {
72            let config_file = path.join("config.yaml");
73            if config_file.exists() {
74                super::loader::ConfigLoader::load_from_file(&config_file).ok()
75            } else {
76                None
77            }
78        } else {
79            None
80        };
81
82        let (merged, _) = super::merge::ConfigMerger::merge(
83            Config::default(),
84            global_config,
85            project_config,
86            None,
87        );
88
89        Ok(merged)
90    }
91
92    /// Verify that a mode is properly isolated
93    ///
94    /// For GlobalOnly mode, ensures no project config is loaded.
95    /// For ProjectOnly mode, ensures no global config is loaded.
96    pub fn verify_isolation(
97        mode: StorageMode,
98        global_path: Option<&Path>,
99        project_path: Option<&Path>,
100    ) -> crate::error::StorageResult<bool> {
101        match mode {
102            StorageMode::GlobalOnly => {
103                // Verify that project config is not loaded
104                if let Some(path) = project_path {
105                    let config_file = path.join("config.yaml");
106                    Ok(!config_file.exists())
107                } else {
108                    Ok(true)
109                }
110            }
111            StorageMode::ProjectOnly => {
112                // Verify that global config is not loaded
113                if let Some(path) = global_path {
114                    let config_file = path.join("config.yaml");
115                    Ok(!config_file.exists())
116                } else {
117                    Ok(true)
118                }
119            }
120            StorageMode::Merged => {
121                // Merged mode doesn't have isolation requirements
122                Ok(true)
123            }
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use std::fs;
132    use tempfile::TempDir;
133
134    #[test]
135    fn test_global_only_mode_loads_global() {
136        let temp_dir = TempDir::new().expect("Failed to create temp dir");
137        let global_path = temp_dir.path();
138
139        // Create a global config
140        let config_file = global_path.join("config.yaml");
141        let config_content = r#"
142providers:
143  default_provider: openai
144defaults:
145  model: gpt-4
146steering: []
147"#;
148        fs::write(&config_file, config_content).expect("Failed to write config");
149
150        let config =
151            StorageModeHandler::load_for_mode(StorageMode::GlobalOnly, Some(global_path), None)
152                .expect("Failed to load config");
153
154        assert_eq!(
155            config.providers.default_provider,
156            Some("openai".to_string())
157        );
158        assert_eq!(config.defaults.model, Some("gpt-4".to_string()));
159    }
160
161    #[test]
162    fn test_project_only_mode_loads_project() {
163        let temp_dir = TempDir::new().expect("Failed to create temp dir");
164        let project_path = temp_dir.path();
165
166        // Create a project config
167        let config_file = project_path.join("config.yaml");
168        let config_content = r#"
169providers:
170  default_provider: anthropic
171defaults:
172  model: claude-3
173steering: []
174"#;
175        fs::write(&config_file, config_content).expect("Failed to write config");
176
177        let config =
178            StorageModeHandler::load_for_mode(StorageMode::ProjectOnly, None, Some(project_path))
179                .expect("Failed to load config");
180
181        assert_eq!(
182            config.providers.default_provider,
183            Some("anthropic".to_string())
184        );
185        assert_eq!(config.defaults.model, Some("claude-3".to_string()));
186    }
187
188    #[test]
189    fn test_merged_mode_project_overrides_global() {
190        let global_dir = TempDir::new().expect("Failed to create temp dir");
191        let project_dir = TempDir::new().expect("Failed to create temp dir");
192
193        // Create global config
194        let global_config_file = global_dir.path().join("config.yaml");
195        let global_content = r#"
196providers:
197  default_provider: openai
198defaults:
199  model: gpt-4
200steering: []
201"#;
202        fs::write(&global_config_file, global_content).expect("Failed to write global config");
203
204        // Create project config
205        let project_config_file = project_dir.path().join("config.yaml");
206        let project_content = r#"
207providers:
208  default_provider: anthropic
209defaults:
210  model: claude-3
211steering: []
212"#;
213        fs::write(&project_config_file, project_content).expect("Failed to write project config");
214
215        let config = StorageModeHandler::load_for_mode(
216            StorageMode::Merged,
217            Some(global_dir.path()),
218            Some(project_dir.path()),
219        )
220        .expect("Failed to load config");
221
222        // Project should override global
223        assert_eq!(
224            config.providers.default_provider,
225            Some("anthropic".to_string())
226        );
227        assert_eq!(config.defaults.model, Some("claude-3".to_string()));
228    }
229
230    #[test]
231    fn test_global_only_isolation() {
232        let global_dir = TempDir::new().expect("Failed to create temp dir");
233        let project_dir = TempDir::new().expect("Failed to create temp dir");
234
235        // Create both configs
236        let global_config_file = global_dir.path().join("config.yaml");
237        fs::write(
238            &global_config_file,
239            "providers:\n  default_provider: openai\ndefaults: {}\nsteering: []",
240        )
241        .expect("Failed to write global config");
242
243        let project_config_file = project_dir.path().join("config.yaml");
244        fs::write(
245            &project_config_file,
246            "providers:\n  default_provider: anthropic\ndefaults: {}\nsteering: []",
247        )
248        .expect("Failed to write project config");
249
250        // Load in GlobalOnly mode
251        let config = StorageModeHandler::load_for_mode(
252            StorageMode::GlobalOnly,
253            Some(global_dir.path()),
254            Some(project_dir.path()),
255        )
256        .expect("Failed to load config");
257
258        // Should only have global config
259        assert_eq!(
260            config.providers.default_provider,
261            Some("openai".to_string())
262        );
263    }
264
265    #[test]
266    fn test_project_only_isolation() {
267        let global_dir = TempDir::new().expect("Failed to create temp dir");
268        let project_dir = TempDir::new().expect("Failed to create temp dir");
269
270        // Create both configs
271        let global_config_file = global_dir.path().join("config.yaml");
272        fs::write(
273            &global_config_file,
274            "providers:\n  default_provider: openai\ndefaults: {}\nsteering: []",
275        )
276        .expect("Failed to write global config");
277
278        let project_config_file = project_dir.path().join("config.yaml");
279        fs::write(
280            &project_config_file,
281            "providers:\n  default_provider: anthropic\ndefaults: {}\nsteering: []",
282        )
283        .expect("Failed to write project config");
284
285        // Load in ProjectOnly mode
286        let config = StorageModeHandler::load_for_mode(
287            StorageMode::ProjectOnly,
288            Some(global_dir.path()),
289            Some(project_dir.path()),
290        )
291        .expect("Failed to load config");
292
293        // Should only have project config
294        assert_eq!(
295            config.providers.default_provider,
296            Some("anthropic".to_string())
297        );
298    }
299}