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 /// ```rust
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 /// ```rust
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 /// ```rust
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 /// ```rust
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 /// ```rust
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 /// ```rust
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}