foundationdb_macros/
lib.rs

1//! Macro definitions used to maintain the crate
2use proc_macro::TokenStream;
3use quote::quote;
4use std::collections::HashMap;
5use syn::__private::TokenStream2;
6use syn::parse::Parser;
7use syn::{Item, ItemFn, LitInt};
8use try_map::FallibleMapExt;
9
10/// Allow to compute the range of supported api versions for a functionality.
11///
12/// This macro came out from the frustration of bumping fdb's version, where
13/// we are spending most of our time searching for things like:
14/// `#[cfg(any(feature = "fdb-5_1", feature = "fdb-5_2", feature = "fdb-6_0"))]`
15/// and adding the new version manually.
16///
17/// Thanks to the macro, we can now specify a `minimum` and an optional `max` version, and
18/// generate the right list of any. Not specifying a `max` allow easy bump to a new version.
19///
20/// `#[cfg_api_versions(min = 510, max = 600)]` will be translated to:
21/// `#[cfg(any(feature = "fdb-5_1", feature = "fdb-5_2", feature = "fdb-6_0"))]`
22#[proc_macro_attribute]
23pub fn cfg_api_versions(args: TokenStream, input: TokenStream) -> TokenStream {
24    let args = proc_macro2::TokenStream::from(args);
25    let input = proc_macro2::TokenStream::from(input);
26    cfg_api_versions_impl(args, input).into()
27}
28
29fn cfg_api_versions_impl(
30    args: proc_macro2::TokenStream,
31    input: proc_macro2::TokenStream,
32) -> proc_macro2::TokenStream {
33    let mut min: Option<LitInt> = None;
34    let mut max: Option<LitInt> = None;
35    let version_parser = syn::meta::parser(|meta| {
36        if meta.path.is_ident("min") {
37            min = Some(meta.value()?.parse()?);
38            Ok(())
39        } else if meta.path.is_ident("max") {
40            max = Some(meta.value()?.parse()?);
41            Ok(())
42        } else {
43            Err(meta.error("unsupported cfg_api_versions property"))
44        }
45    });
46
47    Parser::parse2(version_parser, args).expect("Unable to parse attribute cfg_api_versions");
48
49    let input: Item = syn::parse2(input).expect("Unable to parse input");
50
51    let minimum_version = min
52        .expect("min property must be provided")
53        .base10_parse::<i32>()
54        .expect("Unable to parse min version");
55    let maximum_version = max
56        .try_map(|x| x.base10_parse::<i32>())
57        .expect("Unable to parse max version");
58    generate_feature_range(&input, minimum_version, maximum_version)
59}
60
61fn generate_feature_range(
62    input: &Item,
63    minimum_version: i32,
64    maximum_version: Option<i32>,
65) -> proc_macro2::TokenStream {
66    let allowed_fdb_versions: Vec<TokenStream2> =
67        get_supported_feature_range(minimum_version, maximum_version)
68            .iter()
69            .map(|fdb_version| quote!(feature = #fdb_version))
70            .collect();
71
72    quote!(
73        #[cfg(any(#(#allowed_fdb_versions),*))]
74        #input
75    )
76}
77
78/// Given a range of version, this function will generate the appropriate macro text.
79fn get_supported_feature_range(minimum_version: i32, maximum_version: Option<i32>) -> Vec<String> {
80    let mut values: Vec<String> = get_version_mapping()
81        .iter()
82        .filter(|(_, version)| match maximum_version {
83            None => minimum_version <= **version,
84            Some(maximum) => minimum_version <= **version && version <= &&maximum,
85        })
86        .map(|(feature, _)| feature.to_owned())
87        .collect();
88    values.sort();
89
90    values
91}
92
93// TODO: Should we import something like lazy_static?
94fn get_version_mapping() -> HashMap<String, i32> {
95    let mut version_mapping = HashMap::with_capacity(9);
96    version_mapping.insert("fdb-7_3".into(), 730);
97    version_mapping.insert("fdb-7_1".into(), 710);
98    version_mapping.insert("fdb-7_0".into(), 700);
99    version_mapping.insert("fdb-6_3".into(), 630);
100    version_mapping.insert("fdb-6_2".into(), 620);
101    version_mapping.insert("fdb-6_1".into(), 610);
102    version_mapping.insert("fdb-6_0".into(), 600);
103    version_mapping.insert("fdb-5_2".into(), 520);
104    version_mapping.insert("fdb-5_1".into(), 510);
105    version_mapping.insert("fdb-5_0".into(), 500);
106    version_mapping
107}
108
109#[proc_macro_attribute]
110pub fn simulation_entrypoint(_attr: TokenStream, item: TokenStream) -> TokenStream {
111    let input = syn::parse_macro_input!(item as ItemFn);
112
113    let block = &input.block;
114    let attrs = &input.attrs;
115
116    // FIXME: we silently ignore original function signature which can lead to confusion
117    // we should use input signature and validate it is (&str, WorkloadContext) -> Box<dyn RustWorkload>
118    // it will allow the user to choose its parameters name
119    quote::quote!(
120        #(#attrs)*
121        #[no_mangle]
122        fn workload_instantiate_hook(name: &str, context: WorkloadContext) -> Box<dyn RustWorkload> {
123            #block
124        }
125        #[no_mangle]
126        pub extern "C" fn workloadFactory(logger: *const u8) -> *const u8 {
127            unsafe { ::foundationdb_simulation::CPPWorkloadFactory(logger as *const _) }
128        }
129    )
130    .into()
131}
132
133#[cfg(test)]
134mod tests {
135    use crate::cfg_api_versions_impl;
136    use crate::get_supported_feature_range;
137    use proc_macro2::TokenStream;
138    use quote::quote;
139
140    #[test]
141    fn test_create_supported_list() {
142        let v = get_supported_feature_range(700, None);
143        assert_eq!(v.len(), 3);
144        assert!(v.contains(&String::from("fdb-7_0")));
145        assert!(v.contains(&String::from("fdb-7_1")));
146        assert!(v.contains(&String::from("fdb-7_3")));
147
148        let v = get_supported_feature_range(600, Some(700));
149        assert_eq!(v.len(), 5);
150        assert!(v.contains(&String::from("fdb-7_0")));
151        assert!(v.contains(&String::from("fdb-6_3")));
152        assert!(v.contains(&String::from("fdb-6_2")));
153        assert!(v.contains(&String::from("fdb-6_1")));
154        assert!(v.contains(&String::from("fdb-6_0")));
155
156        let v = get_supported_feature_range(500, Some(610));
157        assert_eq!(v.len(), 5);
158        assert!(v.contains(&String::from("fdb-6_1")));
159        assert!(v.contains(&String::from("fdb-6_0")));
160        assert!(v.contains(&String::from("fdb-5_2")));
161        assert!(v.contains(&String::from("fdb-5_1")));
162        assert!(v.contains(&String::from("fdb-5_0")));
163
164        let v = get_supported_feature_range(500, None);
165        assert_eq!(v.len(), 10);
166        assert!(v.contains(&String::from("fdb-7_3")));
167        assert!(v.contains(&String::from("fdb-7_1")));
168        assert!(v.contains(&String::from("fdb-7_0")));
169        assert!(v.contains(&String::from("fdb-6_3")));
170        assert!(v.contains(&String::from("fdb-6_2")));
171        assert!(v.contains(&String::from("fdb-6_1")));
172        assert!(v.contains(&String::from("fdb-6_0")));
173        assert!(v.contains(&String::from("fdb-5_2")));
174        assert!(v.contains(&String::from("fdb-5_1")));
175        assert!(v.contains(&String::from("fdb-5_0")));
176    }
177
178    fn test_cfg_versions(expected_versions: TokenStream, attrs: TokenStream) {
179        let input = quote! {
180            fn ma_fonction() {}
181        };
182
183        let expected = quote! {
184            #[cfg(any(#expected_versions))]
185            fn ma_fonction() {}
186        };
187
188        let result = cfg_api_versions_impl(attrs, input);
189        assert_eq!(result.to_string(), expected.to_string())
190    }
191
192    #[test]
193    fn test_min_700_no_max_version() {
194        let data = quote!(
195            feature = "fdb-7_0",
196            feature = "fdb-7_1",
197            feature = "fdb-7_3"
198        );
199
200        let attrs = quote!(min = 700);
201
202        test_cfg_versions(data, attrs)
203    }
204
205    #[test]
206    fn test_min_600_max_700() {
207        let expected_versions = quote!(
208            feature = "fdb-6_0",
209            feature = "fdb-6_1",
210            feature = "fdb-6_2",
211            feature = "fdb-6_3",
212            feature = "fdb-7_0"
213        );
214
215        let attrs = quote!(min = 600, max = 700);
216
217        test_cfg_versions(expected_versions, attrs)
218    }
219
220    #[test]
221    fn test_min_500_max_610() {
222        let expected_versions = quote!(
223            feature = "fdb-5_0",
224            feature = "fdb-5_1",
225            feature = "fdb-5_2",
226            feature = "fdb-6_0",
227            feature = "fdb-6_1"
228        );
229
230        let attrs = quote!(min = 500, max = 610);
231
232        test_cfg_versions(expected_versions, attrs)
233    }
234
235    #[test]
236    fn test_min_500_no_max() {
237        let expected_versions = quote!(
238            feature = "fdb-5_0",
239            feature = "fdb-5_1",
240            feature = "fdb-5_2",
241            feature = "fdb-6_0",
242            feature = "fdb-6_1",
243            feature = "fdb-6_2",
244            feature = "fdb-6_3",
245            feature = "fdb-7_0",
246            feature = "fdb-7_1",
247            feature = "fdb-7_3"
248        );
249
250        let attrs = quote!(min = 500);
251
252        test_cfg_versions(expected_versions, attrs)
253    }
254
255    #[test]
256    #[should_panic]
257    fn test_no_min_version() {
258        let expected_versions = quote!(feature = "fdb-5_0",);
259
260        let attrs = quote!(max = 500);
261
262        test_cfg_versions(expected_versions, attrs)
263    }
264}