clap_doc_generator 0.1.0

Automatically generate CLI documentation from clap definitions and update readme files
use syn::Attribute;
use syn::Expr;
use syn::Lit;
use syn::Meta;
use syn::meta::ParseNestedMeta;

use super::model_parsed::ArgAttr;
use super::model_parsed::CommandAttr;
use super::model_parsed::FieldRole;

pub fn extract_doc_comment(attrs: &[Attribute]) -> String {
    let lines: Vec<String> = attrs
        .iter()
        .filter(|attr| attr.path().is_ident("doc"))
        .filter_map(|attr| {
            if let Meta::NameValue(nv) = &attr.meta
                && let Expr::Lit(expr_lit) = &nv.value
                && let Lit::Str(lit_str) = &expr_lit.lit
            {
                Some(lit_str.value().trim().to_string())
            } else {
                None
            }
        })
        .collect();

    lines.join(" ")
}

pub fn extract_command_attr(attrs: &[Attribute]) -> CommandAttr {
    let mut result = CommandAttr::default();

    for attr in attrs {
        if !attr.path().is_ident("command") {
            continue;
        }
        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("name") {
                result.name = meta_str(&meta);
            } else if meta.path.is_ident("about") {
                result.about = meta_str(&meta);
            }
            Ok(())
        });
    }

    result
}

pub fn extract_arg_attr(attrs: &[Attribute]) -> ArgAttr {
    let mut result = ArgAttr::default();

    for attr in attrs {
        if !attr.path().is_ident("arg") {
            continue;
        }
        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("short") {
                result.short = Some(meta_char(&meta).unwrap_or('\0'));
            } else if meta.path.is_ident("long") {
                result.long = Some(meta_str(&meta).unwrap_or_default());
            } else if meta.path.is_ident("help") {
                result.help = meta_str(&meta);
            } else if meta.path.is_ident("long_help") {
                result.long_help = meta_str(&meta);
            } else if meta.path.is_ident("default_value") {
                result.default_value = meta_str(&meta);
            } else if meta.path.is_ident("default_value_t") {
                result.default_value_t = meta_lit_to_string(&meta);
            } else if meta.path.is_ident("value_name") {
                result.value_name = meta_str(&meta);
            } else if meta.path.is_ident("required") {
                result.required = Some(meta_bool(&meta).unwrap_or(true));
            } else if meta.path.is_ident("hide") {
                result.hide = meta_bool(&meta).unwrap_or(true);
            } else if meta.path.is_ident("value_enum") {
                result.value_enum = true;
            } else if meta.path.is_ident("env") {
                result.env = Some(meta_str(&meta).unwrap_or_default());
            }
            Ok(())
        });
    }

    result
}

pub fn extract_field_role(attrs: &[Attribute]) -> FieldRole {
    for attr in attrs {
        if !attr.path().is_ident("command") {
            continue;
        }
        let mut role = FieldRole::Normal;
        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("subcommand") {
                role = FieldRole::Subcommand;
            } else if meta.path.is_ident("flatten") {
                role = FieldRole::Flatten;
            }
            Ok(())
        });
        if role != FieldRole::Normal {
            return role;
        }
    }
    FieldRole::Normal
}

pub fn extract_rename_from_attrs(attrs: &[Attribute]) -> Option<String> {
    for attr in attrs {
        if !attr.path().is_ident("clap") && !attr.path().is_ident("command") {
            continue;
        }
        let mut rename = None;
        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("name") {
                rename = meta_str(&meta);
            }
            Ok(())
        });
        if rename.is_some() {
            return rename;
        }
    }
    None
}

fn meta_str(meta: &ParseNestedMeta) -> Option<String> {
    let lit: Lit = meta.value().ok()?.parse().ok()?;
    if let Lit::Str(s) = lit {
        Some(s.value())
    } else {
        None
    }
}

fn meta_char(meta: &ParseNestedMeta) -> Option<char> {
    let lit: Lit = meta.value().ok()?.parse().ok()?;
    if let Lit::Char(c) = lit {
        Some(c.value())
    } else {
        None
    }
}

fn meta_bool(meta: &ParseNestedMeta) -> Option<bool> {
    let lit: Lit = meta.value().ok()?.parse().ok()?;
    if let Lit::Bool(b) = lit {
        Some(b.value)
    } else {
        None
    }
}

fn meta_lit_to_string(meta: &ParseNestedMeta) -> Option<String> {
    let lit: Lit = meta.value().ok()?.parse().ok()?;
    match lit {
        Lit::Str(s) => Some(s.value()),
        Lit::Int(i) => Some(i.to_string()),
        Lit::Float(f) => Some(f.to_string()),
        Lit::Bool(b) => Some(b.value.to_string()),
        _ => None,
    }
}