1use crate::{Result, SlotKind};
2use std::process::Command;
3use std::io::Write;
4use tempfile::NamedTempFile;
5
6#[derive(Debug, Clone, PartialEq)]
8pub enum ValidationResult {
9 Valid,
11 Invalid(String),
13}
14
15pub trait Validator: Send + Sync {
17 fn validate(&self, kind: &SlotKind, code: &str) -> Result<ValidationResult>;
19
20 fn validate_with_slot(&self, _slot: &crate::Slot, code: &str) -> Result<ValidationResult> {
22 self.validate(&_slot.kind, code)
23 }
24
25 fn format(&self, kind: &SlotKind, code: &str) -> Result<String>;
27}
28
29pub 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 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 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 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
141pub 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 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 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
206pub 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 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 let ruff_output = Command::new("ruff")
238 .arg("check")
239 .arg("--select=E,F") .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 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 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
288pub 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 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 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 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 command_str = command_str.replace("{{FILE}}", &tmp_file.path().display().to_string());
353
354 #[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
389pub 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 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 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 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 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}