vespertide_loader/
config.rs

1use std::fs;
2use std::path::PathBuf;
3
4use anyhow::{Context, Result};
5use vespertide_config::VespertideConfig;
6
7/// Load vespertide.json config from current directory.
8pub fn load_config() -> Result<VespertideConfig> {
9    let path = PathBuf::from("vespertide.json");
10    if !path.exists() {
11        anyhow::bail!("vespertide.json not found. Run 'vespertide init' first.");
12    }
13
14    let content = fs::read_to_string(&path).context("read vespertide.json")?;
15    let config: VespertideConfig =
16        serde_json::from_str(&content).context("parse vespertide.json")?;
17    Ok(config)
18}
19
20/// Load config from a specific path.
21pub fn load_config_from_path(path: PathBuf) -> Result<VespertideConfig> {
22    if !path.exists() {
23        anyhow::bail!("vespertide.json not found at: {}", path.display());
24    }
25
26    let content = fs::read_to_string(&path).context("read vespertide.json")?;
27    let config: VespertideConfig =
28        serde_json::from_str(&content).context("parse vespertide.json")?;
29    Ok(config)
30}
31
32/// Load config from project root, with fallback to defaults.
33pub fn load_config_or_default(project_root: Option<PathBuf>) -> Result<VespertideConfig> {
34    let config_path = if let Some(root) = project_root {
35        root.join("vespertide.json")
36    } else {
37        PathBuf::from("vespertide.json")
38    };
39
40    if config_path.exists() {
41        load_config_from_path(config_path)
42    } else {
43        Ok(VespertideConfig::default())
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use serial_test::serial;
51    use std::fs;
52    use tempfile::tempdir;
53
54    struct CwdGuard {
55        original: PathBuf,
56    }
57
58    impl CwdGuard {
59        fn new(dir: &PathBuf) -> Self {
60            let original = std::env::current_dir().unwrap();
61            std::env::set_current_dir(dir).unwrap();
62            Self { original }
63        }
64    }
65
66    impl Drop for CwdGuard {
67        fn drop(&mut self) {
68            let _ = std::env::set_current_dir(&self.original);
69        }
70    }
71
72    fn write_config(path: &PathBuf) {
73        let cfg = VespertideConfig::default();
74        let text = serde_json::to_string_pretty(&cfg).unwrap();
75        fs::write(path, text).unwrap();
76    }
77
78    #[test]
79    #[serial]
80    fn test_load_config_from_path_success() {
81        let tmp = tempdir().unwrap();
82        let config_path = tmp.path().join("vespertide.json");
83        write_config(&config_path);
84
85        let result = load_config_from_path(config_path);
86        assert!(result.is_ok());
87        let config = result.unwrap();
88        assert_eq!(config.models_dir, PathBuf::from("models"));
89    }
90
91    #[test]
92    #[serial]
93    fn test_load_config_from_path_not_found() {
94        let tmp = tempdir().unwrap();
95        let config_path = tmp.path().join("nonexistent.json");
96
97        let result = load_config_from_path(config_path.clone());
98        assert!(result.is_err());
99        let err_msg = result.unwrap_err().to_string();
100        assert!(err_msg.contains("vespertide.json not found at:"));
101        assert!(err_msg.contains(&config_path.display().to_string()));
102    }
103
104    #[test]
105    #[serial]
106    fn test_load_config_or_default_with_root() {
107        let tmp = tempdir().unwrap();
108        let config_path = tmp.path().join("vespertide.json");
109        write_config(&config_path);
110
111        let result = load_config_or_default(Some(tmp.path().to_path_buf()));
112        assert!(result.is_ok());
113        let config = result.unwrap();
114        assert_eq!(config.models_dir, PathBuf::from("models"));
115    }
116
117    #[test]
118    #[serial]
119    fn test_load_config_or_default_without_root() {
120        let tmp = tempdir().unwrap();
121        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
122        let config_path = PathBuf::from("vespertide.json");
123        write_config(&config_path);
124
125        let result = load_config_or_default(None);
126        assert!(result.is_ok());
127        let config = result.unwrap();
128        assert_eq!(config.models_dir, PathBuf::from("models"));
129    }
130
131    #[test]
132    #[serial]
133    fn test_load_config_or_default_fallback_to_default() {
134        let tmp = tempdir().unwrap();
135        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
136
137        let result = load_config_or_default(None);
138        assert!(result.is_ok());
139        let config = result.unwrap();
140        assert_eq!(config.models_dir, PathBuf::from("models"));
141    }
142
143    #[test]
144    #[serial]
145    fn test_load_config_success() {
146        let tmp = tempdir().unwrap();
147        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
148        let config_path = PathBuf::from("vespertide.json");
149        write_config(&config_path);
150
151        let result = load_config();
152        assert!(result.is_ok());
153        let config = result.unwrap();
154        assert_eq!(config.models_dir, PathBuf::from("models"));
155    }
156
157    #[test]
158    #[serial]
159    fn test_load_config_not_found() {
160        let tmp = tempdir().unwrap();
161        let _guard = CwdGuard::new(&tmp.path().to_path_buf());
162
163        let result = load_config();
164        assert!(result.is_err());
165        let err_msg = result.unwrap_err().to_string();
166        assert!(err_msg.contains("vespertide.json not found"));
167    }
168}