ricecoder_hooks/config/
loader.rs1use crate::error::{HooksError, Result};
8use crate::types::Hook;
9use ricecoder_storage::PathResolver;
10use std::collections::HashMap;
11use std::fs;
12use std::path::{Path, PathBuf};
13
14pub struct ConfigLoader;
23
24impl ConfigLoader {
25 pub fn load() -> Result<HashMap<String, Hook>> {
38 let mut hooks = HashMap::new();
39
40 if let Ok(project_hooks) = Self::load_project_config() {
42 hooks.extend(project_hooks);
43 }
44
45 if let Ok(user_hooks) = Self::load_user_config() {
47 hooks.extend(user_hooks);
49 }
50
51 if let Ok(builtin_hooks) = Self::load_builtin_config() {
53 for (id, hook) in builtin_hooks {
55 hooks.entry(id).or_insert(hook);
56 }
57 }
58
59 Ok(hooks)
60 }
61
62 fn load_project_config() -> Result<HashMap<String, Hook>> {
66 let project_path = PathBuf::from(".ricecoder/hooks.yaml");
67 Self::load_from_path(&project_path)
68 }
69
70 fn load_user_config() -> Result<HashMap<String, Hook>> {
74 let global_path = PathResolver::resolve_global_path()
75 .map_err(|e| HooksError::StorageError(e.to_string()))?;
76 let user_config_path = global_path.join("hooks.yaml");
77 Self::load_from_path(&user_config_path)
78 }
79
80 fn load_builtin_config() -> Result<HashMap<String, Hook>> {
84 Ok(HashMap::new())
86 }
87
88 fn load_from_path(path: &Path) -> Result<HashMap<String, Hook>> {
97 if !path.exists() {
99 return Ok(HashMap::new());
100 }
101
102 let content = fs::read_to_string(path)
103 .map_err(|e| HooksError::StorageError(format!("Failed to read config file: {}", e)))?;
104
105 Self::parse_yaml(&content)
106 }
107
108 fn parse_yaml(content: &str) -> Result<HashMap<String, Hook>> {
125 let value: serde_yaml::Value = serde_yaml::from_str(content)
126 .map_err(|e| HooksError::InvalidConfiguration(format!("Invalid YAML: {}", e)))?;
127
128 let mut hooks = HashMap::new();
129
130 if let Some(hooks_array) = value.get("hooks").and_then(|v| v.as_sequence()) {
132 for hook_value in hooks_array {
133 match serde_yaml::from_value::<Hook>(hook_value.clone()) {
134 Ok(hook) => {
135 hooks.insert(hook.id.clone(), hook);
136 }
137 Err(e) => {
138 return Err(HooksError::InvalidConfiguration(format!(
139 "Failed to parse hook: {}",
140 e
141 )));
142 }
143 }
144 }
145 }
146
147 Ok(hooks)
148 }
149
150 #[cfg(test)]
154 pub fn load_from_string(content: &str) -> Result<HashMap<String, Hook>> {
155 Self::parse_yaml(content)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn test_parse_yaml_with_command_action() {
165 let yaml = r#"
166hooks:
167 - id: test-hook
168 name: Test Hook
169 description: A test hook
170 event: file_saved
171 action:
172 type: command
173 command: echo
174 args:
175 - hello
176 timeout_ms: 5000
177 capture_output: true
178 enabled: true
179 tags:
180 - test
181 metadata: {}
182"#;
183
184 let hooks = ConfigLoader::load_from_string(yaml).expect("Should parse YAML");
185 assert_eq!(hooks.len(), 1);
186
187 let hook = hooks.get("test-hook").expect("Should find hook");
188 assert_eq!(hook.name, "Test Hook");
189 assert_eq!(hook.event, "file_saved");
190 assert!(hook.enabled);
191 assert_eq!(hook.tags.len(), 1);
192 }
193
194 #[test]
195 fn test_parse_yaml_multiple_hooks() {
196 let yaml = r#"
197hooks:
198 - id: hook1
199 name: Hook 1
200 event: event1
201 action:
202 type: command
203 command: cmd1
204 args: []
205 timeout_ms: null
206 capture_output: false
207 enabled: true
208 tags: []
209 metadata: {}
210 - id: hook2
211 name: Hook 2
212 event: event2
213 action:
214 type: command
215 command: cmd2
216 args: []
217 timeout_ms: null
218 capture_output: false
219 enabled: false
220 tags: []
221 metadata: {}
222"#;
223
224 let hooks = ConfigLoader::load_from_string(yaml).expect("Should parse YAML");
225 assert_eq!(hooks.len(), 2);
226 assert!(hooks.contains_key("hook1"));
227 assert!(hooks.contains_key("hook2"));
228 }
229
230 #[test]
231 fn test_parse_yaml_empty() {
232 let yaml = "hooks: []";
233 let hooks = ConfigLoader::load_from_string(yaml).expect("Should parse empty YAML");
234 assert_eq!(hooks.len(), 0);
235 }
236
237 #[test]
238 fn test_parse_yaml_invalid() {
239 let yaml = "invalid: yaml: content:";
240 let result = ConfigLoader::load_from_string(yaml);
241 assert!(result.is_err());
242 }
243
244 #[test]
245 fn test_parse_yaml_missing_required_field() {
246 let yaml = r#"
247hooks:
248 - id: test-hook
249 name: Test Hook
250"#;
251 let result = ConfigLoader::load_from_string(yaml);
252 assert!(result.is_err());
253 }
254
255 #[test]
256 fn test_load_from_nonexistent_path() {
257 let path = PathBuf::from("/nonexistent/path/hooks.yaml");
258 let hooks = ConfigLoader::load_from_path(&path).expect("Should return empty map");
259 assert_eq!(hooks.len(), 0);
260 }
261}