Skip to main content

alef_e2e/
validate.rs

1//! JSON Schema validation for e2e fixture files.
2
3use anyhow::{Context, Result};
4use std::fmt;
5use std::path::Path;
6
7static FIXTURE_SCHEMA: &str = include_str!("../schema/fixture.schema.json");
8
9/// A validation error with its source file and message.
10#[derive(Debug, Clone)]
11pub struct ValidationError {
12    /// Relative path of the fixture file that failed validation.
13    pub file: String,
14    /// Human-readable error message.
15    pub message: String,
16}
17
18impl fmt::Display for ValidationError {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        write!(f, "{}: {}", self.file, self.message)
21    }
22}
23
24/// Validate all JSON fixture files in a directory against the fixture schema.
25///
26/// Returns a list of validation errors. An empty list means all fixtures are valid.
27pub fn validate_fixtures(fixtures_dir: &Path) -> Result<Vec<ValidationError>> {
28    let schema_value: serde_json::Value =
29        serde_json::from_str(FIXTURE_SCHEMA).context("failed to parse embedded fixture schema")?;
30    let validator = jsonschema::validator_for(&schema_value).context("failed to compile fixture schema")?;
31
32    let mut errors = Vec::new();
33    validate_recursive(fixtures_dir, fixtures_dir, &validator, &mut errors)?;
34    Ok(errors)
35}
36
37fn validate_recursive(
38    base: &Path,
39    dir: &Path,
40    validator: &jsonschema::Validator,
41    errors: &mut Vec<ValidationError>,
42) -> Result<()> {
43    let entries = std::fs::read_dir(dir).with_context(|| format!("failed to read directory: {}", dir.display()))?;
44
45    let mut paths: Vec<_> = entries.filter_map(|e| e.ok()).map(|e| e.path()).collect();
46    paths.sort();
47
48    for path in paths {
49        if path.is_dir() {
50            validate_recursive(base, &path, validator, errors)?;
51        } else if path.extension().is_some_and(|ext| ext == "json") {
52            let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
53            // Skip schema files and files starting with _
54            if filename == "schema.json" || filename.starts_with('_') {
55                continue;
56            }
57
58            let relative = path.strip_prefix(base).unwrap_or(&path).to_string_lossy().to_string();
59
60            let content = match std::fs::read_to_string(&path) {
61                Ok(c) => c,
62                Err(e) => {
63                    errors.push(ValidationError {
64                        file: relative,
65                        message: format!("failed to read file: {e}"),
66                    });
67                    continue;
68                }
69            };
70
71            let value: serde_json::Value = match serde_json::from_str(&content) {
72                Ok(v) => v,
73                Err(e) => {
74                    errors.push(ValidationError {
75                        file: relative,
76                        message: format!("invalid JSON: {e}"),
77                    });
78                    continue;
79                }
80            };
81
82            for error in validator.iter_errors(&value) {
83                errors.push(ValidationError {
84                    file: relative.clone(),
85                    message: format!("{} at {}", error, error.instance_path),
86                });
87            }
88        }
89    }
90    Ok(())
91}