cucumber_trellis_macro/
lib.rs

1// no-coverage:start
2use syn::{
3    parse::{Parse, ParseStream},
4    punctuated::Punctuated,
5    Path,
6    Ident,
7    Error,
8    Result,
9    ItemFn,
10    LitStr,
11    Token,
12    parse_macro_input,
13};
14
15use proc_macro::TokenStream;
16use proc_macro2::Span;
17use quote::quote;
18use std::{
19    hash::{Hasher, Hash},
20    collections::HashSet,
21};
22
23#[derive(Clone)]
24enum AttributeOption {
25    Features(LitStr),
26    Executor(Path),
27    UseTokio,
28}
29
30impl AttributeOption {
31    fn name(&self) -> String {
32        match self {
33            Self::Features(_) => String::from("features"),
34            Self::Executor(_) => String::from("executor"),
35            Self::UseTokio => String::from("use_tokio"),
36        }
37    }
38}
39
40impl Eq for AttributeOption {}
41
42impl PartialEq for AttributeOption {
43    fn eq(&self, other: &Self) -> bool {
44        match (self, other) {
45            (Self::Features(_), Self::Features(_)) => true,
46            (Self::Executor(_), Self::Executor(_)) => true,
47            (Self::UseTokio, Self::UseTokio) => true,
48            _ => false,
49        }
50    }
51}
52
53impl Hash for AttributeOption {
54    fn hash<H: Hasher>(&self, state: &mut H) {
55        match self {
56            Self::Features(_) => 0.hash(state),
57            Self::Executor(_) => 1.hash(state),
58            Self::UseTokio => 2.hash(state),
59        }
60    }
61}
62
63#[derive(Clone)]
64struct AttributeOptionItem {
65    option: AttributeOption,
66    span: Span,
67}
68
69impl PartialEq for AttributeOptionItem {
70    fn eq(&self, other: &Self) -> bool {
71        self.option == other.option
72    }
73}
74
75impl Eq for AttributeOptionItem {}
76
77impl Hash for AttributeOptionItem {
78    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
79        self.option.hash(state);
80    }
81}
82
83impl Parse for AttributeOptionItem {
84    fn parse(input: ParseStream) -> Result<Self> {
85        let option_name = input.parse::<Ident>()?;
86        let span = option_name.span();
87        let option_name = option_name.to_string();
88
89        match option_name.as_str() {
90            "features" => {
91                input.parse::<Token![=]>()?;
92
93                Ok(AttributeOptionItem {
94                    option: AttributeOption::Features(input.parse::<syn::LitStr>()?),
95                    span,
96                })
97            }
98
99            "executor" => {
100                input.parse::<Token![=]>()?;
101                let executor = input.parse::<Path>()?;
102
103                Ok(AttributeOptionItem {
104                    option: AttributeOption::Executor(executor),
105                    span,
106                })
107            }
108
109            "use_tokio" => Ok(AttributeOptionItem {
110                option: AttributeOption::UseTokio,
111                span,
112            }),
113
114            _ => Err(Error::new(span, "Unknown option")),
115        }
116    }
117}
118
119struct AttributeOptions {
120    features: Option<LitStr>,
121    executor: Option<Path>,
122    use_tokio: bool,
123}
124
125impl Parse for AttributeOptions {
126    fn parse(input: ParseStream) -> Result<Self> {
127        let mut features = None::<LitStr>;
128        let mut executor = None::<Path>;
129        let mut use_tokio = None::<bool>;
130
131        {
132            let mut options = HashSet::new();
133
134            for option in Punctuated::<AttributeOptionItem, Token![,]>::parse_terminated(input)? {
135                if !options.insert(option.option.clone()) {
136                    return Err(Error::new(
137                        option.span.clone(),
138                        format!("Duplicate option: {}", option.option.name()),
139                    ));
140                };
141
142                match option.option {
143                    AttributeOption::Features(value) => {
144                        features.replace(value);
145                    }
146                    AttributeOption::Executor(value) => {
147                        executor.replace(value);
148                    }
149                    AttributeOption::UseTokio => {
150                        use_tokio.replace(true);
151                    }
152                }
153            }
154        }
155
156        let use_tokio = use_tokio.unwrap_or(false);
157        if use_tokio && executor.is_some() {
158            return Err(Error::new(
159                Span::call_site(),
160                "Cannot use both `use_tokio` and `executor`",
161            ));
162        }
163
164        Ok(AttributeOptions {
165            features,
166            executor,
167            use_tokio,
168        })
169    }
170}
171
172/// This is an attribute procedural macro that will be used to define the cucumber tests
173#[proc_macro_attribute]
174pub fn cucumber_test(attr: TokenStream, item: TokenStream) -> TokenStream {
175    let options = parse_macro_input!(attr as AttributeOptions);
176    let function = parse_macro_input!(item as ItemFn);
177
178    let name = function.sig.ident.clone();
179
180    let features = if let Some(features) = options.features {
181        quote! {
182            let feature_path = std::path::PathBuf::from(#features);
183            let features = Some(feature_path.as_path());
184        }
185    } else {
186        quote! {
187            let features = None::<&std::path::Path>;
188        }
189    };
190
191    let (fn_main, run_tests) = if options.use_tokio {
192        (
193            quote! {
194                #[tokio::main]
195                async fn main()
196            },
197            quote! {
198                trellis.run_tests().await
199            },
200        )
201    } else {
202        let executor = match options.executor {
203            Some(executor) => quote! {
204                #executor
205            },
206            None => quote! {
207                futures::executor::block_on
208            },
209        };
210
211        (
212            quote! {
213                fn main()
214            },
215            quote! {
216                #executor(trellis.run_tests())
217            },
218        )
219    };
220
221    let output = quote! {
222        #function
223
224        #fn_main {
225            let mut trellis = {
226                #features
227                cucumber_trellis::CucumberTrellis::new(features)
228            };
229
230            #name(&mut trellis);
231
232            #run_tests;
233        }
234    };
235
236    output.into()
237}
238// no-coverage:stop