os_info/
matcher.rs

1/// An implementation to match on simple strings.
2#[derive(Debug, Clone)]
3#[allow(dead_code)]
4pub enum Matcher {
5    /// Considers the entire string (trimmed) to be the match.
6    AllTrimmed,
7
8    /// After finding the `prefix` followed by one or more spaces, returns the following word.
9    PrefixedWord { prefix: &'static str },
10
11    /// Similar to `PrefixedWord`, but only if the word is a valid version.
12    PrefixedVersion { prefix: &'static str },
13
14    /// Takes a set of lines (separated by `\n`) and searches for the value in a key/value pair
15    /// separated by the `=` character. For example `VERSION_ID="8.1"`.
16    KeyValue { key: &'static str },
17}
18
19impl Matcher {
20    /// Find the match on the input `string`.
21    pub fn find(&self, string: &str) -> Option<String> {
22        match *self {
23            Self::AllTrimmed => Some(string.trim().to_string()),
24            Self::PrefixedWord { prefix } => find_prefixed_word(string, prefix).map(str::to_owned),
25            Self::PrefixedVersion { prefix } => find_prefixed_word(string, prefix)
26                .filter(|&v| is_valid_version(v))
27                .map(str::to_owned),
28            Self::KeyValue { key } => find_by_key(string, key).map(str::to_owned),
29        }
30    }
31}
32
33fn find_by_key<'a>(string: &'a str, key: &str) -> Option<&'a str> {
34    let key = [key, "="].concat();
35    for line in string.lines() {
36        if line.starts_with(&key) {
37            return Some(line[key.len()..].trim_matches(|c: char| c == '"' || c.is_whitespace()));
38        }
39    }
40
41    None
42}
43
44fn find_prefixed_word<'a>(string: &'a str, prefix: &str) -> Option<&'a str> {
45    if let Some(prefix_start) = string.find(prefix) {
46        // Ignore prefix and leading whitespace
47        let string = &string[prefix_start + prefix.len()..].trim_start();
48
49        // Find where the word boundary ends
50        let word_end = string
51            .find(|c: char| c.is_whitespace())
52            .unwrap_or(string.len());
53        let string = &string[..word_end];
54
55        Some(string)
56    } else {
57        None
58    }
59}
60
61fn is_valid_version(word: &str) -> bool {
62    !word.starts_with('.') && !word.ends_with('.')
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use pretty_assertions::assert_eq;
69
70    #[test]
71    fn trimmed() {
72        let data = [
73            ("", Some("")),
74            ("test", Some("test")),
75            (" 		 test", Some("test")),
76            ("test  	   ", Some("test")),
77            ("  test 	", Some("test")),
78        ];
79
80        let matcher = Matcher::AllTrimmed;
81
82        for (input, expected) in &data {
83            let result = matcher.find(input);
84            assert_eq!(result.as_deref(), *expected);
85        }
86    }
87
88    #[test]
89    fn prefixed_word() {
90        let data = [
91            ("", None),
92            ("test", Some("")),
93            ("test1", Some("1")),
94            ("test 1", Some("1")),
95            (" test 1", Some("1")),
96            ("test 1.2.3", Some("1.2.3")),
97            (" 		test 1.2.3", Some("1.2.3")),
98        ];
99
100        let matcher = Matcher::PrefixedWord { prefix: "test" };
101
102        for (input, expected) in &data {
103            let result = matcher.find(input);
104            assert_eq!(result.as_deref(), *expected);
105        }
106    }
107
108    #[test]
109    fn prefixed_version() {
110        let data = [
111            ("", None),
112            ("test", Some("")),
113            ("test 1", Some("1")),
114            ("test .1", None),
115            ("test 1.", None),
116            ("test .1.", None),
117            (" test 1", Some("1")),
118            ("test 1.2.3", Some("1.2.3")),
119            (" 		test 1.2.3", Some("1.2.3")),
120        ];
121
122        let matcher = Matcher::PrefixedVersion { prefix: "test" };
123
124        for (input, expected) in &data {
125            let result = matcher.find(input);
126            assert_eq!(result.as_deref(), *expected);
127        }
128    }
129
130    #[test]
131    fn key_value() {
132        let data = [
133            ("", None),
134            ("key", None),
135            ("key=value", Some("value")),
136            ("key=1", Some("1")),
137            ("key=\"1\"", Some("1")),
138            ("key=\"CentOS Linux\"", Some("CentOS Linux")),
139        ];
140
141        let matcher = Matcher::KeyValue { key: "key" };
142
143        for (input, expected) in &data {
144            let result = matcher.find(input);
145            assert_eq!(result.as_deref(), *expected);
146        }
147    }
148}