aether_core/
validation.rs1use 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 format(&self, kind: &SlotKind, code: &str) -> Result<String>;
22}
23
24pub 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 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 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 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
136pub 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 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 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
201pub 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 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 let ruff_output = Command::new("ruff")
233 .arg("check")
234 .arg("--select=E,F") .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 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 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
283pub 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), SlotKind::Raw => Ok(ValidationResult::Valid),
316 _ => {
318 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 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}