catchr_core/
section.rs

1use proc_macro2::{Ident, Span, TokenStream};
2use quote::{quote, ToTokens, TokenStreamExt};
3use syn::parse::{self, Parse, ParseStream};
4use utils::extract_literal_string;
5
6use crate::catchr_mode::CatchrMode;
7use crate::scope::Scope;
8use crate::section_body::SectionBody;
9use crate::section_item::SectionItem;
10use crate::section_keyword::SectionKeyword;
11use crate::utils;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Section {
15    section_kind: SectionKeyword,
16    name: String,
17    body: SectionBody,
18
19    test_attribute: CatchrMode,
20}
21
22impl Section {
23    pub fn new(
24        section_kind: SectionKeyword,
25        name: impl ToString,
26        body: SectionBody,
27    ) -> Self {
28        Self {
29            section_kind,
30            name: name.to_string(),
31            body,
32            test_attribute: CatchrMode::Regular,
33        }
34    }
35
36    pub fn with_mode(mut self, test_attribute: CatchrMode) -> Self {
37        self.test_attribute = test_attribute;
38        self.body = self.body.with_mode(test_attribute);
39        self
40    }
41
42    fn quote_name(&self) -> Ident {
43        let name = utils::escape_name(&self.name);
44        let kind = self.section_kind.to_name();
45
46        let name = if kind.is_empty() {
47            name
48        } else {
49            format!("{}_{}", kind, name)
50        };
51
52        Ident::new(&name, Span::call_site())
53    }
54
55    pub fn quote_inner(&self, scope: Scope) -> TokenStream {
56        let mut token_stream = TokenStream::default();
57
58        self.to_tokens_inner(scope, &mut token_stream);
59
60        token_stream
61    }
62
63    pub fn peek(input: ParseStream) -> bool {
64        SectionKeyword::peek(input)
65    }
66
67    fn to_tokens_inner(&self, scope: Scope, tokens: &mut TokenStream) {
68        if self.body.is_top_level() {
69            let my_stmts: Vec<_> =
70                self.body.items().iter().filter_map(|i| i.stmt()).collect();
71
72            let name = self.quote_name();
73
74            let inner = scope.quote_with(&my_stmts);
75
76            match self.test_attribute {
77                CatchrMode::Regular => tokens.append_all(quote! {
78                    #[test]
79                    fn #name() {
80                        #inner
81                    }
82                }),
83                CatchrMode::Tokio => tokens.append_all(quote! {
84                    #[tokio::test]
85                    async fn #name() {
86                        #inner
87                    }
88                }),
89            }
90
91            return;
92        }
93
94        let mut stream = vec![];
95
96        for (idx, item) in self.body.items().iter().enumerate() {
97            if let SectionItem::Sep(section) = item {
98                let sb = self.body.get_stmts_before(idx);
99                let sa = self.body.get_stmts_after(idx);
100
101                let mut scope = scope.clone();
102                scope.push_mut(&sb, &sa);
103
104                let inner = section.quote_inner(scope);
105
106                stream.push(inner);
107            }
108        }
109
110        let name = self.quote_name();
111
112        tokens.append_all(quote! {
113            mod #name {
114                use super::*;
115
116                #(#stream)*
117            }
118        });
119    }
120}
121
122impl ToTokens for Section {
123    fn to_tokens(&self, tokens: &mut TokenStream) {
124        let scope = Scope::empty();
125
126        self.to_tokens_inner(scope, tokens);
127    }
128}
129
130impl Parse for Section {
131    fn parse(input: ParseStream) -> parse::Result<Self> {
132        let section_keyword: SectionKeyword = input.parse()?;
133        let name: syn::Lit = input.parse()?;
134        let name = extract_literal_string(name).ok_or_else(|| {
135            parse::Error::new(Span::call_site(), "Invalid section literal")
136        })?;
137
138        let content;
139        syn::braced!(content in input);
140        let inner_body = content.parse::<SectionBody>()?;
141
142        Ok(Section::new(section_keyword, name, inner_body))
143    }
144}
145
146#[cfg(test)]
147mod tests {
148
149    use super::*;
150
151    mod regular {
152        use test_case::test_case;
153
154        use super::*;
155
156        #[test_case(
157            r#"
158                section "tests" {
159                    let x = 1;
160                    when "hello" {
161                        assert!(true);
162                        then "whatever" {
163                            assert!(true);
164                        }
165                    }
166
167                    assert_eq!(x, 1);
168                }
169            "#,
170            quote!(
171                mod section_tests {
172                    use super::*;
173
174                    mod when_hello {
175                        use super::*;
176
177                        #[test]
178                        fn then_whatever() {
179                            {
180                                let x = 1;
181                                {
182                                    assert!(true);
183                                    {
184                                        assert!(true);
185                                    }
186                                }
187                                assert_eq!(x, 1);
188                            }
189                        }
190                    }
191                }
192            )
193        )]
194        #[test_case(
195            r#"
196                section "tests" {
197                    assert!(1 == 1);
198
199                    case "one" {
200                        assert!(2 == 2);
201                    }
202
203                    assert!(3 == 3);
204
205                    case "two" {
206                        assert!(4 == 4);
207                    }
208
209                    assert!(5 == 5);
210                }
211            "#,
212            quote!(
213                mod section_tests {
214                    use super::*;
215
216                    #[test]
217                    fn case_one() {
218                        {
219                            assert!(1 == 1);
220                            {
221                                assert!(2 == 2);
222                            }
223                            assert!(3 == 3);
224                            assert!(5 == 5);
225                        }
226                    }
227
228                    #[test]
229                    fn case_two() {
230                        {
231                            assert!(1 == 1);
232                            assert!(3 == 3);
233                            {
234                                assert!(4 == 4);
235                            }
236                            assert!(5 == 5);
237                        }
238                    }
239                }
240            )
241        )]
242        fn parse_and_quote(s: &str, exp: TokenStream) {
243            let section = syn::parse_str::<Section>(s).unwrap();
244            let section = section.to_token_stream();
245
246            assert_eq!(exp.to_string(), section.to_string());
247        }
248    }
249
250    mod tokio {
251        use test_case::test_case;
252
253        use super::*;
254
255        #[test_case(
256            r#"
257                section "tests" {
258                    let x = 1;
259                    when "hello" {
260                        assert!(true);
261                        then "whatever" {
262                            assert!(true);
263                        }
264                    }
265
266                    assert_eq!(x, 1);
267                }
268            "#,
269            quote!(
270                mod section_tests {
271                    use super::*;
272
273                    mod when_hello {
274                        use super::*;
275
276                        #[tokio::test]
277                        async fn then_whatever() {
278                            {
279                                let x = 1;
280                                {
281                                    assert!(true);
282                                    {
283                                        assert!(true);
284                                    }
285                                }
286                                assert_eq!(x, 1);
287                            }
288                        }
289                    }
290                }
291            )
292        )]
293        #[test_case(
294            r#"
295                section "tests" {
296                    assert!(1 == 1);
297
298                    case "one" {
299                        assert!(2 == 2);
300                    }
301
302                    assert!(3 == 3);
303
304                    case "two" {
305                        assert!(4 == 4);
306                    }
307
308                    assert!(5 == 5);
309                }
310            "#,
311            quote!(
312                mod section_tests {
313                    use super::*;
314
315                    #[tokio::test]
316                    async fn case_one() {
317                        {
318                            assert!(1 == 1);
319                            {
320                                assert!(2 == 2);
321                            }
322                            assert!(3 == 3);
323                            assert!(5 == 5);
324                        }
325                    }
326
327                    #[tokio::test]
328                    async fn case_two() {
329                        {
330                            assert!(1 == 1);
331                            assert!(3 == 3);
332                            {
333                                assert!(4 == 4);
334                            }
335                            assert!(5 == 5);
336                        }
337                    }
338                }
339            )
340        )]
341        fn parse_and_quote(s: &str, exp: TokenStream) {
342            let section = syn::parse_str::<Section>(s).unwrap();
343            let section =
344                section.with_mode(CatchrMode::Tokio).to_token_stream();
345
346            assert_eq!(exp.to_string(), section.to_string());
347        }
348    }
349}