cucumber_trellis_macro/
lib.rs1use 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#[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