audb-cli 0.1.11

Command-line interface for AuDB database application framework
//! Validation command
//!
//! Implements the `au check` command for validating gold files without generating code.

use anyhow::Result;
use audb::parser::GoldParser;
use audb::model::Project;
use console::{Term, style};
use std::path::PathBuf;
use walkdir::WalkDir;

/// Run the check command
pub fn run(gold_dir: &str) -> Result<()> {
    let term = Term::stdout();

    term.write_line("")?;
    term.write_line(&format!(
        "{} {}",
        style("Validating gold files in:").bold().cyan(),
        style(gold_dir).white()
    ))?;
    term.write_line("")?;

    // Discover gold files
    let files = discover_gold_files(gold_dir)?;

    if files.is_empty() {
        term.write_line(&format!(
            "{} No .au files found in {}",
            style("").yellow(),
            gold_dir
        ))?;
        term.write_line("")?;
        return Ok(());
    }

    term.write_line(&format!(
        "Found {} gold file{}",
        style(files.len()).green(),
        if files.len() == 1 { "" } else { "s" }
    ))?;
    term.write_line("")?;

    // Parse all gold files
    let mut gold_files = Vec::new();
    let mut errors = Vec::new();

    for path in &files {
        let relative_path = path.strip_prefix(gold_dir).unwrap_or(path);
        match GoldParser::parse_file(path) {
            Ok(gold_file) => {
                term.write_line(&format!(
                    "  {} {}",
                    style("").green(),
                    style(relative_path.display()).dim()
                ))?;
                gold_files.push(gold_file);
            }
            Err(e) => {
                term.write_line(&format!(
                    "  {} {}",
                    style("").red(),
                    style(relative_path.display()).dim()
                ))?;
                term.write_line(&format!("    {}", style(format!("Error: {}", e)).red()))?;
                errors.push((relative_path.to_path_buf(), e));
            }
        }
    }

    term.write_line("")?;

    // If there were parse errors, exit
    if !errors.is_empty() {
        term.write_line(&format!(
            "{} {} error{} found",
            style("").red().bold(),
            errors.len(),
            if errors.len() == 1 { "" } else { "s" }
        ))?;
        term.write_line("")?;
        anyhow::bail!("Validation failed with {} error(s)", errors.len());
    }

    // Build project model
    match Project::from_gold_files(gold_files) {
        Ok(project) => {
            term.write_line(&format!(
                "{} Project validation successful",
                style("").green().bold()
            ))?;
            term.write_line("")?;
            term.write_line(&format!(
                "  {} {} schemas",
                style("Schemas:").dim(),
                style(project.schemas.len()).green()
            ))?;
            term.write_line(&format!(
                "  {} {} queries",
                style("Queries:").dim(),
                style(project.queries.len()).green()
            ))?;
            term.write_line(&format!(
                "  {} {} endpoints",
                style("Endpoints:").dim(),
                style(project.endpoints.len()).green()
            ))?;
            term.write_line("")?;
        }
        Err(e) => {
            term.write_line(&format!(
                "{} Project validation failed",
                style("").red().bold()
            ))?;
            term.write_line(&format!("  {}", style(format!("Error: {}", e)).red()))?;
            term.write_line("")?;
            anyhow::bail!("Project validation failed: {}", e);
        }
    }

    Ok(())
}

/// Discover all `.au` files in a directory
fn discover_gold_files(gold_dir: &str) -> Result<Vec<PathBuf>> {
    let gold_path = PathBuf::from(gold_dir);

    if !gold_path.exists() {
        anyhow::bail!("Gold directory does not exist: {}", gold_dir);
    }

    let mut files = Vec::new();

    for entry in WalkDir::new(&gold_path)
        .follow_links(true)
        .into_iter()
        .filter_map(|e| e.ok())
    {
        let path = entry.path();
        if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("au") {
            files.push(path.to_path_buf());
        }
    }

    Ok(files)
}