krabka_replicator/
selector.rs1use regex::Regex;
4
5use crate::error::ReplicatorError;
6
7#[derive(Debug, Clone)]
15pub struct Selector {
16 include: Vec<Regex>,
17 exclude: Vec<Regex>,
18}
19
20fn glob_to_regex(pat: &str) -> Result<Regex, ReplicatorError> {
21 let mut re = String::from("^");
22 for ch in pat.chars() {
23 match ch {
24 '*' => re.push_str(".*"),
25 c if "\\.[]{}()+?^$|".contains(c) => {
26 re.push('\\');
27 re.push(c);
28 }
29 c => re.push(c),
30 }
31 }
32 re.push('$');
33 Regex::new(&re).map_err(|e| ReplicatorError::Config(format!("bad selector `{pat}`: {e}")))
34}
35
36impl Selector {
37 pub fn compile(include: &[String], exclude: &[String]) -> Result<Self, ReplicatorError> {
42 Ok(Self {
43 include: include
44 .iter()
45 .map(|p| glob_to_regex(p))
46 .collect::<Result<_, _>>()?,
47 exclude: exclude
48 .iter()
49 .map(|p| glob_to_regex(p))
50 .collect::<Result<_, _>>()?,
51 })
52 }
53
54 #[must_use]
56 pub fn matches(&self, name: &str) -> bool {
57 if self.exclude.iter().any(|r| r.is_match(name)) {
58 return false;
59 }
60 self.include.iter().any(|r| r.is_match(name))
61 }
62}
63
64#[cfg(test)]
65mod tests {
66
67 use super::*;
68
69 #[test]
70 fn include_exclude_with_glob_and_regex() {
71 let s = Selector::compile(
72 &["orders".into(), "telemetry.*".into()],
73 &["*.internal".into()],
74 )
75 .unwrap();
76 for (name, want) in [
77 ("orders", true),
78 ("telemetry.cpu", true),
79 ("payments", false),
80 ("telemetry.internal", false), ] {
82 assert2::assert!(s.matches(name) == want);
83 }
84 }
85
86 #[test]
87 fn empty_include_matches_nothing() {
88 let s = Selector::compile(&[], &[]).unwrap();
89 assert2::assert!(!s.matches("anything"));
90 }
91
92 #[test]
93 fn regex_metacharacters_are_escaped_to_literals() {
94 let s = Selector::compile(&["a.b".into()], &[]).unwrap();
98 assert2::assert!(s.matches("a.b"));
99 assert2::assert!(!s.matches("axb"));
100 }
101}