rxtui_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::parse::{Parse, ParseStream};
4use syn::{
5    DeriveInput, Expr, FnArg, Ident, ImplItem, ItemFn, ItemImpl, LitStr, Pat, PatType, Token, Type,
6    parse_macro_input,
7};
8
9//--------------------------------------------------------------------------------------------------
10// Types
11//--------------------------------------------------------------------------------------------------
12
13/// Represents a topic mapping like "timer" => TimerMsg or self.topic => TimerMsg
14enum TopicKey {
15    Static(LitStr),
16    Dynamic(Expr),
17}
18
19struct TopicMapping {
20    key: TopicKey,
21    _arrow: Token![=>],
22    msg_type: Type,
23}
24
25/// Parse the update attribute arguments with new syntax
26struct UpdateArgs {
27    msg_type: Option<Type>,
28    topics: Vec<TopicMapping>,
29}
30
31//--------------------------------------------------------------------------------------------------
32// Trait Implementations
33//--------------------------------------------------------------------------------------------------
34
35impl Parse for TopicMapping {
36    fn parse(input: ParseStream) -> syn::Result<Self> {
37        // Try to parse as a string literal first
38        let key = if input.peek(LitStr) {
39            TopicKey::Static(input.parse()?)
40        } else {
41            // Otherwise parse as an expression (e.g., self.topic_name)
42            TopicKey::Dynamic(input.parse()?)
43        };
44
45        Ok(TopicMapping {
46            key,
47            _arrow: input.parse()?,
48            msg_type: input.parse()?,
49        })
50    }
51}
52
53impl Parse for UpdateArgs {
54    fn parse(input: ParseStream) -> syn::Result<Self> {
55        let mut msg_type = None;
56        let mut topics = Vec::new();
57
58        while !input.is_empty() {
59            // Parse identifier (msg or topics)
60            let ident: Ident = input.parse()?;
61            input.parse::<Token![=]>()?;
62
63            if ident == "msg" {
64                msg_type = Some(input.parse()?);
65            } else if ident == "topics" {
66                // Parse array of topic mappings
67                let content;
68                syn::bracketed!(content in input);
69
70                while !content.is_empty() {
71                    topics.push(content.parse::<TopicMapping>()?);
72
73                    if !content.is_empty() {
74                        content.parse::<Token![,]>()?;
75                    }
76                }
77            }
78
79            if !input.is_empty() {
80                input.parse::<Token![,]>()?;
81            }
82        }
83
84        Ok(UpdateArgs { msg_type, topics })
85    }
86}
87
88//--------------------------------------------------------------------------------------------------
89// Functions
90//--------------------------------------------------------------------------------------------------
91
92/// Extract parameter name and type from a function argument
93fn extract_param_info(arg: &FnArg) -> Option<(Ident, Type)> {
94    if let FnArg::Typed(PatType { pat, ty, .. }) = arg
95        && let Pat::Ident(pat_ident) = &**pat
96    {
97        let name = pat_ident.ident.clone();
98        let ty = (**ty).clone();
99        return Some((name, ty));
100    }
101    None
102}
103
104/// Derive macro that implements the Component trait
105///
106/// This macro automatically implements all the boilerplate methods
107/// required by the Component trait.
108///
109/// # Example
110///
111/// ```ignore
112/// #[derive(Component)]
113/// struct MyComponent {
114///     // any fields you need
115/// }
116///
117/// // Or for unit structs:
118/// #[derive(Component)]
119/// struct MyComponent;
120///
121/// impl MyComponent {
122///     fn update(&self, ctx: &Context, msg: Box<dyn Message>, topic: Option<&str>) -> Action {
123///         // your implementation
124///     }
125///
126///     fn view(&self, ctx: &Context) -> Node {
127///         // your implementation
128///     }
129/// }
130/// ```
131#[proc_macro_derive(Component)]
132pub fn derive_component(input: TokenStream) -> TokenStream {
133    let input = parse_macro_input!(input as DeriveInput);
134    let name = &input.ident;
135
136    // Generate the implementation
137    let expanded = quote! {
138        impl rxtui::Component for #name {
139            fn as_any(&self) -> &dyn std::any::Any {
140                self
141            }
142
143            fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
144                self
145            }
146
147            // Use method resolution to call inherent __component_update_impl if it exists,
148            // otherwise fall back to the trait's default implementation (Action::None)
149            fn update(&self, ctx: &rxtui::Context, msg: Box<dyn rxtui::Message>, topic: Option<&str>) -> rxtui::Action {
150                use rxtui::providers::UpdateProvider;
151                self.__component_update_impl(ctx, msg, topic)
152            }
153
154            // Use method resolution to call inherent __component_view_impl if it exists,
155            // otherwise fall back to the trait's default implementation (empty Node)
156            fn view(&self, ctx: &rxtui::Context) -> rxtui::Node {
157                use rxtui::providers::ViewProvider;
158                self.__component_view_impl(ctx)
159            }
160
161            // Use method resolution to call inherent __component_effects_impl if it exists,
162            // otherwise fall back to the trait's default implementation (empty vec)
163            fn effects(&self, ctx: &rxtui::Context) -> Vec<rxtui::effect::Effect> {
164                use rxtui::providers::EffectsProvider;
165                self.__component_effects_impl(ctx)
166            }
167        }
168
169    };
170
171    TokenStream::from(expanded)
172}
173
174/// Simplifies component update methods by automatically handling message downcasting,
175/// state fetching, and topic routing.
176///
177/// # Basic usage
178///
179/// The simplest form just handles a single message type:
180///
181/// ```ignore
182/// #[update]
183/// fn update(&self, ctx: &Context, msg: CounterMsg) -> Action {
184///     match msg {
185///         CounterMsg::Exit => Action::Exit,
186///         _ => Action::None,
187///     }
188/// }
189/// ```
190///
191/// # With state management
192///
193/// Add a state parameter and it will be automatically fetched and passed in:
194///
195/// ```ignore
196/// #[update]
197/// fn update(&self, ctx: &Context, msg: CounterMsg, mut state: CounterState) -> Action {
198///     match msg {
199///         CounterMsg::Increment => {
200///             state.count += 1;
201///             Action::Update(Box::new(state))
202///         }
203///         CounterMsg::Exit => Action::Exit,
204///     }
205/// }
206/// ```
207///
208/// # With topic-based messaging
209///
210/// Components can also listen to topic messages. Topics can be static strings or
211/// dynamic expressions from self:
212///
213/// ```ignore
214/// #[update(msg = AppMsg, topics = ["timer" => TimerMsg, self.topic_name => UpdateMsg])]
215/// fn update(&self, ctx: &Context, messages: Messages, mut state: AppState) -> Action {
216///     match messages {
217///         Messages::AppMsg(msg) => { /* handle regular message */ }
218///         Messages::TimerMsg(msg) => { /* handle timer topic */ }
219///         Messages::UpdateMsg(msg) => { /* handle dynamic topic */ }
220///     }
221/// }
222/// ```
223///
224/// # How it works
225///
226/// The macro transforms your simplified function into the full Component trait implementation:
227///
228/// ```text
229/// ┌─────────────────────────────────────────────────────────────────┐
230/// │ #[update(msg = CounterMsg, topics = [self.topic => ResetMsg])]  │
231/// │ fn update(&self, ctx: &Context, msg: Messages,                  │
232/// │           mut state: CounterState) -> Action {                  │
233/// │     match msg {                                                 │
234/// │         Messages::CounterMsg(m) => { ... }                      │
235/// │         Messages::ResetMsg(m) => { ... }                        │
236/// │     }                                                           │
237/// │ }                                                               │
238/// └─────────────────────────────────────────────────────────────────┘
239///                                 ↓
240/// ┌─────────────────────────────────────────────────────────────────┐
241/// │ fn update(&self, ctx: &Context,                                 │
242/// │           msg: Box<dyn Message>,                                │
243/// │           topic: Option<&str>) -> Action {                      │
244/// │                                                                 │
245/// │     enum Messages { /* generated */ }                           │
246/// │     let mut state = ctx.get_state::<CounterState>();            │
247/// │                                                                 │
248/// │     if let Some(topic) = topic {                                │
249/// │         if topic == &*(self.topic) {                            │
250/// │             if let Some(m) = msg.downcast::<ResetMsg>() {       │
251/// │                 let msg = Messages::ResetMsg(m.clone());        │
252/// │                 return { /* user's match block */ };            │
253/// │             }                                                   │
254/// │         }                                                       │
255/// │         return Action::None;                                    │
256/// │     }                                                           │
257/// │                                                                 │
258/// │     if let Some(m) = msg.downcast::<CounterMsg>() {             │
259/// │         let msg = Messages::CounterMsg(m.clone());              │
260/// │         return { /* user's match block */ };                    │
261/// │     }                                                           │
262/// │                                                                 │
263/// │     Action::None                                                │
264/// │ }                                                               │
265/// └─────────────────────────────────────────────────────────────────┘
266/// ```
267///
268/// # Parameters
269///
270/// The function parameters are detected by position:
271/// - `&self` (required)
272/// - `&Context` (required) - any name allowed
273/// - Message type (required) - any name allowed
274/// - State type (optional) - any name allowed
275#[proc_macro_attribute]
276pub fn update(args: TokenStream, input: TokenStream) -> TokenStream {
277    let input_fn = parse_macro_input!(input as ItemFn);
278
279    let _fn_name = &input_fn.sig.ident;
280    let fn_vis = &input_fn.vis;
281    let fn_block = &input_fn.block;
282
283    // Parse function parameters by position
284    let mut params = input_fn.sig.inputs.iter();
285
286    // Position 0: &self (skip it)
287    params
288        .next()
289        .expect("#[update] function must have &self as first parameter");
290
291    // Position 1: &Context
292    let ctx_param = params
293        .next()
294        .expect("#[update] function must have &Context as second parameter");
295    let (ctx_name, _ctx_type) =
296        extract_param_info(ctx_param).expect("Failed to extract context parameter info");
297
298    // Position 2: Message type
299    let msg_param = params
300        .next()
301        .expect("#[update] function must have message type as third parameter");
302    let (msg_name, msg_type) =
303        extract_param_info(msg_param).expect("Failed to extract message parameter info");
304
305    // Position 3: State type (optional)
306    let state_info = params.next().and_then(extract_param_info);
307
308    // Check if we have topic arguments
309    if args.is_empty() {
310        // Simple case: no topics specified
311        // Generate state fetching code if state parameter exists
312        let state_setup = if let Some((state_name, state_type)) = &state_info {
313            quote! { let mut #state_name = #ctx_name.get_state::<#state_type>(); }
314        } else {
315            quote! {}
316        };
317
318        let expanded = quote! {
319            #fn_vis fn __component_update_impl(&self, #ctx_name: &rxtui::Context, msg: Box<dyn rxtui::Message>, _topic: Option<&str>) -> rxtui::Action {
320                if let Some(#msg_name) = msg.downcast::<#msg_type>() {
321                    #state_setup
322                    let #msg_name = #msg_name.clone();
323                    return #fn_block;
324                }
325
326                rxtui::Action::None
327            }
328        };
329
330        TokenStream::from(expanded)
331    } else {
332        // Complex case: with topics
333        let args = parse_macro_input!(args as UpdateArgs);
334
335        // Use provided msg type or fall back to first positional arg
336        let regular_type = args.msg_type.unwrap_or(msg_type.clone());
337
338        // Generate enum name from the message parameter type
339        let enum_name = &msg_type;
340
341        // Generate enum variants
342        let mut enum_variants = vec![];
343        let regular_variant =
344            format_ident!("{}", quote!(#regular_type).to_string().replace("::", "_"));
345        enum_variants.push(quote! { #regular_variant(#regular_type) });
346
347        // Generate topic handling code
348        let mut topic_matches = vec![];
349        for topic in &args.topics {
350            let topic_type = &topic.msg_type;
351            let variant_name =
352                format_ident!("{}", quote!(#topic_type).to_string().replace("::", "_"));
353
354            enum_variants.push(quote! { #variant_name(#topic_type) });
355
356            let topic_check = match &topic.key {
357                TopicKey::Static(lit_str) => {
358                    quote! { topic == #lit_str }
359                }
360                TopicKey::Dynamic(expr) => {
361                    // Use &* to convert String to &str
362                    quote! { topic == &*(#expr) }
363                }
364            };
365
366            topic_matches.push(quote! {
367                if #topic_check {
368                    if let Some(msg) = msg.downcast::<#topic_type>() {
369                        let #msg_name = #enum_name::#variant_name(msg.clone());
370                        return #fn_block;
371                    }
372                }
373            });
374        }
375
376        // Generate state setup
377        let state_setup = if let Some((state_name, state_type)) = &state_info {
378            quote! { let mut #state_name = #ctx_name.get_state::<#state_type>(); }
379        } else {
380            quote! {}
381        };
382
383        // Generate the complete function
384        let expanded = quote! {
385            #fn_vis fn __component_update_impl(&self, #ctx_name: &rxtui::Context, msg: Box<dyn rxtui::Message>, topic: Option<&str>) -> rxtui::Action {
386                // Generate the enum for message types
387                #[allow(non_camel_case_types)]
388                enum #enum_name {
389                    #(#enum_variants),*
390                }
391
392                #state_setup
393
394                // Handle topic messages first
395                if let Some(topic) = topic {
396                    #(#topic_matches)*
397                    return rxtui::Action::None;
398                }
399
400                // Handle regular message
401                if let Some(msg) = msg.downcast::<#regular_type>() {
402                    let #msg_name = #enum_name::#regular_variant(msg.clone());
403                    return #fn_block;
404                }
405
406                rxtui::Action::None
407            }
408        };
409
410        TokenStream::from(expanded)
411    }
412}
413
414/// Simplifies component view methods by automatically fetching state from the context.
415///
416/// # With state
417///
418/// If you include a state parameter, it will be automatically fetched:
419///
420/// ```ignore
421/// #[view]
422/// fn view(&self, ctx: &Context, state: CounterState) -> Node {
423///     node! {
424///         div [
425///             text(format!("Count: {}", state.count))
426///         ]
427///     }
428/// }
429/// ```
430///
431/// # Without state
432///
433/// For stateless components, just omit the state parameter:
434///
435/// ```ignore
436/// #[view]
437/// fn view(&self, ctx: &Context) -> Node {
438///     node! {
439///         div [
440///             text("Static content")
441///         ]
442///     }
443/// }
444/// ```
445///
446/// The macro automatically detects whether a state parameter is present and generates
447/// the appropriate code to fetch it from the context.
448///
449/// # Parameters
450///
451/// The function parameters are detected by position:
452/// - `&self` (required)
453/// - `&Context` (required) - any name allowed
454/// - State type (optional) - any name allowed
455#[proc_macro_attribute]
456pub fn view(_args: TokenStream, input: TokenStream) -> TokenStream {
457    let input_fn = parse_macro_input!(input as ItemFn);
458
459    let _fn_name = &input_fn.sig.ident;
460    let fn_vis = &input_fn.vis;
461    let fn_block = &input_fn.block;
462
463    // Parse function parameters by position
464    let mut params = input_fn.sig.inputs.iter();
465
466    // Position 0: &self (skip it)
467    params
468        .next()
469        .expect("#[view] function must have &self as first parameter");
470
471    // Position 1: &Context
472    let ctx_param = params
473        .next()
474        .expect("#[view] function must have &Context as second parameter");
475    let (ctx_name, _ctx_type) =
476        extract_param_info(ctx_param).expect("Failed to extract context parameter info");
477
478    // Position 2: State type (optional)
479    if let Some(state_param) = params.next() {
480        let (state_name, state_type) =
481            extract_param_info(state_param).expect("Failed to extract state parameter info");
482
483        // Generate with state fetching
484        let expanded = quote! {
485            #fn_vis fn __component_view_impl(&self, #ctx_name: &rxtui::Context) -> rxtui::Node {
486                let #state_name = #ctx_name.get_state::<#state_type>();
487                #fn_block
488            }
489        };
490
491        TokenStream::from(expanded)
492    } else {
493        // No state parameter - just forward as-is
494        let expanded = quote! {
495            #fn_vis fn __component_view_impl(&self, #ctx_name: &rxtui::Context) -> rxtui::Node {
496                #fn_block
497            }
498        };
499
500        TokenStream::from(expanded)
501    }
502}
503
504/// Marks an async method as a single effect that runs in the background.
505///
506/// # Basic usage
507///
508/// Define an async effect that runs in the background:
509///
510/// ```ignore
511/// #[effect]
512/// async fn timer_effect(&self, ctx: &Context) {
513///     loop {
514///         tokio::time::sleep(Duration::from_secs(1)).await;
515///         ctx.send(Msg::Tick);
516///     }
517/// }
518/// ```
519///
520/// # With state
521///
522/// Effects can access component state:
523///
524/// ```ignore
525/// #[effect]
526/// async fn fetch_data(&self, ctx: &Context, state: MyState) {
527///     let url = &state.api_url;
528///     let data = fetch(url).await;
529///     ctx.send(Msg::DataLoaded(data));
530/// }
531/// ```
532///
533/// # Multiple effects
534///
535/// You can define multiple effects on a component - they will all be collected
536/// into a single `effects()` method:
537///
538/// ```ignore
539/// impl MyComponent {
540///     #[effect]
541///     async fn timer(&self, ctx: &Context) {
542///         // Timer logic
543///     }
544///
545///     #[effect]
546///     async fn websocket(&self, ctx: &Context) {
547///         // WebSocket logic
548///     }
549/// }
550/// ```
551///
552/// # Parameters
553///
554/// The function parameters are detected by position:
555/// - `&self` (required)
556/// - `&Context` (required) - any name allowed
557/// - State type (optional) - any name allowed
558///
559/// Note: Use the #[component] macro on the impl block to automatically collect
560/// all methods marked with #[effect] into the effects() method.
561#[proc_macro_attribute]
562pub fn effect(_args: TokenStream, input: TokenStream) -> TokenStream {
563    let input_fn = parse_macro_input!(input as ItemFn);
564
565    let fn_name = &input_fn.sig.ident;
566    let fn_vis = &input_fn.vis;
567    let fn_block = &input_fn.block;
568
569    // Parse function parameters by position
570    let mut params = input_fn.sig.inputs.iter();
571
572    // Position 0: &self (skip it)
573    params
574        .next()
575        .expect("#[effects] function must have &self as first parameter");
576
577    // Position 1: &Context
578    let ctx_param = params
579        .next()
580        .expect("#[effects] function must have &Context as second parameter");
581    let (ctx_name, _ctx_type) =
582        extract_param_info(ctx_param).expect("Failed to extract context parameter info");
583
584    // Position 2: State type (optional)
585    let state_setup = if let Some(state_param) = params.next() {
586        let (state_name, state_type) =
587            extract_param_info(state_param).expect("Failed to extract state parameter info");
588        quote! { let #state_name = #ctx_name.get_state::<#state_type>(); }
589    } else {
590        quote! {}
591    };
592
593    // Generate a helper method that creates the effect
594    let helper_name = format_ident!("__{}_effect", fn_name);
595
596    let expanded = quote! {
597        #[allow(dead_code)]
598        #fn_vis fn #helper_name(&self, #ctx_name: &rxtui::Context) -> rxtui::effect::Effect {
599            Box::pin({
600                let #ctx_name = #ctx_name.clone();
601                #state_setup
602                async move #fn_block
603            })
604        }
605
606        // Keep the original async function for reference/testing if needed
607        #[allow(dead_code)]
608        #fn_vis async fn #fn_name(&self, #ctx_name: &rxtui::Context) #fn_block
609    };
610
611    TokenStream::from(expanded)
612}
613
614/// Impl-level macro that automatically handles Component trait boilerplate.
615///
616/// This macro processes an impl block and:
617/// 1. Collects all methods marked with `#[effect]`
618/// 2. Generates helper methods for each effect
619/// 3. Automatically creates the `effects()` method
620///
621/// # Example
622///
623/// ```ignore
624/// #[component]
625/// impl MyComponent {
626///     #[update]
627///     fn update(&self, ctx: &Context, msg: Msg, mut state: State) -> Action {
628///         // update logic
629///     }
630///
631///     #[view]
632///     fn view(&self, ctx: &Context, state: State) -> Node {
633///         // view logic
634///     }
635///
636///     #[effect]
637///     async fn timer(&self, ctx: &Context) {
638///         // async effect logic
639///     }
640/// }
641/// ```
642///
643/// The macro will automatically generate the `effects()` method that collects
644/// all methods marked with `#[effect]`.
645#[proc_macro_attribute]
646pub fn component(_args: TokenStream, input: TokenStream) -> TokenStream {
647    let mut impl_block = parse_macro_input!(input as ItemImpl);
648
649    // Find all methods marked with #[effect]
650    let mut effect_methods = Vec::new();
651    let mut processed_items = Vec::new();
652
653    for item in impl_block.items.drain(..) {
654        if let ImplItem::Fn(mut method) = item {
655            // Check if this method has the #[effect] attribute
656            let has_effect_attr = method
657                .attrs
658                .iter()
659                .any(|attr| attr.path().is_ident("effect"));
660
661            if has_effect_attr {
662                // Remove the #[effect] attribute
663                method.attrs.retain(|attr| !attr.path().is_ident("effect"));
664
665                let method_name = &method.sig.ident;
666                let helper_name = format_ident!("__{}_effect", method_name);
667
668                // Parse parameters
669                let mut params = method.sig.inputs.iter();
670
671                // Skip &self
672                params.next();
673
674                // Get context parameter
675                let ctx_param = params.next();
676                let ctx_name = if let Some(FnArg::Typed(PatType { pat, .. })) = ctx_param {
677                    if let Pat::Ident(pat_ident) = &**pat {
678                        &pat_ident.ident
679                    } else {
680                        panic!("Expected context parameter");
681                    }
682                } else {
683                    panic!("Expected context parameter");
684                };
685
686                // Check for state parameter
687                let state_setup = if let Some(FnArg::Typed(PatType { pat, ty, .. })) = params.next()
688                {
689                    if let Pat::Ident(pat_ident) = &**pat {
690                        let state_name = &pat_ident.ident;
691                        let state_type = &**ty;
692                        quote! { let #state_name = #ctx_name.get_state::<#state_type>(); }
693                    } else {
694                        quote! {}
695                    }
696                } else {
697                    quote! {}
698                };
699
700                let method_block = &method.block;
701
702                // Generate helper method
703                let helper_method = quote! {
704                    #[allow(dead_code)]
705                    fn #helper_name(&self, #ctx_name: &rxtui::Context) -> rxtui::effect::Effect {
706                        Box::pin({
707                            let #ctx_name = #ctx_name.clone();
708                            #state_setup
709                            async move #method_block
710                        })
711                    }
712                };
713
714                // Store effect method info for later
715                effect_methods.push((helper_name, ctx_name.clone()));
716
717                // Add both the helper and original method
718                let helper_item: ImplItem = syn::parse2(helper_method).unwrap();
719                processed_items.push(helper_item);
720
721                // Add #[allow(dead_code)] to the original async method
722                method.attrs.push(syn::parse_quote! { #[allow(dead_code)] });
723                processed_items.push(ImplItem::Fn(method));
724            } else {
725                processed_items.push(ImplItem::Fn(method));
726            }
727        } else {
728            processed_items.push(item);
729        }
730    }
731
732    // Add all processed items back
733    impl_block.items = processed_items;
734
735    // Always generate effects() method - either with collected effects or empty vec
736    // If rxtui is compiled without effects, the Effect type won't exist and compilation will fail
737    // This is the correct behavior - using effects without the feature should be a compile error
738    let effects_method = if !effect_methods.is_empty() {
739        let effect_calls = effect_methods
740            .iter()
741            .map(|(helper_name, _)| {
742                quote! { self.#helper_name(ctx) }
743            })
744            .collect::<Vec<_>>();
745
746        quote! {
747            // Generated method that shadows the EffectsProvider trait method
748            // This will be called by Component::effects() through method resolution
749            fn __component_effects_impl(&self, ctx: &rxtui::Context) -> Vec<rxtui::effect::Effect> {
750                vec![#(#effect_calls),*]
751            }
752        }
753    } else {
754        quote! {
755            // No effects defined, but still generate the method to shadow the trait
756            // This ensures consistent behavior whether effects are present or not
757            fn __component_effects_impl(&self, _ctx: &rxtui::Context) -> Vec<rxtui::effect::Effect> {
758                vec![]
759            }
760        }
761    };
762
763    let effects_item: ImplItem = syn::parse2(effects_method).unwrap();
764    impl_block.items.push(effects_item);
765
766    // Just return the impl block with the effects method
767    TokenStream::from(quote! { #impl_block })
768}