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    /// Optional: Validate using the full slot context (for TDD).
21    fn validate_with_slot(&self, _slot: &crate::Slot, code: &str) -> Result<ValidationResult> {
22        self.validate(&_slot.kind, code)
23    }
24    
25    /// Format the code to comply with style guides.
26    fn format(&self, kind: &SlotKind, code: &str) -> Result<String>;
27}
28
29// ============================================================
30// RustValidator - Uses rustc and rustfmt
31// ============================================================
32
33/// A validator that uses Rust-specific tools (rustc, rustfmt).
34pub struct RustValidator;
35
36impl Validator for RustValidator {
37    fn validate(&self, kind: &SlotKind, code: &str) -> Result<ValidationResult> {
38        match kind {
39            SlotKind::Function | SlotKind::Class | SlotKind::Component => {
40                let has_tests = code.contains("#[test]");
41
42                let mut tmp_file = NamedTempFile::with_suffix(".rs")
43                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
44                
45                let wrapper = if has_tests {
46                    code.to_string()
47                } else {
48                    format!(
49                        "#[allow(dead_code, unused_variables, unused_imports)]\nmod validation_module {{\n{}\n}}",
50                        code
51                    )
52                };
53                
54                tmp_file.write_all(wrapper.as_bytes())
55                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
56
57                // Check syntax and compilation
58                // Create temp output in same dir as source to avoid cross-drive issues on Windows
59                let out_file = tmp_file.path().with_extension("rmeta");
60                let output = Command::new("rustc")
61                    .arg("--crate-type=lib")
62                    .arg("--crate-name=aether_validation_check")
63                    .arg("--emit=metadata")
64                    .arg("-o")
65                    .arg(&out_file)
66                    .arg(tmp_file.path())
67                    .output()
68                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
69                
70                // Clean up output file
71                let _ = std::fs::remove_file(&out_file);
72
73                if !output.status.success() {
74                    let err = String::from_utf8_lossy(&output.stderr).to_string();
75                    return Ok(ValidationResult::Invalid(format!("Rust Compilation Error:\n{}", err)));
76                }
77
78                // Run tests if present
79                if has_tests {
80                    let test_exe = NamedTempFile::new()
81                        .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
82                    
83                    let test_compile = Command::new("rustc")
84                        .arg("--test")
85                        .arg("-o")
86                        .arg(test_exe.path())
87                        .arg(tmp_file.path())
88                        .output()
89                        .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
90
91                    if !test_compile.status.success() {
92                        let err = String::from_utf8_lossy(&test_compile.stderr).to_string();
93                        return Ok(ValidationResult::Invalid(format!("Test Compilation Error:\n{}", err)));
94                    }
95
96                    let test_run = Command::new(test_exe.path())
97                        .output()
98                        .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
99
100                    if !test_run.status.success() {
101                        let err = String::from_utf8_lossy(&test_run.stdout).to_string();
102                        let stderr = String::from_utf8_lossy(&test_run.stderr).to_string();
103                        return Ok(ValidationResult::Invalid(format!("Unit Test Failed:\n{}\n{}", err, stderr)));
104                    }
105                }
106
107                Ok(ValidationResult::Valid)
108            }
109            _ => Ok(ValidationResult::Valid), 
110        }
111    }
112
113    fn format(&self, kind: &SlotKind, code: &str) -> Result<String> {
114        match kind {
115            SlotKind::Function | SlotKind::Class | SlotKind::Component => {
116                let mut tmp_file = NamedTempFile::with_suffix(".rs")
117                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
118                
119                tmp_file.write_all(code.as_bytes())
120                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
121
122                let output = Command::new("rustfmt")
123                    .arg(tmp_file.path())
124                    .output();
125
126                if let Ok(out) = output {
127                    if out.status.success() {
128                        let formatted = std::fs::read_to_string(tmp_file.path())
129                            .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
130                        return Ok(formatted);
131                    }
132                }
133                
134                Ok(code.to_string())
135            }
136            _ => Ok(code.to_string()),
137        }
138    }
139}
140
141// ============================================================
142// JsValidator - Uses node and prettier/eslint
143// ============================================================
144
145/// A validator that uses JavaScript/Node.js tools.
146pub struct JsValidator;
147
148impl Validator for JsValidator {
149    fn validate(&self, kind: &SlotKind, code: &str) -> Result<ValidationResult> {
150        match kind {
151            SlotKind::JavaScript | SlotKind::Component => {
152                let mut tmp_file = NamedTempFile::with_suffix(".js")
153                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
154                
155                tmp_file.write_all(code.as_bytes())
156                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
157
158                // Use node --check for syntax validation
159                let output = Command::new("node")
160                    .arg("--check")
161                    .arg(tmp_file.path())
162                    .output()
163                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
164
165                if !output.status.success() {
166                    let err = String::from_utf8_lossy(&output.stderr).to_string();
167                    return Ok(ValidationResult::Invalid(format!("JavaScript Syntax Error:\n{}", err)));
168                }
169
170                Ok(ValidationResult::Valid)
171            }
172            _ => Ok(ValidationResult::Valid),
173        }
174    }
175
176    fn format(&self, kind: &SlotKind, code: &str) -> Result<String> {
177        match kind {
178            SlotKind::JavaScript | SlotKind::Component => {
179                // Try prettier first, fallback to original
180                let output = Command::new("npx")
181                    .arg("prettier")
182                    .arg("--parser=babel")
183                    .arg("--stdin-filepath=temp.js")
184                    .stdin(std::process::Stdio::piped())
185                    .stdout(std::process::Stdio::piped())
186                    .spawn();
187
188                if let Ok(mut child) = output {
189                    if let Some(ref mut stdin) = child.stdin {
190                        let _ = stdin.write_all(code.as_bytes());
191                    }
192                    if let Ok(output) = child.wait_with_output() {
193                        if output.status.success() {
194                            return Ok(String::from_utf8_lossy(&output.stdout).to_string());
195                        }
196                    }
197                }
198
199                Ok(code.to_string())
200            }
201            _ => Ok(code.to_string()),
202        }
203    }
204}
205
206// ============================================================
207// PythonValidator - Uses python and ruff
208// ============================================================
209
210/// A validator that uses Python tools.
211pub struct PythonValidator;
212
213impl Validator for PythonValidator {
214    fn validate(&self, kind: &SlotKind, code: &str) -> Result<ValidationResult> {
215        match kind {
216            SlotKind::Function | SlotKind::Class => {
217                let mut tmp_file = NamedTempFile::with_suffix(".py")
218                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
219                
220                tmp_file.write_all(code.as_bytes())
221                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
222
223                // Use python -m py_compile for syntax check
224                let output = Command::new("python")
225                    .arg("-m")
226                    .arg("py_compile")
227                    .arg(tmp_file.path())
228                    .output()
229                    .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
230
231                if !output.status.success() {
232                    let err = String::from_utf8_lossy(&output.stderr).to_string();
233                    return Ok(ValidationResult::Invalid(format!("Python Syntax Error:\n{}", err)));
234                }
235
236                // Optional: Run ruff for linting
237                let ruff_output = Command::new("ruff")
238                    .arg("check")
239                    .arg("--select=E,F") // Errors and Pyflakes only
240                    .arg(tmp_file.path())
241                    .output();
242
243                if let Ok(out) = ruff_output {
244                    if !out.status.success() {
245                        let warnings = String::from_utf8_lossy(&out.stdout).to_string();
246                        if !warnings.is_empty() {
247                            // Return as invalid with lint warnings
248                            return Ok(ValidationResult::Invalid(format!("Python Lint Issues:\n{}", warnings)));
249                        }
250                    }
251                }
252
253                Ok(ValidationResult::Valid)
254            }
255            _ => Ok(ValidationResult::Valid),
256        }
257    }
258
259    fn format(&self, kind: &SlotKind, code: &str) -> Result<String> {
260        match kind {
261            SlotKind::Function | SlotKind::Class => {
262                // Use ruff format (or black as fallback)
263                let output = Command::new("ruff")
264                    .arg("format")
265                    .arg("--stdin-filename=temp.py")
266                    .stdin(std::process::Stdio::piped())
267                    .stdout(std::process::Stdio::piped())
268                    .spawn();
269
270                if let Ok(mut child) = output {
271                    if let Some(ref mut stdin) = child.stdin {
272                        let _ = stdin.write_all(code.as_bytes());
273                    }
274                    if let Ok(output) = child.wait_with_output() {
275                        if output.status.success() {
276                            return Ok(String::from_utf8_lossy(&output.stdout).to_string());
277                        }
278                    }
279                }
280
281                Ok(code.to_string())
282            }
283            _ => Ok(code.to_string()),
284        }
285    }
286}
287
288// ============================================================
289// TddValidator - Runs tests against generated code
290// ============================================================
291
292/// A validator that runs functional tests against code using a harness.
293pub struct TddValidator;
294
295impl TddValidator {
296    fn detect_suffix(kind: &SlotKind, code: &str) -> &'static str {
297        match kind {
298            SlotKind::JavaScript => ".js",
299            SlotKind::Html => ".html",
300            SlotKind::Css => ".css",
301            _ => {
302                if code.contains("def ") || code.contains("import ") && code.contains(":") {
303                    ".py"
304                } else {
305                    ".rs"
306                }
307            }
308        }
309    }
310}
311
312impl Validator for TddValidator {
313    fn validate(&self, _kind: &SlotKind, _code: &str) -> Result<ValidationResult> {
314        // This validator requires constraints to be present for meaningful work
315        // (Handled by MultiValidator delegating to this)
316        Ok(ValidationResult::Valid)
317    }
318
319    fn validate_with_slot(&self, slot: &crate::Slot, code: &str) -> Result<ValidationResult> {
320        let constraints = match &slot.constraints {
321            Some(c) => c,
322            None => return Ok(ValidationResult::Valid),
323        };
324
325        let harness = match &constraints.test_harness {
326            Some(h) => h,
327            None => return Ok(ValidationResult::Valid),
328        };
329
330        let test_code = harness.replace("{{CODE}}", code);
331        let suffix = Self::detect_suffix(&slot.kind, code);
332
333        // For Rust, use a temporary directory if possible to handle multiple files or complex builds
334        // For now, single file is fine.
335        let mut tmp_file = NamedTempFile::with_suffix(suffix)
336            .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
337        
338        tmp_file.write_all(test_code.as_bytes())
339            .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
340
341        // Determine test command
342        let mut command_str = constraints.test_command.clone().unwrap_or_else(|| {
343            match suffix {
344                ".rs" => format!("rustc --test -o {}.exe {} && {}.exe", tmp_file.path().display(), tmp_file.path().display(), tmp_file.path().display()),
345                ".js" => format!("node {}", tmp_file.path().display()),
346                ".py" => format!("python {}", tmp_file.path().display()),
347                _ => "echo 'No test command'".to_string(),
348            }
349        });
350
351        // Replace {{FILE}} placeholder in custom commands
352        command_str = command_str.replace("{{FILE}}", &tmp_file.path().display().to_string());
353
354        // Run command (Shell execution for complex commands)
355        #[cfg(windows)]
356        let shell = "powershell";
357        #[cfg(not(windows))]
358        let shell = "sh";
359
360        #[cfg(windows)]
361        let arg = "-Command";
362        #[cfg(not(windows))]
363        let arg = "-c";
364
365        let output = Command::new(shell)
366            .arg(arg)
367            .arg(&command_str)
368            .output()
369            .map_err(|e| crate::AetherError::InjectionError(e.to_string()))?;
370
371        if !output.status.success() {
372            let stdout = String::from_utf8_lossy(&output.stdout).to_string();
373            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
374            
375            return Ok(ValidationResult::Invalid(format!(
376                "TDD Test Failure:\nSTDOUT:\n{}\nSTDERR:\n{}",
377                stdout, stderr
378            )));
379        }
380
381        Ok(ValidationResult::Valid)
382    }
383
384    fn format(&self, _kind: &SlotKind, code: &str) -> Result<String> {
385        Ok(code.to_string())
386    }
387}
388
389// ============================================================
390// MultiValidator - Auto-selects based on SlotKind
391// ============================================================
392
393/// A multi-language validator that auto-selects the appropriate validator.
394pub struct MultiValidator {
395    rust: RustValidator,
396    js: JsValidator,
397    python: PythonValidator,
398    tdd: TddValidator,
399}
400
401impl Default for MultiValidator {
402    fn default() -> Self {
403        Self::new()
404    }
405}
406
407impl MultiValidator {
408    pub fn new() -> Self {
409        Self {
410            rust: RustValidator,
411            js: JsValidator,
412            python: PythonValidator,
413            tdd: TddValidator,
414        }
415    }
416}
417
418impl Validator for MultiValidator {
419    fn validate(&self, kind: &SlotKind, code: &str) -> Result<ValidationResult> {
420        // MultiValidator generally delegates to validate_with_slot if possible
421        self.validate_with_slot(&crate::Slot::new("unknown", "").with_kind(kind.clone()), code)
422    }
423
424    fn validate_with_slot(&self, slot: &crate::Slot, code: &str) -> Result<ValidationResult> {
425        let kind = &slot.kind;
426        
427        // 1. Run language-specific validation first
428        let base_result = match kind {
429            SlotKind::JavaScript => self.js.validate(kind, code)?,
430            SlotKind::Html | SlotKind::Css => ValidationResult::Valid,
431            SlotKind::Raw => ValidationResult::Valid,
432            _ => {
433                if code.contains("def ") || code.contains("import ") && code.contains(":") {
434                    self.python.validate(kind, code)?
435                } else if code.contains("function ") || code.contains("const ") || code.contains("=>") {
436                    self.js.validate(kind, code)?
437                } else {
438                    self.rust.validate(kind, code)?
439                }
440            }
441        };
442
443        if let ValidationResult::Invalid(e) = base_result {
444            return Ok(ValidationResult::Invalid(e));
445        }
446
447        // 2. Run TDD validation if harness is present
448        if let Some(ref constraints) = slot.constraints {
449            if constraints.test_harness.is_some() {
450                return self.tdd.validate_with_slot(slot, code);
451            }
452        }
453
454        Ok(ValidationResult::Valid)
455    }
456
457    fn format(&self, kind: &SlotKind, code: &str) -> Result<String> {
458        match kind {
459            SlotKind::JavaScript => self.js.format(kind, code),
460            SlotKind::Html | SlotKind::Css | SlotKind::Raw => Ok(code.to_string()),
461            _ => {
462                if code.contains("def ") || code.contains("import ") && code.contains(":") {
463                    self.python.format(kind, code)
464                } else if code.contains("function ") || code.contains("const ") || code.contains("=>") {
465                    self.js.format(kind, code)
466                } else {
467                    self.rust.format(kind, code)
468                }
469            }
470        }
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    #[test]
479    fn test_rust_validator_valid_code() {
480        let validator = RustValidator;
481        let code = "fn hello() -> i32 { 42 }";
482        let result = validator.validate(&SlotKind::Function, code).unwrap();
483        assert_eq!(result, ValidationResult::Valid);
484    }
485
486    #[test]
487    fn test_multi_validator_detects_python() {
488        let validator = MultiValidator::new();
489        let code = "def hello():\n    return 42";
490        // Should detect as Python and validate
491        let result = validator.validate(&SlotKind::Function, code);
492        assert!(result.is_ok());
493    }
494
495    #[test]
496    fn test_multi_validator_detects_js() {
497        let validator = MultiValidator::new();
498        let code = "const hello = () => 42;";
499        let result = validator.validate(&SlotKind::Function, code);
500        assert!(result.is_ok());
501    }
502}