clap_doc_generator 0.1.0

Automatically generate CLI documentation from clap definitions and update readme files
use std::fs;
use std::path::Path;

use super::command_tree::build_command_info;
use super::extraction::extract_items;
use super::model_command::CommandInfo;
use super::model_parsed::ParsedSource;

pub fn analyze_project_source(
    project_path: &Path,
    binary_name: &str,
) -> Result<CommandInfo, String> {
    let source_files = collect_rust_sources(project_path)?;
    let mut parsed = ParsedSource::default();

    for file_path in &source_files {
        let content = fs::read_to_string(file_path)
            .map_err(|err| format!("Failed to read {}: {err}", file_path.display()))?;

        let syntax = syn::parse_file(&content)
            .map_err(|err| format!("Failed to parse {}: {err}", file_path.display()))?;

        extract_items(&syntax.items, &mut parsed);
    }

    let root_struct = parsed
        .arg_structs
        .iter()
        .find(|s| s.command_attr.name.is_some())
        .or_else(|| parsed.arg_structs.first())
        .ok_or_else(|| "No struct deriving Parser or Args found in project".to_string())?
        .clone();

    Ok(build_command_info(binary_name, &root_struct, &parsed))
}

fn collect_rust_sources(project_path: &Path) -> Result<Vec<std::path::PathBuf>, String> {
    let src_dir = project_path.join("src");
    if !src_dir.exists() {
        return Err(format!(
            "No src/ directory found in {}",
            project_path.display()
        ));
    }

    let mut files = Vec::new();
    collect_rs_files(&src_dir, &mut files)
        .map_err(|err| format!("Failed to scan source files: {err}"))?;

    if files.is_empty() {
        return Err("No .rs files found in src/".to_string());
    }

    Ok(files)
}

fn collect_rs_files(dir: &Path, files: &mut Vec<std::path::PathBuf>) -> std::io::Result<()> {
    for entry in fs::read_dir(dir)? {
        let path = entry?.path();

        if path.is_dir() {
            collect_rs_files(&path, files)?;
        } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
            files.push(path);
        }
    }
    Ok(())
}