ggen_cli_lib/cmds/hook/
validate.rs

1//! Validate hook configurations without executing them.
2//!
3//! This module provides validation for hook configurations, checking:
4//! - Hook configuration file syntax
5//! - Template reference validity
6//! - Trigger-specific requirements (cron schedule, file paths, etc.)
7//! - Variable definitions
8//! - Git hook installation requirements
9//!
10//! # Examples
11//!
12//! ```bash
13//! # Validate a hook
14//! ggen hook validate "pre-commit"
15//!
16//! # Output validation result as JSON
17//! ggen hook validate "nightly-rebuild" --json
18//! ```
19
20use clap::Args;
21use ggen_utils::error::Result;
22use serde::{Deserialize, Serialize};
23use std::fs;
24use std::path::PathBuf;
25
26#[derive(Args, Debug)]
27pub struct ValidateArgs {
28    /// Hook name to validate
29    pub name: String,
30
31    /// Output validation result as JSON
32    #[arg(long)]
33    pub json: bool,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ValidationResult {
38    pub valid: bool,
39    pub hook_name: String,
40    pub errors: Vec<String>,
41    pub warnings: Vec<String>,
42    pub checks: Vec<ValidationCheck>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ValidationCheck {
47    pub name: String,
48    pub passed: bool,
49    pub message: String,
50}
51
52/// Validate hook configuration by checking file existence and basic structure
53async fn validate_hook_configuration(hook_name: &str) -> Result<ValidationResult> {
54    let mut result = ValidationResult {
55        valid: true,
56        hook_name: hook_name.to_string(),
57        errors: Vec::new(),
58        warnings: Vec::new(),
59        checks: Vec::new(),
60    };
61
62    // Check 1: Hook configuration file exists
63    let hook_dir = PathBuf::from(".ggen").join("hooks");
64    let hook_file = hook_dir.join(format!("{}.toml", hook_name));
65
66    if !hook_file.exists() {
67        result.valid = false;
68        result.errors.push(format!(
69            "Hook configuration file not found: {}",
70            hook_file.display()
71        ));
72        return Ok(result);
73    }
74
75    result.checks.push(ValidationCheck {
76        name: "Configuration file exists".to_string(),
77        passed: true,
78        message: format!("Found: {}", hook_file.display()),
79    });
80
81    // Check 2: File is readable and valid TOML
82    match fs::read_to_string(&hook_file) {
83        Ok(content) => match toml::from_str::<toml::Value>(&content) {
84            Ok(_) => {
85                result.checks.push(ValidationCheck {
86                    name: "Valid TOML syntax".to_string(),
87                    passed: true,
88                    message: "Configuration file is valid TOML".to_string(),
89                });
90            }
91            Err(e) => {
92                result.valid = false;
93                result.errors.push(format!("Invalid TOML syntax: {}", e));
94                return Ok(result);
95            }
96        },
97        Err(e) => {
98            result.valid = false;
99            result.errors.push(format!("Cannot read hook file: {}", e));
100            return Ok(result);
101        }
102    }
103
104    Ok(result)
105}
106
107/// Main entry point for `ggen hook validate`
108pub async fn run(args: &ValidateArgs) -> Result<()> {
109    // Validate hook name
110    if args.name.trim().is_empty() {
111        return Err(ggen_utils::error::Error::new("Hook name cannot be empty"));
112    }
113
114    println!("🔍 Validating hook '{}'...", args.name);
115
116    // Implement actual validation
117    let result = validate_hook_configuration(&args.name).await?;
118
119    if args.json {
120        let json = serde_json::to_string_pretty(&result).map_err(|e| {
121            ggen_utils::error::Error::new_fmt(format_args!("JSON serialization failed: {}", e))
122        })?;
123        println!("{}", json);
124        return Ok(());
125    }
126
127    // Human-readable output
128    println!("\nValidation Results:");
129    println!();
130
131    for check in &result.checks {
132        let icon = if check.passed { "✅" } else { "❌" };
133        println!("{} {}", icon, check.name);
134        println!("   {}", check.message);
135        println!();
136    }
137
138    if !result.warnings.is_empty() {
139        println!("⚠️  Warnings:");
140        for warning in &result.warnings {
141            println!("  - {}", warning);
142        }
143        println!();
144    }
145
146    if !result.errors.is_empty() {
147        println!("❌ Errors:");
148        for error in &result.errors {
149            println!("  - {}", error);
150        }
151        println!();
152        return Err(ggen_utils::error::Error::new_fmt(format_args!(
153            "Hook '{}' validation failed",
154            args.name
155        )));
156    }
157
158    println!("✅ Hook '{}' is valid and ready to use!", args.name);
159
160    Ok(())
161}
162
163/// Validate hook name format
164#[allow(dead_code)]
165fn is_valid_hook_name(name: &str) -> bool {
166    if name.is_empty() || name.len() > 50 {
167        return false;
168    }
169
170    // Allow alphanumeric, hyphens, underscores, and dots
171    name.chars()
172        .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.')
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[tokio::test]
180    #[ignore] // TODO: Needs mock hook config setup
181    async fn test_validate_hook_basic() {
182        let args = ValidateArgs {
183            name: "test-hook".to_string(),
184            json: false,
185        };
186        let result = run(&args).await;
187        assert!(result.is_ok());
188    }
189
190    #[tokio::test]
191    async fn test_validate_hook_empty_name() {
192        let args = ValidateArgs {
193            name: "".to_string(),
194            json: false,
195        };
196        let result = run(&args).await;
197        assert!(result.is_err());
198    }
199
200    #[tokio::test]
201    async fn test_validate_hook_json_output() {
202        let args = ValidateArgs {
203            name: "test-hook".to_string(),
204            json: true,
205        };
206        let result = run(&args).await;
207        assert!(result.is_ok());
208    }
209}