Skip to main content

aversion_macros/
lib.rs

1//! ## aversion-macros: macros for deriving traits in the `aversion` crate.
2//!
3
4extern crate proc_macro;
5
6use proc_macro::TokenStream;
7use proc_macro2::Ident;
8use quote::{format_ident, quote};
9use syn::parse::{Parse, ParseStream};
10use syn::{parse_macro_input, punctuated::Punctuated, DeriveInput, LitInt, Path, Token, Variant};
11
12/// Information extracted from the name of a struct.
13struct NameInfo {
14    struct_name: Ident,
15    struct_base: Ident,
16    struct_version: u16,
17}
18
19/// `str::rsplit_once` function is introduced in Rust v1.52. This provides
20/// the same functionality until v1.52 is available widely enough that we
21/// can require it.
22fn rsplit_once<'a>(s: &'a str, delimiter: char) -> Option<(&'a str, &'a str)> {
23    let split_pos = s.rfind(delimiter)?;
24
25    let a = &s[..split_pos];
26    let b = &s[(split_pos + 1)..];
27
28    Some((a, b))
29}
30
31impl NameInfo {
32    fn from_name(ident: &Ident) -> Self {
33        let struct_name = ident.clone();
34        let struct_name_string = struct_name.to_string();
35
36        // Split the struct into base and version fields
37        let (struct_base, struct_version) = match rsplit_once(&struct_name_string, 'V') {
38            Some((base, version)) => {
39                if base.is_empty() {
40                    panic!("failed to parse struct name");
41                }
42                let base = Ident::new(&base, ident.span());
43                let version: u16 = version.parse().expect("failed to parse struct version");
44                (base, version)
45            }
46            None => panic!("failed to parse struct name into base+version"),
47        };
48
49        NameInfo {
50            struct_name,
51            struct_base,
52            struct_version,
53        }
54    }
55}
56
57fn versioned_name(base: &Ident, version: u16) -> Ident {
58    let name = format!("{}V{}", base, version);
59    Ident::new(&name, base.span())
60}
61
62/// Derive the `Versioned` trait on a struct.
63///
64#[proc_macro_derive(Versioned)]
65pub fn derive_versioned(input: TokenStream) -> TokenStream {
66    // parse the input into a DeriveInput syntax tree
67    let input = parse_macro_input!(input as DeriveInput);
68
69    let NameInfo {
70        struct_name,
71        struct_base,
72        struct_version,
73    } = NameInfo::from_name(&input.ident);
74
75    // The original generic parameters from the input struct
76    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
77
78    let expanded = quote! {
79        #[doc(hidden)]
80        #[allow(
81            non_upper_case_globals,
82            unused_attributes,
83            unused_qualifications,
84            non_camel_case_types,
85            non_snake_case
86        )]
87        const _: () = {
88            #[allow(rust_2018_idioms, clippy::useless_attribute)]
89            extern crate aversion as _aversion;
90
91            #[automatically_derived]
92            impl #impl_generics _aversion::Versioned
93            for #struct_name #ty_generics #where_clause {
94                const VER: u16 = #struct_version;
95                type Base = #struct_base;
96            }
97        };
98    };
99    // proc_macro2::TokenStream -> proc_macro::TokenStream
100    expanded.into()
101}
102
103/// Derive the `UpgradeLatest` trait on a struct.
104///
105/// It is assumed that all versions 1..N exist, i.e. if `UpgradeLatest`
106/// is implemented for `FooV3`, that `FooV2` and `FooV1` both exist and
107/// implement `Versioned`.
108///
109/// It is further assumed that a type alias `Foo` exists and is equivalent
110/// to the latest version. In other words: `type Foo = FooV3`
111///
112#[proc_macro_derive(UpgradeLatest)]
113pub fn derive_upgrade_latest(input: TokenStream) -> TokenStream {
114    // parse the input into a DeriveInput syntax tree
115    let input = parse_macro_input!(input as DeriveInput);
116
117    let NameInfo {
118        struct_name,
119        struct_base,
120        struct_version,
121    } = NameInfo::from_name(&input.ident);
122
123    // The original generic parameters from the input struct
124    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
125
126    // Create a list of (version, StructVx), one for each version between 1 and this.
127    let all_versions = (1..=struct_version)
128        .into_iter()
129        .map(|ii| (ii, versioned_name(&struct_base, ii)))
130        .collect::<Vec<_>>();
131
132    // Generate the match arm tokens for each version.
133    let read_message_arms = all_versions
134        .iter()
135        .map(|(v, n)| quote_read_message_arm(*v, n, &struct_name));
136
137    // Generate the FromVersion impls that skip intermediate versions,
138    // and jump directly to the latest.
139    let all_hops = (1..struct_version - 1)
140        .into_iter()
141        .map(|ii| quote_from_version_hop(&struct_base, ii, struct_version))
142        .collect::<Vec<_>>();
143
144    let expanded = quote! {
145        #[doc(hidden)]
146        #[allow(
147            non_upper_case_globals,
148            unused_attributes,
149            unused_qualifications,
150            non_camel_case_types,
151            non_snake_case
152        )]
153        const _: () = {
154            #[allow(rust_2018_idioms, clippy::useless_attribute)]
155            extern crate aversion as _aversion;
156
157            #[automatically_derived]
158            impl #impl_generics _aversion::group::UpgradeLatest
159            for #struct_name #ty_generics #where_clause {
160
161                fn upgrade_latest<Src>(src: &mut Src, header: Src::Header) -> ::std::result::Result<Self, Src::Error>
162                where
163                    Src: _aversion::group::DataSource,
164                {
165                    use _aversion::group::GroupHeader;
166
167                    let ver = header.msg_ver();
168                    match ver {
169                        #(#read_message_arms)*
170
171                        _ => Err(src.unknown_version::<#struct_base>(ver)),
172                    }
173                }
174            }
175
176            #(#all_hops)*
177        };
178    };
179    // proc_macro2::TokenStream -> proc_macro::TokenStream
180    expanded.into()
181}
182
183fn quote_read_message_arm(
184    version: u16,
185    versioned_name: &Ident,
186    target_name: &Ident,
187) -> proc_macro2::TokenStream {
188    quote! {
189        #version => {
190            let msg = src.read_message::<#versioned_name>(&header)?;
191            let upgraded = <#target_name as _aversion::FromVersion::<#versioned_name>>::from_version(msg);
192            Ok(upgraded)
193        }
194    }
195}
196
197/// Chain FromVersion implementations to skip directly to the latest version.
198///
199/// If there is a FooV1..FooV4, and there is a FromVersion for each N to N+1,
200/// generate the code for `FromVersion<FooV1> for FooV4`.
201///
202fn quote_from_version_hop(base: &Ident, lo: u16, hi: u16) -> proc_macro2::TokenStream {
203    assert!(hi > lo);
204    if hi - lo < 2 {
205        // The user should already have provided FromVersion<___N> for ___M
206        return quote! {};
207    }
208
209    // Create identifiers like `v1`, `v2`, etc.
210    fn tmp_ident(x: u16) -> Ident {
211        format_ident!("v{}", x)
212    }
213
214    // Create a chain of upgrades.
215    let upgrade_chain = (lo..hi)
216        .into_iter()
217        .map(|ii| {
218            let jj = ii + 1;
219            let tmp_ii = tmp_ident(ii);
220            let tmp_jj = tmp_ident(jj);
221            let ident_jj = versioned_name(base, jj);
222            quote! {
223                let #tmp_jj = #ident_jj::from_version(#tmp_ii);
224            }
225        })
226        .collect::<Vec<_>>();
227
228    let lo_ident = versioned_name(base, lo);
229    let hi_ident = versioned_name(base, hi);
230    let lo_tmp = tmp_ident(lo);
231    let hi_tmp = tmp_ident(hi);
232
233    quote! {
234        impl FromVersion<#lo_ident> for #hi_ident {
235            fn from_version(#lo_tmp: #lo_ident) -> Self {
236                #(#upgrade_chain)*
237                #hi_tmp
238            }
239        }
240    }
241}
242
243/// Derive the `GroupDeserialize` trait on a struct.
244///
245/// This macro expects an enum as input, where each variant contains exactly
246/// one field: a type that implements `Versioned + MessageId`.
247///
248#[proc_macro_derive(GroupDeserialize)]
249pub fn derive_group_deserialize(input: TokenStream) -> TokenStream {
250    // parse the input into a DeriveInput syntax tree
251    let input = parse_macro_input!(input as DeriveInput);
252    let enum_name = &input.ident;
253
254    // The original generic parameters from the input struct
255    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
256
257    let source_struct = input.data;
258    let variants = if let syn::Data::Enum(syn::DataEnum { variants, .. }) = source_struct {
259        variants
260    } else {
261        panic!("couldn't find enum variants");
262    };
263
264    let match_arms = variants
265        .iter()
266        .map(|v| {
267            // Extract basic data about this variant
268            let gv = GroupVariant::from_enum_variant(v);
269            // Write the GroupDeserialize match arm for this variant
270            gv.to_match_arm(enum_name)
271        })
272        .collect::<Vec<_>>();
273
274    let expanded = quote! {
275        #[doc(hidden)]
276        #[allow(
277            non_upper_case_globals,
278            unused_attributes,
279            unused_qualifications,
280            non_camel_case_types,
281            non_snake_case
282        )]
283        const _: () = {
284            #[allow(rust_2018_idioms, clippy::useless_attribute)]
285            extern crate aversion as _aversion;
286
287            #[automatically_derived]
288            impl #impl_generics _aversion::GroupDeserialize
289            for #enum_name #ty_generics #where_clause {
290                fn read_message<Src>(src: &mut Src) -> ::std::result::Result<Self, Src::Error>
291                where
292                    Src: _aversion::group::DataSource,
293                {
294                    use _aversion::{MessageId, group::{GroupHeader, UpgradeLatest}};
295
296                    let header = src.read_header()?;
297                    match header.msg_id() {
298                        #(#match_arms)*
299                        _ => {
300                            Err(src.unknown_message(header.msg_id()))
301                        }
302                    }
303                }
304            }
305        };
306    };
307
308    // proc_macro2::TokenStream -> proc_macro::TokenStream
309    expanded.into()
310}
311
312#[derive(Debug)]
313struct GroupVariant {
314    name: Ident,
315    target: Path,
316}
317
318impl GroupVariant {
319    fn from_enum_variant(variant: &Variant) -> GroupVariant {
320        let name = variant.ident.clone();
321        let mut target_path: Option<Path> = None;
322        if let syn::Fields::Unnamed(syn::FieldsUnnamed {
323            unnamed: variant_fields,
324            ..
325        }) = &variant.fields
326        {
327            if variant_fields.len() != 1 {
328                panic!("enum must contain exactly 1 field");
329            }
330            let field = variant_fields.first().unwrap();
331
332            if let syn::Type::Path(syn::TypePath { path, .. }) = &field.ty {
333                target_path = Some(path.clone());
334            }
335        }
336
337        let target = target_path
338            .unwrap_or_else(|| panic!("failed to extract enum target path for {}", name));
339
340        GroupVariant { name, target }
341    }
342
343    fn to_match_arm(&self, enum_name: &Ident) -> proc_macro2::TokenStream {
344        let enum_variant = &self.name;
345        let struct_name = &self.target;
346
347        quote! {
348            #struct_name::MSG_ID => {
349                let msg = #struct_name::upgrade_latest(src, header)?;
350                Ok(#enum_name::#enum_variant(msg))
351            }
352        }
353    }
354}
355
356// The documentation for this macro is in aversion/src/lib.rs,
357// so that links to other aversion types will work (they're not
358// in scope here).
359//
360// Just as a reminder, the syntax is:
361//  assign_message_ids! {
362//      Foo: 100,
363//      Bar: 101,
364//      Baz: 109,
365//  }
366//
367#[proc_macro]
368pub fn assign_message_ids(tokens: TokenStream) -> TokenStream {
369    let id_list = parse_macro_input!(tokens as MessageIdList);
370    let id_impls = id_list.to_impl();
371    let expanded = quote! {
372        #[doc(hidden)]
373        #[allow(
374            non_upper_case_globals,
375            unused_attributes,
376            unused_qualifications,
377            non_camel_case_types,
378            non_snake_case
379        )]
380        const _: () = {
381            #[allow(rust_2018_idioms, clippy::useless_attribute)]
382            extern crate aversion as _aversion;
383
384            #id_impls
385        };
386    };
387    expanded.into()
388}
389
390#[derive(Debug, Default)]
391struct MessageIdList {
392    list: Vec<MessageIdValue>,
393}
394
395#[derive(Debug)]
396struct MessageIdValue {
397    name: Path,
398    // FIXME: validate this fits in u16.
399    msg_id: LitInt,
400}
401
402/// Parse a single message-id value, e.g. `Foo: 123`
403///
404/// This is used by `assign_message_ids`.
405impl Parse for MessageIdValue {
406    fn parse(input: ParseStream) -> syn::parse::Result<Self> {
407        let name: Path = input.parse()?;
408        input.parse::<Token![:]>()?;
409        let msg_id: LitInt = input.parse()?;
410        Ok(MessageIdValue { name, msg_id })
411    }
412}
413
414/// Parse a comma-separated sequence of `MessageIdValue`.
415///
416/// This is used by `assign_message_ids`.
417impl Parse for MessageIdList {
418    fn parse(input: ParseStream) -> syn::parse::Result<Self> {
419        type IdList = Punctuated<MessageIdValue, Token![,]>;
420
421        let id_list = IdList::parse_terminated(input)?;
422
423        Ok(MessageIdList {
424            list: id_list.into_iter().collect(),
425        })
426    }
427}
428
429/// Convert a `MessageIdList` to a sequence of `MessageId` impls.
430impl MessageIdList {
431    fn to_impl(&self) -> proc_macro2::TokenStream {
432        let mut tokens = proc_macro2::TokenStream::new();
433        for id_val in &self.list {
434            tokens.extend(id_val.to_impl())
435        }
436        tokens
437    }
438}
439
440/// Convert a single MessageIdValue to a MessageId impl.
441impl MessageIdValue {
442    fn to_impl(&self) -> proc_macro2::TokenStream {
443        let name = &self.name;
444        let msg_id = &self.msg_id;
445        quote! {
446            #[automatically_derived]
447            impl _aversion::MessageId for #name {
448                const MSG_ID: u16 = #msg_id;
449            }
450        }
451    }
452}