open_sesame/config/
validator.rs1use crate::config::Config;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Severity {
11 Warning,
13 Error,
15}
16
17#[derive(Debug, Clone)]
19pub struct ValidationIssue {
20 pub severity: Severity,
22 pub message: String,
24}
25
26impl ValidationIssue {
27 pub fn warning(message: impl Into<String>) -> Self {
29 Self {
30 severity: Severity::Warning,
31 message: message.into(),
32 }
33 }
34
35 pub fn error(message: impl Into<String>) -> Self {
37 Self {
38 severity: Severity::Error,
39 message: message.into(),
40 }
41 }
42}
43
44pub struct ConfigValidator;
46
47impl ConfigValidator {
48 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 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 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 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}