Skip to main content

cfgd_core/generate/
session.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use crate::atomic_write_str;
5use crate::errors::{CfgdError, GenerateError};
6use crate::generate::SchemaKind;
7use crate::generate::validate::validate_yaml;
8
9/// Tracks state for a generate session.
10#[derive(Debug)]
11pub struct GenerateSession {
12    repo_root: PathBuf,
13    generated: HashMap<String, GeneratedItem>,
14}
15
16#[derive(Debug, Clone)]
17pub struct GeneratedItem {
18    pub kind: SchemaKind,
19    pub name: String,
20    pub path: PathBuf,
21}
22
23impl GenerateSession {
24    pub fn new(repo_root: PathBuf) -> Self {
25        Self {
26            repo_root,
27            generated: HashMap::new(),
28        }
29    }
30
31    pub fn repo_root(&self) -> &Path {
32        &self.repo_root
33    }
34
35    pub fn write_module_yaml(&mut self, name: &str, content: &str) -> Result<PathBuf, CfgdError> {
36        let result = validate_yaml(content, SchemaKind::Module);
37        if !result.valid {
38            return Err(GenerateError::ValidationFailed {
39                message: format!("Invalid module YAML: {}", result.errors.join("; ")),
40            }
41            .into());
42        }
43        let dir = self.repo_root.join("modules").join(name);
44        std::fs::create_dir_all(&dir)?;
45        let path = dir.join("module.yaml");
46        atomic_write_str(&path, content)?;
47        let key = format!("module:{}", name);
48        self.generated.insert(
49            key,
50            GeneratedItem {
51                kind: SchemaKind::Module,
52                name: name.to_string(),
53                path: path.clone(),
54            },
55        );
56        Ok(path)
57    }
58
59    pub fn write_profile_yaml(&mut self, name: &str, content: &str) -> Result<PathBuf, CfgdError> {
60        let result = validate_yaml(content, SchemaKind::Profile);
61        if !result.valid {
62            return Err(GenerateError::ValidationFailed {
63                message: format!("Invalid profile YAML: {}", result.errors.join("; ")),
64            }
65            .into());
66        }
67        let dir = self.repo_root.join("profiles");
68        std::fs::create_dir_all(&dir)?;
69        let path = dir.join(format!("{}.yaml", name));
70        atomic_write_str(&path, content)?;
71        let key = format!("profile:{}", name);
72        self.generated.insert(
73            key,
74            GeneratedItem {
75                kind: SchemaKind::Profile,
76                name: name.to_string(),
77                path: path.clone(),
78            },
79        );
80        Ok(path)
81    }
82
83    pub fn list_generated(&self) -> Vec<&GeneratedItem> {
84        self.generated.values().collect()
85    }
86
87    pub fn get_existing_modules(&self) -> Result<Vec<String>, CfgdError> {
88        let modules_dir = self.repo_root.join("modules");
89        if !modules_dir.exists() {
90            return Ok(vec![]);
91        }
92        let mut names = vec![];
93        for entry in std::fs::read_dir(&modules_dir)? {
94            let entry = entry?;
95            if entry.path().is_dir()
96                && entry.path().join("module.yaml").exists()
97                && let Some(name) = entry.file_name().to_str()
98            {
99                names.push(name.to_string());
100            }
101        }
102        names.sort();
103        Ok(names)
104    }
105
106    pub fn get_existing_profiles(&self) -> Result<Vec<String>, CfgdError> {
107        let profiles_dir = self.repo_root.join("profiles");
108        if !profiles_dir.exists() {
109            return Ok(vec![]);
110        }
111        let mut names = vec![];
112        for entry in std::fs::read_dir(&profiles_dir)? {
113            let entry = entry?;
114            let path = entry.path();
115            if path.extension().and_then(|e| e.to_str()) == Some("yaml")
116                && let Some(stem) = path.file_stem().and_then(|s| s.to_str())
117            {
118                names.push(stem.to_string());
119            }
120        }
121        names.sort();
122        Ok(names)
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use tempfile::TempDir;
130
131    #[test]
132    fn test_get_existing_modules_finds_modules() {
133        let tmp = TempDir::new().unwrap();
134        let nvim_dir = tmp.path().join("modules").join("nvim");
135        std::fs::create_dir_all(&nvim_dir).unwrap();
136        std::fs::write(nvim_dir.join("module.yaml"), "test").unwrap();
137        let tmux_dir = tmp.path().join("modules").join("tmux");
138        std::fs::create_dir_all(&tmux_dir).unwrap();
139        std::fs::write(tmux_dir.join("module.yaml"), "test").unwrap();
140
141        let session = GenerateSession::new(tmp.path().to_path_buf());
142        let modules = session.get_existing_modules().unwrap();
143        assert_eq!(modules, vec!["nvim", "tmux"]);
144    }
145
146    #[test]
147    fn test_get_existing_profiles_finds_profiles() {
148        let tmp = TempDir::new().unwrap();
149        let profiles_dir = tmp.path().join("profiles");
150        std::fs::create_dir_all(&profiles_dir).unwrap();
151        std::fs::write(profiles_dir.join("base.yaml"), "test").unwrap();
152        std::fs::write(profiles_dir.join("work.yaml"), "test").unwrap();
153
154        let session = GenerateSession::new(tmp.path().to_path_buf());
155        let profiles = session.get_existing_profiles().unwrap();
156        assert_eq!(profiles, vec!["base", "work"]);
157    }
158
159    #[test]
160    fn test_write_module_yaml_valid() {
161        let tmp = TempDir::new().unwrap();
162        let mut session = GenerateSession::new(tmp.path().to_path_buf());
163        let yaml = "apiVersion: cfgd.io/v1alpha1\nkind: Module\nmetadata:\n  name: nvim\nspec:\n  packages:\n    - name: neovim\n";
164        let path = session.write_module_yaml("nvim", yaml).unwrap();
165        assert_eq!(path, tmp.path().join("modules/nvim/module.yaml"));
166        assert!(path.exists());
167        assert_eq!(std::fs::read_to_string(&path).unwrap(), yaml);
168        assert_eq!(session.list_generated().len(), 1);
169    }
170
171    #[test]
172    fn test_write_module_yaml_invalid_rejected() {
173        let tmp = TempDir::new().unwrap();
174        let mut session = GenerateSession::new(tmp.path().to_path_buf());
175        let result = session.write_module_yaml("bad", "not valid yaml {{");
176        assert!(result.is_err());
177        assert!(session.list_generated().is_empty());
178    }
179
180    #[test]
181    fn test_write_profile_yaml_valid() {
182        let tmp = TempDir::new().unwrap();
183        let mut session = GenerateSession::new(tmp.path().to_path_buf());
184        let yaml = "apiVersion: cfgd.io/v1alpha1\nkind: Profile\nmetadata:\n  name: base\nspec:\n  modules:\n    - nvim\n";
185        let path = session.write_profile_yaml("base", yaml).unwrap();
186        assert_eq!(path, tmp.path().join("profiles/base.yaml"));
187        assert!(path.exists());
188        assert_eq!(session.list_generated().len(), 1);
189    }
190
191    #[test]
192    fn test_write_profile_yaml_wrong_kind_rejected() {
193        let tmp = TempDir::new().unwrap();
194        let mut session = GenerateSession::new(tmp.path().to_path_buf());
195        let yaml =
196            "apiVersion: cfgd.io/v1alpha1\nkind: Module\nmetadata:\n  name: nvim\nspec: {}\n";
197        let result = session.write_profile_yaml("nvim", yaml);
198        let err_msg = format!("{}", result.unwrap_err());
199        assert!(
200            err_msg.contains("Invalid profile YAML"),
201            "expected validation error about wrong kind, got: {err_msg}"
202        );
203    }
204}