Skip to main content

dotm/
config.rs

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        // Validate ownership format
68        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        // Validate permissions format
76        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        // Validate preserve entries don't conflict
84        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}