braze_sync/cli/
validate.rs1use crate::config::ConfigFile;
17use crate::error::Error;
18use crate::fs::catalog_io;
19use crate::resource::ResourceKind;
20use anyhow::anyhow;
21use clap::Args;
22use regex_lite::Regex;
23use std::path::{Path, PathBuf};
24
25use super::{selected_kinds, warn_unimplemented};
26
27#[derive(Args, Debug)]
28pub struct ValidateArgs {
29 #[arg(long, value_enum)]
31 pub resource: Option<ResourceKind>,
32}
33
34#[derive(Debug)]
35struct ValidationIssue {
36 path: PathBuf,
37 message: String,
38}
39
40pub async fn run(args: &ValidateArgs, cfg: &ConfigFile, config_dir: &Path) -> anyhow::Result<()> {
41 let kinds = selected_kinds(args.resource, &cfg.resources);
42
43 let mut issues: Vec<ValidationIssue> = Vec::new();
44
45 for kind in kinds {
46 match kind {
47 ResourceKind::CatalogSchema => {
48 let catalogs_root = config_dir.join(&cfg.resources.catalog_schema.path);
49 validate_catalog_schemas(
50 &catalogs_root,
51 cfg.naming.catalog_name_pattern.as_deref(),
52 &mut issues,
53 )?;
54 }
55 other => warn_unimplemented(other),
56 }
57 }
58
59 if issues.is_empty() {
60 eprintln!("✓ All checks passed.");
61 return Ok(());
62 }
63
64 eprintln!("✗ Validation found {} issue(s):", issues.len());
65 for issue in &issues {
66 eprintln!(" • {}: {}", issue.path.display(), issue.message);
67 }
68
69 Err(Error::Config(format!("{} validation issue(s) found", issues.len())).into())
73}
74
75fn validate_catalog_schemas(
76 catalogs_root: &Path,
77 name_pattern: Option<&str>,
78 issues: &mut Vec<ValidationIssue>,
79) -> anyhow::Result<()> {
80 if !catalogs_root.exists() {
81 return Ok(());
83 }
84 if !catalogs_root.is_dir() {
85 issues.push(ValidationIssue {
86 path: catalogs_root.to_path_buf(),
87 message: "expected directory for catalogs root".into(),
88 });
89 return Ok(());
90 }
91
92 let pattern: Option<(String, Regex)> = match name_pattern {
96 Some(p) => Some((
97 p.to_string(),
98 Regex::new(p).map_err(|e| anyhow!("invalid catalog_name_pattern regex {p:?}: {e}"))?,
99 )),
100 None => None,
101 };
102
103 for entry in std::fs::read_dir(catalogs_root)? {
104 let entry = entry?;
105 if !entry.file_type()?.is_dir() {
106 tracing::debug!(path = %entry.path().display(), "skipping non-directory entry");
107 continue;
108 }
109 let dir = entry.path();
110 let schema_path = dir.join("schema.yaml");
111 if !schema_path.is_file() {
112 continue;
116 }
117
118 let cat = match catalog_io::read_schema_file(&schema_path) {
122 Ok(c) => c,
123 Err(e) => {
124 issues.push(ValidationIssue {
125 path: schema_path.clone(),
126 message: format!("parse error: {e}"),
127 });
128 continue;
129 }
130 };
131
132 let dir_name = entry.file_name().to_string_lossy().into_owned();
137 if cat.name != dir_name {
138 issues.push(ValidationIssue {
139 path: schema_path.clone(),
140 message: format!(
141 "catalog name '{}' does not match its directory '{}'",
142 cat.name, dir_name
143 ),
144 });
145 }
146
147 if let Some((pattern_str, re)) = &pattern {
149 if !re.is_match(&cat.name) {
150 issues.push(ValidationIssue {
151 path: schema_path.clone(),
152 message: format!(
153 "catalog name '{}' does not match catalog_name_pattern '{}'",
154 cat.name, pattern_str
155 ),
156 });
157 }
158 }
159 }
160
161 Ok(())
162}