1use anyhow::{Context, Result};
4use std::fmt;
5use std::path::Path;
6
7static FIXTURE_SCHEMA: &str = include_str!("../schema/fixture.schema.json");
8
9#[derive(Debug, Clone)]
11pub struct ValidationError {
12 pub file: String,
14 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
24pub 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 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}