1use serde::Deserialize;
2use std::collections::HashMap;
3use toml::map::Map;
4use toml::Value;
5
6#[derive(Debug, Deserialize)]
7pub struct RootConfig {
8 pub dotm: DotmSettings,
9 #[serde(default)]
10 pub packages: HashMap<String, PackageConfig>,
11}
12
13#[derive(Debug, Deserialize)]
14pub struct DotmSettings {
15 pub target: String,
16 #[serde(default = "default_packages_dir")]
17 pub packages_dir: String,
18 #[serde(default)]
19 pub auto_prune: bool,
20}
21
22fn default_packages_dir() -> String {
23 "packages".to_string()
24}
25
26#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy)]
27#[serde(rename_all = "lowercase")]
28pub enum DeployStrategy {
29 Stage,
30 Copy,
31}
32
33#[derive(Debug, Default, Deserialize)]
34pub struct PackageConfig {
35 pub description: Option<String>,
36 #[serde(default)]
37 pub depends: Vec<String>,
38 #[serde(default)]
39 pub suggests: Vec<String>,
40 pub target: Option<String>,
41 pub strategy: Option<DeployStrategy>,
42 #[serde(default)]
43 pub permissions: HashMap<String, String>,
44 #[serde(default)]
45 pub system: bool,
46 pub owner: Option<String>,
47 pub group: Option<String>,
48 #[serde(default)]
49 pub ownership: HashMap<String, String>,
50 #[serde(default)]
51 pub preserve: HashMap<String, Vec<String>>,
52 pub pre_deploy: Option<String>,
53 pub post_deploy: Option<String>,
54 pub pre_undeploy: Option<String>,
55 pub post_undeploy: Option<String>,
56}
57
58pub fn validate_system_packages(root: &RootConfig) -> Vec<String> {
59 let mut errors = Vec::new();
60 for (name, pkg) in &root.packages {
61 if pkg.system && pkg.target.is_none() {
62 errors.push(format!(
63 "system package '{name}' must specify a target directory"
64 ));
65 }
66 for (path, value) in &pkg.ownership {
68 if value.split(':').count() != 2 {
69 errors.push(format!(
70 "package '{name}': invalid ownership format for '{path}': expected 'user:group', got '{value}'"
71 ));
72 }
73 }
74 for (path, value) in &pkg.permissions {
76 if u32::from_str_radix(value, 8).is_err() {
77 errors.push(format!(
78 "package '{name}': invalid permission for '{path}': '{value}' is not valid octal"
79 ));
80 }
81 }
82 for (path, preserve_fields) in &pkg.preserve {
84 for field in preserve_fields {
85 match field.as_str() {
86 "owner" | "group" => {
87 if pkg.ownership.contains_key(path) {
88 errors.push(format!(
89 "package '{name}': file '{path}' has both preserve {field} and ownership override"
90 ));
91 }
92 }
93 "mode" => {
94 if pkg.permissions.contains_key(path) {
95 errors.push(format!(
96 "package '{name}': file '{path}' has both preserve mode and permission override"
97 ));
98 }
99 }
100 other => {
101 errors.push(format!(
102 "package '{name}': file '{path}': unknown preserve field '{other}'"
103 ));
104 }
105 }
106 }
107 }
108 }
109 errors
110}
111
112pub fn deprecated_strategy_warnings(root: &RootConfig) -> Vec<String> {
113 let mut warnings = Vec::new();
114 for (name, pkg) in &root.packages {
115 if pkg.strategy.is_some() {
116 warnings.push(format!(
117 "warning: 'strategy' field on package '{name}' is deprecated and ignored; deployment mode is now determined automatically"
118 ));
119 }
120 }
121 warnings
122}
123
124#[derive(Debug, Deserialize)]
125pub struct HostConfig {
126 pub hostname: String,
127 pub roles: Vec<String>,
128 #[serde(default)]
129 pub vars: Map<String, Value>,
130}
131
132#[derive(Debug, Deserialize)]
133pub struct RoleConfig {
134 pub packages: Vec<String>,
135 #[serde(default)]
136 pub vars: Map<String, Value>,
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn validate_system_packages_does_not_require_strategy() {
145 let toml_str = r#"
146[dotm]
147target = "~"
148
149[packages.sys]
150system = true
151target = "/etc/sys"
152"#;
153 let root: RootConfig = toml::from_str(toml_str).unwrap();
154 let errors = validate_system_packages(&root);
155 assert!(errors.is_empty(), "unexpected errors: {:?}", errors);
156 }
157
158 #[test]
159 fn strategy_field_still_parses() {
160 let toml_str = r#"
161[dotm]
162target = "~"
163
164[packages.sys]
165system = true
166target = "/etc/sys"
167strategy = "copy"
168"#;
169 let root: RootConfig = toml::from_str(toml_str).unwrap();
170 assert!(root.packages["sys"].strategy.is_some());
171 }
172
173 #[test]
174 fn deprecated_strategy_warning_emitted() {
175 let toml_str = r#"
176[dotm]
177target = "~"
178
179[packages.shell]
180strategy = "stage"
181"#;
182 let root: RootConfig = toml::from_str(toml_str).unwrap();
183 let warnings = deprecated_strategy_warnings(&root);
184 assert_eq!(warnings.len(), 1);
185 assert!(warnings[0].contains("shell"));
186 assert!(warnings[0].contains("deprecated"));
187 }
188
189 #[test]
190 fn no_deprecation_warning_without_strategy() {
191 let toml_str = r#"
192[dotm]
193target = "~"
194
195[packages.shell]
196"#;
197 let root: RootConfig = toml::from_str(toml_str).unwrap();
198 let warnings = deprecated_strategy_warnings(&root);
199 assert!(warnings.is_empty());
200 }
201}