alef-snippets 0.16.22

Reusable documentation snippet discovery and validation for Alef
Documentation
use alef_snippets::discovery;
use alef_snippets::output;
use alef_snippets::runner::{RunnerConfig, run_validation};
use alef_snippets::types::{Language, ValidationLevel};
use alef_snippets::validators::ValidatorRegistry;
use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf};
use std::process::ExitCode;

#[derive(Parser)]
#[command(name = "snippet-runner")]
#[command(about = "Validate documentation code snippets across languages")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    List {
        #[arg(short, long, required = true, num_args = 1..)]
        snippets: Vec<PathBuf>,

        #[arg(short, long, value_delimiter = ',')]
        languages: Option<Vec<String>>,
    },

    Validate {
        #[arg(short, long, required = true, num_args = 1..)]
        snippets: Vec<PathBuf>,

        #[arg(short = 'L', long, default_value = "syntax")]
        level: ValidationLevel,

        #[arg(short, long, value_delimiter = ',')]
        languages: Option<Vec<String>>,

        #[arg(short, long)]
        output: Option<PathBuf>,

        #[arg(short = 'j', long, default_value = "4")]
        jobs: usize,

        #[arg(short = 't', long, default_value = "30")]
        timeout: u64,

        #[arg(long)]
        fail_fast: bool,

        #[arg(long)]
        include: Option<String>,

        #[arg(long)]
        show_code: bool,
    },

    Parse {
        file: PathBuf,
    },
}

fn main() -> ExitCode {
    let cli = Cli::parse();

    match cli.command {
        Commands::List { snippets, languages } => run_list_command(&snippets, languages.as_ref()),
        Commands::Validate {
            snippets,
            level,
            languages,
            output: output_path,
            jobs,
            timeout,
            fail_fast,
            include,
            show_code,
        } => run_validate_command(
            &snippets,
            level,
            languages.as_ref(),
            output_path,
            jobs,
            timeout,
            fail_fast,
            include.as_ref(),
            show_code,
        ),
        Commands::Parse { file } => run_parse_command(&file),
    }
}

fn parse_language_filter(languages: Option<&[String]>) -> Option<Vec<Language>> {
    let languages = languages?;
    Some(
        languages
            .iter()
            .map(|language| Language::from_fence_tag(language))
            .filter(|language| *language != Language::Unknown)
            .collect(),
    )
}

fn run_list_command(snippets: &[PathBuf], languages: Option<&Vec<String>>) -> ExitCode {
    let filter = parse_language_filter(languages.map(Vec::as_slice));
    match discovery::discover_snippets(snippets, filter.as_deref()) {
        Ok(found) => {
            output::print_snippet_list(&found);
            println!();
            for (language, count) in &discovery::count_by_language(&found) {
                println!("  {language:<12} {count}");
            }
            println!();
            ExitCode::SUCCESS
        }
        Err(err) => {
            eprintln!("Error discovering snippets: {err}");
            ExitCode::FAILURE
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn run_validate_command(
    snippets: &[PathBuf],
    level: ValidationLevel,
    languages: Option<&Vec<String>>,
    output_path: Option<PathBuf>,
    jobs: usize,
    timeout: u64,
    fail_fast: bool,
    include: Option<&String>,
    show_code: bool,
) -> ExitCode {
    let filter = parse_language_filter(languages.map(Vec::as_slice));
    let mut found = match discovery::discover_snippets(snippets, filter.as_deref()) {
        Ok(found) => found,
        Err(err) => {
            eprintln!("Error discovering snippets: {err}");
            return ExitCode::FAILURE;
        }
    };

    if let Some(pattern) = &include {
        found.retain(|snippet| snippet.path.to_string_lossy().contains(pattern.as_str()));
    }

    if found.is_empty() {
        println!("No snippets found.");
        return ExitCode::SUCCESS;
    }

    println!("Validating {} snippets at level '{level}'...", found.len());
    let registry = ValidatorRegistry::new();
    let config = RunnerConfig {
        level,
        parallelism: jobs,
        timeout_secs: timeout,
        fail_fast,
    };

    match run_validation(&found, &registry, &config) {
        Ok(summary) => {
            output::print_summary(&summary, show_code);

            if let Some(path) = output_path {
                if let Err(err) = output::write_json(&summary.results, &path) {
                    eprintln!("Error writing JSON output: {err}");
                } else {
                    println!("Results written to {}", path.display());
                }
            }

            if summary.has_failures() {
                ExitCode::FAILURE
            } else {
                ExitCode::SUCCESS
            }
        }
        Err(err) => {
            eprintln!("Error running validation: {err}");
            ExitCode::FAILURE
        }
    }
}

fn run_parse_command(file: &Path) -> ExitCode {
    match alef_snippets::parser::parse_code_blocks(file) {
        Ok(blocks) => {
            if blocks.is_empty() {
                println!("No code blocks found in {}", file.display());
            } else {
                for (index, block) in blocks.iter().enumerate() {
                    println!("--- Block {} (line {}) ---", index + 1, block.start_line);
                    println!("Language: {}", block.lang);
                    if let Some(title) = &block.title {
                        println!("Title: {title}");
                    }
                    if let Some(comment) = &block.preceding_comment {
                        println!("Annotation: {comment}");
                    }
                    println!("Code ({} lines):", block.code.lines().count());
                    println!("{}", block.code);
                    println!();
                }
            }
            ExitCode::SUCCESS
        }
        Err(err) => {
            eprintln!("Error parsing {}: {err}", file.display());
            ExitCode::FAILURE
        }
    }
}