crabka_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 use assert2::assert;
67
68 use super::*;
69
70 #[test]
71 fn include_exclude_with_glob_and_regex() {
72 let s = Selector::compile(
73 &["orders".into(), "telemetry.*".into()],
74 &["*.internal".into()],
75 )
76 .unwrap();
77 assert!(s.matches("orders"));
78 assert!(s.matches("telemetry.cpu"));
79 assert!(!s.matches("payments"));
80 assert!(!s.matches("telemetry.internal")); }
82
83 #[test]
84 fn empty_include_matches_nothing() {
85 let s = Selector::compile(&[], &[]).unwrap();
86 assert!(!s.matches("anything"));
87 }
88
89 #[test]
90 fn regex_metacharacters_are_escaped_to_literals() {
91 let s = Selector::compile(&["a.b".into()], &[]).unwrap();
95 assert!(s.matches("a.b"));
96 assert!(!s.matches("axb"));
97 }
98}