/// An implementation to match on simple strings. #[derive(Debug, Clone)] #[allow(dead_code)] pub enum Matcher { /// Considers the entire string (trimmed) to be the match. AllTrimmed, /// After finding the `prefix` followed by one or more spaces, returns the following word. PrefixedWord { prefix: &'static str }, /// Similar to `PrefixedWord`, but only if the word is a valid version. PrefixedVersion { prefix: &'static str }, /// Takes a set of lines (separated by `\n`) and searches for the value in a key/value pair /// separated by the `=` character. For example `VERSION_ID="8.1"`. KeyValue { key: &'static str }, } impl Matcher { /// Find the match on the input `string`. pub fn find(&self, string: &str) -> Option { match *self { Self::AllTrimmed => Some(string.trim().to_string()), Self::PrefixedWord { prefix } => find_prefixed_word(string, prefix).map(str::to_owned), Self::PrefixedVersion { prefix } => find_prefixed_word(string, prefix) .filter(|&v| is_valid_version(v)) .map(str::to_owned), Self::KeyValue { key } => find_by_key(string, key).map(str::to_owned), } } } fn find_by_key<'a>(string: &'a str, key: &str) -> Option<&'a str> { let key = [key, "="].concat(); for line in string.lines() { if line.starts_with(&key) { return Some(line[key.len()..].trim_matches(|c: char| c == '"' || c.is_whitespace())); } } None } fn find_prefixed_word<'a>(string: &'a str, prefix: &str) -> Option<&'a str> { if let Some(prefix_start) = string.find(prefix) { // Ignore prefix and leading whitespace let string = &string[prefix_start + prefix.len()..].trim_start(); // Find where the word boundary ends let word_end = string .find(|c: char| c.is_whitespace()) .unwrap_or(string.len()); let string = &string[..word_end]; Some(string) } else { None } } fn is_valid_version(word: &str) -> bool { !word.starts_with('.') && !word.ends_with('.') } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn trimmed() { let data = [ ("", Some("")), ("test", Some("test")), (" test", Some("test")), ("test ", Some("test")), (" test ", Some("test")), ]; let matcher = Matcher::AllTrimmed; for (input, expected) in &data { let result = matcher.find(input); assert_eq!(result.as_deref(), *expected); } } #[test] fn prefixed_word() { let data = [ ("", None), ("test", Some("")), ("test1", Some("1")), ("test 1", Some("1")), (" test 1", Some("1")), ("test 1.2.3", Some("1.2.3")), (" test 1.2.3", Some("1.2.3")), ]; let matcher = Matcher::PrefixedWord { prefix: "test" }; for (input, expected) in &data { let result = matcher.find(input); assert_eq!(result.as_deref(), *expected); } } #[test] fn prefixed_version() { let data = [ ("", None), ("test", Some("")), ("test 1", Some("1")), ("test .1", None), ("test 1.", None), ("test .1.", None), (" test 1", Some("1")), ("test 1.2.3", Some("1.2.3")), (" test 1.2.3", Some("1.2.3")), ]; let matcher = Matcher::PrefixedVersion { prefix: "test" }; for (input, expected) in &data { let result = matcher.find(input); assert_eq!(result.as_deref(), *expected); } } #[test] fn key_value() { let data = [ ("", None), ("key", None), ("key=value", Some("value")), ("key=1", Some("1")), ("key=\"1\"", Some("1")), ("key=\"CentOS Linux\"", Some("CentOS Linux")), ]; let matcher = Matcher::KeyValue { key: "key" }; for (input, expected) in &data { let result = matcher.find(input); assert_eq!(result.as_deref(), *expected); } } }