Skip to main content

tusks_lib/codegen/cli/
module.rs

1use quote::quote;
2use proc_macro2::TokenStream;
3use syn::Ident;
4
5use crate::codegen::util::enum_util::to_variant_ident;
6use crate::codegen::util::field_util::is_generated_field;
7
8use crate::{TusksModule, models::{Tusk, TusksParameters}};
9use super::CliCodegen;
10
11/// Extract `#[doc = "..."]` attributes (i.e. `///` comments) from an attribute list.
12/// When placed on a clap derive variant, these become the `about` text.
13fn extract_doc_attrs(attrs: &[syn::Attribute]) -> Vec<&syn::Attribute> {
14    attrs.iter().filter(|a| a.path().is_ident("doc")).collect()
15}
16
17impl CliCodegen for TusksModule {
18    fn build_cli(&self, path: Vec<&Ident>, debug: bool) -> TokenStream {
19        let mut items = Vec::new();
20
21        items.push(quote! {use ::tusks::clap;});
22
23        // Re-export value enums so they are accessible from the generated cli module
24        for item_enum in &self.value_enums {
25            let enum_name = &item_enum.ident;
26            items.push(quote! { use super::super::#enum_name; });
27        }
28
29        // 1. If root (path empty): generate Cli struct
30        if path.is_empty() {
31            items.push(self.build_cli_struct(debug));
32        }
33        
34        // 2. Generate ExternalCommands enum if needed
35        if !self.external_modules.is_empty() {
36            items.push(self.build_external_commands_enum(&path, debug));
37        }
38        
39        // 3. Generate Commands enum if needed
40        if !self.tusks.is_empty() || !self.external_modules.is_empty() {
41            items.push(self.build_commands_enum(debug));
42        }
43        
44        // 4. Generate submodule modules and recurse
45        for submodule in &self.submodules {
46            let mut sub_path = path.clone();
47            sub_path.push(&submodule.name);
48            let submod_name = &submodule.name;
49            let submod_content = submodule.build_cli(sub_path, debug);
50            
51            items.push(quote! {
52                pub mod #submod_name {
53                    #submod_content
54                }
55            });
56        }
57        
58        quote! {
59            #(#items)*
60        }
61    }
62}
63
64/// Internal helpers for CLI code generation.
65impl TusksModule {
66    /// Generate the root Cli struct
67    fn build_cli_struct(&self, debug: bool) -> TokenStream {
68        // Extract fields from parameters struct
69        let fields = if let Some(ref params) = self.parameters {
70            self.build_cli_fields_from_parameters(params)
71        } else {
72            quote! {}
73        };
74        
75        // Add subcommand field if we have commands
76        let subcommand_field = if !self.tusks.is_empty() || !self.external_modules.is_empty() {
77            let subcommand_attr = self.generate_command_attribute_for_subcommands();
78            quote! {
79                #subcommand_attr
80                pub sub: Option<Commands>,
81            }
82        } else {
83            quote! {}
84        };
85        
86        let derive_attr = if debug {
87            quote! {}
88        } else {
89            quote! { #[derive(::tusks::clap::Parser)] }
90        };
91        
92        let command_attr = self.generate_command_attribute();
93        
94        quote! {
95            #derive_attr
96            #command_attr
97            pub struct Cli {
98                #fields
99                #subcommand_field
100            }
101        }
102    }
103    
104    /// Build fields for Cli struct from Parameters struct
105    fn build_cli_fields_from_parameters(&self, params: &TusksParameters) -> TokenStream {
106        let mut fields = Vec::new();
107
108        for field in &params.pstruct.fields {
109            let field_name = &field.ident;
110
111            if field_name.as_ref().map(|id| is_generated_field(&id.to_string())).unwrap_or(false) {
112                continue;
113            }
114
115            let field_type = Self::dereference_type(&field.ty);
116
117            // Filter and keep #[arg(...)] attributes with original spans
118            let attrs: Vec<_> = field.attrs.iter()
119                .filter(|attr| attr.path().is_ident("arg"))
120                .collect();
121
122            fields.push(quote! {
123                #(#attrs)*
124                pub #field_name: #field_type,
125            });
126        }
127
128        quote! {
129            #(#fields)*
130        }
131    }
132    
133    /// Convert a reference type to its dereferenced type
134    /// e.g., &Option<String> -> Option<String>
135    fn dereference_type(ty: &syn::Type) -> syn::Type {
136        if let syn::Type::Reference(type_ref) = ty {
137            (*type_ref.elem).clone()
138        } else {
139            ty.clone()
140        }
141    }
142    
143    /// Generate the ExternalCommands enum
144    fn build_external_commands_enum(&self, path: &Vec<&Ident>, debug: bool) -> TokenStream {
145        let variants: Vec<_> = self.external_modules.iter().map(|ext_mod| {
146            let variant_ident = to_variant_ident(&ext_mod.alias);
147
148            // Anzahl super:: Prefixe: path.len() + 2
149            let mut full_path: Vec<syn::Ident> = (0..path.len() + 2)
150                .map(|_| syn::Ident::new("super", ext_mod.alias.span()))
151                .collect();
152
153            // Originalpfad anhängen
154            for p in path {
155                full_path.push((*p).clone());
156            }
157
158            // Alias anhängen
159            full_path.push(ext_mod.alias.clone());
160
161            let command_attr = ext_mod.generate_command_attribute();
162            
163            quote! {
164                #command_attr
165                #[allow(non_camel_case_types)]
166                #variant_ident(
167                    #(#full_path)::*::__internal_tusks_module::cli::Cli
168                ),
169            }
170        }).collect();
171
172        let derive_attr = if debug {
173            quote! {}
174        } else {
175            quote! { #[derive(::tusks::clap::Subcommand)] }
176        };
177
178        quote! {
179            #derive_attr
180            pub enum ExternalCommands {
181                #(#variants)*
182            }
183        }
184    }
185
186    /// Generate the Commands enum
187    fn build_commands_enum(&self, debug: bool) -> TokenStream {
188        let mut variants = Vec::new();
189        
190        // Add variants for tusks (command functions)
191        for tusk in &self.tusks {
192            variants.push(self.build_command_variant_from_tusk(tusk));
193        }
194        
195        // Add variants for submodules
196        for submodule in &self.submodules {
197            variants.push(self.build_command_variant_from_submodule(submodule));
198        }
199        
200        if !self.external_modules.is_empty() {
201            let attr = self.generate_command_attribute_for_external_subcommands();
202            variants.push(quote! {
203                #attr
204                TuskExternalCommands(ExternalCommands),
205            });
206        }
207
208        if self.allow_external_subcommands {
209            variants.push(quote! {
210                #[command(external_subcommand)]
211                ClapExternalSubcommand(Vec<String>),
212            });
213        }
214        
215        let derive_attr = if debug {
216            quote! {}
217        } else {
218            quote! { #[derive(::tusks::clap::Subcommand)] }
219        };
220        
221        quote! {
222            #derive_attr
223            pub enum Commands {
224                #(#variants)*
225            }
226        }
227    }
228    
229    /// Build a command variant from a Tusk (command function)
230    fn build_command_variant_from_tusk(&self, tusk: &Tusk) -> TokenStream {
231        let func_name = &tusk.func.sig.ident;
232        let variant_ident = to_variant_ident(func_name);
233        let fields = self.build_fields_from_tusk_params(tusk);
234        let command_attr = tusk.generate_command_attribute();
235        let doc_attrs = extract_doc_attrs(&tusk.func.attrs);
236
237        quote! {
238            #(#doc_attrs)*
239            #command_attr
240            #[allow(non_camel_case_types)]
241            #variant_ident {
242                #fields
243            },
244        }
245    }
246
247    /// Build fields from tusk function parameters
248    fn build_fields_from_tusk_params(&self, tusk: &Tusk) -> TokenStream {
249        let mut fields = Vec::new();
250
251        let mut params_iter = tusk.func.sig.inputs.iter();
252
253        // Check if first parameter is &Parameters (matching our parameters struct)
254        let skip_first = if let Some(syn::FnArg::Typed(first_param)) = params_iter.next() {
255            if let Some(ref params) = self.parameters {
256                // Check if the type matches &ParametersStructName
257                Self::is_parameters_type(&first_param.ty, &params.pstruct.ident)
258            } else {
259                false
260            }
261        } else {
262            false
263        };
264
265        // If we didn't skip first, reset iterator
266        let params_to_process: Vec<_> = if skip_first {
267            params_iter.collect()
268        } else {
269            tusk.func.sig.inputs.iter().collect()
270        };
271
272        for param in params_to_process {
273            if let syn::FnArg::Typed(pat_type) = param {
274                let param_name = if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
275                    &pat_ident.ident
276                } else {
277                    continue;
278                };
279
280                let param_type = &pat_type.ty;
281
282                // Filter #[arg(...)] attributes
283                let attrs: Vec<_> = pat_type.attrs.iter()
284                    .filter(|attr| attr.path().is_ident("arg"))
285                    .collect();
286
287                fields.push(quote! {
288                    #(#attrs)*
289                    #param_name: #param_type,
290                });
291            }
292        }
293
294        quote! {
295            #(#fields)*
296        }
297    }
298
299    /// Check if a type is a reference to a parameters struct
300    pub fn is_parameters_type(ty: &syn::Type, params_ident: &Ident) -> bool {
301        if let syn::Type::Reference(type_ref) = ty
302            && let syn::Type::Path(type_path) = &*type_ref.elem
303                && let Some(segment) = type_path.path.segments.last() {
304                    return segment.ident == *params_ident;
305                }
306        false
307    }
308    
309    /// Build a command variant from a submodule
310    fn build_command_variant_from_submodule(&self, submodule: &TusksModule) -> TokenStream {
311        let submod_name = &submodule.name;
312        let variant_ident = to_variant_ident(submod_name);
313
314        // Extract fields from submodule's parameters
315        let fields = if let Some(ref params) = submodule.parameters {
316            self.build_enum_fields_from_parameters(params)
317        } else {
318            quote! {}
319        };
320
321        // Add subcommand field if submodule has commands
322        let subcommand_field = if !submodule.tusks.is_empty() || !submodule.external_modules.is_empty() {
323            let subcommand_attr = submodule.generate_command_attribute_for_subcommands();
324            quote! {
325                #subcommand_attr
326                sub: Option<#submod_name::Commands>,
327            }
328        } else {
329            quote! {}
330        };
331
332        let command_attr = submodule.generate_command_attribute();
333        let doc_attrs = extract_doc_attrs(&submodule.attrs.0);
334
335        quote! {
336            #(#doc_attrs)*
337            #command_attr
338            #[allow(non_camel_case_types)]
339            #variant_ident {
340                #fields
341                #subcommand_field
342            },
343        }
344    }
345
346    /// Build fields for enum variants from Parameters struct (without pub)
347    fn build_enum_fields_from_parameters(&self, params: &TusksParameters) -> TokenStream {
348        let mut fields = Vec::new();
349
350        for field in &params.pstruct.fields {
351            let field_name = &field.ident;
352
353            if field_name.as_ref().map(|id| is_generated_field(&id.to_string())).unwrap_or(false) {
354                continue;
355            }
356
357            let field_type = Self::dereference_type(&field.ty);
358
359            // Filter and keep #[arg(...)] attributes with original spans
360            let attrs: Vec<_> = field.attrs.iter()
361                .filter(|attr| attr.path().is_ident("arg"))
362                .collect();
363
364            fields.push(quote! {
365                #(#attrs)*
366                #field_name: #field_type,
367            });
368        }
369
370        quote! {
371            #(#fields)*
372        }
373    }
374    
375    
376}