use std::collections::HashMap;
pub type ValidationResult = Result<(), ValidationError>;
#[derive(Debug, Clone)]
pub struct ValidationError {
pub field: String,
pub message: String,
pub suggestion: Option<String>,
}
impl ValidationError {
pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
Self {
field: field.into(),
message: message.into(),
suggestion: None,
}
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
}
pub struct ConfigValidator {
rules: HashMap<String, Vec<Box<dyn Fn(&str) -> ValidationResult + Send + Sync>>>,
}
impl ConfigValidator {
pub fn new() -> Self {
Self {
rules: HashMap::new(),
}
}
pub fn add_rule<F>(&mut self, field: &str, rule: F)
where
F: Fn(&str) -> ValidationResult + Send + Sync + 'static,
{
self.rules
.entry(field.to_string())
.or_insert_with(Vec::new)
.push(Box::new(rule));
}
pub fn validate(&self, field: &str, value: &str) -> ValidationResult {
if let Some(rules) = self.rules.get(field) {
for rule in rules {
rule(value)?;
}
}
Ok(())
}
pub fn with_common_rules() -> Self {
let mut v = Self::new();
v.add_rule("theme", |value| {
if value.is_empty() {
return Err(ValidationError::new("theme", "Theme name cannot be empty")
.with_suggestion("Use 'default' for the default theme"));
}
Ok(())
});
v.add_rule("diff_context", |value| {
match value.parse::<usize>() {
Ok(n) if n <= 20 => Ok(()),
Ok(_) => Err(ValidationError::new("diff_context", "Context must be 0-20")),
Err(_) => Err(ValidationError::new("diff_context", "Must be a number")),
}
});
v.add_rule("auto_refresh_ms", |value| {
match value.parse::<u64>() {
Ok(n) if n >= 100 && n <= 60000 => Ok(()),
Ok(_) => Err(ValidationError::new("auto_refresh_ms", "Must be 100-60000ms")),
Err(_) => Err(ValidationError::new("auto_refresh_ms", "Must be a number")),
}
});
v
}
}
impl Default for ConfigValidator {
fn default() -> Self {
Self::with_common_rules()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_theme_validation() {
let v = ConfigValidator::default();
assert!(v.validate("theme", "dark").is_ok());
assert!(v.validate("theme", "").is_err());
}
#[test]
fn test_diff_context_validation() {
let v = ConfigValidator::default();
assert!(v.validate("diff_context", "3").is_ok());
assert!(v.validate("diff_context", "25").is_err());
assert!(v.validate("diff_context", "abc").is_err());
}
#[test]
fn test_custom_rule() {
let mut v = ConfigValidator::new();
v.add_rule("foo", |value| {
if value.starts_with("bar") {
Ok(())
} else {
Err(ValidationError::new("foo", "Must start with 'bar'"))
}
});
assert!(v.validate("foo", "bar123").is_ok());
assert!(v.validate("foo", "baz").is_err());
}
}