use crate::config::ConfigFile;
use crate::error::Error;
use crate::fs::catalog_io;
use crate::resource::ResourceKind;
use anyhow::anyhow;
use clap::Args;
use regex_lite::Regex;
use std::path::{Path, PathBuf};
use super::{selected_kinds, warn_unimplemented};
#[derive(Args, Debug)]
pub struct ValidateArgs {
#[arg(long, value_enum)]
pub resource: Option<ResourceKind>,
}
#[derive(Debug)]
struct ValidationIssue {
path: PathBuf,
message: String,
}
pub async fn run(args: &ValidateArgs, cfg: &ConfigFile, config_dir: &Path) -> anyhow::Result<()> {
let kinds = selected_kinds(args.resource, &cfg.resources);
let mut issues: Vec<ValidationIssue> = Vec::new();
for kind in kinds {
match kind {
ResourceKind::CatalogSchema => {
let catalogs_root = config_dir.join(&cfg.resources.catalog_schema.path);
validate_catalog_schemas(
&catalogs_root,
cfg.naming.catalog_name_pattern.as_deref(),
&mut issues,
)?;
}
other => warn_unimplemented(other),
}
}
if issues.is_empty() {
eprintln!("✓ All checks passed.");
return Ok(());
}
eprintln!("✗ Validation found {} issue(s):", issues.len());
for issue in &issues {
eprintln!(" • {}: {}", issue.path.display(), issue.message);
}
Err(Error::Config(format!("{} validation issue(s) found", issues.len())).into())
}
fn validate_catalog_schemas(
catalogs_root: &Path,
name_pattern: Option<&str>,
issues: &mut Vec<ValidationIssue>,
) -> anyhow::Result<()> {
if !catalogs_root.exists() {
return Ok(());
}
if !catalogs_root.is_dir() {
issues.push(ValidationIssue {
path: catalogs_root.to_path_buf(),
message: "expected directory for catalogs root".into(),
});
return Ok(());
}
let pattern: Option<(String, Regex)> = match name_pattern {
Some(p) => Some((
p.to_string(),
Regex::new(p).map_err(|e| anyhow!("invalid catalog_name_pattern regex {p:?}: {e}"))?,
)),
None => None,
};
for entry in std::fs::read_dir(catalogs_root)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
tracing::debug!(path = %entry.path().display(), "skipping non-directory entry");
continue;
}
let dir = entry.path();
let schema_path = dir.join("schema.yaml");
if !schema_path.is_file() {
continue;
}
let cat = match catalog_io::read_schema_file(&schema_path) {
Ok(c) => c,
Err(e) => {
issues.push(ValidationIssue {
path: schema_path.clone(),
message: format!("parse error: {e}"),
});
continue;
}
};
let dir_name = entry.file_name().to_string_lossy().into_owned();
if cat.name != dir_name {
issues.push(ValidationIssue {
path: schema_path.clone(),
message: format!(
"catalog name '{}' does not match its directory '{}'",
cat.name, dir_name
),
});
}
if let Some((pattern_str, re)) = &pattern {
if !re.is_match(&cat.name) {
issues.push(ValidationIssue {
path: schema_path.clone(),
message: format!(
"catalog name '{}' does not match catalog_name_pattern '{}'",
cat.name, pattern_str
),
});
}
}
}
Ok(())
}