1use proc_macro2::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4 Attribute, Data, DataEnum, DeriveInput, Error, Field, Fields, Index, Member, Path,
5 parse_macro_input, parse_quote,
6};
7
8#[proc_macro_derive(TypeSift, attributes(typesift))]
22pub fn derive_type_sift(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
23 let input = parse_macro_input!(input as DeriveInput);
24 expand(input)
25 .unwrap_or_else(Error::into_compile_error)
26 .into()
27}
28
29fn expand(mut input: DeriveInput) -> syn::Result<TokenStream> {
30 reject_typesift_attributes(&input.attrs)?;
31
32 if let Some(lifetime) = input.generics.lifetimes().next() {
33 return Err(Error::new_spanned(
34 lifetime,
35 "`TypeSift` requires `'static` types, so it cannot be derived for types with lifetime parameters",
36 ));
37 }
38
39 let body = match &input.data {
40 Data::Struct(data) => visit_struct(&data.fields)?,
41 Data::Enum(data) => visit_enum(data)?,
42 Data::Union(data) => {
43 return Err(Error::new_spanned(
44 data.union_token,
45 "`TypeSift` cannot be derived for unions",
46 ));
47 }
48 };
49
50 for param in input.generics.type_params_mut() {
51 param.bounds.push(parse_quote!(::typesift::TypeSift));
52 }
53
54 let name = &input.ident;
55 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
56
57 Ok(quote! {
59 impl #impl_generics ::typesift::TypeSift for #name #ty_generics #where_clause {
60 fn visit<'__a, __T: 'static, __B, __F>(
61 &'__a self,
62 __visitor: &mut __F,
63 ) -> ::core::ops::ControlFlow<__B>
64 where
65 __F: ::core::ops::FnMut(&'__a __T) -> ::core::ops::ControlFlow<__B>,
66 {
67 ::typesift::TypeSift::visit_self::<__T, __B, __F>(self, __visitor)?;
68 #body
69 }
70 }
71 })
72}
73
74fn visit_struct(fields: &Fields) -> syn::Result<TokenStream> {
75 let mut visits = Vec::new();
76 for (index, field) in fields.iter().enumerate() {
77 let mode = field_mode(field)?;
78 if matches!(mode, FieldMode::Skip) {
79 continue;
80 }
81 let member = match &field.ident {
82 Some(ident) => Member::Named(ident.clone()),
83 None => Member::Unnamed(Index::from(index)),
84 };
85 visits.push(mode.apply("e!(&self.#member)));
86 }
87
88 Ok(quote! {
89 #(#visits)*
90 ::core::ops::ControlFlow::Continue(())
91 })
92}
93
94fn visit_enum(data: &DataEnum) -> syn::Result<TokenStream> {
95 if data.variants.is_empty() {
96 return Ok(quote!(match *self {}));
97 }
98
99 let mut arms = Vec::new();
100 for variant in &data.variants {
101 reject_typesift_attributes(&variant.attrs)?;
102
103 let mut patterns = Vec::new();
105 let mut visits = Vec::new();
106 for (index, field) in variant.fields.iter().enumerate() {
107 let mode = field_mode(field)?;
108 if matches!(mode, FieldMode::Skip) {
109 patterns.push(quote!(_));
110 } else {
111 let binding = format_ident!("__field{}", index);
112 visits.push(mode.apply("e!(#binding)));
113 patterns.push(quote!(#binding));
114 }
115 }
116
117 let variant_name = &variant.ident;
118 let pattern = match &variant.fields {
119 Fields::Named(fields) => {
120 let names = fields.named.iter().map(|field| &field.ident);
121 quote!(Self::#variant_name { #(#names: #patterns),* })
122 }
123 Fields::Unnamed(_) => quote!(Self::#variant_name(#(#patterns),*)),
124 Fields::Unit => quote!(Self::#variant_name),
125 };
126
127 arms.push(quote! {
128 #pattern => {
129 #(#visits)*
130 ::core::ops::ControlFlow::Continue(())
131 }
132 });
133 }
134
135 Ok(quote! {
136 match self {
137 #(#arms)*
138 }
139 })
140}
141
142enum FieldMode {
144 Visit,
146 Skip,
148 Leaf,
150 With(Path),
152}
153
154impl FieldMode {
155 fn name(&self) -> &'static str {
157 match self {
158 FieldMode::Visit => "",
159 FieldMode::Skip => "skip",
160 FieldMode::Leaf => "leaf",
161 FieldMode::With(_) => "with",
162 }
163 }
164
165 fn apply(&self, field: &TokenStream) -> TokenStream {
168 match self {
169 FieldMode::Visit => quote! {
170 ::typesift::TypeSift::visit::<__T, __B, __F>(#field, __visitor)?;
171 },
172 FieldMode::Leaf => quote! {
175 if let ::core::option::Option::Some(__matched) =
176 (#field as &dyn ::core::any::Any).downcast_ref::<__T>()
177 {
178 __visitor(__matched)?;
179 }
180 },
181 FieldMode::With(path) => quote! {
183 #path(
184 #field,
185 &mut ::typesift::Sift::<__T, __B, __F>::new(&mut *__visitor),
186 )?;
187 },
188 FieldMode::Skip => TokenStream::new(),
189 }
190 }
191}
192
193fn field_mode(field: &Field) -> syn::Result<FieldMode> {
195 let mut mode = FieldMode::Visit;
196 for attr in typesift_attributes(&field.attrs) {
197 attr.parse_nested_meta(|meta| {
198 let found = if meta.path.is_ident("skip") {
199 FieldMode::Skip
200 } else if meta.path.is_ident("leaf") {
201 FieldMode::Leaf
202 } else if meta.path.is_ident("with") {
203 FieldMode::With(meta.value()?.parse()?)
204 } else {
205 return Err(
206 meta.error("unknown `typesift` argument, expected `skip`, `leaf` or `with`")
207 );
208 };
209
210 let conflict = match (&mode, &found) {
211 (FieldMode::Visit, _) => None,
212 (existing, found) if existing.name() == found.name() => {
213 Some(format!("duplicate `{}`", found.name()))
214 }
215 (existing, found) => Some(format!(
216 "`{}` and `{}` cannot be combined",
217 existing.name(),
218 found.name()
219 )),
220 };
221
222 match conflict {
223 Some(message) => Err(meta.error(message)),
224 None => {
225 mode = found;
226 Ok(())
227 }
228 }
229 })?;
230 }
231 Ok(mode)
232}
233
234fn reject_typesift_attributes(attrs: &[Attribute]) -> syn::Result<()> {
236 match typesift_attributes(attrs).next() {
237 Some(attr) => Err(Error::new_spanned(
238 attr,
239 "`#[typesift(...)]` is only allowed on fields",
240 )),
241 None => Ok(()),
242 }
243}
244
245fn typesift_attributes(attrs: &[Attribute]) -> impl Iterator<Item = &Attribute> {
246 attrs.iter().filter(|attr| attr.path().is_ident("typesift"))
247}
248
249#[cfg(test)]
250mod tests {
251 use syn::{DeriveInput, parse_quote};
252
253 use super::expand;
254
255 fn error_message(input: DeriveInput) -> String {
256 expand(input)
257 .expect_err("the derive should fail")
258 .to_string()
259 }
260
261 fn generated_code(input: DeriveInput) -> String {
262 expand(input)
263 .expect("the derive should succeed")
264 .to_string()
265 }
266
267 #[test]
268 fn typesift_attribute_is_rejected_outside_fields() {
269 let on_struct = parse_quote! {
270 #[typesift(skip)]
271 struct Session {
272 user: u64,
273 }
274 };
275 assert_eq!(
276 error_message(on_struct),
277 "`#[typesift(...)]` is only allowed on fields"
278 );
279
280 let on_variant = parse_quote! {
281 enum Event {
282 #[typesift(leaf)]
283 Login(u64),
284 }
285 };
286 assert_eq!(
287 error_message(on_variant),
288 "`#[typesift(...)]` is only allowed on fields"
289 );
290 }
291
292 #[test]
293 fn arguments_are_known_and_appear_once() {
294 let unknown = parse_quote! {
295 struct Session {
296 #[typesift(rename)]
297 user: u64,
298 }
299 };
300 assert_eq!(
301 error_message(unknown),
302 "unknown `typesift` argument, expected `skip`, `leaf` or `with`"
303 );
304
305 let duplicate_skip = parse_quote! {
306 struct Session {
307 #[typesift(skip, skip)]
308 user: u64,
309 }
310 };
311 assert_eq!(error_message(duplicate_skip), "duplicate `skip`");
312
313 let duplicate_leaf = parse_quote! {
314 struct Session {
315 #[typesift(leaf)]
316 #[typesift(leaf)]
317 user: u64,
318 }
319 };
320 assert_eq!(error_message(duplicate_leaf), "duplicate `leaf`");
321
322 let combined = parse_quote! {
323 enum Event {
324 Login(#[typesift(skip, leaf)] u64),
325 }
326 };
327 assert_eq!(
328 error_message(combined),
329 "`skip` and `leaf` cannot be combined"
330 );
331
332 let without_arguments = parse_quote! {
333 struct Session {
334 #[typesift]
335 user: u64,
336 }
337 };
338 assert!(error_message(without_arguments).contains("expected attribute arguments"));
339 }
340
341 #[test]
342 fn skipped_fields_are_left_out_of_the_generated_code() {
343 let generated = generated_code(parse_quote! {
344 struct Session {
345 user: u64,
346 #[typesift(skip)]
347 cache: Cache,
348 }
349 });
350 assert!(generated.contains("user"));
351 assert!(!generated.contains("cache"));
352
353 let generated = generated_code(parse_quote! {
354 enum Event {
355 Login { user: u64, #[typesift(skip)] token: Token },
356 }
357 });
358 assert!(generated.contains("token : _"));
359 }
360
361 #[test]
362 fn leaf_fields_are_checked_by_type_instead_of_visited() {
363 let generated = generated_code(parse_quote! {
364 struct Invoice {
365 #[typesift(leaf)]
366 id: Uuid,
367 amount: u64,
368 }
369 });
370 assert!(generated.contains("downcast_ref"));
371 assert!(generated.contains("self . id"));
372 assert!(!generated.contains("visit :: < __T , __B , __F > (& self . id"));
374 assert!(generated.contains("visit :: < __T , __B , __F > (& self . amount"));
375 }
376
377 #[test]
378 fn with_takes_a_path_and_no_other_argument() {
379 let duplicate = parse_quote! {
380 struct Request {
381 #[typesift(with = visit_headers, with = visit_headers)]
382 headers: Headers,
383 }
384 };
385 assert_eq!(error_message(duplicate), "duplicate `with`");
386
387 let combined = parse_quote! {
388 struct Request {
389 #[typesift(skip, with = visit_headers)]
390 headers: Headers,
391 }
392 };
393 assert_eq!(
394 error_message(combined),
395 "`skip` and `with` cannot be combined"
396 );
397
398 let without_a_path = parse_quote! {
399 struct Request {
400 #[typesift(with)]
401 headers: Headers,
402 }
403 };
404 assert!(error_message(without_a_path).contains("expected"));
405
406 let generated = generated_code(parse_quote! {
407 struct Request {
408 path: String,
409 #[typesift(with = visit_headers)]
410 headers: Headers,
411 }
412 });
413 assert!(generated.contains("visit_headers"));
414 assert!(generated.contains("Sift"));
415 assert!(generated.contains("visit :: < __T , __B , __F > (& self . path"));
416 }
417}