aether_core/
validation.rs

1use crate::{Result, SlotKind};
2use std::process::Command;
3use std::io::Write;
4use tempfile::NamedTempFile;
5
6/// Result of a code validation check.
7#[derive(Debug, Clone, PartialEq)]
8pub enum ValidationResult {
9    /// Validation passed.
10    Valid,
11    /// Validation failed with a specific error message.
12    Invalid(String),
13}
14
15/// Trait for implementing code validators and formatters.
16pub trait Validator: Send + Sync {
17    /// Check if the code is valid according to the validator's rules.
18    fn validate(&self, kind: &SlotKind, code: &str) -> Result<ValidationResult>;
19    
20    /// Format the code to comply with style guides.
21    fn format(&self, kind: &SlotKind, code: &str) -> Result<String>;
22}
23
24/// A validator that uses Rust-specific tools (cargo check, rustfmt).
25pub struct RustValidator;
26
27impl Validator for RustValidator {
28    fn validate(&self, kind: &SlotKind, code: &str) -> Result<ValidationResult> {
29        // Only validate Rust-compatible slots
30        match kind {
31            SlotKind::Function | SlotKind::Class | SlotKind::Component => {
32                // Wrap in a basic module structure if it looks like a snippet
33                // If it contains tests, we will run them
34                let has_tests = code.contains("#[test]");
35
36                let mut tmp_file = NamedTempFile::new()
37                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
38                
39                let wrapper = if has_tests {
40                    code.to_string()
41                } else {
42                    format!(
43                        "#[allow(dead_code, unused_variables, unused_imports)]\nmod validation_module {{\n{}\n}}",
44                        code
45                    )
46                };
47                
48                tmp_file.write_all(wrapper.as_bytes())
49                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
50
51                // 1. Check syntax and compilation first
52                let output = Command::new("rustc")
53                    .arg("--crate-type=lib")
54                    .arg("--emit=metadata")
55                    .arg("-o")
56                    .arg("NUL") 
57                    .arg(tmp_file.path())
58                    .output()
59                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
60
61                if !output.status.success() {
62                    let err = String::from_utf8_lossy(&output.stderr).to_string();
63                    return Ok(ValidationResult::Invalid(format!("Compilation Error:\n{}", err)));
64                }
65
66                // 2. If it has tests, run them!
67                if has_tests {
68                    // We need to compile as a test executable and run it
69                    let test_exe = NamedTempFile::new()
70                        .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
71                    
72                    let test_compile = Command::new("rustc")
73                        .arg("--test")
74                        .arg("-o")
75                        .arg(test_exe.path())
76                        .arg(tmp_file.path())
77                        .output()
78                        .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
79
80                    if !test_compile.status.success() {
81                        let err = String::from_utf8_lossy(&test_compile.stderr).to_string();
82                        return Ok(ValidationResult::Invalid(format!("Test Compilation Error:\n{}", err)));
83                    }
84
85                    let test_run = Command::new(test_exe.path())
86                        .output()
87                        .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
88
89                    if !test_run.status.success() {
90                        let err = String::from_utf8_lossy(&test_run.stdout).to_string(); // cargo test outputs to stdout
91                        let stderr = String::from_utf8_lossy(&test_run.stderr).to_string();
92                        return Ok(ValidationResult::Invalid(format!("Unit Test Failed:\n{}\n{}", err, stderr)));
93                    }
94                }
95
96                Ok(ValidationResult::Valid)
97            }
98            _ => Ok(ValidationResult::Valid), 
99        }
100    }
101
102    fn format(&self, kind: &SlotKind, code: &str) -> Result<String> {
103        match kind {
104            SlotKind::Function | SlotKind::Class | SlotKind::Component | SlotKind::JavaScript => {
105                let mut tmp_file = NamedTempFile::new()
106                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
107                
108                tmp_file.write_all(code.as_bytes())
109                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
110
111                // Try rustfmt
112                let output = Command::new("rustfmt")
113                    .arg(tmp_file.path())
114                    .output();
115
116                if let Ok(out) = output {
117                    if out.status.success() {
118                        let formatted = std::fs::read_to_string(tmp_file.path())
119                            .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
120                        return Ok(formatted);
121                    }
122                }
123                
124                Ok(code.to_string())
125            }
126            _ => Ok(code.to_string()),
127        }
128    }
129}