rstest_bdd_patterns/pattern/
compiler.rs

1//! Convert lexed tokens into anchored regular-expression sources.
2
3use crate::errors::{PatternError, placeholder_error};
4use crate::hint::get_type_pattern;
5
6use super::lexer::{Token, lex_pattern};
7
8/// Build an anchored regular expression from lexed pattern tokens.
9///
10/// # Errors
11/// Returns [`PatternError`] when the tokens describe malformed placeholders or
12/// unbalanced braces.
13///
14/// # Examples
15/// ```ignore
16/// # use rstest_bdd_patterns::build_regex_from_pattern;
17/// let regex = build_regex_from_pattern("Given {item}")
18///     .expect("example ensures fallible call succeeds");
19/// assert_eq!(regex, r"^Given (.+?)$");
20/// ```
21pub fn build_regex_from_pattern(pat: &str) -> Result<String, PatternError> {
22    let tokens = lex_pattern(pat)?;
23    let mut regex = String::with_capacity(pat.len().saturating_mul(2) + 2);
24    regex.push('^');
25    let mut stray_depth = 0usize;
26
27    for token in tokens {
28        match token {
29            Token::Literal(text) => regex.push_str(&regex::escape(&text)),
30            Token::Placeholder { hint, .. } => {
31                regex.push('(');
32                regex.push_str(get_type_pattern(hint.as_deref()));
33                regex.push(')');
34            }
35            Token::OpenBrace { .. } => {
36                stray_depth = stray_depth.saturating_add(1);
37                regex.push_str(&regex::escape("{"));
38            }
39            Token::CloseBrace { index } => {
40                if stray_depth == 0 {
41                    return Err(placeholder_error(
42                        "unmatched closing brace '}' in step pattern",
43                        index,
44                        None,
45                    ));
46                }
47                stray_depth -= 1;
48                regex.push_str(&regex::escape("}"));
49            }
50        }
51    }
52
53    if stray_depth != 0 {
54        return Err(placeholder_error(
55            "unbalanced braces in step pattern",
56            pat.len(),
57            None,
58        ));
59    }
60
61    regex.push('$');
62    Ok(regex)
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn builds_regex_for_placeholder_patterns() {
71        let regex = build_regex_from_pattern("I have {count:u32} cukes")
72            .unwrap_or_else(|err| panic!("pattern should compile: {err}"));
73        assert_eq!(regex, r"^I have (\d+) cukes$");
74    }
75
76    #[test]
77    fn errors_when_closing_brace_unmatched() {
78        let Err(err) = build_regex_from_pattern("broken}") else {
79            panic!("should fail");
80        };
81        assert!(
82            err.to_string()
83                .contains("unmatched closing brace '}' in step pattern")
84        );
85    }
86
87    #[test]
88    fn errors_when_open_braces_remain() {
89        let Err(err) = build_regex_from_pattern("{open") else {
90            panic!("should fail");
91        };
92        assert!(
93            err.to_string()
94                .contains("missing closing '}' for placeholder")
95        );
96    }
97}