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};
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    Set,
20    GetMut,
21}
22
23impl GenMode {
24    pub fn name(self) -> &'static str {
25        match self {
26            Get => "get",
27            GetCopy => "get_copy",
28            Set => "set",
29            GetMut => "get_mut",
30        }
31    }
32
33    pub fn prefix(self) -> &'static str {
34        match self {
35            Get | GetCopy | GetMut => "",
36            Set => "set_",
37        }
38    }
39
40    pub fn suffix(self) -> &'static str {
41        match self {
42            Get | GetCopy | Set => "",
43            GetMut => "_mut",
44        }
45    }
46
47    fn is_get(self) -> bool {
48        match self {
49            GenMode::Get | GenMode::GetCopy | GenMode::GetMut => true,
50            GenMode::Set => false,
51        }
52    }
53}
54
55// Helper function to extract string from Expr
56fn expr_to_string(expr: &Expr) -> Option<String> {
57    if let Expr::Lit(expr_lit) = expr {
58        if let Lit::Str(s) = &expr_lit.lit {
59            Some(s.value())
60        } else {
61            None
62        }
63    } else {
64        None
65    }
66}
67
68// Helper function to parse visibility
69fn parse_vis_str(s: &str, span: proc_macro2::Span) -> Visibility {
70    match syn::parse_str(s) {
71        Ok(vis) => vis,
72        Err(e) => abort!(span, "Invalid visibility found: {}", e),
73    }
74}
75
76// Helper function to parse visibility attribute
77pub fn parse_visibility(attr: Option<&Meta>, meta_name: &str) -> Option<Visibility> {
78    let meta = attr?;
79    let Meta::NameValue(MetaNameValue { value, path, .. }) = meta else {
80        return None;
81    };
82
83    if !path.is_ident(meta_name) {
84        return None;
85    }
86
87    let value_str = expr_to_string(value)?;
88    let vis_str = value_str.split(' ').find(|v| *v != "with_prefix")?;
89
90    Some(parse_vis_str(vis_str, value.span()))
91}
92
93/// Some users want legacy/compatibility.
94/// (Getters are often prefixed with `get_`)
95fn has_prefix_attr(f: &Field, params: &GenParams) -> bool {
96    // helper function to check if meta has `with_prefix` attribute
97    let meta_has_prefix = |meta: &Meta| -> bool {
98        if let Meta::NameValue(name_value) = meta {
99            if let Some(s) = expr_to_string(&name_value.value) {
100                return s.split(" ").any(|v| v == "with_prefix");
101            }
102        }
103        false
104    };
105
106    let field_attr_has_prefix = f
107        .attrs
108        .iter()
109        .filter_map(|attr| parse_attr(attr, params.mode))
110        .find(|meta| meta.path().is_ident("get") || meta.path().is_ident("get_copy"))
111        .as_ref()
112        .map_or(false, meta_has_prefix);
113
114    let global_attr_has_prefix = params.global_attr.as_ref().map_or(false, meta_has_prefix);
115
116    field_attr_has_prefix || global_attr_has_prefix
117}
118
119pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 {
120    let field_name = field
121        .ident
122        .clone()
123        .unwrap_or_else(|| abort!(field.span(), "Expected the field to have a name"));
124
125    let fn_name = if !has_prefix_attr(field, params)
126        && (params.mode.is_get())
127        && params.mode.suffix().is_empty()
128        && field_name.to_string().starts_with("r#")
129    {
130        field_name.clone()
131    } else {
132        Ident::new(
133            &format!(
134                "{}{}{}{}",
135                if has_prefix_attr(field, params) && (params.mode.is_get()) {
136                    "get_"
137                } else {
138                    ""
139                },
140                params.mode.prefix(),
141                field_name.unraw(),
142                params.mode.suffix()
143            ),
144            Span::call_site(),
145        )
146    };
147    let ty = field.ty.clone();
148
149    let doc = field.attrs.iter().filter(|v| v.meta.path().is_ident("doc"));
150
151    let attr = field
152        .attrs
153        .iter()
154        .filter_map(|v| parse_attr(v, params.mode))
155        .last()
156        .or_else(|| params.global_attr.clone());
157
158    let visibility = parse_visibility(attr.as_ref(), params.mode.name());
159    match attr {
160        // Generate nothing for skipped field
161        Some(meta) if meta.path().is_ident("skip") => quote! {},
162        Some(_) => match params.mode {
163            GenMode::Get => {
164                quote! {
165                    #(#doc)*
166                    #[inline(always)]
167                    #visibility fn #fn_name(&self) -> &#ty {
168                        &self.#field_name
169                    }
170                }
171            }
172            GenMode::GetCopy => {
173                quote! {
174                    #(#doc)*
175                    #[inline(always)]
176                    #visibility fn #fn_name(&self) -> #ty {
177                        self.#field_name
178                    }
179                }
180            }
181            GenMode::Set => {
182                quote! {
183                    #(#doc)*
184                    #[inline(always)]
185                    #visibility fn #fn_name(&mut self, val: #ty) -> &mut Self {
186                        self.#field_name = val;
187                        self
188                    }
189                }
190            }
191            GenMode::GetMut => {
192                quote! {
193                    #(#doc)*
194                    #[inline(always)]
195                    #visibility fn #fn_name(&mut self) -> &mut #ty {
196                        &mut self.#field_name
197                    }
198                }
199            }
200        },
201        None => quote! {},
202    }
203}