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 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}