> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/dotandev/envcheck/llms.txt
> Use this file to discover all available pages before exploring further.

# Adding Validators

> Step-by-step guide to adding new validator types to envcheck

This guide walks you through adding a new validator type to envcheck. We'll use real examples from the codebase to illustrate the process.

## Overview

Validators in envcheck are modular components that check specific aspects of your development environment. Each validator implements the `Validator` trait and returns validation results.

## Understanding the Validator Trait

All validators implement this trait (defined in `src/validators/mod.rs:52`):

```rust theme={null}
pub trait Validator {
    fn validate(&self) -> Result<Vec<ValidationResult>>;
}
```

### ValidationResult Structure

Validators return `ValidationResult` objects (defined in `src/validators/mod.rs:20`):

```rust theme={null}
pub struct ValidationResult {
    pub status: ValidationStatus,
    pub message: String,
    pub suggestion: Option<String>,
}
```

With three status types:

* `ValidationStatus::Success` - Check passed
* `ValidationStatus::Warning` - Optional check failed
* `ValidationStatus::Error` - Required check failed

### Helper Methods

Use these helper methods to create results:

```rust theme={null}
ValidationResult::success("Message")
ValidationResult::warning("Message", Some("Suggestion"))
ValidationResult::error("Message", Some("Suggestion"))
```

## Step-by-Step Guide

### Step 1: Create the Validator File

Create a new file in `src/validators/` for your validator. For example, to add a database validator:

```bash theme={null}
touch src/validators/database.rs
```

### Step 2: Define the Configuration Structure

First, add a configuration struct in `src/config.rs`. This defines what users can configure:

```rust theme={null}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct DatabaseCheck {
    pub connection_string: String,
    #[serde(default = "default_true")]
    pub required: bool,
    #[serde(default)]
    pub timeout_ms: Option<u64>,
}
```

Then add it to the main `Config` struct:

```rust theme={null}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config {
    pub version: String,
    #[serde(default)]
    pub tools: Vec<ToolCheck>,
    // ... other fields ...
    #[serde(default)]
    pub databases: Vec<DatabaseCheck>,  // Add your new field
}
```

### Step 3: Implement the Validator

Here's a complete example based on the `EnvValidator` pattern (`src/validators/env.rs:1`):

```rust theme={null}
use crate::config::DatabaseCheck;
use crate::validators::{ValidationResult, Validator};
use anyhow::Result;

pub struct DatabaseValidator {
    check: DatabaseCheck,
}

impl DatabaseValidator {
    pub fn new(check: DatabaseCheck) -> Self {
        Self { check }
    }
}

impl Validator for DatabaseValidator {
    fn validate(&self) -> Result<Vec<ValidationResult>> {
        let mut results = Vec::new();

        // Your validation logic here
        match self.test_connection() {
            Ok(_) => {
                results.push(ValidationResult::success(
                    format!("Database connection successful"),
                ));
            }
            Err(e) => {
                if self.check.required {
                    results.push(ValidationResult::error(
                        format!("Database connection failed: {}", e),
                        Some("Check your connection string and ensure the database is running".to_string()),
                    ));
                } else {
                    results.push(ValidationResult::warning(
                        format!("Database connection failed (optional): {}", e),
                        None,
                    ));
                }
            }
        }

        Ok(results)
    }
}

impl DatabaseValidator {
    fn test_connection(&self) -> Result<()> {
        // Implementation of connection test
        // This is where your specific validation logic goes
        Ok(())
    }
}
```

### Step 4: Register the Validator Module

Add your validator to `src/validators/mod.rs:5`:

```rust theme={null}
pub mod tool;
pub mod env;
pub mod port;
pub mod file;
pub mod network;
pub mod database;  // Add your new validator
```

### Step 5: Integrate Into Validation Runner

Add your validator to the `run_all_validations` function in `src/validators/mod.rs:56`:

```rust theme={null}
pub fn run_all_validations(config: &Config) -> Result<Vec<ValidationResult>> {
    let mut results = Vec::new();

    // ... existing validators ...

    // Validate databases
    for db_check in &config.databases {
        let validator = database::DatabaseValidator::new(db_check.clone());
        results.extend(validator.validate()?);
    }

    Ok(results)
}
```

## Real-World Example: EnvValidator

Let's examine the `EnvValidator` (`src/validators/env.rs:1`) as a complete example:

```rust theme={null}
use crate::config::EnvVarCheck;
use crate::validators::{ValidationResult, Validator};
use anyhow::Result;
use std::env;

pub struct EnvValidator {
    check: EnvVarCheck,
}

impl EnvValidator {
    pub fn new(check: EnvVarCheck) -> Self {
        Self { check }
    }
}

impl Validator for EnvValidator {
    fn validate(&self) -> Result<Vec<ValidationResult>> {
        let mut results = Vec::new();

        match env::var(&self.check.name) {
            Ok(value) => {
                if let Some(pattern) = &self.check.pattern {
                    let is_match = match regex::Regex::new(pattern) {
                        Ok(re) => re.is_match(&value),
                        Err(_) => value.contains(pattern),
                    };

                    if is_match {
                        results.push(ValidationResult::success(
                            format!("{} is set and matches pattern", self.check.name),
                        ));
                    } else {
                        results.push(ValidationResult::error(
                            format!("{} is set but does not match pattern", self.check.name),
                            Some(format!("Ensure {} matches pattern: {}", self.check.name, pattern)),
                        ));
                    }
                } else {
                    results.push(ValidationResult::success(
                        format!("{} is set", self.check.name),
                    ));
                }
            }
            Err(_) => {
                if self.check.required {
                    results.push(ValidationResult::error(
                        format!("{} is not set", self.check.name),
                        Some(format!("Set {} environment variable", self.check.name)),
                    ));
                } else {
                    results.push(ValidationResult::warning(
                        format!("{} is not set (optional)", self.check.name),
                        None,
                    ));
                }
            }
        }

        Ok(results)
    }
}
```

Key patterns to note:

1. **Required vs Optional**: Check `self.check.required` to return errors vs warnings
2. **Helpful suggestions**: Always provide actionable suggestions in error/warning messages
3. **Multiple results**: You can push multiple `ValidationResult` objects for complex checks

## Real-World Example: FileValidator

The `FileValidator` (`src/validators/file.rs:1`) shows more complex validation:

```rust theme={null}
impl Validator for FileValidator {
    fn validate(&self) -> Result<Vec<ValidationResult>> {
        let mut results = Vec::new();
        let path = Path::new(&self.check.path);

        if path.exists() {
            let mut item_passed = true;
            
            // Check if it's a directory when required
            if self.check.is_directory {
                if path.is_dir() {
                    results.push(ValidationResult::success(
                        format!("Directory {} exists", self.check.path),
                    ));
                } else {
                    item_passed = false;
                    results.push(ValidationResult::error(
                        format!("{} exists but is not a directory", self.check.path),
                        Some(format!("Ensure {} is a directory", self.check.path)),
                    ));
                }
            }

            // Only check permissions if the item passed previous checks
            if item_passed {
                if let Some(required_perms) = self.check.permissions {
                    #[cfg(unix)]
                    {
                        use std::os::unix::fs::PermissionsExt;
                        if let Ok(metadata) = path.metadata() {
                            let actual_perms = metadata.permissions().mode() & 0o777;
                            if actual_perms == required_perms {
                                results.push(ValidationResult::success(
                                    format!("{} has correct permissions ({:o})", 
                                        self.check.path, actual_perms),
                                ));
                            } else {
                                results.push(ValidationResult::error(
                                    format!("{} has permissions {:o}, but {:o} is required", 
                                        self.check.path, actual_perms, required_perms),
                                    Some(format!("Run 'chmod {:o} {}' to fix", 
                                        required_perms, self.check.path)),
                                ));
                            }
                        }
                    }
                }
            }
        } else {
            // Handle missing file/directory
            let item_type = if self.check.is_directory { "Directory" } else { "File" };
            if self.check.required {
                results.push(ValidationResult::error(
                    format!("{} {} does not exist", item_type, self.check.path),
                    Some(format!("Create {} {}", self.check.path, item_type.to_lowercase())),
                ));
            }
        }

        Ok(results)
    }
}
```

This example demonstrates:

* **Multiple validation steps**: First check existence, then type, then permissions
* **Conditional checks**: Only check permissions if the file exists and is the right type
* **Platform-specific code**: Using `#[cfg(unix)]` for Unix-only features
* **Specific error messages**: Different messages for different failure modes

## Adding Tests

Always add tests for your validator. Here's an example pattern:

```rust theme={null}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::DatabaseCheck;

    #[test]
    fn test_database_validator_success() {
        let check = DatabaseCheck {
            connection_string: "postgresql://localhost".to_string(),
            required: true,
            timeout_ms: Some(5000),
        };
        let validator = DatabaseValidator::new(check);
        let results = validator.validate().unwrap();
        
        // Add assertions based on expected behavior
        assert!(!results.is_empty());
    }

    #[test]
    fn test_database_validator_failure() {
        let check = DatabaseCheck {
            connection_string: "invalid://connection".to_string(),
            required: true,
            timeout_ms: Some(5000),
        };
        let validator = DatabaseValidator::new(check);
        let results = validator.validate().unwrap();
        
        assert!(!results.is_empty());
        // Verify error message contains helpful information
    }
}
```

## Best Practices

### 1. Clear Error Messages

Always provide clear, actionable error messages:

```rust theme={null}
// Good
ValidationResult::error(
    "Node.js version 12.0.0 does not meet requirement >=14.0.0",
    Some("Update Node.js to version 14.0.0 or higher"),
)

// Bad
ValidationResult::error("Node.js version mismatch", None)
```

### 2. Respect the Required Flag

Always check the `required` field and return appropriate status:

```rust theme={null}
if self.check.required {
    results.push(ValidationResult::error(msg, suggestion));
} else {
    results.push(ValidationResult::warning(msg, None));
}
```

### 3. Return Multiple Results When Appropriate

Don't hesitate to return multiple validation results for complex checks:

```rust theme={null}
// Check existence
results.push(ValidationResult::success("File exists"));

// Check permissions  
results.push(ValidationResult::success("Permissions are correct"));

// Check content
results.push(ValidationResult::warning("File is empty", Some("Add content")));
```

### 4. Use Rust Idioms

* Use pattern matching instead of if-else chains
* Leverage the `?` operator for error propagation
* Use descriptive variable names
* Keep functions focused and small

### 5. Handle Edge Cases

Consider edge cases in your validation:

* Missing dependencies
* Permission issues
* Platform differences (Windows vs Unix)
* Network timeouts
* Malformed input

## Documentation

After implementing your validator, update the documentation:

1. Add examples to `README.md` showing how to use your validator
2. Document the configuration options
3. Provide real-world use cases

## Checklist

Before submitting your validator:

* [ ] Created validator file in `src/validators/`
* [ ] Added configuration struct to `src/config.rs`
* [ ] Implemented the `Validator` trait
* [ ] Registered module in `src/validators/mod.rs`
* [ ] Integrated into `run_all_validations`
* [ ] Added unit tests
* [ ] Tested with real configuration
* [ ] Ran `cargo fmt` and `cargo clippy`
* [ ] Updated documentation
* [ ] All tests pass (`cargo test`)

## Getting Help

If you're stuck:

* Look at existing validators for patterns
* Open a draft PR and ask for feedback
* Create an issue with your questions
* Check for `good first issue` labels for simpler starting points
