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        let mut names = vec![];
109        crate::config::for_each_yaml_file(&profiles_dir, |path| {
110            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
111                names.push(stem.to_string());
112            }
113            Ok(())
114        })?;
115        names.sort();
116        Ok(names)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use tempfile::TempDir;
124
125    #[test]
126    fn test_get_existing_modules_finds_modules() {
127        let tmp = TempDir::new().unwrap();
128        let nvim_dir = tmp.path().join("modules").join("nvim");
129        std::fs::create_dir_all(&nvim_dir).unwrap();
130        std::fs::write(nvim_dir.join("module.yaml"), "test").unwrap();
131        let tmux_dir = tmp.path().join("modules").join("tmux");
132        std::fs::create_dir_all(&tmux_dir).unwrap();
133        std::fs::write(tmux_dir.join("module.yaml"), "test").unwrap();
134
135        let session = GenerateSession::new(tmp.path().to_path_buf());
136        let modules = session.get_existing_modules().unwrap();
137        assert_eq!(modules, vec!["nvim", "tmux"]);
138    }
139
140    #[test]
141    fn test_get_existing_profiles_finds_profiles() {
142        let tmp = TempDir::new().unwrap();
143        let profiles_dir = tmp.path().join("profiles");
144        std::fs::create_dir_all(&profiles_dir).unwrap();
145        std::fs::write(profiles_dir.join("base.yaml"), "test").unwrap();
146        std::fs::write(profiles_dir.join("work.yaml"), "test").unwrap();
147
148        let session = GenerateSession::new(tmp.path().to_path_buf());
149        let profiles = session.get_existing_profiles().unwrap();
150        assert_eq!(profiles, vec!["base", "work"]);
151    }
152
153    #[test]
154    fn test_write_module_yaml_valid() {
155        let tmp = TempDir::new().unwrap();
156        let mut session = GenerateSession::new(tmp.path().to_path_buf());
157        let yaml = "apiVersion: cfgd.io/v1alpha1\nkind: Module\nmetadata:\n  name: nvim\nspec:\n  packages:\n    - name: neovim\n";
158        let path = session.write_module_yaml("nvim", yaml).unwrap();
159        assert_eq!(path, tmp.path().join("modules/nvim/module.yaml"));
160        assert!(path.exists());
161        assert_eq!(std::fs::read_to_string(&path).unwrap(), yaml);
162        assert_eq!(session.list_generated().len(), 1);
163    }
164
165    #[test]
166    fn test_write_module_yaml_invalid_rejected() {
167        let tmp = TempDir::new().unwrap();
168        let mut session = GenerateSession::new(tmp.path().to_path_buf());
169        let result = session.write_module_yaml("bad", "not valid yaml {{");
170        assert!(result.is_err());
171        assert!(session.list_generated().is_empty());
172    }
173
174    #[test]
175    fn test_write_profile_yaml_valid() {
176        let tmp = TempDir::new().unwrap();
177        let mut session = GenerateSession::new(tmp.path().to_path_buf());
178        let yaml = "apiVersion: cfgd.io/v1alpha1\nkind: Profile\nmetadata:\n  name: base\nspec:\n  modules:\n    - nvim\n";
179        let path = session.write_profile_yaml("base", yaml).unwrap();
180        assert_eq!(path, tmp.path().join("profiles/base.yaml"));
181        assert!(path.exists());
182        assert_eq!(session.list_generated().len(), 1);
183    }
184
185    #[test]
186    fn test_write_profile_yaml_wrong_kind_rejected() {
187        let tmp = TempDir::new().unwrap();
188        let mut session = GenerateSession::new(tmp.path().to_path_buf());
189        let yaml =
190            "apiVersion: cfgd.io/v1alpha1\nkind: Module\nmetadata:\n  name: nvim\nspec: {}\n";
191        let result = session.write_profile_yaml("nvim", yaml);
192        let err_msg = format!("{}", result.unwrap_err());
193        assert!(
194            err_msg.contains("Invalid profile YAML"),
195            "expected validation error about wrong kind, got: {err_msg}"
196        );
197    }
198}