open_sesame/config/
validator.rs

1//! Configuration validation
2//!
3//! Validates configuration and reports issues.
4
5use crate::config::Config;
6use std::collections::HashMap;
7
8/// Validation issue severity
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Severity {
11    /// Warning - configuration is valid but may have issues
12    Warning,
13    /// Error - configuration is invalid and must be fixed
14    Error,
15}
16
17/// A configuration validation issue
18#[derive(Debug, Clone)]
19pub struct ValidationIssue {
20    /// Severity level of the issue
21    pub severity: Severity,
22    /// Human-readable description of the issue
23    pub message: String,
24}
25
26impl ValidationIssue {
27    /// Create a new warning issue
28    pub fn warning(message: impl Into<String>) -> Self {
29        Self {
30            severity: Severity::Warning,
31            message: message.into(),
32        }
33    }
34
35    /// Create a new error issue
36    pub fn error(message: impl Into<String>) -> Self {
37        Self {
38            severity: Severity::Error,
39            message: message.into(),
40        }
41    }
42}
43
44/// Configuration validator
45pub struct ConfigValidator;
46
47impl ConfigValidator {
48    /// Validate configuration and return any issues
49    pub fn validate(config: &Config) -> Vec<ValidationIssue> {
50        let mut issues = Vec::new();
51
52        Self::validate_settings(&config.settings, &mut issues);
53        Self::validate_keys(&config.keys, &mut issues);
54
55        issues
56    }
57
58    /// Returns true if configuration is valid (no errors, warnings allowed).
59    pub fn is_valid(config: &Config) -> bool {
60        Self::validate(config)
61            .iter()
62            .all(|i| i.severity != Severity::Error)
63    }
64
65    fn validate_settings(settings: &crate::config::Settings, issues: &mut Vec<ValidationIssue>) {
66        if settings.activation_delay > 5000 {
67            issues.push(ValidationIssue::warning(
68                "activation_delay > 5s is very slow",
69            ));
70        }
71
72        if settings.border_width < 0.0 {
73            issues.push(ValidationIssue::error("border_width cannot be negative"));
74        }
75
76        if settings.border_width > 100.0 {
77            issues.push(ValidationIssue::warning(
78                "border_width > 100px is unusually large",
79            ));
80        }
81    }
82
83    fn validate_keys(
84        keys: &HashMap<String, crate::config::KeyBinding>,
85        issues: &mut Vec<ValidationIssue>,
86    ) {
87        // Validates key names and bindings
88        for (key, binding) in keys {
89            if key.is_empty() {
90                issues.push(ValidationIssue::error("Empty key name found"));
91            }
92            if key.len() > 1 {
93                issues.push(ValidationIssue::warning(format!(
94                    "Key '{}' should be a single character",
95                    key
96                )));
97            }
98            if binding.apps.is_empty() && binding.launch.is_none() {
99                issues.push(ValidationIssue::warning(format!(
100                    "Key '{}' has no apps and no launch command",
101                    key
102                )));
103            }
104        }
105
106        // Detects duplicate app_ids across different keys
107        let mut app_to_key: HashMap<String, String> = HashMap::new();
108        for (key, binding) in keys {
109            for app in &binding.apps {
110                let app_lower = app.to_lowercase();
111                if let Some(existing_key) = app_to_key.get(&app_lower) {
112                    if existing_key != key {
113                        issues.push(ValidationIssue::warning(format!(
114                            "App '{}' is mapped to both '{}' and '{}'",
115                            app, existing_key, key
116                        )));
117                    }
118                } else {
119                    app_to_key.insert(app_lower, key.clone());
120                }
121            }
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_validate_default_config() {
132        let config = Config::default();
133        let issues = ConfigValidator::validate(&config);
134        assert!(issues.is_empty(), "Default config should have no issues");
135        assert!(ConfigValidator::is_valid(&config));
136    }
137
138    #[test]
139    fn test_validate_slow_activation_delay() {
140        let mut config = Config::default();
141        config.settings.activation_delay = 10000;
142        let issues = ConfigValidator::validate(&config);
143        assert!(!issues.is_empty());
144        assert_eq!(issues[0].severity, Severity::Warning);
145    }
146
147    #[test]
148    fn test_validate_negative_border_width() {
149        let mut config = Config::default();
150        config.settings.border_width = -1.0;
151        let issues = ConfigValidator::validate(&config);
152        assert!(!issues.is_empty());
153        assert_eq!(issues[0].severity, Severity::Error);
154        assert!(!ConfigValidator::is_valid(&config));
155    }
156}