Skip to main content

tusks_lib/codegen/util/
command_attribute.rs

1use proc_macro2::TokenStream;
2use syn::{Attribute, parse_quote, spanned::Spanned};
3
4use crate::models::{
5    Tusk,
6    ExternalModule,
7    TusksModule
8};
9
10use quote::quote;
11
12impl TusksModule {
13    /// Example 1 - Root module of tusks
14    /// ```ignore
15    /// #[command(name = "tasks")] /// <===== here =====
16    ///     pub struct Cli {
17    ///         #[arg(long)]
18    ///         pub root_param: String,
19    ///         #[arg(short, long)]
20    ///         pub verbose: bool,
21    ///         #[command(subcommand)] /// see generate_command_attribute_for_subcommands
22    ///         pub sub: Option<Commands>,
23    ///     }
24    /// ```
25    ///
26    /// Example 2 - This is a submodule-subcommand:
27    /// ```ignore
28    /// pub enum Commands {
29    ///     /// ... other subcommands and submodule-subcommands
30    ///     #[command(name = "level1")] /// <===== here =====
31    ///     #[allow(non_camel_case_types)]
32    ///     level1 {
33    ///         #[arg(long)]
34    ///         level1_field: Option<String>,
35    ///         #[arg(long, default_value = "42")]
36    ///         level1_number: i32,
37    ///         #[command(subcommand)] /// see generate_command_attribute_for_subcommands
38    ///         sub: Option<level1::Commands>,
39    ///     },
40    /// }
41    /// ```
42    pub fn generate_command_attribute(&self) -> TokenStream {
43        let existing_attrs = self.extract_attributes(&["command"]);
44
45        if !existing_attrs.is_empty() {
46            // Use existing command attribute
47            quote! { #(#existing_attrs)* }
48        } else {
49            // Generate default command attribute
50            quote! { #[command()] }
51        }
52    }
53
54    /// Example 1 - Root module of tusks
55    /// ```ignore
56    /// #[command(name = "tasks")] /// see generate_command_attribute
57    ///     pub struct Cli {
58    ///         #[arg(long)]
59    ///         pub root_param: String,
60    ///         #[arg(short, long)]
61    ///         pub verbose: bool,
62    ///         #[command(subcommand)] /// <===== here =====
63    ///         pub sub: Option<Commands>,
64    ///     }
65    /// ```
66    ///
67    /// Example 2 - This is a submodule-subcommand:
68    /// ```ignore
69    /// pub enum Commands {
70    ///     /// ... other subcommands and submodule-subcommands
71    ///     #[command(name = "level1")] /// see generate_command_attribute
72    ///     #[allow(non_camel_case_types)]
73    ///     level1 {
74    ///         #[arg(long)]
75    ///         level1_field: Option<String>,
76    ///         #[arg(long, default_value = "42")]
77    ///         level1_number: i32,
78    ///         #[command(subcommand)] /// <===== here =====
79    ///         sub: Option<level1::Commands>,
80    ///     },
81    /// }
82    /// ```
83    pub fn generate_command_attribute_for_subcommands(&self) -> TokenStream {
84        let existing_attrs = self.extract_attributes(&["subcommands"]);
85
86        if !existing_attrs.is_empty() {
87            transform_attributes_to_command(existing_attrs, "subcommand")
88        } else {
89            quote! { #[command(subcommand)] }
90        }
91    }
92
93    /// Example:
94    /// ```ignore
95    /// pub enum Commands {
96    ///     // ... other non-external subcommands ...
97    ///     #[command(flatten)]
98    ///     TuskExternalCommands(ExternalCommands),
99    /// }
100    /// ```
101    pub fn generate_command_attribute_for_external_subcommands(&self) -> TokenStream {
102        let existing_attrs = self.extract_attributes(&["external_subcommands"]);
103        
104        if !existing_attrs.is_empty() {
105            transform_attributes_to_command(existing_attrs, "flatten")
106        } else {
107            quote! { #[command(flatten)] }
108        }
109    }
110}
111
112impl Tusk {
113    pub fn generate_command_attribute(&self) -> TokenStream {
114        let existing_attrs = self.extract_attributes(&["command"]);
115        use_attributes_or_default(&existing_attrs, quote! { #[command()] })
116    }
117}
118
119impl ExternalModule {
120    /// Example:
121    /// ```ignore
122    /// pub enum ExternalCommands {
123    ///    #[command(name = "ext2")]
124    ///    #[allow(non_camel_case_types)]
125    ///    ext2(super::super::ext2::__internal_tusks_module::cli::Cli),
126    ///}
127    ///```
128    pub fn generate_command_attribute(&self) -> TokenStream {
129        let existing_attrs = self.extract_attributes(&["command"]);
130        use_attributes_or_default(&existing_attrs, quote! { #[command()] })
131    }
132}
133
134fn use_attributes_or_default(attrs: &[&Attribute], default: TokenStream) -> TokenStream {
135    if !attrs.is_empty() {
136        quote! { #(#attrs)* }
137    } else {
138        default
139    }
140}
141
142/// Helper function to transform attributes from one form to another while preserving spans.
143/// 
144/// Transforms attributes like `#[source_name(params)]` to `#[command(target_keyword, params)]`
145/// 
146/// # Arguments
147/// * `attrs` - The attributes to transform
148/// * `target_keyword` - The keyword to use in the command attribute (e.g., "subcommand", "flatten")
149/// 
150/// # Examples
151/// * `#[subcommands(arg1, arg2)]` with target "subcommand" → `#[command(subcommand, arg1, arg2)]`
152/// * `#[external_subcommands]` with target "flatten" → `#[command(flatten)]`
153fn transform_attributes_to_command(attrs: Vec<&syn::Attribute>, target_keyword: &str) -> TokenStream {
154    let mut result = TokenStream::new();
155    let mut target_ident = syn::Ident::new(target_keyword, proc_macro2::Span::call_site());
156    
157    for attr in attrs {
158        let pound_span = attr.pound_token.span;
159        let bracket_span = attr.bracket_token.span;
160
161        target_ident.set_span(attr.span());
162        
163        // Parse the tokens inside the attribute
164        if let syn::Meta::List(meta_list) = &attr.meta {
165            let inner_tokens = &meta_list.tokens;
166            
167            // Create new attribute: #[command(target_keyword, inner_tokens)]
168            let new_attr: syn::Attribute = parse_quote! {
169                #[command(#target_ident, #inner_tokens)]
170            };
171            
172            // Transfer the original spans
173            let mut new_attr_with_span = new_attr;
174            new_attr_with_span.pound_token.span = pound_span;
175            new_attr_with_span.bracket_token.span = bracket_span;
176            
177            result.extend(quote! { #new_attr_with_span });
178        } else {
179            // If it's just #[attr_name] without parameters
180            let new_attr: syn::Attribute = parse_quote! {
181                #[command(#target_ident)]
182            };
183            
184            let mut new_attr_with_span = new_attr;
185            new_attr_with_span.pound_token.span = pound_span;
186            new_attr_with_span.bracket_token.span = bracket_span;
187            
188            result.extend(quote! { #new_attr_with_span });
189        }
190    }
191    
192    result
193}