syncthing_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::DeriveInput;
4
5fn is_required(attrs: &[syn::Attribute]) -> bool {
6    attrs.iter().any(|attr| attr.path().is_ident("required"))
7}
8
9fn get_rename(attrs: &[syn::Attribute]) -> Option<proc_macro2::TokenStream> {
10    for attr in attrs {
11        // Only interested in attributes that look like #[serde(...)]
12        if attr.path().is_ident("serde") {
13            if let syn::Attribute {
14                meta: syn::Meta::List(syn::MetaList { tokens, .. }),
15                ..
16            } = attr
17            {
18                let tokens: Vec<proc_macro2::TokenTree> = tokens.clone().into_iter().collect();
19                if let Some(proc_macro2::TokenTree::Ident(ident)) = tokens.first() {
20                    if *ident == "rename" {
21                        if let Some(proc_macro2::TokenTree::Literal(name)) = tokens.get(2) {
22                            // Convert the literal into a string like "\"bar\""
23                            let value = name.to_string();
24
25                            // Parse that string back into a syn::Lit (which handles quotes properly)
26                            if let Ok(lit) = syn::parse_str::<syn::LitStr>(&value) {
27                                return Some(quote! { #[serde(rename = #lit)] });
28                            }
29                        }
30                    }
31                }
32            }
33        }
34    }
35    None
36}
37
38fn get_rename_all(attrs: &[syn::Attribute]) -> Option<proc_macro2::TokenStream> {
39    for attr in attrs {
40        // Only interested in attributes that look like #[serde(...)]
41        if attr.path().is_ident("serde") {
42            if let syn::Attribute {
43                meta: syn::Meta::List(syn::MetaList { tokens, .. }),
44                ..
45            } = attr
46            {
47                let tokens: Vec<proc_macro2::TokenTree> = tokens.clone().into_iter().collect();
48                if let Some(proc_macro2::TokenTree::Ident(ident)) = tokens.first() {
49                    if *ident == "rename_all" {
50                        if let Some(proc_macro2::TokenTree::Literal(name)) = tokens.get(2) {
51                            // Convert the literal into a string like "\"bar\""
52                            let value = name.to_string();
53
54                            // Parse that string back into a syn::Lit (which handles quotes properly)
55                            if let Ok(lit) = syn::parse_str::<syn::LitStr>(&value) {
56                                return Some(quote! { #[serde(rename_all = #lit)] });
57                            }
58                        }
59                    }
60                }
61            }
62        }
63    }
64    None
65}
66
67#[proc_macro_derive(New, attributes(required))]
68pub fn derive(input: TokenStream) -> TokenStream {
69    let input = syn::parse_macro_input!(input as DeriveInput);
70    let ident = &input.ident;
71    let builder_ident = syn::Ident::new(&format!("New{}", ident), ident.span());
72
73    let fields = if let syn::Data::Struct(syn::DataStruct {
74        fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
75        ..
76    }) = input.data
77    {
78        named.iter()
79    } else {
80        unimplemented!()
81    };
82
83    let new_fields = fields.clone().map(|field| {
84        let name = &field.ident;
85        if !is_required(&field.attrs) {
86            quote! {#name: std::option::Option::None}
87        } else {
88            quote! {#name}
89        }
90    });
91
92    let required_args = fields.clone().filter_map(|field| {
93        if is_required(&field.attrs) {
94            let name = &field.ident;
95            let ty = &field.ty;
96            Some(quote! {#name: #ty})
97        } else {
98            None
99        }
100    });
101
102    let options = fields.clone().map(|field| {
103        let name = &field.ident;
104        let ty = &field.ty;
105        let rename = get_rename(&field.attrs);
106        if !is_required(&field.attrs) {
107            quote! {
108            #rename
109            #[serde(skip_serializing_if = "Option::is_none")]
110            #name: std::option::Option<#ty>}
111        } else {
112            quote! {
113                #rename
114                #name: #ty
115            }
116        }
117    });
118
119    let funcs = fields.clone().map(|field| {
120        let name = &field.ident;
121        if let Some(name) = name {
122            let getter = format!("get_{}", name);
123            let getter = proc_macro2::Ident::new(&getter, name.span());
124            let ty = &field.ty;
125            if !is_required(&field.attrs) {
126                quote! {
127                    pub fn #name(mut self, #name: #ty) -> Self {
128                        self.#name = std::option::Option::Some(#name);
129                        self
130                    }
131                        pub fn #getter(&self) -> &std::option::Option<#ty> {
132                            &self.#name
133                        }
134                }
135            } else {
136                quote! {
137                    pub fn #name(mut self, #name: #ty) -> Self {
138                        self.#name = #name;
139                        self
140                    }
141
142                    pub fn #getter(&self) -> &#ty {
143                        &self.#name
144                    }
145                }
146            }
147        } else {
148            // TODO raise error
149            quote! {}
150        }
151    });
152
153    let from = fields.clone().map(|field| {
154        let name = &field.ident;
155        if !is_required(&field.attrs) {
156            quote! { #name: Some(value.#name) }
157        } else {
158            quote! { #name: value.#name }
159        }
160    });
161
162    let rename_all = get_rename_all(&input.attrs);
163
164    let expanded = quote! {
165        #[derive(Clone, Debug, PartialEq, serde::Serialize)]
166        #rename_all
167        pub struct #builder_ident {
168            #(#options),*
169        }
170
171        impl #builder_ident {
172            pub fn new(#(#required_args),*) -> Self {
173                Self {
174                    #(#new_fields),*
175                }
176            }
177
178            #(#funcs)*
179        }
180
181        impl From<#ident> for #builder_ident {
182            fn from(value: #ident) -> Self {
183                Self {
184                    #(#from),*
185                }
186            }
187
188        }
189    };
190
191    expanded.into()
192}
193
194#[cfg(test)]
195mod tests {
196    #[test]
197    fn test_name() {
198        let t = trybuild::TestCases::new();
199        t.pass("tests/ui/*.rs");
200    }
201}