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// ============================================================
25// RustValidator - Uses rustc and rustfmt
26// ============================================================
27
28/// A validator that uses Rust-specific tools (rustc, rustfmt).
29pub struct RustValidator;
30
31impl Validator for RustValidator {
32    fn validate(&self, kind: &SlotKind, code: &str) -> Result<ValidationResult> {
33        match kind {
34            SlotKind::Function | SlotKind::Class | SlotKind::Component => {
35                let has_tests = code.contains("#[test]");
36
37                let mut tmp_file = NamedTempFile::with_suffix(".rs")
38                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
39                
40                let wrapper = if has_tests {
41                    code.to_string()
42                } else {
43                    format!(
44                        "#[allow(dead_code, unused_variables, unused_imports)]\nmod validation_module {{\n{}\n}}",
45                        code
46                    )
47                };
48                
49                tmp_file.write_all(wrapper.as_bytes())
50                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
51
52                // Check syntax and compilation
53                // Create temp output in same dir as source to avoid cross-drive issues on Windows
54                let out_file = tmp_file.path().with_extension("rmeta");
55                let output = Command::new("rustc")
56                    .arg("--crate-type=lib")
57                    .arg("--crate-name=aether_validation_check")
58                    .arg("--emit=metadata")
59                    .arg("-o")
60                    .arg(&out_file)
61                    .arg(tmp_file.path())
62                    .output()
63                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
64                
65                // Clean up output file
66                let _ = std::fs::remove_file(&out_file);
67
68                if !output.status.success() {
69                    let err = String::from_utf8_lossy(&output.stderr).to_string();
70                    return Ok(ValidationResult::Invalid(format!("Rust Compilation Error:\n{}", err)));
71                }
72
73                // Run tests if present
74                if has_tests {
75                    let test_exe = NamedTempFile::new()
76                        .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
77                    
78                    let test_compile = Command::new("rustc")
79                        .arg("--test")
80                        .arg("-o")
81                        .arg(test_exe.path())
82                        .arg(tmp_file.path())
83                        .output()
84                        .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
85
86                    if !test_compile.status.success() {
87                        let err = String::from_utf8_lossy(&test_compile.stderr).to_string();
88                        return Ok(ValidationResult::Invalid(format!("Test Compilation Error:\n{}", err)));
89                    }
90
91                    let test_run = Command::new(test_exe.path())
92                        .output()
93                        .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
94
95                    if !test_run.status.success() {
96                        let err = String::from_utf8_lossy(&test_run.stdout).to_string();
97                        let stderr = String::from_utf8_lossy(&test_run.stderr).to_string();
98                        return Ok(ValidationResult::Invalid(format!("Unit Test Failed:\n{}\n{}", err, stderr)));
99                    }
100                }
101
102                Ok(ValidationResult::Valid)
103            }
104            _ => Ok(ValidationResult::Valid), 
105        }
106    }
107
108    fn format(&self, kind: &SlotKind, code: &str) -> Result<String> {
109        match kind {
110            SlotKind::Function | SlotKind::Class | SlotKind::Component => {
111                let mut tmp_file = NamedTempFile::with_suffix(".rs")
112                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
113                
114                tmp_file.write_all(code.as_bytes())
115                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
116
117                let output = Command::new("rustfmt")
118                    .arg(tmp_file.path())
119                    .output();
120
121                if let Ok(out) = output {
122                    if out.status.success() {
123                        let formatted = std::fs::read_to_string(tmp_file.path())
124                            .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
125                        return Ok(formatted);
126                    }
127                }
128                
129                Ok(code.to_string())
130            }
131            _ => Ok(code.to_string()),
132        }
133    }
134}
135
136// ============================================================
137// JsValidator - Uses node and prettier/eslint
138// ============================================================
139
140/// A validator that uses JavaScript/Node.js tools.
141pub struct JsValidator;
142
143impl Validator for JsValidator {
144    fn validate(&self, kind: &SlotKind, code: &str) -> Result<ValidationResult> {
145        match kind {
146            SlotKind::JavaScript | SlotKind::Component => {
147                let mut tmp_file = NamedTempFile::with_suffix(".js")
148                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
149                
150                tmp_file.write_all(code.as_bytes())
151                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
152
153                // Use node --check for syntax validation
154                let output = Command::new("node")
155                    .arg("--check")
156                    .arg(tmp_file.path())
157                    .output()
158                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
159
160                if !output.status.success() {
161                    let err = String::from_utf8_lossy(&output.stderr).to_string();
162                    return Ok(ValidationResult::Invalid(format!("JavaScript Syntax Error:\n{}", err)));
163                }
164
165                Ok(ValidationResult::Valid)
166            }
167            _ => Ok(ValidationResult::Valid),
168        }
169    }
170
171    fn format(&self, kind: &SlotKind, code: &str) -> Result<String> {
172        match kind {
173            SlotKind::JavaScript | SlotKind::Component => {
174                // Try prettier first, fallback to original
175                let output = Command::new("npx")
176                    .arg("prettier")
177                    .arg("--parser=babel")
178                    .arg("--stdin-filepath=temp.js")
179                    .stdin(std::process::Stdio::piped())
180                    .stdout(std::process::Stdio::piped())
181                    .spawn();
182
183                if let Ok(mut child) = output {
184                    if let Some(ref mut stdin) = child.stdin {
185                        let _ = stdin.write_all(code.as_bytes());
186                    }
187                    if let Ok(output) = child.wait_with_output() {
188                        if output.status.success() {
189                            return Ok(String::from_utf8_lossy(&output.stdout).to_string());
190                        }
191                    }
192                }
193
194                Ok(code.to_string())
195            }
196            _ => Ok(code.to_string()),
197        }
198    }
199}
200
201// ============================================================
202// PythonValidator - Uses python and ruff
203// ============================================================
204
205/// A validator that uses Python tools.
206pub struct PythonValidator;
207
208impl Validator for PythonValidator {
209    fn validate(&self, kind: &SlotKind, code: &str) -> Result<ValidationResult> {
210        match kind {
211            SlotKind::Function | SlotKind::Class => {
212                let mut tmp_file = NamedTempFile::with_suffix(".py")
213                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
214                
215                tmp_file.write_all(code.as_bytes())
216                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
217
218                // Use python -m py_compile for syntax check
219                let output = Command::new("python")
220                    .arg("-m")
221                    .arg("py_compile")
222                    .arg(tmp_file.path())
223                    .output()
224                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
225
226                if !output.status.success() {
227                    let err = String::from_utf8_lossy(&output.stderr).to_string();
228                    return Ok(ValidationResult::Invalid(format!("Python Syntax Error:\n{}", err)));
229                }
230
231                // Optional: Run ruff for linting
232                let ruff_output = Command::new("ruff")
233                    .arg("check")
234                    .arg("--select=E,F") // Errors and Pyflakes only
235                    .arg(tmp_file.path())
236                    .output();
237
238                if let Ok(out) = ruff_output {
239                    if !out.status.success() {
240                        let warnings = String::from_utf8_lossy(&out.stdout).to_string();
241                        if !warnings.is_empty() {
242                            // Return as invalid with lint warnings
243                            return Ok(ValidationResult::Invalid(format!("Python Lint Issues:\n{}", warnings)));
244                        }
245                    }
246                }
247
248                Ok(ValidationResult::Valid)
249            }
250            _ => Ok(ValidationResult::Valid),
251        }
252    }
253
254    fn format(&self, kind: &SlotKind, code: &str) -> Result<String> {
255        match kind {
256            SlotKind::Function | SlotKind::Class => {
257                // Use ruff format (or black as fallback)
258                let output = Command::new("ruff")
259                    .arg("format")
260                    .arg("--stdin-filename=temp.py")
261                    .stdin(std::process::Stdio::piped())
262                    .stdout(std::process::Stdio::piped())
263                    .spawn();
264
265                if let Ok(mut child) = output {
266                    if let Some(ref mut stdin) = child.stdin {
267                        let _ = stdin.write_all(code.as_bytes());
268                    }
269                    if let Ok(output) = child.wait_with_output() {
270                        if output.status.success() {
271                            return Ok(String::from_utf8_lossy(&output.stdout).to_string());
272                        }
273                    }
274                }
275
276                Ok(code.to_string())
277            }
278            _ => Ok(code.to_string()),
279        }
280    }
281}
282
283// ============================================================
284// MultiValidator - Auto-selects based on SlotKind
285// ============================================================
286
287/// A multi-language validator that auto-selects the appropriate validator.
288pub struct MultiValidator {
289    rust: RustValidator,
290    js: JsValidator,
291    python: PythonValidator,
292}
293
294impl Default for MultiValidator {
295    fn default() -> Self {
296        Self::new()
297    }
298}
299
300impl MultiValidator {
301    pub fn new() -> Self {
302        Self {
303            rust: RustValidator,
304            js: JsValidator,
305            python: PythonValidator,
306        }
307    }
308}
309
310impl Validator for MultiValidator {
311    fn validate(&self, kind: &SlotKind, code: &str) -> Result<ValidationResult> {
312        match kind {
313            SlotKind::JavaScript => self.js.validate(kind, code),
314            SlotKind::Html | SlotKind::Css => Ok(ValidationResult::Valid), // TODO: Add HTML/CSS validators
315            SlotKind::Raw => Ok(ValidationResult::Valid),
316            // Default to Rust for function/class/component (legacy behavior)
317            _ => {
318                // Heuristic: detect language from code patterns
319                if code.contains("def ") || code.contains("import ") && code.contains(":") {
320                    self.python.validate(kind, code)
321                } else if code.contains("function ") || code.contains("const ") || code.contains("=>") {
322                    self.js.validate(kind, code)
323                } else {
324                    self.rust.validate(kind, code)
325                }
326            }
327        }
328    }
329
330    fn format(&self, kind: &SlotKind, code: &str) -> Result<String> {
331        match kind {
332            SlotKind::JavaScript => self.js.format(kind, code),
333            SlotKind::Html | SlotKind::Css | SlotKind::Raw => Ok(code.to_string()),
334            _ => {
335                if code.contains("def ") || code.contains("import ") && code.contains(":") {
336                    self.python.format(kind, code)
337                } else if code.contains("function ") || code.contains("const ") || code.contains("=>") {
338                    self.js.format(kind, code)
339                } else {
340                    self.rust.format(kind, code)
341                }
342            }
343        }
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_rust_validator_valid_code() {
353        let validator = RustValidator;
354        let code = "fn hello() -> i32 { 42 }";
355        let result = validator.validate(&SlotKind::Function, code).unwrap();
356        assert_eq!(result, ValidationResult::Valid);
357    }
358
359    #[test]
360    fn test_multi_validator_detects_python() {
361        let validator = MultiValidator::new();
362        let code = "def hello():\n    return 42";
363        // Should detect as Python and validate
364        let result = validator.validate(&SlotKind::Function, code);
365        assert!(result.is_ok());
366    }
367
368    #[test]
369    fn test_multi_validator_detects_js() {
370        let validator = MultiValidator::new();
371        let code = "const hello = () => 42;";
372        let result = validator.validate(&SlotKind::Function, code);
373        assert!(result.is_ok());
374    }
375}