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