kal-derive 0.3.1

Collection of derive macro for kal
Documentation
use darling::{FromField, FromVariant};
use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, Fields};

use crate::common::{
    codegen::command_option_codegen::{CommandOption, CommandOptionsExt},
    config::{argument_config::ArgumentConfig, command_config::CommandConfig},
    error::{self, Error},
};

pub fn actual_derive_transform_hint(derive_input: DeriveInput) -> error::Result<TokenStream> {
    let name = derive_input.ident;

    let mut options = Vec::new();
    let mut subcommands = Vec::new();

    let mut self_discovered = Vec::new();

    match derive_input.data {
        syn::Data::Struct(data) => {
            self_discovered.push(quote! {
                #name
            });
            for field in data.fields {
                let argument_config = ArgumentConfig::from_field(&field)?;
                let field_ident = field
                    .ident
                    .clone()
                    .ok_or_else(|| Error::new(&field, "enum variant field must have a name"))?;

                options.push(CommandOption {
                    ident: field_ident,
                    name: argument_config.name,
                    position: options.len(),
                    description: argument_config.description,
                    ty: field.ty,
                    take_rest: argument_config.take_rest,
                });
            }
        }
        syn::Data::Enum(data) => {
            for variant in data.variants {
                let command_config = CommandConfig::from_variant(&variant);

                match variant.fields {
                    Fields::Named(fields) => {
                        let mut inner_options = Vec::new();
                        for field in fields.named {
                            let argument_config = ArgumentConfig::from_field(&field)?;
                            let ident = field.ident.clone().ok_or_else(|| {
                                Error::new(&field, "enum variant field must have a name")
                            })?;
                            inner_options.push(CommandOption {
                                ident,
                                name: argument_config.name,
                                position: inner_options.len(),
                                description: argument_config.description,
                                ty: field.ty,
                                take_rest: argument_config.take_rest,
                            });
                        }

                        let command_config = command_config?;
                        let variant_ident = variant.ident;
                        let variant_full_name = quote! { #name::#variant_ident };

                        if command_config.for_self.unwrap_or(false) {
                            self_discovered.push(variant_full_name);
                            options = inner_options;
                        } else {
                            let command_name =
                                command_config.name_or_error_from(&variant_full_name)?;
                            let transform_hint_vec = inner_options.make_transform_hint_vec();

                            subcommands.push((
                                quote! { #command_name },
                                quote! { ::kal::lex::TransformHint::Execute(#transform_hint_vec) },
                            ));
                        }
                    }
                    Fields::Unnamed(fields) => {
                        if fields.unnamed.len() != 1 {
                            return Err(Error::new(
                                fields,
                                "Unnamed enum variant must have one field",
                            ));
                        }
                        let ty = &fields.unnamed[0].ty;
                        subcommands.push((
                            quote! { <#ty as ::kal::Command>::NAME },
                            quote! { <#ty as ::kal::lex::TransformHint>::hint() },
                        ));
                    }
                    Fields::Unit => {
                        let command_config = command_config?;

                        let command_name = command_config.name_or_error_from(&variant.ident)?;
                        subcommands
                            .push((quote! { #command_name }, quote! { ::std::vec::Vec::new() }));
                    }
                };
            }
        }
        syn::Data::Union(data) => {
            return Err(Error::new(
                data.union_token,
                "Cannot derive Command for union",
            ))
        }
    };

    if self_discovered.len() > 1 {
        return Err(Error::new(
            &self_discovered[0],
            "Cannot set #[command(self)] more than once",
        ));
    }

    let transform_hint_vec = if options.is_empty() {
        None
    } else {
        Some(options.make_transform_hint_vec())
    };
    let subcommand_select = if subcommands.is_empty() {
        None
    } else {
        let (name, value): (Vec<_>, Vec<_>) = subcommands.into_iter().unzip();
        Some(quote! {
            {
                ::std::collections::HashMap::from_iter(
                    [#((#name, #value)),*]
                )
            }
        })
    };

    let hint_variant = match (transform_hint_vec, subcommand_select) {
        (None, None) => quote! { ::kal::lex::TransformHint::Execute(::std::vec::Vec::new()) },
        (None, Some(subcommand_select)) => {
            quote! { ::kal::lex::TransformHint::Select(#subcommand_select) }
        }
        (Some(transform_hint_vec), None) => {
            quote! { ::kal::lex::TransformHint::Execute(#transform_hint_vec) }
        }
        (Some(transform_hint_vec), Some(subcommand_select)) => {
            quote! { ::kal::lex::TransformHint::SelectOrExecute(#subcommand_select, #transform_hint_vec) }
        }
    };

    Ok(quote! {
        impl ::kal::lex::TransformHintProvider for #name {
            fn hint() -> ::kal::lex::TransformHint {
                #hint_variant
            }
        }
    }
    .into())
}