Skip to main content

apcore_toolkit/output/
verifiers.rs

1// Built-in verifiers for output writers.
2//
3// Each verifier implements the Verifier trait and checks a specific
4// aspect of a written artifact.
5
6use std::fs;
7use std::panic::{catch_unwind, AssertUnwindSafe};
8
9use crate::output::types::{Verifier, VerifyResult};
10
11/// Verify that a Rust source file has valid syntax.
12///
13/// Uses the `syn` crate to parse the file as a Rust source file.
14/// Analogous to `SyntaxVerifier` in Python (which uses `ast.parse`)
15/// and TypeScript (which does a basic readability check).
16pub struct SyntaxVerifier;
17
18impl Verifier for SyntaxVerifier {
19    fn verify(&self, path: &str, _module_id: &str) -> VerifyResult {
20        let content = match fs::read_to_string(path) {
21            Ok(c) => c,
22            Err(e) => return VerifyResult::fail(format!("Cannot read file: {e}")),
23        };
24
25        if content.trim().is_empty() {
26            return VerifyResult::fail("File is empty".into());
27        }
28
29        match syn::parse_file(&content) {
30            Ok(_) => VerifyResult::ok(),
31            Err(e) => VerifyResult::fail(format!("Invalid Rust syntax: {e}")),
32        }
33    }
34}
35
36/// Verify that a YAML binding file is parseable and contains required fields.
37pub struct YAMLVerifier;
38
39impl Verifier for YAMLVerifier {
40    fn verify(&self, path: &str, _module_id: &str) -> VerifyResult {
41        let content = match fs::read_to_string(path) {
42            Ok(c) => c,
43            Err(e) => return VerifyResult::fail(format!("Cannot read file: {e}")),
44        };
45
46        let parsed: serde_yaml::Value = match serde_yaml::from_str(&content) {
47            Ok(v) => v,
48            Err(e) => return VerifyResult::fail(format!("Invalid YAML: {e}")),
49        };
50
51        let bindings = match parsed.get("bindings") {
52            Some(b) => b,
53            None => return VerifyResult::fail("Missing or empty 'bindings' list".into()),
54        };
55
56        let bindings_seq = match bindings.as_sequence() {
57            Some(s) if !s.is_empty() => s,
58            _ => return VerifyResult::fail("Missing or empty 'bindings' list".into()),
59        };
60
61        let first = &bindings_seq[0];
62        for field in &["module_id", "target"] {
63            match first.get(*field) {
64                Some(v) if !v.is_null() => {}
65                _ => {
66                    return VerifyResult::fail(format!(
67                        "Missing required field '{field}' in binding"
68                    ))
69                }
70            }
71        }
72
73        VerifyResult::ok()
74    }
75}
76
77/// Verify that a file contains valid JSON, with optional schema validation.
78pub struct JSONVerifier {
79    // Schema validation is omitted (would require jsonschema crate).
80    // Only checks that the file is valid JSON.
81}
82
83impl JSONVerifier {
84    /// Create a new JSONVerifier.
85    pub fn new() -> Self {
86        Self {}
87    }
88}
89
90impl Default for JSONVerifier {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl Verifier for JSONVerifier {
97    fn verify(&self, path: &str, _module_id: &str) -> VerifyResult {
98        let content = match fs::read_to_string(path) {
99            Ok(c) => c,
100            Err(e) => return VerifyResult::fail(format!("Cannot read file: {e}")),
101        };
102
103        match serde_json::from_str::<serde_json::Value>(&content) {
104            Ok(_) => VerifyResult::ok(),
105            Err(e) => VerifyResult::fail(format!("Invalid JSON: {e}")),
106        }
107    }
108}
109
110/// Verify that a file starts with expected magic bytes.
111pub struct MagicBytesVerifier {
112    expected: Vec<u8>,
113}
114
115impl MagicBytesVerifier {
116    /// Create a new MagicBytesVerifier with the expected byte sequence.
117    pub fn new(expected: Vec<u8>) -> Self {
118        Self { expected }
119    }
120}
121
122impl Verifier for MagicBytesVerifier {
123    fn verify(&self, path: &str, _module_id: &str) -> VerifyResult {
124        let content = match fs::read(path) {
125            Ok(c) => c,
126            Err(e) => return VerifyResult::fail(format!("Cannot read file: {e}")),
127        };
128
129        if content.len() < self.expected.len() {
130            return VerifyResult::fail(format!(
131                "File too short: expected at least {} bytes, got {}",
132                self.expected.len(),
133                content.len()
134            ));
135        }
136
137        let header = &content[..self.expected.len()];
138        if header != self.expected.as_slice() {
139            return VerifyResult::fail(format!(
140                "Magic bytes mismatch: expected {:?}, got {:?}",
141                self.expected, header
142            ));
143        }
144
145        VerifyResult::ok()
146    }
147}
148
149/// Verify that a module is registered and retrievable from a registry.
150pub struct RegistryVerifier<'a> {
151    registry: &'a apcore::Registry,
152}
153
154impl<'a> RegistryVerifier<'a> {
155    /// Create a new RegistryVerifier checking against the given registry.
156    pub fn new(registry: &'a apcore::Registry) -> Self {
157        Self { registry }
158    }
159}
160
161impl Verifier for RegistryVerifier<'_> {
162    fn verify(&self, _path: &str, module_id: &str) -> VerifyResult {
163        if self.registry.has(module_id) {
164            VerifyResult::ok()
165        } else {
166            VerifyResult::fail(format!(
167                "Module '{module_id}' not found in registry after registration"
168            ))
169        }
170    }
171}
172
173/// Run verifiers in order; stop on first failure.
174///
175/// Each verifier call is wrapped in `catch_unwind` so that a panicking
176/// verifier does not crash the caller. A panic is reported as a
177/// `VerifyResult::fail` with a descriptive message.
178pub fn run_verifier_chain(
179    verifiers: &[&dyn Verifier],
180    path: &str,
181    module_id: &str,
182) -> VerifyResult {
183    for verifier in verifiers {
184        let verifier = AssertUnwindSafe(verifier);
185        let path = path.to_string();
186        let module_id = module_id.to_string();
187        let outcome = catch_unwind(move || verifier.verify(&path, &module_id));
188        match outcome {
189            Ok(result) if !result.ok => return result,
190            Ok(_) => {} // passed, continue
191            Err(panic_info) => {
192                let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
193                    (*s).to_string()
194                } else if let Some(s) = panic_info.downcast_ref::<String>() {
195                    s.clone()
196                } else {
197                    "unknown panic".to_string()
198                };
199                return VerifyResult::fail(format!("Verifier crashed: {msg}"));
200            }
201        }
202    }
203    VerifyResult::ok()
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use std::io::Write;
210    use tempfile::NamedTempFile;
211
212    #[test]
213    fn test_yaml_verifier_valid() {
214        let mut f = NamedTempFile::new().unwrap();
215        writeln!(f, "bindings:\n  - module_id: test\n    target: app:func").unwrap();
216        let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
217        assert!(result.ok);
218    }
219
220    #[test]
221    fn test_yaml_verifier_invalid_yaml() {
222        let mut f = NamedTempFile::new().unwrap();
223        writeln!(f, "{{invalid: yaml: [}}").unwrap();
224        let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
225        assert!(!result.ok);
226        assert!(result.error.unwrap().contains("Invalid YAML"));
227    }
228
229    #[test]
230    fn test_yaml_verifier_missing_bindings() {
231        let mut f = NamedTempFile::new().unwrap();
232        writeln!(f, "other_key: value").unwrap();
233        let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
234        assert!(!result.ok);
235    }
236
237    #[test]
238    fn test_yaml_verifier_missing_required_field() {
239        let mut f = NamedTempFile::new().unwrap();
240        writeln!(f, "bindings:\n  - module_id: test").unwrap();
241        let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
242        assert!(!result.ok);
243        assert!(result.error.unwrap().contains("target"));
244    }
245
246    #[test]
247    fn test_json_verifier_valid() {
248        let mut f = NamedTempFile::new().unwrap();
249        writeln!(f, r#"{{"key": "value"}}"#).unwrap();
250        let result = JSONVerifier::new().verify(f.path().to_str().unwrap(), "test");
251        assert!(result.ok);
252    }
253
254    #[test]
255    fn test_json_verifier_invalid() {
256        let mut f = NamedTempFile::new().unwrap();
257        writeln!(f, "not json").unwrap();
258        let result = JSONVerifier::new().verify(f.path().to_str().unwrap(), "test");
259        assert!(!result.ok);
260    }
261
262    #[test]
263    fn test_magic_bytes_verifier_match() {
264        let mut f = NamedTempFile::new().unwrap();
265        f.write_all(b"\x89PNG\r\n\x1a\nrest of file").unwrap();
266        let verifier = MagicBytesVerifier::new(b"\x89PNG\r\n\x1a\n".to_vec());
267        let result = verifier.verify(f.path().to_str().unwrap(), "test");
268        assert!(result.ok);
269    }
270
271    #[test]
272    fn test_magic_bytes_verifier_mismatch() {
273        let mut f = NamedTempFile::new().unwrap();
274        f.write_all(b"NOT PNG").unwrap();
275        let verifier = MagicBytesVerifier::new(b"\x89PNG".to_vec());
276        let result = verifier.verify(f.path().to_str().unwrap(), "test");
277        assert!(!result.ok);
278        assert!(result.error.unwrap().contains("mismatch"));
279    }
280
281    #[test]
282    fn test_run_verifier_chain_all_pass() {
283        let v1 = JSONVerifier::new();
284        let mut f = NamedTempFile::new().unwrap();
285        writeln!(f, r#"{{"ok": true}}"#).unwrap();
286        let verifiers: Vec<&dyn Verifier> = vec![&v1];
287        let result = run_verifier_chain(&verifiers, f.path().to_str().unwrap(), "test");
288        assert!(result.ok);
289    }
290
291    #[test]
292    fn test_run_verifier_chain_stops_on_failure() {
293        let v1 = JSONVerifier::new();
294        let mut f = NamedTempFile::new().unwrap();
295        writeln!(f, "not json").unwrap();
296        let verifiers: Vec<&dyn Verifier> = vec![&v1];
297        let result = run_verifier_chain(&verifiers, f.path().to_str().unwrap(), "test");
298        assert!(!result.ok);
299    }
300
301    #[test]
302    fn test_run_verifier_chain_empty() {
303        let verifiers: Vec<&dyn Verifier> = vec![];
304        let result = run_verifier_chain(&verifiers, "", "test");
305        assert!(result.ok);
306    }
307
308    #[test]
309    fn test_yaml_verifier_nonexistent_file() {
310        let result = YAMLVerifier.verify("/tmp/nonexistent_file_abc123.yaml", "test");
311        assert!(!result.ok);
312        assert!(result.error.unwrap().contains("Cannot read file"));
313    }
314
315    #[test]
316    fn test_json_verifier_nonexistent_file() {
317        let result = JSONVerifier::new().verify("/tmp/nonexistent_file_abc123.json", "test");
318        assert!(!result.ok);
319        assert!(result.error.unwrap().contains("Cannot read file"));
320    }
321
322    #[test]
323    fn test_magic_bytes_verifier_file_too_short() {
324        let mut f = NamedTempFile::new().unwrap();
325        f.write_all(b"AB").unwrap();
326        let verifier = MagicBytesVerifier::new(b"ABCDEF".to_vec());
327        let result = verifier.verify(f.path().to_str().unwrap(), "test");
328        assert!(!result.ok);
329        let err = result.error.unwrap();
330        assert!(err.contains("File too short"), "got: {err}");
331        assert!(err.contains("6"), "should mention expected length");
332        assert!(err.contains("2"), "should mention actual length");
333    }
334
335    /// A verifier that panics, used to test catch_unwind in the chain.
336    struct PanickingVerifier;
337
338    impl Verifier for PanickingVerifier {
339        fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
340            panic!("verifier exploded");
341        }
342    }
343
344    #[test]
345    fn test_run_verifier_chain_panic_caught() {
346        let bad = PanickingVerifier;
347        let verifiers: Vec<&dyn Verifier> = vec![&bad];
348        let result = run_verifier_chain(&verifiers, "/fake", "test");
349        assert!(!result.ok);
350        let err = result.error.unwrap();
351        assert!(
352            err.contains("Verifier crashed"),
353            "expected crash message, got: {err}"
354        );
355        assert!(
356            err.contains("verifier exploded"),
357            "expected panic message, got: {err}"
358        );
359    }
360
361    /// A verifier that always returns an error, simulating a "crash" without panicking.
362    struct AlwaysFailVerifier {
363        message: String,
364    }
365
366    impl AlwaysFailVerifier {
367        fn new(message: &str) -> Self {
368            Self {
369                message: message.to_string(),
370            }
371        }
372    }
373
374    impl Verifier for AlwaysFailVerifier {
375        fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
376            VerifyResult::fail(self.message.clone())
377        }
378    }
379
380    /// A verifier that always passes.
381    struct AlwaysPassVerifier;
382
383    impl Verifier for AlwaysPassVerifier {
384        fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
385            VerifyResult::ok()
386        }
387    }
388
389    #[test]
390    fn test_run_verifier_chain_crash_caught() {
391        let bad = AlwaysFailVerifier::new("simulated crash");
392        let verifiers: Vec<&dyn Verifier> = vec![&bad];
393        let result = run_verifier_chain(&verifiers, "/fake", "test");
394        assert!(!result.ok);
395        assert_eq!(result.error.as_deref(), Some("simulated crash"));
396    }
397
398    #[test]
399    fn test_run_verifier_chain_first_failure_stops() {
400        // The first verifier fails, so the second should never matter.
401        let fail_v = AlwaysFailVerifier::new("first failed");
402        let pass_v = AlwaysPassVerifier;
403        let verifiers: Vec<&dyn Verifier> = vec![&fail_v, &pass_v];
404        let result = run_verifier_chain(&verifiers, "/fake", "test");
405        assert!(!result.ok);
406        assert_eq!(result.error.as_deref(), Some("first failed"));
407    }
408
409    #[test]
410    fn test_syntax_verifier_valid_rust() {
411        let mut f = NamedTempFile::new().unwrap();
412        writeln!(f, "fn main() {{\n    println!(\"hello\");\n}}").unwrap();
413        let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
414        assert!(result.ok, "expected ok, got: {:?}", result.error);
415    }
416
417    #[test]
418    fn test_syntax_verifier_invalid_rust() {
419        let mut f = NamedTempFile::new().unwrap();
420        writeln!(f, "fn main() {{{{{{").unwrap();
421        let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
422        assert!(!result.ok);
423        assert!(result.error.unwrap().contains("Invalid Rust syntax"));
424    }
425
426    #[test]
427    fn test_syntax_verifier_empty_file() {
428        let mut f = NamedTempFile::new().unwrap();
429        write!(f, "").unwrap();
430        let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
431        assert!(!result.ok);
432        assert!(result.error.unwrap().contains("File is empty"));
433    }
434
435    #[test]
436    fn test_syntax_verifier_nonexistent_file() {
437        let result = SyntaxVerifier.verify("/tmp/nonexistent_rs_file_abc123.rs", "test");
438        assert!(!result.ok);
439        assert!(result.error.unwrap().contains("Cannot read file"));
440    }
441
442    #[test]
443    fn test_syntax_verifier_whitespace_only() {
444        let mut f = NamedTempFile::new().unwrap();
445        write!(f, "   \n\n  \t  ").unwrap();
446        let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
447        assert!(!result.ok);
448        assert!(result.error.unwrap().contains("File is empty"));
449    }
450
451    #[test]
452    fn test_syntax_verifier_complex_valid() {
453        let mut f = NamedTempFile::new().unwrap();
454        writeln!(
455            f,
456            "use std::collections::HashMap;\n\
457             \n\
458             pub struct Foo {{\n\
459                 pub name: String,\n\
460                 pub values: HashMap<String, i32>,\n\
461             }}\n\
462             \n\
463             impl Foo {{\n\
464                 pub fn new(name: String) -> Self {{\n\
465                     Self {{ name, values: HashMap::new() }}\n\
466                 }}\n\
467             }}"
468        )
469        .unwrap();
470        let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
471        assert!(result.ok, "expected ok, got: {:?}", result.error);
472    }
473}