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}
19
20fn default_packages_dir() -> String {
21 "packages".to_string()
22}
23
24#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy)]
25#[serde(rename_all = "lowercase")]
26pub enum DeployStrategy {
27 Stage,
28 Copy,
29}
30
31#[derive(Debug, Default, Deserialize)]
32pub struct PackageConfig {
33 pub description: Option<String>,
34 #[serde(default)]
35 pub depends: Vec<String>,
36 #[serde(default)]
37 pub suggests: Vec<String>,
38 pub target: Option<String>,
39 pub strategy: Option<DeployStrategy>,
40 #[serde(default)]
41 pub permissions: HashMap<String, String>,
42 #[serde(default)]
43 pub system: bool,
44 pub owner: Option<String>,
45 pub group: Option<String>,
46 #[serde(default)]
47 pub ownership: HashMap<String, String>,
48 #[serde(default)]
49 pub preserve: HashMap<String, Vec<String>>,
50}
51
52pub fn validate_system_packages(root: &RootConfig) -> Vec<String> {
53 let mut errors = Vec::new();
54 for (name, pkg) in &root.packages {
55 if pkg.system {
56 if pkg.target.is_none() {
57 errors.push(format!(
58 "system package '{name}' must specify a target directory"
59 ));
60 }
61 if pkg.strategy.is_none() {
62 errors.push(format!(
63 "system package '{name}' must specify a deployment strategy"
64 ));
65 }
66 }
67 for (path, value) in &pkg.ownership {
69 if value.split(':').count() != 2 {
70 errors.push(format!(
71 "package '{name}': invalid ownership format for '{path}': expected 'user:group', got '{value}'"
72 ));
73 }
74 }
75 for (path, value) in &pkg.permissions {
77 if u32::from_str_radix(value, 8).is_err() {
78 errors.push(format!(
79 "package '{name}': invalid permission for '{path}': '{value}' is not valid octal"
80 ));
81 }
82 }
83 for (path, preserve_fields) in &pkg.preserve {
85 for field in preserve_fields {
86 match field.as_str() {
87 "owner" | "group" => {
88 if pkg.ownership.contains_key(path) {
89 errors.push(format!(
90 "package '{name}': file '{path}' has both preserve {field} and ownership override"
91 ));
92 }
93 }
94 "mode" => {
95 if pkg.permissions.contains_key(path) {
96 errors.push(format!(
97 "package '{name}': file '{path}' has both preserve mode and permission override"
98 ));
99 }
100 }
101 other => {
102 errors.push(format!(
103 "package '{name}': file '{path}': unknown preserve field '{other}'"
104 ));
105 }
106 }
107 }
108 }
109 }
110 errors
111}
112
113#[derive(Debug, Deserialize)]
114pub struct HostConfig {
115 pub hostname: String,
116 pub roles: Vec<String>,
117 #[serde(default)]
118 pub vars: Map<String, Value>,
119}
120
121#[derive(Debug, Deserialize)]
122pub struct RoleConfig {
123 pub packages: Vec<String>,
124 #[serde(default)]
125 pub vars: Map<String, Value>,
126}