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