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    #[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 {
62            if pkg.target.is_none() {
63                errors.push(format!(
64                    "system package '{name}' must specify a target directory"
65                ));
66            }
67            if pkg.strategy.is_none() {
68                errors.push(format!(
69                    "system package '{name}' must specify a deployment strategy"
70                ));
71            }
72        }
73        // Validate ownership format
74        for (path, value) in &pkg.ownership {
75            if value.split(':').count() != 2 {
76                errors.push(format!(
77                    "package '{name}': invalid ownership format for '{path}': expected 'user:group', got '{value}'"
78                ));
79            }
80        }
81        // Validate permissions format
82        for (path, value) in &pkg.permissions {
83            if u32::from_str_radix(value, 8).is_err() {
84                errors.push(format!(
85                    "package '{name}': invalid permission for '{path}': '{value}' is not valid octal"
86                ));
87            }
88        }
89        // Validate preserve entries don't conflict
90        for (path, preserve_fields) in &pkg.preserve {
91            for field in preserve_fields {
92                match field.as_str() {
93                    "owner" | "group" => {
94                        if pkg.ownership.contains_key(path) {
95                            errors.push(format!(
96                                "package '{name}': file '{path}' has both preserve {field} and ownership override"
97                            ));
98                        }
99                    }
100                    "mode" => {
101                        if pkg.permissions.contains_key(path) {
102                            errors.push(format!(
103                                "package '{name}': file '{path}' has both preserve mode and permission override"
104                            ));
105                        }
106                    }
107                    other => {
108                        errors.push(format!(
109                            "package '{name}': file '{path}': unknown preserve field '{other}'"
110                        ));
111                    }
112                }
113            }
114        }
115    }
116    errors
117}
118
119#[derive(Debug, Deserialize)]
120pub struct HostConfig {
121    pub hostname: String,
122    pub roles: Vec<String>,
123    #[serde(default)]
124    pub vars: Map<String, Value>,
125}
126
127#[derive(Debug, Deserialize)]
128pub struct RoleConfig {
129    pub packages: Vec<String>,
130    #[serde(default)]
131    pub vars: Map<String, Value>,
132}