rxtui_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{DeriveInput, Fields, parse_macro_input};
4
5/// Derive macro that implements the Component trait
6///
7/// This macro automatically implements all the boilerplate methods
8/// required by the Component trait.
9///
10/// By default, it looks for a field named `id`. You can specify a different
11/// field using the `#[component_id]` attribute.
12///
13/// # Example
14///
15/// ```ignore
16/// #[derive(Component, Clone)]
17/// struct MyComponent {
18///     id: Option<ComponentId>,  // Default field name
19///     // other fields...
20/// }
21///
22/// // Or with a custom field name:
23/// #[derive(Component, Clone)]
24/// struct MyComponent {
25///     #[component_id]
26///     key: Option<ComponentId>,
27///     // other fields...
28/// }
29///
30/// impl MyComponent {
31///     fn update(&self, ctx: &Context, msg: Box<dyn Message>, topic: Option<&str>) -> Action {
32///         // your implementation
33///     }
34///
35///     fn view(&self, ctx: &Context) -> Node {
36///         // your implementation
37///     }
38/// }
39/// ```
40#[proc_macro_derive(Component, attributes(component_id))]
41pub fn derive_component(input: TokenStream) -> TokenStream {
42    let input = parse_macro_input!(input as DeriveInput);
43    let name = &input.ident;
44
45    // Verify it's a struct
46    let fields = match &input.data {
47        syn::Data::Struct(data) => &data.fields,
48        _ => panic!("Component can only be derived for structs"),
49    };
50
51    // Find the ID field - either marked with #[component_id] or named "id"
52    let id_field =
53        match fields {
54            Fields::Named(fields) => {
55                // First, look for a field with #[component_id] attribute
56                let marked_field = fields.named.iter().find(|f| {
57                    f.attrs
58                        .iter()
59                        .any(|attr| attr.path().is_ident("component_id"))
60                });
61
62                if let Some(field) = marked_field {
63                    field
64                        .ident
65                        .as_ref()
66                        .expect("Named field should have an identifier")
67                } else {
68                    // Otherwise, look for a field named "id"
69                    fields.named.iter()
70                    .find(|f| f.ident.as_ref().is_some_and(|ident| ident == "id"))
71                    .and_then(|f| f.ident.as_ref())
72                    .expect(
73                        "Component derive requires either a field named `id: Option<ComponentId>` \
74                         or a field marked with `#[component_id]` attribute"
75                    )
76                }
77            }
78            _ => panic!("Component can only be derived for structs with named fields"),
79        };
80
81    // Generate the implementation
82    let expanded = quote! {
83        impl rxtui::Component for #name {
84            fn get_id(&self) -> Option<rxtui::ComponentId> {
85                self.#id_field.clone()
86            }
87
88            fn set_id(&mut self, id: rxtui::ComponentId) {
89                self.#id_field = Some(id);
90            }
91
92            fn as_any(&self) -> &dyn std::any::Any {
93                self
94            }
95
96            fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
97                self
98            }
99
100            fn clone_box(&self) -> Box<dyn rxtui::Component> {
101                Box::new(self.clone())
102            }
103
104            // Forward to the user's implementations
105            fn update(&self, ctx: &rxtui::Context, msg: Box<dyn rxtui::Message>, topic: Option<&str>) -> rxtui::Action {
106                <#name>::update(self, ctx, msg, topic)
107            }
108
109            fn view(&self, ctx: &rxtui::Context) -> rxtui::Node {
110                <#name>::view(self, ctx)
111            }
112        }
113    };
114
115    TokenStream::from(expanded)
116}