supp_macro/lib.rs
1use proc_macro::TokenStream;
2use quote::quote;
3use syn::Lit;
4use syn::{Data, DeriveInput, Fields, ItemFn, Path, parse_macro_input};
5
6#[proc_macro_attribute]
7pub fn local_db_sqlx_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
8 let input = parse_macro_input!(item as ItemFn);
9 let fn_name = &input.sig.ident;
10 let block = &input.block;
11
12 let expanded = quote! {
13 #[sqlx::test(migrator = "server::db::MIGRATOR")]
14 async fn #fn_name(pool: PgPool) -> Result<(), anyhow::Error> {
15 setup().await;
16 DB_POOL.set(Some(pool.clone()));
17 #block
18 Ok(())
19 }
20 };
21
22 TokenStream::from(expanded)
23}
24
25/// Runs an async test body on ONE runtime shared by the whole test binary,
26/// obtained from `crate::common::runtime()` (as [`local_db_sqlx_test`] expects
27/// `setup()`).
28///
29/// `#[tokio::test]` builds a runtime per test and drops it when the test ends.
30/// `server::db`'s production pool is a process-wide `OnceCell`, so connections
31/// opened inside the first test's runtime are still pooled after that runtime is
32/// gone, and a later acquire fails with "A Tokio 1.x context was found, but it
33/// is being shutdown" — inside whatever test happened to run next, which is why
34/// it reads as a bug in unrelated code.
35#[proc_macro_attribute]
36pub fn shared_runtime_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
37 let input = parse_macro_input!(item as ItemFn);
38 if !input.sig.inputs.is_empty() {
39 return syn::Error::new_spanned(&input.sig, "shared_runtime_test takes no arguments")
40 .to_compile_error()
41 .into();
42 }
43 let fn_name = &input.sig.ident;
44 let output = &input.sig.output;
45 let block = &input.block;
46 let attrs = &input.attrs;
47
48 // The return type is preserved and the value returned: erasing it would let
49 // a test that returns `Err` pass with only an unused-`Result` warning.
50 TokenStream::from(quote! {
51 #[test]
52 #(#attrs)*
53 fn #fn_name() #output {
54 crate::common::runtime().block_on(async #block)
55 }
56 })
57}
58
59#[proc_macro_derive(Builder, attributes(builder))]
60pub fn builder_macro(input: TokenStream) -> TokenStream {
61 // Parse the input tokens into a syntax tree
62 let input = parse_macro_input!(input as DeriveInput);
63 let name = &input.ident;
64 let generics = &input.generics; // Capture generics (including lifetimes)
65 let builder_name = syn::Ident::new(&format!("{name}Builder"), name.span());
66
67 // Check for custom error_kind attribute
68 let mut error_kind = None;
69
70 // Parse attributes
71 for attr in &input.attrs {
72 if attr.path().is_ident("builder") {
73 attr.parse_nested_meta(|meta| {
74 if meta.path.is_ident("error_kind")
75 && let Ok(Lit::Str(lit_str)) = meta.value()?.parse()
76 {
77 error_kind = Some(lit_str.parse::<Path>().unwrap());
78 }
79 Ok(())
80 })
81 .unwrap();
82 }
83 }
84
85 // Set a default error kind if none is provided
86 let error_kind = error_kind.expect(
87 "Error kind (e.g., FinanceError) must be specified with #[builder(error_kind = \"...\")]",
88 );
89
90 // Define a custom error type based on the struct name, e.g., CommodityError for Commodity
91 let custom_error_name = syn::Ident::new(&format!("{name}Error"), name.span());
92
93 let fields = if let Data::Struct(data) = &input.data {
94 if let Fields::Named(fields) = &data.fields {
95 fields.named.iter().collect::<Vec<_>>()
96 } else {
97 panic!("Builder macro only supports structs with named fields");
98 }
99 } else {
100 panic!("Builder macro only supports structs");
101 };
102
103 // Generate builder struct fields with the same generics (including lifetimes)
104 let builder_fields = fields.iter().map(|field| {
105 let field_name = &field.ident;
106 let field_ty = &field.ty;
107 let builder_field_type = quote! { Option<#field_ty> };
108 quote! {
109 #field_name: #builder_field_type
110 }
111 });
112
113 // Generate initialization in new()
114 let builder_fields_init = fields.iter().map(|field| {
115 let field_name = &field.ident;
116 quote! {
117 #field_name: None
118 }
119 });
120
121 // Generate setter methods
122 let setters = fields.iter().map(|field| {
123 let field_name = &field.ident;
124 let field_type = &field.ty;
125
126 if is_option_type(field_type) {
127 let inner_type = get_inner_type(field_type);
128 if is_string_type(&inner_type) {
129 // For Option<String>, accept &str
130 quote! {
131 pub fn #field_name(&mut self, value: &str) -> &mut Self {
132 self.#field_name = Some(Some(value.to_string()));
133 self
134 }
135 }
136 } else {
137 // For Option<T>, accept T directly
138 quote! {
139 pub fn #field_name(&mut self, value: #inner_type) -> &mut Self {
140 self.#field_name = Some(Some(value));
141 self
142 }
143 }
144 }
145 } else if is_string_type(field_type) {
146 // For String, accept &str
147 quote! {
148 pub fn #field_name(&mut self, value: &str) -> &mut Self {
149 self.#field_name = Some(value.to_string());
150 self
151 }
152 }
153 } else {
154 // For non-Option<T> and non-String fields, accept T directly
155 quote! {
156 pub fn #field_name(&mut self, value: #field_type) -> &mut Self {
157 self.#field_name = Some(value);
158 self
159 }
160 }
161 }
162 });
163
164 // Generate code to check for missing required fields
165 let check_required_fields = fields
166 .iter()
167 .filter(|field| !is_option_type(&field.ty))
168 .map(|field| {
169 let field_name = &field.ident;
170 let field_name_str = field_name.as_ref().unwrap().to_string();
171 quote! {
172 if self.#field_name.is_none() {
173 missing_fields.push(#field_name_str);
174 }
175 }
176 });
177
178 // Generate build_fields
179 let build_fields = fields.iter().map(|field| {
180 let field_name = &field.ident;
181 if is_option_type(&field.ty) {
182 quote! {
183 #field_name: self.#field_name.clone().unwrap_or(None)
184 }
185 } else {
186 quote! {
187 #field_name: self.#field_name.clone().unwrap()
188 }
189 }
190 });
191
192 // Extract the lifetime parameters from generics for use in the builder struct
193 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
194
195 let expanded = quote! {
196 pub struct #builder_name #impl_generics #where_clause {
197 #(#builder_fields),*
198 }
199
200 impl #impl_generics #builder_name #ty_generics #where_clause {
201 pub fn new() -> Self {
202 Self {
203 #(#builder_fields_init),*
204 }
205 }
206
207 #(#setters)*
208
209 pub fn build(&self) -> Result<#name #ty_generics, #error_kind> {
210 let mut missing_fields = Vec::new();
211 #(#check_required_fields)*
212
213 if !missing_fields.is_empty() {
214 return Err(#error_kind::from(#custom_error_name::Build(format!(
215 "{} fields are missing: {}",
216 stringify!(#name),
217 missing_fields.join(", ")
218 ))));
219 }
220
221 Ok(#name {
222 #(#build_fields),*
223 })
224 }
225 }
226
227 impl #impl_generics #name #ty_generics #where_clause {
228 pub fn builder() -> #builder_name #ty_generics {
229 #builder_name::new()
230 }
231 }
232 };
233
234 TokenStream::from(expanded)
235}
236
237/// Helper function to determine if a type is an `Option<T>`
238fn is_option_type(ty: &syn::Type) -> bool {
239 matches!(ty, syn::Type::Path(syn::TypePath { path: syn::Path { segments, .. }, .. }) if segments.iter().any(|segment| segment.ident == "Option"))
240}
241
242/// Helper function to get the inner type of an `Option<T>`
243fn get_inner_type(ty: &syn::Type) -> syn::Type {
244 if let syn::Type::Path(type_path) = ty
245 && let Some(segment) = type_path.path.segments.first()
246 && segment.ident == "Option"
247 && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
248 && let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
249 {
250 return inner_type.clone();
251 }
252 ty.clone()
253}
254
255/// Helper function to check if the type is String
256fn is_string_type(ty: &syn::Type) -> bool {
257 if let syn::Type::Path(type_path) = ty
258 && let Some(segment) = type_path.path.segments.last()
259 {
260 return segment.ident == "String";
261 }
262 false
263}
264
265/// A procedural macro for generating typed Command implementations with compile-time validation.
266///
267/// This macro provides pure value-based argument passing with compile-time type safety by generating:
268/// - Typed Args structs with proper field types passed by value only
269/// - Commands that accept Args structs directly (no `HashMap` usage)
270/// - Individual typed variables available directly in command scope
271/// - Compile-time validation of argument types and required/optional fields
272/// - Zero runtime argument parsing or validation overhead
273///
274/// # Syntax
275///
276/// ```ignore
277/// command! {
278/// CommandName {
279/// #[required]
280/// arg_name: Type,
281/// #[optional]
282/// opt_name: Type,
283/// } => {
284/// // Command implementation body
285/// // Individual typed variables are available in scope
286/// }
287/// }
288/// ```
289///
290/// # Generated Code
291///
292/// The macro generates:
293/// - A `CommandNameArgs` struct with typed fields (required fields as `Type`, optional as `Option<Type>`)
294/// - A `CommandName` struct implementing `Command` trait with typed `run(args: CommandNameArgs)` method
295/// - Individual typed variables extracted from the Args struct and available in the command body
296/// - Pure compile-time type validation with no runtime overhead
297///
298/// # Examples
299///
300/// ## Simple command with no arguments
301///
302/// ```rust
303/// # use supp_macro::command;
304/// # use async_trait::async_trait;
305/// #
306/// # #[derive(Debug)]
307/// # pub enum CmdError {
308/// # Args(String),
309/// # }
310/// #
311/// # impl std::fmt::Display for CmdError {
312/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313/// # match self {
314/// # CmdError::Args(msg) => write!(f, "Argument error: {}", msg),
315/// # }
316/// # }
317/// # }
318/// #
319/// # impl std::error::Error for CmdError {}
320/// #
321/// # #[derive(Debug)]
322/// # pub enum CmdResult {
323/// # String(String),
324/// # }
325/// #
326/// # #[derive(Debug, Default)]
327/// # pub struct CommandArgs {}
328/// # impl CommandArgs { pub fn new() -> Self { Self::default() } }
329/// #
330/// # #[async_trait]
331/// # pub trait Command: std::fmt::Debug {
332/// # type Args;
333/// # async fn run(&self, args: Self::Args) -> Result<Option<CmdResult>, CmdError>;
334/// # }
335///
336/// command! {
337/// GetVersion {
338/// } => {
339/// Ok(Some(CmdResult::String("1.0.0".to_string())))
340/// }
341/// }
342///
343/// # #[tokio::main]
344/// # async fn main() {
345/// let result = GetVersion::new().run().await.unwrap();
346/// # }
347/// ```
348///
349/// ## Command with required arguments (server-compatible types)
350///
351/// ```rust
352/// # use supp_macro::command;
353/// # use async_trait::async_trait;
354/// # use uuid::Uuid;
355/// # use num_rational::Rational64;
356/// #
357/// # #[derive(Debug, Clone)]
358/// # pub enum Argument {
359/// # String(String),
360/// # Uuid(Uuid),
361/// # Rational(Rational64),
362/// # }
363/// #
364/// # #[derive(Debug)]
365/// # pub enum CmdError {
366/// # Args(String),
367/// # }
368/// #
369/// # impl std::fmt::Display for CmdError {
370/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371/// # match self {
372/// # CmdError::Args(msg) => write!(f, "Argument error: {}", msg),
373/// # }
374/// # }
375/// # }
376/// #
377/// # impl std::error::Error for CmdError {}
378/// #
379/// # #[derive(Debug)]
380/// # pub enum CmdResult {
381/// # String(String),
382/// # }
383/// #
384/// # #[derive(Debug, Default)]
385/// # pub struct CommandArgs {
386/// # pub symbol: Option<String>,
387/// # pub name: Option<String>,
388/// # pub user_id: Option<uuid::Uuid>,
389/// # }
390/// # impl CommandArgs {
391/// # pub fn new() -> Self { Self::default() }
392/// # pub fn symbol(mut self, v: String) -> Self { self.symbol = Some(v); self }
393/// # pub fn name(mut self, v: String) -> Self { self.name = Some(v); self }
394/// # pub fn user_id(mut self, v: uuid::Uuid) -> Self { self.user_id = Some(v); self }
395/// # }
396/// #
397/// # #[async_trait]
398/// # pub trait Command: std::fmt::Debug {
399/// # type Args;
400/// # async fn run(&self, args: Self::Args) -> Result<Option<CmdResult>, CmdError>;
401/// # }
402///
403/// // This creates a commodity in the financial system
404/// command! {
405/// CreateCommodity {
406/// #[required]
407/// symbol: String,
408/// #[required]
409/// name: String,
410/// #[required]
411/// user_id: Uuid,
412/// } => {
413/// // Individual typed variables are automatically available
414/// Ok(Some(CmdResult::String(format!(
415/// "Created commodity {} ({}) for user {}",
416/// name, symbol, user_id
417/// ))))
418/// }
419/// }
420///
421/// # #[tokio::main]
422/// # async fn main() {
423/// let result = CreateCommodity::new()
424/// .symbol("USD".to_string())
425/// .name("US Dollar".to_string())
426/// .user_id(uuid::Uuid::new_v4())
427/// .run()
428/// .await
429/// .unwrap();
430/// # }
431/// ```
432///
433/// ## Command with optional arguments
434///
435/// ```rust
436/// # use supp_macro::command;
437/// # use async_trait::async_trait;
438/// # use uuid::Uuid;
439/// #
440/// # #[derive(Debug, Clone)]
441/// # pub enum Argument {
442/// # String(String),
443/// # Uuid(Uuid),
444/// # }
445/// #
446/// # #[derive(Debug)]
447/// # pub enum CmdError {
448/// # Args(String),
449/// # }
450/// #
451/// # impl std::fmt::Display for CmdError {
452/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
453/// # match self {
454/// # CmdError::Args(msg) => write!(f, "Argument error: {}", msg),
455/// # }
456/// # }
457/// # }
458/// #
459/// # impl std::error::Error for CmdError {}
460/// #
461/// # #[derive(Debug)]
462/// # pub enum CmdResult {
463/// # String(String),
464/// # }
465/// #
466/// # #[derive(Debug, Default)]
467/// # pub struct CommandArgs {
468/// # pub user_id: Option<uuid::Uuid>,
469/// # pub account: Option<String>,
470/// # }
471/// # impl CommandArgs {
472/// # pub fn new() -> Self { Self::default() }
473/// # pub fn user_id(mut self, v: uuid::Uuid) -> Self { self.user_id = Some(v); self }
474/// # pub fn account(mut self, v: String) -> Self { self.account = Some(v); self }
475/// # }
476/// #
477/// # #[async_trait]
478/// # pub trait Command: std::fmt::Debug {
479/// # type Args;
480/// # async fn run(&self, args: Self::Args) -> Result<Option<CmdResult>, CmdError>;
481/// # }
482///
483/// command! {
484/// ListTransactions {
485/// #[required]
486/// user_id: Uuid,
487/// #[optional]
488/// account: String,
489/// } => {
490/// let filter = if let Some(account) = account {
491/// format!(" for account {}", account)
492/// } else {
493/// String::new()
494/// };
495/// Ok(Some(CmdResult::String(format!("Listing transactions for user {}{}", user_id, filter))))
496/// }
497/// }
498/// ```
499///
500/// ## Command with mixed required and optional arguments
501///
502/// ```rust
503/// # use supp_macro::command;
504/// # use async_trait::async_trait;
505/// #
506/// # #[derive(Debug, Clone)]
507/// # pub enum Argument {
508/// # String(String),
509/// # Integer(i64),
510/// # Boolean(bool),
511/// # }
512/// #
513/// # #[derive(Debug)]
514/// # pub enum CmdError {
515/// # Args(String),
516/// # }
517/// #
518/// # impl std::fmt::Display for CmdError {
519/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520/// # match self {
521/// # CmdError::Args(msg) => write!(f, "Argument error: {}", msg),
522/// # }
523/// # }
524/// # }
525/// #
526/// # impl std::error::Error for CmdError {}
527/// #
528/// # #[derive(Debug)]
529/// # pub enum CmdResult {
530/// # Success(String),
531/// # }
532/// #
533/// # impl TryFrom<Argument> for String {
534/// # type Error = CmdError;
535/// # fn try_from(arg: Argument) -> Result<Self, Self::Error> {
536/// # match arg {
537/// # Argument::String(s) => Ok(s),
538/// # _ => Err(CmdError::Args(format!("Cannot convert {:?} to String", arg))),
539/// # }
540/// # }
541/// # }
542/// #
543/// # impl TryFrom<Argument> for i64 {
544/// # type Error = CmdError;
545/// # fn try_from(arg: Argument) -> Result<Self, Self::Error> {
546/// # match arg {
547/// # Argument::Integer(i) => Ok(i),
548/// # _ => Err(CmdError::Args(format!("Cannot convert {:?} to i64", arg))),
549/// # }
550/// # }
551/// # }
552/// #
553/// # impl TryFrom<Argument> for bool {
554/// # type Error = CmdError;
555/// # fn try_from(arg: Argument) -> Result<Self, Self::Error> {
556/// # match arg {
557/// # Argument::Boolean(b) => Ok(b),
558/// # _ => Err(CmdError::Args(format!("Cannot convert {:?} to bool", arg))),
559/// # }
560/// # }
561/// # }
562/// #
563/// # #[async_trait]
564/// # pub trait TypedCommand {
565/// # type Args;
566/// # async fn run_typed(&self, args: Self::Args) -> Result<Option<CmdResult>, CmdError>;
567/// # }
568/// #
569/// # #[derive(Debug, Default)]
570/// # pub struct CommandArgs {
571/// # pub user_id: Option<i64>,
572/// # pub username: Option<String>,
573/// # pub email: Option<String>,
574/// # pub is_admin: Option<bool>,
575/// # }
576/// # impl CommandArgs {
577/// # pub fn new() -> Self { Self::default() }
578/// # pub fn user_id(mut self, v: i64) -> Self { self.user_id = Some(v); self }
579/// # pub fn username(mut self, v: String) -> Self { self.username = Some(v); self }
580/// # pub fn email(mut self, v: String) -> Self { self.email = Some(v); self }
581/// # pub fn is_admin(mut self, v: bool) -> Self { self.is_admin = Some(v); self }
582/// # }
583/// #
584/// # #[async_trait]
585/// # pub trait Command {
586/// # type Args;
587/// # async fn run(&self, args: Self::Args) -> Result<Option<CmdResult>, CmdError>;
588/// # }
589///
590/// command! {
591/// CreateUserCommand {
592/// #[required]
593/// user_id: i64,
594/// #[required]
595/// username: String,
596/// #[optional]
597/// email: String,
598/// #[optional]
599/// is_admin: bool,
600/// } => {
601/// let email_str = email.map_or_else(|| format!("{}@example.com", username), |s| s.to_string());
602/// let admin_status = is_admin.unwrap_or(false);
603///
604/// let message = format!(
605/// "Created user {} (ID: {}, Email: {}, Admin: {})",
606/// username, user_id, email_str, admin_status
607/// );
608/// Ok(Some(CmdResult::Success(message)))
609/// }
610/// }
611///
612/// # #[tokio::main]
613/// # async fn main() {
614/// let result = CreateUserCommand::new()
615/// .user_id(123)
616/// .username("alice".to_string())
617/// .is_admin(true)
618/// .run()
619/// .await
620/// .unwrap();
621/// # }
622/// ```
623///
624/// ## Server-compatible Command implementation
625///
626/// ```rust
627/// # use supp_macro::command;
628/// # use async_trait::async_trait;
629/// #
630/// # #[derive(Debug, Clone)]
631/// # pub enum Argument {
632/// # String(String),
633/// # Integer(i64),
634/// # Boolean(bool),
635/// # }
636/// #
637/// # #[derive(Debug)]
638/// # pub enum CmdError {
639/// # Args(String),
640/// # }
641/// #
642/// # impl std::fmt::Display for CmdError {
643/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
644/// # match self {
645/// # CmdError::Args(msg) => write!(f, "Argument error: {}", msg),
646/// # }
647/// # }
648/// # }
649/// #
650/// # impl std::error::Error for CmdError {}
651/// #
652/// # #[derive(Debug)]
653/// # pub enum CmdResult {
654/// # Success(String),
655/// # }
656/// #
657/// # #[derive(Debug, Default)]
658/// # pub struct CommandArgs {
659/// # pub a: Option<i64>,
660/// # pub b: Option<i64>,
661/// # }
662/// # impl CommandArgs {
663/// # pub fn new() -> Self { Self::default() }
664/// # pub fn a(mut self, v: i64) -> Self { self.a = Some(v); self }
665/// # pub fn b(mut self, v: i64) -> Self { self.b = Some(v); self }
666/// # }
667/// #
668/// # #[async_trait]
669/// # pub trait Command {
670/// # type Args;
671/// # async fn run(&self, args: Self::Args) -> Result<Option<CmdResult>, CmdError>;
672/// # }
673///
674/// command! {
675/// CalculateCommand {
676/// #[required]
677/// a: i64,
678/// #[required]
679/// b: i64,
680/// } => {
681/// let result = a + b;
682/// Ok(Some(CmdResult::Success(format!("{} + {} = {}", a, b, result))))
683/// }
684/// }
685///
686/// # #[tokio::main]
687/// # async fn main() {
688/// let result = CalculateCommand::new()
689/// .a(10)
690/// .b(20)
691/// .run()
692/// .await
693/// .unwrap();
694/// # }
695/// ```
696///
697/// ## Migration from Manual Commands
698///
699/// The macro makes it easy to migrate from manual Command implementations:
700///
701/// ```rust,ignore
702/// // BEFORE: Manual implementation
703/// #[derive(Debug)]
704/// pub struct GetConfig;
705///
706/// #[async_trait]
707/// impl Command for GetConfig {
708/// async fn run<'a>(&self, args: &'a HashMap<&'a str, &'a Argument>) -> Result<Option<CmdResult>, CmdError> {
709/// if let Some(Argument::String(name)) = args.get("name") {
710/// Ok(config(name).await?.map(|v| CmdResult::String(v)))
711/// } else {
712/// Err(CmdError::Args("No field name provided".to_string()))
713/// }
714/// }
715/// }
716///
717/// // AFTER: Using the macro
718/// command! {
719/// GetConfig {
720/// #[required]
721/// name: String,
722/// } => {
723/// Ok(config(name).await?.map(|v| CmdResult::String(v)))
724/// }
725/// }
726/// ```
727///
728/// # Error Handling
729///
730/// The new pure typed system provides compile-time error prevention:
731///
732/// - Missing required arguments are compile-time errors (cannot compile without them)
733/// - Invalid argument types are compile-time errors (type checking at build time)
734/// - Runtime errors only occur in the command body logic itself
735/// - No argument validation overhead at runtime
736///
737/// # Supported Argument Types
738///
739/// The macro supports any Rust type for arguments:
740/// - `String` - Text arguments
741/// - `i64`, `u64`, etc. - Integer arguments
742/// - `bool` - Boolean arguments
743/// - `Rational64` - Rational number arguments (for financial precision)
744/// - `Uuid` - UUID arguments
745/// - `Vec<u8>` - Binary data arguments
746/// - `DateTime<Utc>` - `DateTime` arguments
747/// - Custom types - Any type can be used as an argument
748/// - `Option<T>` - Automatically applied for optional arguments
749#[proc_macro]
750pub fn command(input: TokenStream) -> TokenStream {
751 let input = parse_macro_input!(input as CommandInput);
752
753 let name = &input.name;
754 let required_args = &input.required_args;
755 let optional_args = &input.optional_args;
756 let body = &input.body;
757
758 // Generate progressive runner types for all combinations of required fields
759 let runner_types = generate_progressive_runner_types(name, required_args, optional_args, body);
760
761 // Generate the main command struct
762 let command_struct = quote! {
763 #[derive(Debug)]
764 pub struct #name;
765 };
766
767 // Generate the new() method that starts the builder chain
768 let new_method = generate_new_method(name, required_args.len(), optional_args);
769
770 let expanded = quote! {
771 #command_struct
772
773 #runner_types
774
775 #new_method
776 };
777
778 TokenStream::from(expanded)
779}
780
781/// Generate all possible runner type combinations for required fields
782fn generate_progressive_runner_types(
783 command_name: &syn::Ident,
784 required_args: &[(syn::Ident, syn::Type)],
785 optional_args: &[(syn::Ident, syn::Type)],
786 body: &syn::Block,
787) -> proc_macro2::TokenStream {
788 let num_required = required_args.len();
789 let total_combinations = 1 << num_required; // 2^num_required
790
791 let mut runner_types = Vec::new();
792
793 // Generate a runner type for each possible combination of set required fields
794 for combination in 0..total_combinations {
795 let runner_type = generate_single_runner_type(
796 command_name,
797 required_args,
798 optional_args,
799 combination,
800 num_required,
801 body,
802 );
803 runner_types.push(runner_type);
804 }
805
806 quote! {
807 #(#runner_types)*
808 }
809}
810
811/// Generate a single runner type for a specific combination of set fields
812fn generate_single_runner_type(
813 command_name: &syn::Ident,
814 required_args: &[(syn::Ident, syn::Type)],
815 optional_args: &[(syn::Ident, syn::Type)],
816 combination: usize,
817 num_required: usize,
818 body: &syn::Block,
819) -> proc_macro2::TokenStream {
820 // Create binary representation for the runner type name
821 let binary_suffix = format!("{:0width$b}", combination, width = num_required.max(1));
822 let runner_name = syn::Ident::new(
823 &format!("{command_name}Runner{binary_suffix}"),
824 command_name.span(),
825 );
826
827 // Determine which required fields are set in this combination
828 let mut struct_fields = Vec::new();
829 for (i, (field_name, field_type)) in required_args.iter().enumerate() {
830 if (combination >> i) & 1 == 1 {
831 // This required field is set in this combination
832 struct_fields.push(quote! {
833 pub #field_name: #field_type
834 });
835 }
836 }
837
838 // Always include optional fields in all runner types
839 for (field_name, field_type) in optional_args {
840 struct_fields.push(quote! {
841 pub #field_name: Option<#field_type>
842 });
843 }
844
845 // Generate the struct definition
846 let struct_def = if struct_fields.is_empty() {
847 quote! {
848 #[derive(Debug)]
849 pub struct #runner_name;
850 }
851 } else {
852 quote! {
853 #[derive(Debug)]
854 pub struct #runner_name {
855 #(#struct_fields),*
856 }
857 }
858 };
859
860 // Generate transition methods for this runner type
861 let transition_methods = generate_transition_methods(
862 command_name,
863 required_args,
864 optional_args,
865 combination,
866 num_required,
867 );
868
869 // Generate run method if this is the complete state (all required fields set)
870 let complete_mask = (1 << num_required) - 1;
871 let run_method = if combination == complete_mask {
872 generate_run_method(command_name, required_args, optional_args, body)
873 } else {
874 quote! {}
875 };
876
877 quote! {
878 #struct_def
879
880 impl #runner_name {
881 #transition_methods
882 #run_method
883 }
884 }
885}
886
887/// Generate transition methods for a runner type (field setters)
888fn generate_transition_methods(
889 command_name: &syn::Ident,
890 required_args: &[(syn::Ident, syn::Type)],
891 optional_args: &[(syn::Ident, syn::Type)],
892 current_combination: usize,
893 num_required: usize,
894) -> proc_macro2::TokenStream {
895 let mut methods = Vec::new();
896
897 // Generate setter methods for required fields not yet set
898 for (i, (field_name, field_type)) in required_args.iter().enumerate() {
899 if (current_combination >> i) & 1 == 0 {
900 // This required field is not set yet, generate a setter
901 let new_combination = current_combination | (1 << i);
902 let binary_suffix =
903 format!("{:0width$b}", new_combination, width = num_required.max(1));
904 let target_runner = syn::Ident::new(
905 &format!("{command_name}Runner{binary_suffix}"),
906 command_name.span(),
907 );
908
909 let method = generate_field_setter_method(
910 required_args,
911 optional_args,
912 field_name,
913 field_type,
914 current_combination,
915 &target_runner,
916 );
917 methods.push(method);
918 }
919 }
920
921 // Generate setter methods for optional fields (available on all runner types)
922 for (field_name, field_type) in optional_args {
923 let current_runner = syn::Ident::new(
924 &format!(
925 "{}Runner{:0width$b}",
926 command_name,
927 current_combination,
928 width = num_required.max(1)
929 ),
930 command_name.span(),
931 );
932
933 let method = generate_optional_field_setter(
934 field_name,
935 field_type,
936 ¤t_runner,
937 required_args,
938 optional_args,
939 current_combination,
940 num_required,
941 );
942 methods.push(method);
943 }
944
945 quote! {
946 #(#methods)*
947 }
948}
949
950/// Generate a setter method for a required field
951fn generate_field_setter_method(
952 required_args: &[(syn::Ident, syn::Type)],
953 optional_args: &[(syn::Ident, syn::Type)],
954 field_name: &syn::Ident,
955 field_type: &syn::Type,
956 current_combination: usize,
957 target_runner: &syn::Ident,
958) -> proc_macro2::TokenStream {
959 // Generate field assignments for the new state
960 let mut field_assignments = Vec::new();
961
962 // Handle required fields
963 for (i, (req_field_name, _)) in required_args.iter().enumerate() {
964 if req_field_name == field_name {
965 // This is the field being set
966 field_assignments.push(quote! {
967 #req_field_name: value
968 });
969 } else if (current_combination >> i) & 1 == 1 {
970 // This field was already set, move it from self
971 field_assignments.push(quote! {
972 #req_field_name: self.#req_field_name
973 });
974 }
975 // Fields not set in either state are omitted
976 }
977
978 // Handle optional fields (always present, move from self)
979 for (opt_field_name, _) in optional_args {
980 field_assignments.push(quote! {
981 #opt_field_name: self.#opt_field_name
982 });
983 }
984
985 // Generate the constructor call
986 let constructor = if field_assignments.is_empty() {
987 quote! { #target_runner }
988 } else {
989 quote! {
990 #target_runner {
991 #(#field_assignments),*
992 }
993 }
994 };
995
996 quote! {
997 pub fn #field_name(self, value: #field_type) -> #target_runner {
998 #constructor
999 }
1000 }
1001}
1002
1003/// Generate a setter method for an optional field
1004fn generate_optional_field_setter(
1005 field_name: &syn::Ident,
1006 field_type: &syn::Type,
1007 current_runner: &syn::Ident,
1008 required_args: &[(syn::Ident, syn::Type)],
1009 optional_args: &[(syn::Ident, syn::Type)],
1010 current_combination: usize,
1011 _num_required: usize,
1012) -> proc_macro2::TokenStream {
1013 // Generate field assignments (same state, but update the optional field)
1014 let mut field_assignments = Vec::new();
1015
1016 // Handle required fields (move from self if set)
1017 for (i, (req_field_name, _)) in required_args.iter().enumerate() {
1018 if (current_combination >> i) & 1 == 1 {
1019 field_assignments.push(quote! {
1020 #req_field_name: self.#req_field_name
1021 });
1022 }
1023 }
1024
1025 // Handle optional fields
1026 for (opt_field_name, _) in optional_args {
1027 if opt_field_name == field_name {
1028 // This is the field being set
1029 field_assignments.push(quote! {
1030 #opt_field_name: Some(value)
1031 });
1032 } else {
1033 // Move other optional fields from self
1034 field_assignments.push(quote! {
1035 #opt_field_name: self.#opt_field_name
1036 });
1037 }
1038 }
1039
1040 let constructor = if field_assignments.is_empty() {
1041 quote! { #current_runner }
1042 } else {
1043 quote! {
1044 #current_runner {
1045 #(#field_assignments),*
1046 }
1047 }
1048 };
1049
1050 quote! {
1051 pub fn #field_name(self, value: #field_type) -> #current_runner {
1052 #constructor
1053 }
1054 }
1055}
1056
1057/// Generate the run method for the complete runner state
1058fn generate_run_method(
1059 _command_name: &syn::Ident,
1060 required_args: &[(syn::Ident, syn::Type)],
1061 optional_args: &[(syn::Ident, syn::Type)],
1062 body: &syn::Block,
1063) -> proc_macro2::TokenStream {
1064 // Extract field values directly (no unwrap needed!)
1065 let mut variable_assignments = Vec::new();
1066
1067 // Required fields - direct field access
1068 for (field_name, _) in required_args {
1069 variable_assignments.push(quote! {
1070 let #field_name = self.#field_name;
1071 });
1072 }
1073
1074 // Optional fields - direct field access
1075 for (field_name, _) in optional_args {
1076 variable_assignments.push(quote! {
1077 let #field_name = self.#field_name;
1078 });
1079 }
1080
1081 quote! {
1082 pub async fn run(self) -> Result<Option<CmdResult>, CmdError> {
1083 // Zero runtime checks - direct field access!
1084 #(#variable_assignments)*
1085
1086 // Original command body
1087 #body
1088 }
1089 }
1090}
1091
1092/// Generate the `new()` method for the command
1093fn generate_new_method(
1094 command_name: &syn::Ident,
1095 num_required: usize,
1096 optional_args: &[(syn::Ident, syn::Type)],
1097) -> proc_macro2::TokenStream {
1098 let initial_runner = syn::Ident::new(
1099 &format!(
1100 "{}Runner{:0width$b}",
1101 command_name,
1102 0,
1103 width = num_required.max(1)
1104 ),
1105 command_name.span(),
1106 );
1107
1108 // Initial state has no required fields set, but has optional fields as None
1109 let constructor = if optional_args.is_empty() && num_required > 0 {
1110 // Unit struct (no fields at all in initial state)
1111 quote! { #initial_runner }
1112 } else {
1113 // Struct with optional fields initialized to None
1114 let optional_field_inits = optional_args.iter().map(|(field_name, _)| {
1115 quote! { #field_name: None }
1116 });
1117
1118 if optional_field_inits.len() > 0 {
1119 quote! {
1120 #initial_runner {
1121 #(#optional_field_inits),*
1122 }
1123 }
1124 } else {
1125 quote! { #initial_runner }
1126 }
1127 };
1128
1129 quote! {
1130 impl #command_name {
1131 pub fn new() -> #initial_runner {
1132 #constructor
1133 }
1134 }
1135 }
1136}
1137
1138struct CommandInput {
1139 name: syn::Ident,
1140 required_args: Vec<(syn::Ident, syn::Type)>,
1141 optional_args: Vec<(syn::Ident, syn::Type)>,
1142 body: syn::Block,
1143}
1144
1145impl syn::parse::Parse for CommandInput {
1146 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
1147 let name: syn::Ident = input.parse()?;
1148
1149 let content;
1150 syn::braced!(content in input);
1151
1152 let mut required_args = Vec::new();
1153 let mut optional_args = Vec::new();
1154
1155 while !content.is_empty() {
1156 // Parse attributes
1157 let mut is_optional = false;
1158 let mut is_required = false;
1159
1160 while content.peek(syn::Token![#]) {
1161 content.parse::<syn::Token![#]>()?;
1162 let attr_content;
1163 syn::bracketed!(attr_content in content);
1164 let attr_name: syn::Ident = attr_content.parse()?;
1165
1166 if attr_name == "optional" {
1167 is_optional = true;
1168 } else if attr_name == "required" {
1169 is_required = true;
1170 } else {
1171 return Err(syn::Error::new(
1172 attr_name.span(),
1173 "Unknown attribute. Use #[required] or #[optional]",
1174 ));
1175 }
1176 }
1177
1178 // Parse the field
1179 let arg_name: syn::Ident = content.parse()?;
1180 content.parse::<syn::Token![:]>()?;
1181 let arg_type: syn::Type = content.parse()?;
1182
1183 if content.peek(syn::Token![,]) {
1184 content.parse::<syn::Token![,]>()?;
1185 }
1186
1187 // Determine if optional (default to required if no attribute specified)
1188 let is_optional_field = if is_required && is_optional {
1189 return Err(syn::Error::new(
1190 arg_name.span(),
1191 "Field cannot be both #[required] and #[optional]",
1192 ));
1193 } else if is_optional {
1194 true
1195 } else {
1196 false // Default to required
1197 };
1198
1199 if is_optional_field {
1200 optional_args.push((arg_name, arg_type));
1201 } else {
1202 required_args.push((arg_name, arg_type));
1203 }
1204 }
1205
1206 // The '=>' is outside the braces
1207 input.parse::<syn::Token![=>]>()?;
1208 let body: syn::Block = input.parse()?;
1209
1210 Ok(CommandInput {
1211 name,
1212 required_args,
1213 optional_args,
1214 body,
1215 })
1216 }
1217}