Skip to main content

tui_dispatch_macros/
lib.rs

1//! Procedural macros for tui-dispatch
2
3use darling::{FromDeriveInput, FromField, FromVariant};
4use proc_macro::TokenStream;
5use proc_macro2::Ident;
6use quote::{format_ident, quote};
7use std::collections::HashMap;
8use syn::{parse_macro_input, DeriveInput};
9
10/// Container-level attributes for #[derive(Action)]
11#[derive(Debug, FromDeriveInput)]
12#[darling(attributes(action), supports(enum_any))]
13struct ActionOpts {
14    ident: syn::Ident,
15    data: darling::ast::Data<ActionVariant, ()>,
16
17    /// Enable automatic category inference from variant name prefixes
18    #[darling(default)]
19    infer_categories: bool,
20
21    /// Generate dispatcher trait
22    #[darling(default)]
23    generate_dispatcher: bool,
24}
25
26/// Variant-level attributes
27#[derive(Debug, FromVariant)]
28#[darling(attributes(action))]
29struct ActionVariant {
30    ident: syn::Ident,
31    fields: darling::ast::Fields<()>,
32
33    /// Explicit category override
34    #[darling(default)]
35    category: Option<String>,
36
37    /// Exclude from category inference
38    #[darling(default)]
39    skip_category: bool,
40}
41
42/// Common action verbs that typically appear as the last part of a variant name
43// Action verbs that typically END an action name (the actual verb part)
44// Things like "Form", "Panel", "Field" are nouns, not verbs - they should NOT be here
45const ACTION_VERBS: &[&str] = &[
46    // State transitions
47    "Start", "End", "Open", "Close", "Submit", "Confirm", "Cancel", // Navigation
48    "Next", "Prev", "Up", "Down", "Left", "Right", "Enter", "Exit", "Escape",
49    // CRUD operations
50    "Add", "Remove", "Clear", "Update", "Set", "Get", "Load", "Save", "Delete", "Create",
51    // Data operations
52    "Fetch", "Change", "Resize", // Async results
53    "Error",  // Visibility
54    "Show", "Hide", "Enable", "Disable", "Toggle", // Focus
55    "Focus", "Blur", "Select", // Movement
56    "Move", "Copy", "Cycle", "Reset", "Scroll",
57];
58
59/// Split a PascalCase string into parts
60fn split_pascal_case(s: &str) -> Vec<String> {
61    let mut parts = Vec::new();
62    let mut current = String::new();
63
64    for ch in s.chars() {
65        if ch.is_uppercase() && !current.is_empty() {
66            parts.push(current);
67            current = String::new();
68        }
69        current.push(ch);
70    }
71    if !current.is_empty() {
72        parts.push(current);
73    }
74    parts
75}
76
77/// Convert PascalCase to snake_case
78fn to_snake_case(s: &str) -> String {
79    let mut result = String::new();
80    for (i, ch) in s.chars().enumerate() {
81        if ch.is_uppercase() {
82            if i > 0 {
83                result.push('_');
84            }
85            result.push(ch.to_lowercase().next().unwrap());
86        } else {
87            result.push(ch);
88        }
89    }
90    result
91}
92
93/// Convert snake_case to PascalCase
94fn to_pascal_case(s: &str) -> String {
95    s.split('_')
96        .map(|part| {
97            let mut chars = part.chars();
98            match chars.next() {
99                None => String::new(),
100                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
101            }
102        })
103        .collect()
104}
105
106/// Infer category from a variant name using naming patterns
107fn infer_category(name: &str) -> Option<String> {
108    let parts = split_pascal_case(name);
109    if parts.is_empty() {
110        return None;
111    }
112
113    // Check for "Did" prefix (async results)
114    if parts[0] == "Did" {
115        return Some("async_result".to_string());
116    }
117
118    // If only one part, no category
119    if parts.len() < 2 {
120        return None;
121    }
122
123    // Find the longest prefix that ends before an action verb
124    // e.g., ["Connection", "Form", "Submit"] -> "connection_form"
125    // e.g., ["Search", "Add", "Char"] -> "search"
126    // e.g., ["Value", "Viewer", "Scroll", "Up"] -> "value_viewer"
127
128    let first_is_verb = ACTION_VERBS.contains(&parts[0].as_str());
129
130    let mut prefix_end = parts.len();
131    let mut found_verb = false;
132    for (i, part) in parts.iter().enumerate().skip(1) {
133        // "Did" marks the boundary between domain prefix and async result verb
134        // e.g., WeatherDidLoad → ["Weather", "Did", "Load"] → category "weather"
135        if part == "Did" || ACTION_VERBS.contains(&part.as_str()) {
136            prefix_end = i;
137            found_verb = true;
138            break;
139        }
140    }
141
142    // Skip if first part is an action verb - these are primary actions, not categorized
143    // e.g., "OpenConnectionForm" → "Open" is the verb, "ConnectionForm" is the object
144    // e.g., "NextItem" → "Next" is the verb, "Item" is the object
145    if first_is_verb {
146        return None;
147    }
148
149    // Skip if no verb found in the name - can't determine meaningful category
150    if !found_verb {
151        return None;
152    }
153
154    if prefix_end == 0 {
155        return None;
156    }
157
158    let prefix_parts: Vec<&str> = parts[..prefix_end].iter().map(|s| s.as_str()).collect();
159    let prefix = prefix_parts.join("");
160
161    Some(to_snake_case(&prefix))
162}
163
164/// Derive macro for the Action trait
165///
166/// Generates a `name()` method that returns the variant name as a static string.
167///
168/// With `#[action(infer_categories)]`, also generates:
169/// - `category() -> Option<&'static str>` - Get action's category
170/// - `category_enum() -> {Name}Category` - Get category as enum
171/// - `is_{category}()` predicates for each category
172/// - `{Name}Category` enum with all discovered categories
173///
174/// With `#[action(generate_dispatcher)]`, also generates:
175/// - `{Name}Dispatcher` trait with category-based dispatch methods
176///
177/// # Example
178/// ```ignore
179/// #[derive(Action, Clone, Debug)]
180/// #[action(infer_categories, generate_dispatcher)]
181/// enum MyAction {
182///     SearchStart,
183///     SearchClear,
184///     ConnectionFormOpen,
185///     ConnectionFormSubmit,
186///     DidConnect,
187///     Tick,  // uncategorized
188/// }
189///
190/// let action = MyAction::SearchStart;
191/// assert_eq!(action.name(), "SearchStart");
192/// assert_eq!(action.category(), Some("search"));
193/// assert!(action.is_search());
194/// ```
195#[proc_macro_derive(Action, attributes(action))]
196pub fn derive_action(input: TokenStream) -> TokenStream {
197    let input = parse_macro_input!(input as DeriveInput);
198
199    // Try to parse with darling for attributes
200    let opts = match ActionOpts::from_derive_input(&input) {
201        Ok(opts) => opts,
202        Err(e) => return e.write_errors().into(),
203    };
204
205    let name = &opts.ident;
206
207    let variants = match &opts.data {
208        darling::ast::Data::Enum(variants) => variants,
209        _ => {
210            return syn::Error::new_spanned(&input, "Action can only be derived for enums")
211                .to_compile_error()
212                .into();
213        }
214    };
215
216    // Get the original syn variants for field info (darling loses field names)
217    let syn_variants = match &input.data {
218        syn::Data::Enum(data) => &data.variants,
219        _ => unreachable!(), // Already checked above
220    };
221
222    // Generate basic name() implementation
223    let name_arms = variants.iter().map(|v| {
224        let variant_name = &v.ident;
225        let variant_str = variant_name.to_string();
226
227        match &v.fields.style {
228            darling::ast::Style::Unit => quote! {
229                #name::#variant_name => #variant_str
230            },
231            darling::ast::Style::Tuple => quote! {
232                #name::#variant_name(..) => #variant_str
233            },
234            darling::ast::Style::Struct => quote! {
235                #name::#variant_name { .. } => #variant_str
236            },
237        }
238    });
239
240    // Generate params() implementation - outputs field values without variant name
241    let params_arms = syn_variants.iter().map(|v| {
242        let variant_name = &v.ident;
243
244        match &v.fields {
245            syn::Fields::Unit => quote! {
246                #name::#variant_name => ::std::string::String::new()
247            },
248            syn::Fields::Unnamed(fields) => {
249                let field_count = fields.unnamed.len();
250                let field_names: Vec<_> =
251                    (0..field_count).map(|i| format_ident!("_{}", i)).collect();
252                if field_count == 1 {
253                    quote! {
254                        #name::#variant_name(#(#field_names),*) => {
255                            tui_dispatch::debug::debug_string(&#(#field_names),*)
256                        }
257                    }
258                } else {
259                    let parts = field_names.iter().map(|field| {
260                        quote! { tui_dispatch::debug::debug_string(&#field) }
261                    });
262                    quote! {
263                        #name::#variant_name(#(#field_names),*) => {
264                            let values = ::std::vec![#(#parts),*];
265                            format!("({})", values.join(", "))
266                        }
267                    }
268                }
269            }
270            syn::Fields::Named(fields) => {
271                let field_names: Vec<_> = fields
272                    .named
273                    .iter()
274                    .filter_map(|f| f.ident.as_ref())
275                    .collect();
276                if field_names.is_empty() {
277                    quote! {
278                        #name::#variant_name { .. } => ::std::string::String::new()
279                    }
280                } else {
281                    let parts = field_names.iter().map(|field| {
282                        let label = field.to_string();
283                        quote! {
284                            format!("{}: {}", #label, tui_dispatch::debug::debug_string(&#field))
285                        }
286                    });
287                    quote! {
288                        #name::#variant_name { #(#field_names),*, .. } => {
289                            let values = ::std::vec![#(#parts),*];
290                            format!("{{{}}}", values.join(", "))
291                        }
292                    }
293                }
294            }
295        }
296    });
297
298    let params_pretty_arms = syn_variants.iter().map(|v| {
299        let variant_name = &v.ident;
300
301        match &v.fields {
302            syn::Fields::Unit => quote! {
303                #name::#variant_name => ::std::string::String::new()
304            },
305            syn::Fields::Unnamed(fields) => {
306                let field_count = fields.unnamed.len();
307                let field_names: Vec<_> =
308                    (0..field_count).map(|i| format_ident!("_{}", i)).collect();
309                if field_count == 1 {
310                    quote! {
311                        #name::#variant_name(#(#field_names),*) => {
312                            tui_dispatch::debug::debug_string_pretty(&#(#field_names),*)
313                        }
314                    }
315                } else {
316                    let parts = field_names.iter().map(|field| {
317                        quote! { tui_dispatch::debug::debug_string_pretty(&#field) }
318                    });
319                    quote! {
320                        #name::#variant_name(#(#field_names),*) => {
321                            let values = ::std::vec![#(#parts),*];
322                            format!("({})", values.join(", "))
323                        }
324                    }
325                }
326            }
327            syn::Fields::Named(fields) => {
328                let field_names: Vec<_> = fields
329                    .named
330                    .iter()
331                    .filter_map(|f| f.ident.as_ref())
332                    .collect();
333                if field_names.is_empty() {
334                    quote! {
335                        #name::#variant_name { .. } => ::std::string::String::new()
336                    }
337                } else {
338                    let parts = field_names.iter().map(|field| {
339                        let label = field.to_string();
340                        quote! {
341                            format!("{}: {}", #label, tui_dispatch::debug::debug_string_pretty(&#field))
342                        }
343                    });
344                    quote! {
345                        #name::#variant_name { #(#field_names),*, .. } => {
346                            let values = ::std::vec![#(#parts),*];
347                            format!("{{{}}}", values.join(", "))
348                        }
349                    }
350                }
351            }
352        }
353    });
354
355    let mut expanded = quote! {
356        impl tui_dispatch::Action for #name {
357            fn name(&self) -> &'static str {
358                match self {
359                    #(#name_arms),*
360                }
361            }
362        }
363
364        impl tui_dispatch::ActionParams for #name {
365            fn params(&self) -> ::std::string::String {
366                match self {
367                    #(#params_arms),*
368                }
369            }
370
371            fn params_pretty(&self) -> ::std::string::String {
372                match self {
373                    #(#params_pretty_arms),*
374                }
375            }
376        }
377    };
378
379    // If category inference is enabled, generate category-related code
380    if opts.infer_categories {
381        // Collect categories and their variants
382        let mut categories: HashMap<String, Vec<&Ident>> = HashMap::new();
383        let mut variant_categories: Vec<(&Ident, Option<String>)> = Vec::new();
384
385        for v in variants.iter() {
386            let cat = if v.skip_category {
387                None
388            } else if let Some(ref explicit_cat) = v.category {
389                Some(explicit_cat.clone())
390            } else {
391                infer_category(&v.ident.to_string())
392            };
393
394            variant_categories.push((&v.ident, cat.clone()));
395
396            if let Some(ref category) = cat {
397                categories
398                    .entry(category.clone())
399                    .or_default()
400                    .push(&v.ident);
401            }
402        }
403
404        // Sort categories for deterministic output
405        let mut sorted_categories: Vec<_> = categories.keys().cloned().collect();
406        sorted_categories.sort();
407
408        // Create deduplicated category match arms
409        let category_arms_dedup: Vec<_> = variant_categories
410            .iter()
411            .map(|(variant, cat)| {
412                let cat_expr = match cat {
413                    Some(c) => quote! { ::core::option::Option::Some(#c) },
414                    None => quote! { ::core::option::Option::None },
415                };
416                // Use wildcard pattern to handle all field types
417                quote! { #name::#variant { .. } => #cat_expr }
418            })
419            .collect();
420
421        // Generate category enum
422        let category_enum_name = format_ident!("{}Category", name);
423        let category_variants: Vec<_> = sorted_categories
424            .iter()
425            .map(|c| format_ident!("{}", to_pascal_case(c)))
426            .collect();
427        let category_variant_names: Vec<_> = sorted_categories.clone();
428
429        // Generate category_enum() method arms
430        let category_enum_arms: Vec<_> = variant_categories
431            .iter()
432            .map(|(variant, cat)| {
433                let cat_variant = match cat {
434                    Some(c) => format_ident!("{}", to_pascal_case(c)),
435                    None => format_ident!("Uncategorized"),
436                };
437                quote! { #name::#variant { .. } => #category_enum_name::#cat_variant }
438            })
439            .collect();
440
441        // Generate is_* predicates
442        let predicates: Vec<_> = sorted_categories
443            .iter()
444            .map(|cat| {
445                let predicate_name = format_ident!("is_{}", cat);
446                let cat_variants = categories.get(cat).unwrap();
447                let patterns: Vec<_> = cat_variants
448                    .iter()
449                    .map(|v| quote! { #name::#v { .. } })
450                    .collect();
451                let doc = format!(
452                    "Returns true if this action belongs to the `{}` category.",
453                    cat
454                );
455
456                quote! {
457                    #[doc = #doc]
458                    pub fn #predicate_name(&self) -> bool {
459                        matches!(self, #(#patterns)|*)
460                    }
461                }
462            })
463            .collect();
464
465        // Add category-related implementations
466        let category_enum_doc = format!(
467            "Action categories for [`{}`].\n\n\
468             Use [`{}::category_enum()`] to get the category of an action.",
469            name, name
470        );
471
472        expanded = quote! {
473            #expanded
474
475            #[doc = #category_enum_doc]
476            #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
477            pub enum #category_enum_name {
478                #(#category_variants,)*
479                /// Actions that don't belong to any specific category.
480                Uncategorized,
481            }
482
483            impl #category_enum_name {
484                /// Get all category values
485                pub fn all() -> &'static [Self] {
486                    &[#(Self::#category_variants,)* Self::Uncategorized]
487                }
488
489                /// Get category name as string
490                pub fn name(&self) -> &'static str {
491                    match self {
492                        #(Self::#category_variants => #category_variant_names,)*
493                        Self::Uncategorized => "uncategorized",
494                    }
495                }
496            }
497
498            impl #name {
499                /// Get the action's category (if categorized)
500                pub fn category(&self) -> ::core::option::Option<&'static str> {
501                    match self {
502                        #(#category_arms_dedup,)*
503                    }
504                }
505
506                /// Get the category as an enum value
507                pub fn category_enum(&self) -> #category_enum_name {
508                    match self {
509                        #(#category_enum_arms,)*
510                    }
511                }
512
513                #(#predicates)*
514            }
515
516            impl tui_dispatch::ActionCategory for #name {
517                type Category = #category_enum_name;
518
519                fn category(&self) -> ::core::option::Option<&'static str> {
520                    #name::category(self)
521                }
522
523                fn category_enum(&self) -> Self::Category {
524                    #name::category_enum(self)
525                }
526            }
527        };
528
529        // Generate dispatcher trait if requested
530        if opts.generate_dispatcher {
531            let dispatcher_trait_name = format_ident!("{}Dispatcher", name);
532
533            let dispatch_methods: Vec<_> = sorted_categories
534                .iter()
535                .map(|cat| {
536                    let method_name = format_ident!("dispatch_{}", cat);
537                    let doc = format!("Handle actions in the `{}` category.", cat);
538                    quote! {
539                        #[doc = #doc]
540                        fn #method_name(&mut self, action: &#name) -> bool {
541                            false
542                        }
543                    }
544                })
545                .collect();
546
547            let dispatch_arms: Vec<_> = sorted_categories
548                .iter()
549                .map(|cat| {
550                    let method_name = format_ident!("dispatch_{}", cat);
551                    let cat_variant = format_ident!("{}", to_pascal_case(cat));
552                    quote! {
553                        #category_enum_name::#cat_variant => self.#method_name(action)
554                    }
555                })
556                .collect();
557
558            let dispatcher_doc = format!(
559                "Dispatcher trait for [`{}`].\n\n\
560                 Implement the `dispatch_*` methods for each category you want to handle.\n\
561                 The [`dispatch()`](Self::dispatch) method automatically routes to the correct handler.",
562                name
563            );
564
565            expanded = quote! {
566                #expanded
567
568                #[doc = #dispatcher_doc]
569                pub trait #dispatcher_trait_name {
570                    #(#dispatch_methods)*
571
572                    /// Handle uncategorized actions.
573                    fn dispatch_uncategorized(&mut self, action: &#name) -> bool {
574                        false
575                    }
576
577                    /// Main dispatch entry point - routes to category-specific handlers.
578                    fn dispatch(&mut self, action: &#name) -> bool {
579                        match action.category_enum() {
580                            #(#dispatch_arms,)*
581                            #category_enum_name::Uncategorized => self.dispatch_uncategorized(action),
582                        }
583                    }
584                }
585            };
586        }
587    }
588
589    TokenStream::from(expanded)
590}
591
592/// Derive macro for the BindingContext trait
593///
594/// Generates implementations for `name()`, `from_name()`, and `all()` methods.
595/// The context name is derived from the variant name converted to snake_case.
596///
597/// # Example
598/// ```ignore
599/// #[derive(BindingContext, Clone, Copy, PartialEq, Eq, Hash)]
600/// enum MyContext {
601///     Default,
602///     Search,
603///     ConnectionForm,
604/// }
605///
606/// // Generated names: "default", "search", "connection_form"
607/// assert_eq!(MyContext::Default.name(), "default");
608/// assert_eq!(MyContext::from_name("search"), Some(MyContext::Search));
609/// ```
610#[proc_macro_derive(BindingContext)]
611pub fn derive_binding_context(input: TokenStream) -> TokenStream {
612    let input = parse_macro_input!(input as DeriveInput);
613    let name = &input.ident;
614
615    let expanded = match &input.data {
616        syn::Data::Enum(data) => {
617            // Check that all variants are unit variants
618            for variant in &data.variants {
619                if !matches!(variant.fields, syn::Fields::Unit) {
620                    return syn::Error::new_spanned(
621                        variant,
622                        "BindingContext can only be derived for enums with unit variants",
623                    )
624                    .to_compile_error()
625                    .into();
626                }
627            }
628
629            let variant_names: Vec<_> = data.variants.iter().map(|v| &v.ident).collect();
630            let variant_strings: Vec<_> = variant_names
631                .iter()
632                .map(|v| to_snake_case(&v.to_string()))
633                .collect();
634
635            let name_arms = variant_names
636                .iter()
637                .zip(variant_strings.iter())
638                .map(|(v, s)| {
639                    quote! { #name::#v => #s }
640                });
641
642            let from_name_arms = variant_names
643                .iter()
644                .zip(variant_strings.iter())
645                .map(|(v, s)| {
646                    quote! { #s => ::core::option::Option::Some(#name::#v) }
647                });
648
649            let all_variants = variant_names.iter().map(|v| quote! { #name::#v });
650
651            quote! {
652                impl tui_dispatch::BindingContext for #name {
653                    fn name(&self) -> &'static str {
654                        match self {
655                            #(#name_arms),*
656                        }
657                    }
658
659                    fn from_name(name: &str) -> ::core::option::Option<Self> {
660                        match name {
661                            #(#from_name_arms,)*
662                            _ => ::core::option::Option::None,
663                        }
664                    }
665
666                    fn all() -> &'static [Self] {
667                        static ALL: &[#name] = &[#(#all_variants),*];
668                        ALL
669                    }
670                }
671            }
672        }
673        _ => {
674            return syn::Error::new_spanned(input, "BindingContext can only be derived for enums")
675                .to_compile_error()
676                .into();
677        }
678    };
679
680    TokenStream::from(expanded)
681}
682
683/// Derive macro for the ComponentId trait
684///
685/// Generates implementations for `name()` method that returns the variant name.
686///
687/// # Example
688/// ```ignore
689/// #[derive(ComponentId, Clone, Copy, PartialEq, Eq, Hash, Debug)]
690/// enum MyComponentId {
691///     Sidebar,
692///     MainContent,
693///     StatusBar,
694/// }
695///
696/// assert_eq!(MyComponentId::Sidebar.name(), "Sidebar");
697/// ```
698#[proc_macro_derive(ComponentId)]
699pub fn derive_component_id(input: TokenStream) -> TokenStream {
700    let input = parse_macro_input!(input as DeriveInput);
701    let name = &input.ident;
702
703    let expanded = match &input.data {
704        syn::Data::Enum(data) => {
705            // Check that all variants are unit variants
706            for variant in &data.variants {
707                if !matches!(variant.fields, syn::Fields::Unit) {
708                    return syn::Error::new_spanned(
709                        variant,
710                        "ComponentId can only be derived for enums with unit variants",
711                    )
712                    .to_compile_error()
713                    .into();
714                }
715            }
716
717            let variant_names: Vec<_> = data.variants.iter().map(|v| &v.ident).collect();
718            let variant_strings: Vec<_> = variant_names.iter().map(|v| v.to_string()).collect();
719
720            let name_arms = variant_names
721                .iter()
722                .zip(variant_strings.iter())
723                .map(|(v, s)| {
724                    quote! { #name::#v => #s }
725                });
726
727            quote! {
728                impl tui_dispatch::ComponentId for #name {
729                    fn name(&self) -> &'static str {
730                        match self {
731                            #(#name_arms),*
732                        }
733                    }
734                }
735            }
736        }
737        _ => {
738            return syn::Error::new_spanned(input, "ComponentId can only be derived for enums")
739                .to_compile_error()
740                .into();
741        }
742    };
743
744    TokenStream::from(expanded)
745}
746
747// ============================================================================
748// DebugState derive macro
749// ============================================================================
750
751/// Container-level attributes for #[derive(DebugState)]
752#[derive(Debug, FromDeriveInput)]
753#[darling(attributes(debug_state), supports(struct_named))]
754struct DebugStateOpts {
755    ident: syn::Ident,
756    data: darling::ast::Data<(), DebugStateField>,
757}
758
759/// Field-level attributes for DebugState
760#[derive(Debug, FromField)]
761#[darling(attributes(debug))]
762struct DebugStateField {
763    ident: Option<syn::Ident>,
764
765    /// Section name for this field (groups fields together)
766    #[darling(default)]
767    section: Option<String>,
768
769    /// Skip this field in debug output
770    #[darling(default)]
771    skip: bool,
772
773    /// Custom display format (e.g., "{:?}" for Debug, "{:#?}" for pretty Debug)
774    #[darling(default)]
775    format: Option<String>,
776
777    /// Custom label for this field (defaults to field name)
778    #[darling(default)]
779    label: Option<String>,
780
781    /// Use Debug trait instead of Display
782    #[darling(default)]
783    debug_fmt: bool,
784}
785
786/// Derive macro for the DebugState trait
787///
788/// Automatically generates `debug_sections()` implementation from struct fields.
789///
790/// # Attributes
791///
792/// - `#[debug(section = "Name")]` - Group field under a section
793/// - `#[debug(skip)]` - Exclude field from debug output
794/// - `#[debug(label = "Custom Label")]` - Use custom label instead of field name
795/// - `#[debug(debug_fmt)]` - Use `{:?}` format instead of `Display`
796/// - `#[debug(format = "{:#?}")]` - Use custom format string
797///
798/// # Example
799///
800/// ```ignore
801/// use tui_dispatch::DebugState;
802///
803/// #[derive(DebugState)]
804/// struct AppState {
805///     #[debug(section = "Connection")]
806///     host: String,
807///     #[debug(section = "Connection")]
808///     port: u16,
809///
810///     #[debug(section = "UI")]
811///     scroll_offset: usize,
812///
813///     #[debug(skip)]
814///     internal_cache: HashMap<String, Data>,
815///
816///     #[debug(section = "Stats", debug_fmt)]
817///     status: ConnectionStatus,
818/// }
819/// ```
820///
821/// Fields without a section attribute are grouped under a section named after
822/// the struct (e.g., "AppState").
823#[proc_macro_derive(DebugState, attributes(debug, debug_state))]
824pub fn derive_debug_state(input: TokenStream) -> TokenStream {
825    let input = parse_macro_input!(input as DeriveInput);
826
827    let opts = match DebugStateOpts::from_derive_input(&input) {
828        Ok(opts) => opts,
829        Err(e) => return e.write_errors().into(),
830    };
831
832    let name = &opts.ident;
833    let default_section = name.to_string();
834
835    let fields = match &opts.data {
836        darling::ast::Data::Struct(fields) => fields,
837        _ => {
838            return syn::Error::new_spanned(&input, "DebugState can only be derived for structs")
839                .to_compile_error()
840                .into();
841        }
842    };
843
844    // Group fields by section
845    let mut sections: HashMap<String, Vec<&DebugStateField>> = HashMap::new();
846    let mut section_order: Vec<String> = Vec::new();
847
848    for field in fields.iter() {
849        if field.skip {
850            continue;
851        }
852
853        let section_name = field
854            .section
855            .clone()
856            .unwrap_or_else(|| default_section.clone());
857
858        if !section_order.contains(&section_name) {
859            section_order.push(section_name.clone());
860        }
861
862        sections.entry(section_name).or_default().push(field);
863    }
864
865    // Generate code for each section
866    let section_code: Vec<_> = section_order
867        .iter()
868        .map(|section_name| {
869            let fields_in_section = sections.get(section_name).unwrap();
870
871            let entry_calls: Vec<_> = fields_in_section
872                .iter()
873                .filter_map(|field| {
874                    let field_ident = field.ident.as_ref()?;
875                    let label = field
876                        .label
877                        .clone()
878                        .unwrap_or_else(|| field_ident.to_string());
879
880                    let value_expr = if let Some(ref fmt) = field.format {
881                        quote! { format!(#fmt, self.#field_ident) }
882                    } else if field.debug_fmt {
883                        quote! { format!("{:?}", self.#field_ident) }
884                    } else {
885                        quote! { tui_dispatch::debug::debug_string(&self.#field_ident) }
886                    };
887
888                    Some(quote! {
889                        .entry(#label, #value_expr)
890                    })
891                })
892                .collect();
893
894            quote! {
895                tui_dispatch::debug::DebugSection::new(#section_name)
896                    #(#entry_calls)*
897            }
898        })
899        .collect();
900
901    let expanded = quote! {
902        impl tui_dispatch::debug::DebugState for #name {
903            fn debug_sections(&self) -> ::std::vec::Vec<tui_dispatch::debug::DebugSection> {
904                ::std::vec![
905                    #(#section_code),*
906                ]
907            }
908        }
909    };
910
911    TokenStream::from(expanded)
912}
913
914// ============================================================================
915// FeatureFlags derive macro
916// ============================================================================
917
918/// Field-level attributes for FeatureFlags
919#[derive(Debug, FromField)]
920#[darling(attributes(flag))]
921struct FeatureFlagsField {
922    ident: Option<syn::Ident>,
923    ty: syn::Type,
924
925    /// Default value for this feature (defaults to false)
926    #[darling(default)]
927    default: Option<bool>,
928}
929
930/// Container-level attributes for #[derive(FeatureFlags)]
931#[derive(Debug, FromDeriveInput)]
932#[darling(attributes(feature_flags), supports(struct_named))]
933struct FeatureFlagsOpts {
934    ident: syn::Ident,
935    data: darling::ast::Data<(), FeatureFlagsField>,
936}
937
938/// Derive macro for the FeatureFlags trait
939///
940/// Generates implementations for `is_enabled()`, `set()`, and `all_flags()` methods.
941/// Also generates a `Default` implementation using the specified defaults.
942///
943/// # Attributes
944///
945/// - `#[flag(default = true)]` - Set default value (defaults to false)
946///
947/// # Example
948///
949/// ```ignore
950/// use tui_dispatch::FeatureFlags;
951///
952/// #[derive(FeatureFlags)]
953/// struct Features {
954///     #[flag(default = false)]
955///     new_search_ui: bool,
956///
957///     #[flag(default = true)]
958///     vim_bindings: bool,
959/// }
960///
961/// let mut features = Features::default();
962/// assert!(!features.new_search_ui);
963/// assert!(features.vim_bindings);
964///
965/// features.enable("new_search_ui");
966/// assert!(features.new_search_ui);
967/// ```
968#[proc_macro_derive(FeatureFlags, attributes(flag, feature_flags))]
969pub fn derive_feature_flags(input: TokenStream) -> TokenStream {
970    let input = parse_macro_input!(input as DeriveInput);
971
972    let opts = match FeatureFlagsOpts::from_derive_input(&input) {
973        Ok(opts) => opts,
974        Err(e) => return e.write_errors().into(),
975    };
976
977    let name = &opts.ident;
978
979    let fields = match &opts.data {
980        darling::ast::Data::Struct(fields) => fields,
981        _ => {
982            return syn::Error::new_spanned(
983                &input,
984                "FeatureFlags can only be derived for structs with named fields",
985            )
986            .to_compile_error()
987            .into();
988        }
989    };
990
991    // Collect bool fields only
992    let bool_fields: Vec<_> = fields
993        .iter()
994        .filter_map(|f| {
995            let ident = f.ident.as_ref()?;
996            // Check if type is bool
997            if let syn::Type::Path(type_path) = &f.ty {
998                if type_path.path.is_ident("bool") {
999                    return Some((ident.clone(), f.default.unwrap_or(false)));
1000                }
1001            }
1002            None
1003        })
1004        .collect();
1005
1006    if bool_fields.is_empty() {
1007        return syn::Error::new_spanned(
1008            &input,
1009            "FeatureFlags struct must have at least one bool field",
1010        )
1011        .to_compile_error()
1012        .into();
1013    }
1014
1015    // Generate is_enabled match arms
1016    let is_enabled_arms: Vec<_> = bool_fields
1017        .iter()
1018        .map(|(ident, _)| {
1019            let name_str = ident.to_string();
1020            quote! { #name_str => ::core::option::Option::Some(self.#ident) }
1021        })
1022        .collect();
1023
1024    // Generate set match arms
1025    let set_arms: Vec<_> = bool_fields
1026        .iter()
1027        .map(|(ident, _)| {
1028            let name_str = ident.to_string();
1029            quote! {
1030                #name_str => {
1031                    self.#ident = enabled;
1032                    true
1033                }
1034            }
1035        })
1036        .collect();
1037
1038    // Generate all_flags array
1039    let flag_names: Vec<_> = bool_fields
1040        .iter()
1041        .map(|(ident, _)| ident.to_string())
1042        .collect();
1043
1044    // Generate Default impl with proper defaults
1045    let default_fields: Vec<_> = bool_fields
1046        .iter()
1047        .map(|(ident, default)| {
1048            quote! { #ident: #default }
1049        })
1050        .collect();
1051
1052    let expanded = quote! {
1053        impl tui_dispatch::FeatureFlags for #name {
1054            fn is_enabled(&self, name: &str) -> ::core::option::Option<bool> {
1055                match name {
1056                    #(#is_enabled_arms,)*
1057                    _ => ::core::option::Option::None,
1058                }
1059            }
1060
1061            fn set(&mut self, name: &str, enabled: bool) -> bool {
1062                match name {
1063                    #(#set_arms)*
1064                    _ => false,
1065                }
1066            }
1067
1068            fn all_flags() -> &'static [&'static str] {
1069                &[#(#flag_names),*]
1070            }
1071        }
1072
1073        impl ::core::default::Default for #name {
1074            fn default() -> Self {
1075                Self {
1076                    #(#default_fields,)*
1077                }
1078            }
1079        }
1080    };
1081
1082    TokenStream::from(expanded)
1083}