clap_doc_generator 0.1.0

Automatically generate CLI documentation from clap definitions and update readme files
use syn::Attribute;
use syn::Fields;
use syn::Item;
use syn::Meta;

use super::attr_parser::extract_arg_attr;
use super::attr_parser::extract_command_attr;
use super::attr_parser::extract_doc_comment;
use super::attr_parser::extract_field_role;
use super::attr_parser::extract_rename_from_attrs;
use super::model_parsed::EnumKind;
use super::model_parsed::ParsedEnum;
use super::model_parsed::ParsedField;
use super::model_parsed::ParsedSource;
use super::model_parsed::ParsedStruct;
use super::model_parsed::ParsedVariant;
use super::type_helper::classify_type;
use super::type_helper::extract_inner_type_name;
use super::type_helper::type_to_string;

pub fn extract_items(items: &[Item], parsed: &mut ParsedSource) {
    for item in items {
        match item {
            Item::Struct(item_struct) if has_derive(&item_struct.attrs, &["Parser", "Args"]) => {
                parsed.arg_structs.push(parse_struct(item_struct));
            }
            Item::Enum(item_enum) => {
                if has_derive(&item_enum.attrs, &["Subcommand"]) {
                    parsed
                        .enums
                        .push(parse_enum(item_enum, EnumKind::Subcommand));
                } else if has_derive(&item_enum.attrs, &["ValueEnum"]) {
                    parsed
                        .enums
                        .push(parse_enum(item_enum, EnumKind::ValueEnum));
                }
            }
            Item::Mod(item_mod) => {
                if let Some((_, ref mod_items)) = item_mod.content {
                    extract_items(mod_items, parsed);
                }
            }
            _ => {}
        }
    }
}

fn has_derive(attrs: &[Attribute], names: &[&str]) -> bool {
    attrs.iter().any(|attr| {
        if let Meta::List(meta_list) = &attr.meta
            && attr.path().is_ident("derive")
        {
            let tokens = meta_list.tokens.to_string();
            return names.iter().any(|name| tokens.contains(name));
        }
        false
    })
}

fn parse_struct(item: &syn::ItemStruct) -> ParsedStruct {
    let fields = match &item.fields {
        Fields::Named(named) => named.named.iter().map(parse_field).collect(),
        _ => Vec::new(),
    };

    ParsedStruct {
        name: item.ident.to_string(),
        command_attr: extract_command_attr(&item.attrs),
        doc_comment: extract_doc_comment(&item.attrs),
        fields,
    }
}

fn parse_enum(item: &syn::ItemEnum, kind: EnumKind) -> ParsedEnum {
    let variants = item
        .variants
        .iter()
        .map(|variant| {
            let (inner_type_name, fields) = match &variant.fields {
                Fields::Unnamed(unnamed) => {
                    let inner = unnamed.unnamed.first().map(|f| type_to_string(&f.ty));
                    (inner, Vec::new())
                }
                Fields::Named(named) => {
                    let fields: Vec<ParsedField> = named.named.iter().map(parse_field).collect();
                    (None, fields)
                }
                Fields::Unit => (None, Vec::new()),
            };

            ParsedVariant {
                name: variant.ident.to_string(),
                doc_comment: extract_doc_comment(&variant.attrs),
                inner_type_name,
                fields,
                rename: extract_rename_from_attrs(&variant.attrs),
            }
        })
        .collect();

    ParsedEnum {
        name: item.ident.to_string(),
        kind,
        variants,
    }
}

fn parse_field(field: &syn::Field) -> ParsedField {
    let field_name = field
        .ident
        .as_ref()
        .map(|i| i.to_string())
        .unwrap_or_default();

    ParsedField {
        name: field_name,
        doc_comment: extract_doc_comment(&field.attrs),
        arg_attr: extract_arg_attr(&field.attrs),
        role: extract_field_role(&field.attrs),
        type_info: classify_type(&field.ty),
        inner_type_name: extract_inner_type_name(&field.ty),
    }
}