cfgd_core/generate/
session.rs1use 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#[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}