Skip to main content

cli_tutor/
matcher.rs

1use crate::content::types::MatchMode;
2
3pub struct Matcher;
4
5impl Matcher {
6    /// Returns true if `actual` satisfies `expected` under the given `mode`.
7    pub fn check(actual: &str, expected: &str, mode: &MatchMode) -> bool {
8        match mode {
9            // exact — byte-for-byte match
10            MatchMode::Exact => actual == expected,
11
12            // normalized — strip trailing whitespace per line, drop trailing blank lines
13            MatchMode::Normalized => normalize(actual) == normalize(expected),
14
15            // sorted — sort both sides' lines before comparing (normalized)
16            MatchMode::Sorted => {
17                let mut a = sorted_lines(actual);
18                let mut e = sorted_lines(expected);
19                a.sort_unstable();
20                e.sort_unstable();
21                a == e
22            }
23
24            // regex — expected is a pattern that must match actual
25            MatchMode::Regex => regex_match(actual, expected),
26        }
27    }
28}
29
30fn normalize(s: &str) -> String {
31    let mut lines: Vec<&str> = s.lines().map(|l| l.trim_end()).collect();
32    while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
33        lines.pop();
34    }
35    lines.join("\n")
36}
37
38fn sorted_lines(s: &str) -> Vec<String> {
39    s.lines()
40        .map(|l| l.trim_end().to_string())
41        .filter(|l| !l.is_empty())
42        .collect()
43}
44
45fn regex_match(actual: &str, pattern: &str) -> bool {
46    // Minimal regex support using stdlib — only anchored literal matching for now.
47    // A proper regex crate can be added if needed without API changes.
48    // For the v1.0 exercises, patterns are simple enough that contains() covers most cases.
49    // This is an intentional simplification flagged here: regex.MATCH.1
50    actual.contains(pattern.trim())
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn exact_match_passes() {
59        assert!(Matcher::check("hello\n", "hello\n", &MatchMode::Exact));
60    }
61
62    #[test]
63    fn exact_match_fails_on_diff() {
64        assert!(!Matcher::check("hello\n", "world\n", &MatchMode::Exact));
65    }
66
67    #[test]
68    fn normalized_strips_trailing_space() {
69        assert!(Matcher::check(
70            "hello  \n",
71            "hello\n",
72            &MatchMode::Normalized
73        ));
74    }
75
76    #[test]
77    fn normalized_strips_trailing_blank_lines() {
78        assert!(Matcher::check(
79            "hello\n\n\n",
80            "hello\n",
81            &MatchMode::Normalized
82        ));
83    }
84
85    #[test]
86    fn sorted_order_independent() {
87        assert!(Matcher::check("b\na\nc\n", "a\nb\nc\n", &MatchMode::Sorted));
88    }
89
90    #[test]
91    fn sorted_fails_on_missing_line() {
92        assert!(!Matcher::check("a\nb\n", "a\nb\nc\n", &MatchMode::Sorted));
93    }
94
95    #[test]
96    fn regex_simple_match() {
97        assert!(Matcher::check("hello world", "hello", &MatchMode::Regex));
98    }
99
100    #[test]
101    fn regex_no_match() {
102        assert!(!Matcher::check("hello world", "goodbye", &MatchMode::Regex));
103    }
104}