serde_devo_derive/
lib.rs

1use proc_macro2::TokenStream;
2use quote::{format_ident, quote, quote_spanned, ToTokens, TokenStreamExt};
3use syn::{
4    parse_macro_input, spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DataUnion,
5    DeriveInput, Field, Ident, Meta, Type, Variant,
6};
7
8#[proc_macro_derive(Devolve, attributes(devo))]
9pub fn devolve_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
10    let ast = parse_macro_input!(input as DeriveInput);
11    let (vis, name, attrs) = (&ast.vis, &ast.ident, &ast.attrs);
12    let (devo_name, devo_attr) = (format_ident!("Devolved{}", name), format_ident!("devo"));
13
14    let mut serde_attrs = TokenStream::new();
15    let warnings_mod = format_ident!("devolved_{}_warnings", name.to_string().to_lowercase());
16    let devo_fallback_type = attrs.iter().find_map(|attr| match &attr.meta {
17        Meta::List(list) if list.path.get_ident() == Some(&format_ident!("serde")) => {
18            serde_attrs.append_all(quote! { #attr });
19            None
20        }
21        Meta::List(list) if list.path.get_ident() == Some(&devo_attr) => {
22            let mut ft = None;
23            let _ = list.parse_nested_meta(|meta| {
24                let Ok(ty) = meta.value().unwrap().parse::<Type>() else {
25                    return Ok(());
26                };
27                ft = Some(ty);
28
29                Ok(())
30            });
31
32            ft
33        }
34        _ => None,
35    });
36
37    #[cfg(feature = "json")]
38    let fallback_type =
39        { devo_fallback_type.unwrap_or(syn::parse2(quote!(serde_json::Value)).unwrap()) };
40
41    #[cfg(not(feature = "json"))]
42    let fallback_type = {
43        use proc_macro2::Span;
44        if let Some(ty) = devo_fallback_type {
45            ty
46        } else {
47            let warning = syn::Error::new(
48                Span::call_site(),
49                "either enable the \"json\" feature or provide the `#[devo(fallback = Type)]` container attribute",
50            )
51            .into_compile_error();
52            return quote! {
53                mod #warnings_mod {
54                    #warning
55                }
56            }
57            .into();
58        }
59    };
60
61    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
62    let (devo_token, (is_tuple_struct, warn, devo_body, evo_impl, devo_impl)): (
63        TokenStream,
64        (
65            bool,
66            Vec<TokenStream>,
67            TokenStream,
68            TokenStream,
69            TokenStream,
70        ),
71    ) = match ast.data {
72        Data::Struct(DataStruct {
73            fields,
74            struct_token,
75            ..
76        }) => (struct_token.into_token_stream(), {
77            let (is_devo, is_named, tokens, evo_impl, devo_impl): (
78                bool,
79                bool,
80                TokenStream,
81                TokenStream,
82                TokenStream,
83            ) = fields.into_iter().enumerate().fold(
84                (
85                    false,
86                    false,
87                    TokenStream::new(),
88                    TokenStream::new(),
89                    TokenStream::new(),
90                ),
91                |(d, b, mut st, mut evo, mut dvo), (i, f)| {
92                    let is_named = f.ident.is_some();
93                    let (is_devo, tokens, ev, dv) = if is_named {
94                        render_field(
95                            name.to_string().to_token_stream(),
96                            f,
97                            &devo_attr,
98                            false,
99                            &fallback_type,
100                        )
101                    } else {
102                        render_tuple_field(
103                            name.to_string().into_token_stream(),
104                            f,
105                            &devo_attr,
106                            i,
107                            None,
108                            &fallback_type,
109                        )
110                    };
111                    st.append_all(tokens);
112                    evo.append_all(ev);
113                    dvo.append_all(dv);
114                    (is_devo || d, is_named || b, st, evo, dvo)
115                },
116            );
117
118            let span = name.span();
119            let mut warn = vec![];
120            if !is_devo {
121                warn.push(
122                        syn::Error::new(
123                            span,
124                            "using derive(Devolve) without at least one #[devo] attribute on structs does nothing",
125                        )
126                        .into_compile_error(),
127                    );
128            }
129
130            (
131                !is_named,
132                warn,
133                if is_named {
134                    quote! {
135                        {
136                            #tokens
137                        }
138                    }
139                } else {
140                    quote! {
141                        (
142                            #tokens
143                        )
144                    }
145                },
146                if is_named {
147                    quote! {
148                        {
149                            Ok(#name { #evo_impl })
150                        }
151                    }
152                } else {
153                    quote! {
154                        (
155                            Ok(#name ( #evo_impl ))
156                        )
157                    }
158                },
159                if is_named {
160                    quote! {
161                        {
162                            #devo_name { #devo_impl }
163                        }
164                    }
165                } else {
166                    quote! {
167                        (
168                            #devo_name ( #devo_impl )
169                        )
170                    }
171                },
172            )
173        }),
174
175        Data::Enum(DataEnum {
176            variants,
177            enum_token,
178            ..
179        }) => (enum_token.into_token_stream(), {
180            let (is_untagged, tokens, warn, evo_impl, devo_impl): (
181                bool,
182                TokenStream,
183                Vec<TokenStream>,
184                TokenStream,
185                TokenStream,
186            ) = variants.into_iter().fold(
187                (
188                    false,
189                    TokenStream::new(),
190                    vec![],
191                    TokenStream::new(),
192                    TokenStream::new(),
193                ),
194                |(is_untagged, mut st, mut w, mut evo, mut dvo), variant| {
195                    let (b, tokens, warn, ev, dv) =
196                        render_variant(name, &devo_name, variant, &devo_attr, &fallback_type);
197                    w.extend(warn);
198                    st.append_all(tokens);
199                    evo.append_all(dv);
200                    dvo.append_all(ev);
201                    (b || is_untagged, st, w, evo, dvo)
202                },
203            );
204
205            (
206                false,
207                warn,
208                if is_untagged {
209                    quote! {
210                        {
211                            #tokens
212                        }
213                    }
214                } else {
215                    quote! {
216                        {
217                            #tokens
218                            #[serde(untagged)]
219                            UnrecognizedVariant(#fallback_type),
220                        }
221                    }
222                },
223                quote! {
224                    {
225                        match self {
226                            #evo_impl
227                            _ => {
228                                let mut e = ::serde_devo::Error::UnknownVariant { ty: "", path: vec![] };
229                                Err(e)
230                            }
231                        }
232                    }
233                },
234                quote! {
235                    {
236                        match self {
237                            #devo_impl
238                        }
239                    }
240                },
241            )
242        }),
243
244        Data::Union(DataUnion { fields, .. }) => {
245            return quote_spanned! {
246                fields.span() => compile_error!("serde-devolve does not support data unions");
247            }
248            .into()
249        }
250    };
251
252    let d = if is_tuple_struct {
253        quote! {
254            #[derive(::serde::Deserialize, ::serde::Serialize)]
255            #serde_attrs
256            #vis #devo_token #devo_name #ty_generics #devo_body #where_clause;
257        }
258    } else {
259        quote! {
260            #[derive(::serde::Deserialize, ::serde::Serialize)]
261            #serde_attrs
262            #vis #devo_token #devo_name #ty_generics #where_clause #devo_body
263        }
264    };
265    quote! {
266        #d
267
268        impl #impl_generics ::serde_devo::Devolve<#fallback_type> for #name #ty_generics #where_clause {
269            type Devolved = #devo_name #ty_generics;
270
271            fn into_devolved(self) -> Self::Devolved {
272                #devo_impl
273            }
274        }
275
276        impl #impl_generics ::serde_devo::Evolve<#fallback_type> for #devo_name #ty_generics #where_clause {
277            type Evolved = #name #ty_generics;
278
279            fn try_into_evolved(self) -> Result<Self::Evolved, ::serde_devo::Error> {
280                #evo_impl
281            }
282        }
283
284        mod #warnings_mod {
285            #(
286                #warn
287            )*
288        }
289    }
290    .into()
291}
292
293fn render_variant(
294    evo_name: &Ident,
295    devo_name: &Ident,
296    Variant {
297        attrs,
298        ident,
299        fields,
300        ..
301    }: Variant,
302    devo_attr: &Ident,
303    fallback_type: &Type,
304) -> (
305    bool,
306    TokenStream,
307    Vec<TokenStream>,
308    TokenStream,
309    TokenStream,
310) {
311    let mut warn = vec![];
312    let is_empty = fields.is_empty();
313    let field_names = fields
314        .iter()
315        .filter_map(|f| f.ident.as_ref())
316        .map(|id| id.to_string())
317        .collect::<Vec<_>>()
318        .join(", ")
319        .parse::<TokenStream>()
320        .unwrap();
321    let field_letters = fields
322        .iter()
323        .scan(b'a', |letter, _| {
324            let l = *letter;
325            *letter += 1;
326            Some(l as char)
327        })
328        .map(|n| n.to_string())
329        .collect::<Vec<_>>();
330    let (is_devo, is_untagged, attrs) = render_attrs(attrs, devo_attr);
331    let (is_named, tokens, e_impl, d_impl): (bool, TokenStream, TokenStream, TokenStream) =
332        fields.into_iter().zip(&field_letters).enumerate().fold(
333            (
334                false,
335                TokenStream::new(),
336                TokenStream::new(),
337                TokenStream::new(),
338            ),
339            |(b, mut st, mut evo, mut dvo), (i, (f, l))| {
340                let is_named = f.ident.is_some();
341                let (_is_devo, tokens, ev, dv) = if is_named {
342                    render_field(
343                        format!("{evo_name}::{ident}").to_token_stream(),
344                        f,
345                        devo_attr,
346                        true,
347                        fallback_type,
348                    )
349                } else {
350                    render_tuple_field(
351                        format!("{evo_name}::{ident}").to_token_stream(),
352                        f,
353                        devo_attr,
354                        i,
355                        Some(l),
356                        fallback_type,
357                    )
358                };
359                st.append_all(tokens);
360                evo.append_all(ev);
361                dvo.append_all(dv);
362                (is_named || b, st, evo, dvo)
363            },
364        );
365
366    let field_letters = field_letters.join(", ").parse::<TokenStream>().unwrap();
367    let tokens = if is_named {
368        quote! {
369            #attrs
370            #ident {
371                #tokens
372            },
373        }
374    } else if is_empty {
375        quote! {
376            #attrs
377            #ident,
378        }
379    } else {
380        quote! {
381            #attrs
382            #ident (
383                #tokens
384            ),
385        }
386    };
387
388    let member = format!("Self::{}", ident).parse::<TokenStream>().unwrap();
389    let evo_member = format!("{}::{}", evo_name, ident)
390        .parse::<TokenStream>()
391        .unwrap();
392    let devo_member = format!("{}::{}", devo_name, ident)
393        .parse::<TokenStream>()
394        .unwrap();
395    let devo_impl = if is_empty {
396        quote! {
397            #member => Ok(#evo_member),
398        }
399    } else if is_named {
400        quote! {
401            #member { #field_names, .. } => Ok(#evo_member { #e_impl }),
402        }
403    } else {
404        quote! {
405            #member ( #field_letters ) => Ok(#evo_member ( #e_impl )),
406        }
407    };
408    let evo_impl = if is_empty {
409        quote! {
410            #member => #devo_member,
411        }
412    } else if is_named {
413        quote! {
414            #member { #field_names } => #devo_member { #d_impl },
415        }
416    } else {
417        quote! {
418            #member ( #field_letters, .. ) => #devo_member ( #d_impl ),
419        }
420    };
421
422    let span = ident.span();
423    if is_named && is_devo {
424        warn.push(
425            syn::Error::new(
426                span,
427                "#[devo] does nothing on enum variants with named fields",
428            )
429            .into_compile_error(),
430        );
431    }
432
433    if is_empty && is_devo {
434        warn.push(
435            syn::Error::new(span, "#[devo] does nothing on unit enum variants")
436                .into_compile_error(),
437        );
438    }
439
440    (is_untagged, tokens, warn, evo_impl, devo_impl)
441}
442
443fn render_tuple_field(
444    parent_ty: TokenStream,
445    Field { vis, attrs, ty, .. }: Field,
446    devo_attr: &Ident,
447    i: usize,
448    l: Option<&str>,
449    fallback_type: &Type,
450) -> (bool, TokenStream, TokenStream, TokenStream) {
451    let ty = &ty;
452    let (is_devo, _, attrs) = render_attrs(attrs, devo_attr);
453    let member = (if let Some(l) = l {
454        l.to_string()
455    } else {
456        format!("self.{}", i)
457    })
458    .parse::<TokenStream>()
459    .unwrap();
460    let idx = i.to_string().to_token_stream();
461    if is_devo {
462        if let Some(id) = match ty {
463            Type::Path(p) => p.path.get_ident(),
464            _ => None,
465        } {
466            let devolved_ident = format_ident!("{}", id);
467            return (
468                is_devo,
469                quote! {
470                    #attrs
471                    #vis <#devolved_ident as ::serde_devo::Devolve<#fallback_type>>::Devolved,
472                },
473                quote! {
474                    <<#devolved_ident as ::serde_devo::Devolve<#fallback_type>>::Devolved as ::serde_devo::Evolve<#fallback_type>>::try_into_evolved(#member).map_err(|e| e.extend(#parent_ty, #idx))?,
475                },
476                quote! {
477                    <#devolved_ident as ::serde_devo::Devolve<#fallback_type>>::into_devolved(#member),
478                },
479            );
480        }
481    }
482
483    (
484        is_devo,
485        quote! {
486            #attrs
487            #vis #ty,
488        },
489        quote! {
490            #member,
491        },
492        quote! {
493            #member,
494        },
495    )
496}
497
498fn render_field(
499    parent_ty: TokenStream,
500    Field {
501        vis,
502        attrs,
503        ident,
504        ty,
505        ..
506    }: Field,
507    devo_attr: &Ident,
508    is_enum: bool,
509    fallback_type: &Type,
510) -> (bool, TokenStream, TokenStream, TokenStream) {
511    let ty = &ty;
512    let (is_devo, _, attrs) = render_attrs(attrs, devo_attr);
513    let i = format!("{}", ident.as_ref().unwrap());
514    let member = (if is_enum {
515        i.clone()
516    } else {
517        format!("self.{}", ident.as_ref().unwrap())
518    })
519    .parse::<TokenStream>()
520    .unwrap();
521    if is_devo {
522        if let Some(id) = match ty {
523            Type::Path(p) => p.path.get_ident(),
524            _ => None,
525        } {
526            let devolved_ident = format_ident!("{}", id);
527            return (
528                is_devo,
529                quote! {
530                    #attrs
531                    #vis #ident: <#devolved_ident as ::serde_devo::Devolve<#fallback_type>>::Devolved,
532                },
533                quote! {
534                    #ident: <<#devolved_ident as ::serde_devo::Devolve<#fallback_type>>::Devolved as ::serde_devo::Evolve<#fallback_type>>::try_into_evolved(#member).map_err(|e| e.extend(#parent_ty, #i))?,
535                },
536                quote! {
537                    #ident: <#devolved_ident as ::serde_devo::Devolve<#fallback_type>>::into_devolved(#member),
538                },
539            );
540        }
541    }
542
543    (
544        is_devo,
545        quote! {
546            #attrs
547            #vis #ident: #ty,
548        },
549        quote! {
550            #ident: #member,
551        },
552        quote! {
553            #ident: #member,
554        },
555    )
556}
557
558fn render_attrs(
559    attrs: impl IntoIterator<Item = Attribute>,
560    devo_attr: &Ident,
561) -> (bool, bool, TokenStream) {
562    let (is_devo, is_untagged, tokens) = attrs.into_iter().fold(
563        (false, false, vec![]),
564        |(b, mut t, mut v), attr| match &attr.meta {
565            Meta::Path(name) if name.get_ident() == Some(devo_attr) => (true, t, v),
566            Meta::List(list) if list.path.get_ident() == Some(&format_ident!("serde")) => {
567                let _ = list.parse_nested_meta(|meta| {
568                    if meta.path.is_ident("untagged") {
569                        t = true
570                    }
571                    Ok(())
572                });
573                v.push(quote! {
574                    #attr
575                });
576                (b, t, v)
577            }
578            _ => (b, t, v),
579        },
580    );
581
582    (is_devo, is_untagged, tokens.into_iter().collect())
583}