getset/
generate.rs

1use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
2use proc_macro_error2::abort;
3use syn::{
4    self, ext::IdentExt, spanned::Spanned, Expr, Field, Lit, Meta, MetaNameValue, Visibility,
5};
6
7use self::GenMode::{Get, GetClone, GetCopy, GetMut, Set, SetWith};
8use super::parse_attr;
9
10pub struct GenParams {
11    pub mode: GenMode,
12    pub global_attr: Option<Meta>,
13}
14
15#[derive(PartialEq, Eq, Copy, Clone)]
16pub enum GenMode {
17    Get,
18    GetClone,
19    GetCopy,
20    GetMut,
21    Set,
22    SetWith,
23}
24
25impl GenMode {
26    pub fn name(self) -> &'static str {
27        match self {
28            Get => "get",
29            GetClone => "get_clone",
30            GetCopy => "get_copy",
31            GetMut => "get_mut",
32            Set => "set",
33            SetWith => "set_with",
34        }
35    }
36
37    pub fn prefix(self) -> &'static str {
38        match self {
39            Get | GetClone | GetCopy | GetMut => "",
40            Set => "set_",
41            SetWith => "with_",
42        }
43    }
44
45    pub fn suffix(self) -> &'static str {
46        match self {
47            Get | GetClone | GetCopy | Set | SetWith => "",
48            GetMut => "_mut",
49        }
50    }
51
52    fn is_get(self) -> bool {
53        match self {
54            Get | GetClone | GetCopy | GetMut => true,
55            Set | SetWith => false,
56        }
57    }
58}
59
60// Helper function to extract string from Expr
61fn expr_to_string(expr: &Expr) -> Option<String> {
62    if let Expr::Lit(expr_lit) = expr {
63        if let Lit::Str(s) = &expr_lit.lit {
64            Some(s.value())
65        } else {
66            None
67        }
68    } else {
69        None
70    }
71}
72
73// Helper function to parse visibility
74fn parse_vis_str(s: &str, span: proc_macro2::Span) -> Visibility {
75    match syn::parse_str(s) {
76        Ok(vis) => vis,
77        Err(e) => abort!(span, "Invalid visibility found: {}", e),
78    }
79}
80
81// Helper function to parse visibility attribute
82pub fn parse_visibility(attr: Option<&Meta>, meta_name: &str) -> Option<Visibility> {
83    let meta = attr?;
84    let Meta::NameValue(MetaNameValue { value, path, .. }) = meta else {
85        return None;
86    };
87
88    if !path.is_ident(meta_name) {
89        return None;
90    }
91
92    let value_str = expr_to_string(value)?;
93    let vis_str = value_str.split(' ').find(|v| *v != "with_prefix")?;
94
95    Some(parse_vis_str(vis_str, value.span()))
96}
97
98/// Some users want legacy/compatibility.
99/// (Getters are often prefixed with `get_`)
100fn has_prefix_attr(f: &Field, params: &GenParams) -> bool {
101    // helper function to check if meta has `with_prefix` attribute
102    let meta_has_prefix = |meta: &Meta| -> bool {
103        if let Meta::NameValue(name_value) = meta {
104            if let Some(s) = expr_to_string(&name_value.value) {
105                return s.split(" ").any(|v| v == "with_prefix");
106            }
107        }
108        false
109    };
110
111    let field_attr_has_prefix = f
112        .attrs
113        .iter()
114        .filter_map(|attr| parse_attr(attr, params.mode))
115        .find(|meta| {
116            meta.path().is_ident("get")
117                || meta.path().is_ident("get_clone")
118                || meta.path().is_ident("get_copy")
119                || meta.path().is_ident("get_mut")
120        })
121        .as_ref()
122        .is_some_and(meta_has_prefix);
123
124    let global_attr_has_prefix = params.global_attr.as_ref().is_some_and(meta_has_prefix);
125
126    field_attr_has_prefix || global_attr_has_prefix
127}
128
129pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 {
130    let field_name = field
131        .ident
132        .clone()
133        .unwrap_or_else(|| abort!(field.span(), "Expected the field to have a name"));
134
135    let fn_name = if !has_prefix_attr(field, params)
136        && (params.mode.is_get())
137        && params.mode.suffix().is_empty()
138        && field_name.to_string().starts_with("r#")
139    {
140        field_name.clone()
141    } else {
142        Ident::new(
143            &format!(
144                "{}{}{}{}",
145                if has_prefix_attr(field, params) && (params.mode.is_get()) {
146                    "get_"
147                } else {
148                    ""
149                },
150                params.mode.prefix(),
151                field_name.unraw(),
152                params.mode.suffix()
153            ),
154            Span::call_site(),
155        )
156    };
157    let ty = field.ty.clone();
158
159    let doc = field.attrs.iter().filter(|v| v.meta.path().is_ident("doc"));
160
161    let attr = field
162        .attrs
163        .iter()
164        .filter_map(|v| parse_attr(v, params.mode))
165        .next_back()
166        .or_else(|| params.global_attr.clone());
167
168    let visibility = parse_visibility(attr.as_ref(), params.mode.name());
169    match attr {
170        // Generate nothing for skipped field
171        Some(meta) if meta.path().is_ident("skip") => quote! {},
172        Some(_) => match params.mode {
173            Get => {
174                quote! {
175                    #(#doc)*
176                    #[inline(always)]
177                    #visibility fn #fn_name(&self) -> &#ty {
178                        &self.#field_name
179                    }
180                }
181            }
182            GetClone => {
183                quote! {
184                    #(#doc)*
185                    #[inline(always)]
186                    #visibility fn #fn_name(&self) -> #ty {
187                        self.#field_name.clone()
188                    }
189                }
190            }
191            GetCopy => {
192                quote! {
193                    #(#doc)*
194                    #[inline(always)]
195                    #visibility fn #fn_name(&self) -> #ty {
196                        self.#field_name
197                    }
198                }
199            }
200            Set => {
201                quote! {
202                    #(#doc)*
203                    #[inline(always)]
204                    #visibility fn #fn_name(&mut self, val: #ty) -> &mut Self {
205                        self.#field_name = val;
206                        self
207                    }
208                }
209            }
210            GetMut => {
211                quote! {
212                    #(#doc)*
213                    #[inline(always)]
214                    #visibility fn #fn_name(&mut self) -> &mut #ty {
215                        &mut self.#field_name
216                    }
217                }
218            }
219            SetWith => {
220                quote! {
221                    #(#doc)*
222                    #[inline(always)]
223                    #visibility fn #fn_name(mut self, val: #ty) -> Self {
224                        self.#field_name = val;
225                        self
226                    }
227                }
228            }
229        },
230        None => quote! {},
231    }
232}
233
234pub fn implement_for_unnamed(field: &Field, params: &GenParams) -> TokenStream2 {
235    let doc = field.attrs.iter().filter(|v| v.meta.path().is_ident("doc"));
236    let attr = field
237        .attrs
238        .iter()
239        .filter_map(|v| parse_attr(v, params.mode))
240        .next_back()
241        .or_else(|| params.global_attr.clone());
242    let ty = field.ty.clone();
243    let visibility = parse_visibility(attr.as_ref(), params.mode.name());
244
245    match attr {
246        // Generate nothing for skipped field
247        Some(meta) if meta.path().is_ident("skip") => quote! {},
248        Some(_) => match params.mode {
249            Get => {
250                let fn_name = Ident::new("get", Span::call_site());
251                quote! {
252                    #(#doc)*
253                    #[inline(always)]
254                    #visibility fn #fn_name(&self) -> &#ty {
255                        &self.0
256                    }
257                }
258            }
259            GetClone => {
260                let fn_name = Ident::new("get", Span::call_site());
261                quote! {
262                    #(#doc)*
263                    #[inline(always)]
264                    #visibility fn #fn_name(&self) -> #ty {
265                        self.0.clone()
266                    }
267                }
268            }
269            GetCopy => {
270                let fn_name = Ident::new("get", Span::call_site());
271                quote! {
272                    #(#doc)*
273                    #[inline(always)]
274                    #visibility fn #fn_name(&self) -> #ty {
275                        self.0
276                    }
277                }
278            }
279            Set => {
280                let fn_name = Ident::new("set", Span::call_site());
281                quote! {
282                    #(#doc)*
283                    #[inline(always)]
284                    #visibility fn #fn_name(&mut self, val: #ty) -> &mut Self {
285                        self.0 = val;
286                        self
287                    }
288                }
289            }
290            GetMut => {
291                let fn_name = Ident::new("get_mut", Span::call_site());
292                quote! {
293                    #(#doc)*
294                    #[inline(always)]
295                    #visibility fn #fn_name(&mut self) -> &mut #ty {
296                        &mut self.0
297                    }
298                }
299            }
300            SetWith => {
301                let fn_name = Ident::new("set_with", Span::call_site());
302                quote! {
303                    #(#doc)*
304                    #[inline(always)]
305                    #visibility fn #fn_name(mut self, val: #ty) -> Self {
306                        self.0 = val;
307                        self
308                    }
309                }
310            }
311        },
312        None => quote! {},
313    }
314}