leptos_struct_component_macro/
lib.rs

1//! Define [Leptos](https://leptos.dev/) components using structs.
2
3extern crate proc_macro;
4
5use proc_macro2::TokenStream;
6use quote::quote;
7use syn::{
8    AttrStyle, Attribute, Data, DeriveInput, Expr, ExprArray, GenericArgument, Ident, LitBool,
9    LitStr, Meta, PathArguments, Type, parse_macro_input, spanned::Spanned,
10};
11
12#[derive(Debug, Default)]
13struct StructComponentAttrArgs {
14    tag: Option<String>,
15    dynamic_tag: Option<Vec<(Expr, String)>>,
16    no_children: Option<bool>,
17}
18
19fn parse_struct_component_attr(attr: &Attribute) -> Result<StructComponentAttrArgs, syn::Error> {
20    if !matches!(attr.style, AttrStyle::Outer) {
21        Err(syn::Error::new(attr.span(), "not an inner attribute"))
22    } else if let Meta::List(list) = &attr.meta {
23        let mut args = StructComponentAttrArgs::default();
24
25        list.parse_nested_meta(|meta| {
26            if meta.path.is_ident("tag") {
27                let value = meta.value().and_then(|value| value.parse::<LitStr>())?;
28
29                args.tag = Some(value.value());
30
31                Ok(())
32            } else if meta.path.is_ident("dynamic_tag") {
33                let value = meta.value().and_then(|value| value.parse::<ExprArray>())?;
34
35                args.dynamic_tag = Some(
36                    value
37                        .elems
38                        .into_iter()
39                        .filter_map(|elem| match &elem {
40                            Expr::Path(path) => path.path.segments.last().map(|segment| {
41                                (elem.clone(), segment.ident.to_string().to_lowercase())
42                            }),
43                            _ => None,
44                        })
45                        .collect(),
46                );
47
48                Ok(())
49            } else if meta.path.is_ident("no_children") {
50                let value = meta.value().and_then(|value| value.parse::<LitBool>())?;
51
52                args.no_children = Some(value.value());
53
54                Ok(())
55            } else {
56                Err(meta.error("unknown property"))
57            }
58        })?;
59
60        Ok(args)
61    } else {
62        Err(syn::Error::new(attr.span(), "not a list"))
63    }
64}
65
66#[proc_macro_attribute]
67pub fn struct_component(
68    _attr: proc_macro::TokenStream,
69    item: proc_macro::TokenStream,
70) -> proc_macro::TokenStream {
71    item
72}
73
74#[proc_macro_derive(StructComponent, attributes(struct_component))]
75pub fn derive_struct_component(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
76    let derive_input = parse_macro_input!(input as DeriveInput);
77
78    let mut args = StructComponentAttrArgs::default();
79    for attr in &derive_input.attrs {
80        if attr.path().is_ident("struct_component") {
81            match parse_struct_component_attr(attr) {
82                Ok(result) => {
83                    args = result;
84                }
85                Err(error) => {
86                    return error.to_compile_error().into();
87                }
88            }
89        }
90    }
91
92    if let Data::Struct(data_struct) = &derive_input.data {
93        let ident = derive_input.ident.clone();
94
95        let mut attributes: Vec<TokenStream> = vec![];
96        // let mut attribute_checked: Option<TokenStream> = None;
97        // let mut attribute_value: Option<TokenStream> = None;
98        let mut listeners: Vec<TokenStream> = vec![];
99        // let mut attributes_map: Option<TokenStream> = None;
100        let mut dynamic_tag: Option<(Ident, Vec<(Expr, String)>)> = None;
101        let mut node_ref: Option<TokenStream> = None;
102
103        for field in &data_struct.fields {
104            if let Some(ident) = &field.ident {
105                if let Some(attr) = field
106                    .attrs
107                    .iter()
108                    .find(|attr| attr.path().is_ident("struct_component"))
109                {
110                    match parse_struct_component_attr(attr) {
111                        Ok(args) => {
112                            if let Some(tags) = args.dynamic_tag {
113                                dynamic_tag = Some((ident.clone(), tags));
114
115                                continue;
116                            }
117                        }
118                        Err(error) => {
119                            return error.to_compile_error().into();
120                        }
121                    }
122                }
123
124                if ident == "attributes" {
125                    // TODO: dynamic attributes
126
127                    //     attributes_map = Some(quote! {
128                    //         .chain(
129                    //             self.attributes
130                    //                 .into_iter()
131                    //                 .flatten()
132                    //                 .flat_map(|(key, value)| value.map(|value| (
133                    //                     ::yew::virtual_dom::AttrValue::from(key),
134                    //                     ::yew::virtual_dom::AttributeOrProperty::Attribute(AttrValue::from(value)),
135                    //                 )),
136                    //             ),
137                    //         )
138                    //     });
139
140                    continue;
141                }
142
143                if ident == "node_ref" {
144                    node_ref = Some(quote! {
145                        .node_ref(self.node_ref)
146                    });
147
148                    continue;
149                }
150
151                if ident.to_string().starts_with("on") {
152                    if let Type::Path(path) = &field.ty {
153                        let event = ident
154                            .to_string()
155                            .strip_prefix("on")
156                            .expect("String should start with `on`.")
157                            .parse::<TokenStream>()
158                            .expect("String should parse as TokenStream.");
159
160                        let first = path.path.segments.first();
161                        let first_argument = first.and_then(|segment| match &segment.arguments {
162                            PathArguments::None => None,
163                            PathArguments::AngleBracketed(arguments) => {
164                                arguments.args.first().and_then(|arg| match arg {
165                                    GenericArgument::Type(Type::Path(path)) => {
166                                        path.path.segments.first()
167                                    }
168                                    _ => None,
169                                })
170                            }
171                            PathArguments::Parenthesized(_) => None,
172                        });
173
174                        if first.is_some_and(|segment| segment.ident == "Callback") {
175                            listeners.push(quote! {
176                                .on(::leptos::tachys::html::event::#event, move |event| {
177                                    self.#ident.run(event);
178                                })
179                            });
180
181                            continue;
182                        } else if first.is_some_and(|segment| segment.ident == "Option")
183                            && first_argument.is_some_and(|argument| argument.ident == "Callback")
184                        {
185                            listeners.push(quote! {
186                                .on(::leptos::tachys::html::event::#event, move |event| {
187                                    if let Some(listener) = &self.#ident {
188                                        listener.run(event);
189                                    }
190                                })
191                            });
192
193                            continue;
194                        }
195                    }
196                }
197
198                match &field.ty {
199                    Type::Path(path) => {
200                        let first = path.path.segments.first();
201
202                        attributes.push(
203                            if first.is_some_and(|segment| segment.ident == "MaybeProp") {
204                                quote! {
205                                    .#ident(move || self.#ident.get())
206                                }
207                            } else {
208                                quote! {
209                                    .#ident(self.#ident)
210                                }
211                            },
212                        );
213                    }
214                    _ => {
215                        return syn::Error::new(field.ty.span(), "expected type path")
216                            .to_compile_error()
217                            .into();
218                    }
219                }
220            }
221        }
222
223        let arguments = if args.no_children.unwrap_or(false) {
224            quote! {
225                self
226            }
227        } else {
228            quote! {
229                self, children: Option<::leptos::prelude::Children>
230            }
231        };
232
233        let children = (!args.no_children.unwrap_or(false)).then(|| {
234            quote! {
235                .child(children.map(|children| children()))
236            }
237        });
238
239        let tag_methods = quote! {
240            // TODO: dynamic attributes
241
242            #node_ref
243                #(#attributes)*
244                #(#listeners)*
245                #children
246                .into_any()
247        };
248
249        if let Some((tag_ident, tags)) = dynamic_tag {
250            let exprs = tags.iter().map(|(expr, _)| expr).collect::<Vec<_>>();
251            let tags = tags
252                .iter()
253                .map(|(_, tag)| {
254                    let tag = format!("::leptos::html::{tag}()")
255                        .parse::<TokenStream>()
256                        .expect("String should parse as TokenStream.");
257
258                    quote! {
259                        #tag
260                        #tag_methods
261                    }
262                })
263                .collect::<Vec<_>>();
264
265            quote! {
266                impl #ident {
267                    pub fn render(#arguments) -> ::leptos::tachys::view::any_view::AnyView {
268                        match self.#tag_ident {
269                            #(#exprs => #tags,)*
270                        }
271                    }
272                }
273            }
274            .into()
275        } else if let Some(tag) = args.tag {
276            let tag = format!("::leptos::html::{tag}()")
277                .parse::<TokenStream>()
278                .expect("String should parse as TokenStream.");
279
280            quote! {
281                impl #ident {
282                    pub fn render(#arguments) -> ::leptos::tachys::view::any_view::AnyView {
283                        #tag
284                        #tag_methods
285                    }
286                }
287            }
288            .into()
289        } else {
290            return syn::Error::new(derive_input.span(), "`#[struct_component(tag = \"\")] or #[struct_component(dynamic_tag = true)]` is required")
291                    .to_compile_error()
292                    .into();
293        }
294    } else {
295        syn::Error::new(derive_input.span(), "expected struct")
296            .to_compile_error()
297            .into()
298    }
299}