tinc_derive/
message_tracker.rs

1use proc_macro2::TokenStream;
2use quote::quote;
3use syn::spanned::Spanned;
4
5struct TincContainerOptions {
6    pub crate_path: syn::Path,
7    pub tagged: bool,
8}
9
10impl TincContainerOptions {
11    fn from_attributes<'a>(attrs: impl IntoIterator<Item = &'a syn::Attribute>) -> syn::Result<Self> {
12        let mut crate_ = None;
13        let mut tagged = false;
14
15        for attr in attrs {
16            let syn::Meta::List(list) = &attr.meta else {
17                continue;
18            };
19
20            if list.path.is_ident("tinc") {
21                list.parse_nested_meta(|meta| {
22                    if meta.path.is_ident("crate") {
23                        if crate_.is_some() {
24                            return Err(meta.error("crate option already set"));
25                        }
26
27                        let _: syn::token::Eq = meta.input.parse()?;
28                        let path: syn::LitStr = meta.input.parse()?;
29                        crate_ = Some(syn::parse_str(&path.value())?);
30                    } else if meta.path.is_ident("tagged") {
31                        tagged = true;
32                    } else {
33                        return Err(meta.error("unsupported attribute"));
34                    }
35
36                    Ok(())
37                })?;
38            }
39        }
40
41        let mut options = TincContainerOptions::default();
42        if let Some(crate_) = crate_ {
43            options.crate_path = crate_;
44        }
45
46        if tagged {
47            options.tagged = true;
48        }
49
50        Ok(options)
51    }
52}
53
54impl Default for TincContainerOptions {
55    fn default() -> Self {
56        Self {
57            crate_path: syn::parse_str("::tinc").unwrap(),
58            tagged: false,
59        }
60    }
61}
62
63#[derive(Default)]
64struct TincFieldOptions {
65    pub enum_path: Option<syn::Path>,
66    pub oneof: bool,
67}
68
69impl TincFieldOptions {
70    fn from_attributes<'a>(attrs: impl IntoIterator<Item = &'a syn::Attribute>) -> syn::Result<Self> {
71        let mut enum_ = None;
72        let mut oneof = false;
73
74        for attr in attrs {
75            let syn::Meta::List(list) = &attr.meta else {
76                continue;
77            };
78
79            if list.path.is_ident("tinc") {
80                list.parse_nested_meta(|meta| {
81                    if meta.path.is_ident("enum") {
82                        if enum_.is_some() {
83                            return Err(meta.error("enum option already set"));
84                        }
85
86                        let _: syn::token::Eq = meta.input.parse()?;
87                        let path: syn::LitStr = meta.input.parse()?;
88                        enum_ = Some(syn::parse_str(&path.value())?);
89                    } else if meta.path.is_ident("oneof") {
90                        oneof = true;
91                    } else {
92                        return Err(meta.error("unsupported attribute"));
93                    }
94
95                    Ok(())
96                })?;
97            }
98        }
99
100        let mut options = TincFieldOptions::default();
101        if let Some(enum_) = enum_ {
102            options.enum_path = Some(enum_);
103        }
104
105        if oneof {
106            options.oneof = true;
107        }
108
109        Ok(options)
110    }
111}
112
113pub(crate) fn derive_message_tracker(input: TokenStream) -> TokenStream {
114    let input = match syn::parse2::<syn::DeriveInput>(input) {
115        Ok(input) => input,
116        Err(e) => return e.to_compile_error(),
117    };
118
119    let opts = match TincContainerOptions::from_attributes(&input.attrs) {
120        Ok(options) => options,
121        Err(e) => return e.to_compile_error(),
122    };
123
124    match &input.data {
125        syn::Data::Struct(data) => derive_message_tracker_struct(input.ident, opts, data),
126        syn::Data::Enum(data) => derive_message_tracker_enum(input.ident, opts, data),
127        _ => syn::Error::new(input.span(), "Tracker can only be derived for structs or enums").into_compile_error(),
128    }
129}
130
131fn derive_message_tracker_struct(ident: syn::Ident, opts: TincContainerOptions, data: &syn::DataStruct) -> TokenStream {
132    let TincContainerOptions { crate_path, tagged } = opts;
133    if tagged {
134        return syn::Error::new(ident.span(), "tagged can only be used on enums").into_compile_error();
135    }
136
137    let syn::Fields::Named(fields) = &data.fields else {
138        return syn::Error::new(ident.span(), "Tracker can only be derived for structs with named fields")
139            .into_compile_error();
140    };
141
142    let tracker_ident = syn::Ident::new(&format!("{ident}Tracker"), ident.span());
143
144    let struct_fields = fields
145        .named
146        .iter()
147        .map(|f| {
148            let field_ident = f.ident.as_ref().expect("field must have an identifier");
149            let ty = &f.ty;
150
151            let TincFieldOptions { enum_path, oneof } = TincFieldOptions::from_attributes(&f.attrs)?;
152
153            if enum_path.is_some() && oneof {
154                return Err(syn::Error::new(f.span(), "enum and oneof cannot both be set"));
155            }
156
157            let ty = match enum_path {
158                Some(enum_path) => quote! { <#ty as #crate_path::__private::EnumHelper>::Target<#enum_path> },
159                None if oneof => quote! { <#ty as #crate_path::__private::OneOfHelper>::Target },
160                None => quote! { #ty },
161            };
162
163            Ok(quote! {
164                pub #field_ident: Option<<#ty as #crate_path::__private::TrackerFor>::Tracker>
165            })
166        })
167        .collect::<syn::Result<Vec<_>>>();
168
169    let struct_fields = match struct_fields {
170        Ok(fields) => fields,
171        Err(e) => return e.to_compile_error(),
172    };
173
174    quote! {
175        #[allow(clippy::all, dead_code, unused_imports, unused_variables, unused_parens)]
176        const _: () = {
177            #[derive(Debug, Default)]
178            pub struct #tracker_ident {
179                #(#struct_fields),*
180            }
181
182            impl #crate_path::__private::Tracker for #tracker_ident {
183                type Target = #ident;
184
185                #[inline(always)]
186                fn allow_duplicates(&self) -> bool {
187                    true
188                }
189            }
190
191            impl #crate_path::__private::TrackerFor for #ident {
192                type Tracker = #crate_path::__private::StructTracker<#tracker_ident>;
193            }
194        };
195    }
196}
197
198fn derive_message_tracker_enum(ident: syn::Ident, opts: TincContainerOptions, data: &syn::DataEnum) -> TokenStream {
199    let TincContainerOptions { crate_path, tagged } = opts;
200    let tracker_ident = syn::Ident::new(&format!("{ident}Tracker"), ident.span());
201
202    let variants = data
203        .variants
204        .iter()
205        .map(|v| {
206            let variant_ident = &v.ident;
207            let syn::Fields::Unnamed(unnamed) = &v.fields else {
208                return Err(syn::Error::new(
209                    v.span(),
210                    "Tracker can only be derived for enums with unnamed variants",
211                ));
212            };
213
214            if unnamed.unnamed.len() != 1 {
215                return Err(syn::Error::new(
216                    v.span(),
217                    "Tracker can only be derived for enums with a single field variants",
218                ));
219            }
220
221            let field = &unnamed.unnamed[0];
222            let ty = &field.ty;
223
224            let TincFieldOptions { enum_path, oneof } =
225                TincFieldOptions::from_attributes(v.attrs.iter().chain(field.attrs.iter()))?;
226
227            if oneof {
228                return Err(syn::Error::new(
229                    v.span(),
230                    "oneof can only be used on struct fields, not on enum variants",
231                ));
232            }
233
234            let ty = match enum_path {
235                Some(enum_path) => quote! {
236                    <#ty as #crate_path::__private::EnumHelper>::Target<#enum_path>
237                },
238                None => quote! {
239                    #ty
240                },
241            };
242
243            Ok((
244                quote! {
245                    #variant_ident(<#ty as #crate_path::__private::TrackerFor>::Tracker)
246                },
247                quote! {
248                    #variant_ident
249                },
250            ))
251        })
252        .collect::<syn::Result<(Vec<_>, Vec<_>)>>();
253
254    let (variants, variant_idents) = match variants {
255        Ok(variants) => variants,
256        Err(e) => return e.to_compile_error(),
257    };
258
259    let tracker = if tagged {
260        quote! {
261            #crate_path::__private::TaggedOneOfTracker<#tracker_ident>
262        }
263    } else {
264        quote! {
265            #crate_path::__private::OneOfTracker<#tracker_ident>
266        }
267    };
268
269    quote! {
270        #[allow(clippy::all, dead_code, unused_imports, unused_variables, unused_parens)]
271        const _: () = {
272            #[derive(std::fmt::Debug)]
273            pub enum #tracker_ident {
274                #(#variants),*
275            }
276
277            impl #crate_path::__private::Tracker for #tracker_ident {
278                type Target = #ident;
279
280                #[inline(always)]
281                fn allow_duplicates(&self) -> bool {
282                    match self {
283                        #(Self::#variant_idents(v) => v.allow_duplicates()),*
284                    }
285                }
286            }
287
288            impl #crate_path::__private::TrackerFor for #ident {
289                type Tracker = #tracker;
290            }
291        };
292    }
293}