Skip to main content

git_gardener/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::{Path, PathBuf};
3use crate::error::{GitGardenerError, Result};
4
5#[derive(Debug, Serialize, Deserialize, Clone)]
6pub struct Config {
7    #[serde(default = "default_version")]
8    pub version: String,
9    
10    #[serde(default)]
11    pub defaults: DefaultConfig,
12    
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub hooks: Option<Hooks>,
15}
16
17#[derive(Debug, Serialize, Deserialize, Clone, Default)]
18pub struct DefaultConfig {
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub root_dir: Option<String>,
21}
22
23#[derive(Debug, Serialize, Deserialize, Clone)]
24pub struct Hooks {
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub post_create: Option<Vec<Hook>>,
27}
28
29#[derive(Debug, Serialize, Deserialize, Clone)]
30pub struct Hook {
31    #[serde(rename = "type")]
32    pub hook_type: HookType,
33    
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub from: Option<String>,
36    
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub to: Option<String>,
39    
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub command: Option<String>,
42    
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub env: Option<std::collections::HashMap<String, String>>,
45}
46
47#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
48#[serde(rename_all = "lowercase")]
49pub enum HookType {
50    Copy,
51    Command,
52}
53
54impl Default for Config {
55    fn default() -> Self {
56        Self {
57            version: default_version(),
58            defaults: DefaultConfig::default(),
59            hooks: None,
60        }
61    }
62}
63
64fn default_version() -> String {
65    "1.0".to_string()
66}
67
68impl Config {
69    pub fn load_from_file(path: &Path) -> Result<Self> {
70        if !path.exists() {
71            return Err(GitGardenerError::ConfigNotFound {
72                path: path.display().to_string(),
73            });
74        }
75        
76        let contents = std::fs::read_to_string(path)?;
77        let config: Config = serde_yaml::from_str(&contents)
78            .map_err(|e| GitGardenerError::Custom(format!("Failed to parse YAML config: {}", e)))?;
79        Ok(config)
80    }
81    
82    pub fn save_to_file(&self, path: &Path) -> Result<()> {
83        let contents = serde_yaml::to_string(self)
84            .map_err(|e| GitGardenerError::Custom(format!("Failed to serialize config: {}", e)))?;
85        
86        if let Some(parent) = path.parent() {
87            std::fs::create_dir_all(parent)?;
88        }
89        
90        std::fs::write(path, contents)?;
91        Ok(())
92    }
93    
94    pub fn get_config_path(repo_path: &Path) -> PathBuf {
95        repo_path.join(".gardener.yml")
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use tempfile::tempdir;
103    
104    #[test]
105    fn test_default_config() {
106        let config = Config::default();
107        assert_eq!(config.version, "1.0");
108        assert!(config.defaults.root_dir.is_none());
109        assert!(config.hooks.is_none());
110    }
111    
112    #[test]
113    fn test_save_and_load_config() {
114        let temp_dir = tempdir().unwrap();
115        let config_path = temp_dir.path().join("test_config.yml");
116        
117        let config = Config::default();
118        config.save_to_file(&config_path).unwrap();
119        
120        let loaded_config = Config::load_from_file(&config_path).unwrap();
121        assert_eq!(loaded_config.version, config.version);
122    }
123    
124    #[test]
125    fn test_config_load_fails_for_nonexistent_file() {
126        // What: 存在しないファイルを読み込もうとした場合にエラーが返されるかテスト
127        let temp_dir = tempdir().unwrap();
128        let config_path = temp_dir.path().join("nonexistent.yml");
129        
130        let result = Config::load_from_file(&config_path);
131        
132        assert!(result.is_err());
133        assert!(matches!(
134            result.unwrap_err(),
135            GitGardenerError::ConfigNotFound { .. }
136        ));
137    }
138    
139    #[test]
140    fn test_config_with_hooks_serialization() {
141        // What: フック設定を含むconfigの直列化・逆直列化が正しく動作するかテスト
142        use std::collections::HashMap;
143        
144        let mut env = HashMap::new();
145        env.insert("NODE_ENV".to_string(), "development".to_string());
146        
147        let hook = Hook {
148            hook_type: HookType::Command,
149            from: None,
150            to: None,
151            command: Some("npm install".to_string()),
152            env: Some(env),
153        };
154        
155        let hooks = Hooks {
156            post_create: Some(vec![hook]),
157        };
158        
159        let config = Config {
160            version: "1.0".to_string(),
161            defaults: DefaultConfig {
162                root_dir: Some(".gardener".to_string()),
163            },
164            hooks: Some(hooks),
165        };
166        
167        let temp_dir = tempdir().unwrap();
168        let config_path = temp_dir.path().join("hooks_config.yml");
169        
170        config.save_to_file(&config_path).unwrap();
171        let loaded_config = Config::load_from_file(&config_path).unwrap();
172        
173        assert_eq!(loaded_config.version, config.version);
174        assert!(loaded_config.hooks.is_some());
175        
176        let loaded_hooks = loaded_config.hooks.unwrap();
177        assert!(loaded_hooks.post_create.is_some());
178        
179        let hooks_vec = loaded_hooks.post_create.unwrap();
180        assert_eq!(hooks_vec.len(), 1);
181        assert_eq!(hooks_vec[0].hook_type, HookType::Command);
182        assert_eq!(hooks_vec[0].command, Some("npm install".to_string()));
183    }
184    
185    #[test]
186    fn test_config_with_copy_hook_serialization() {
187        // What: copyフックの直列化・逆直列化が正しく動作するかテスト
188        let hook = Hook {
189            hook_type: HookType::Copy,
190            from: Some("README.md".to_string()),
191            to: Some("README.md".to_string()),
192            command: None,
193            env: None,
194        };
195        
196        let hooks = Hooks {
197            post_create: Some(vec![hook]),
198        };
199        
200        let config = Config {
201            version: "1.0".to_string(),
202            defaults: DefaultConfig::default(),
203            hooks: Some(hooks),
204        };
205        
206        let temp_dir = tempdir().unwrap();
207        let config_path = temp_dir.path().join("copy_config.yml");
208        
209        config.save_to_file(&config_path).unwrap();
210        let loaded_config = Config::load_from_file(&config_path).unwrap();
211        
212        let loaded_hooks = loaded_config.hooks.unwrap();
213        let hooks_vec = loaded_hooks.post_create.unwrap();
214        
215        assert_eq!(hooks_vec[0].hook_type, HookType::Copy);
216        assert_eq!(hooks_vec[0].from, Some("README.md".to_string()));
217        assert_eq!(hooks_vec[0].to, Some("README.md".to_string()));
218        assert!(hooks_vec[0].command.is_none());
219    }
220    
221    #[test]
222    fn test_config_fails_with_invalid_yaml() {
223        // What: 不正なYAMLファイルの読み込みでエラーが返されるかテスト
224        let temp_dir = tempdir().unwrap();
225        let config_path = temp_dir.path().join("invalid.yml");
226        
227        // 不正なYAMLを書き込み
228        std::fs::write(&config_path, "invalid: yaml: content: [").unwrap();
229        
230        let result = Config::load_from_file(&config_path);
231        
232        assert!(result.is_err());
233        assert!(matches!(result.unwrap_err(), GitGardenerError::Custom(_)));
234    }
235}