eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Configuration validation for safer config handling.
//!
//! Validates configuration values before they are applied,
//! with helpful error messages for invalid values.

use std::collections::HashMap;

/// Validation result.
pub type ValidationResult = Result<(), ValidationError>;

/// A validation error.
#[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
    }
}

/// Configuration validator.
pub struct ConfigValidator {
    /// Validation rules by field name
    rules: HashMap<String, Vec<Box<dyn Fn(&str) -> ValidationResult + Send + Sync>>>,
}

impl ConfigValidator {
    /// Create a new validator.
    pub fn new() -> Self {
        Self {
            rules: HashMap::new(),
        }
    }

    /// Add a validation rule for a field.
    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));
    }

    /// Validate a field value.
    pub fn validate(&self, field: &str, value: &str) -> ValidationResult {
        if let Some(rules) = self.rules.get(field) {
            for rule in rules {
                rule(value)?;
            }
        }
        Ok(())
    }

    /// Create a validator with common rules.
    pub fn with_common_rules() -> Self {
        let mut v = Self::new();
        
        // Theme name validation
        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(())
        });

        // Diff context validation
        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")),
            }
        });

        // Auto-refresh interval
        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());
    }
}