Skip to main content

precious_core/paths/
matcher.rs

1use anyhow::{Context, Result};
2use ignore::gitignore::{Gitignore, GitignoreBuilder};
3use std::path::Path;
4
5#[derive(Debug)]
6#[allow(clippy::module_name_repetitions)]
7pub struct MatcherBuilder {
8    builder: GitignoreBuilder,
9}
10
11#[allow(clippy::new_without_default)]
12impl MatcherBuilder {
13    pub fn new<P: AsRef<Path>>(root: P) -> Self {
14        Self {
15            builder: GitignoreBuilder::new(root),
16        }
17    }
18
19    pub fn with(mut self, globs: &[impl AsRef<str>]) -> Result<Self> {
20        for g in globs {
21            self.builder.add_line(None, g.as_ref()).with_context(|| {
22                format!(r#"Failed to add glob pattern "{}" to matcher"#, g.as_ref())
23            })?;
24        }
25        Ok(self)
26    }
27
28    pub fn build(self) -> Result<Matcher> {
29        Ok(Matcher {
30            gitignore: self
31                .builder
32                .build()
33                .context("Failed to build gitignore matcher")?,
34        })
35    }
36}
37
38#[derive(Debug)]
39pub struct Matcher {
40    gitignore: Gitignore,
41}
42
43impl Matcher {
44    pub fn path_matches(&self, path: &Path, is_dir: bool) -> bool {
45        self.gitignore.matched(path, is_dir).is_ignore()
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use serial_test::parallel;
53
54    #[test]
55    #[parallel]
56    fn path_matches() -> Result<()> {
57        struct TestSet {
58            globs: &'static [&'static str],
59            yes: &'static [&'static str],
60            no: &'static [&'static str],
61        }
62
63        let tests = &[
64            TestSet {
65                globs: &["*.foo"],
66                yes: &["file.foo", "./file.foo"],
67                no: &["file.bar", "./file.bar"],
68            },
69            TestSet {
70                globs: &["*.foo", "**/foo/*"],
71                yes: &[
72                    "file.foo",
73                    "/baz/bar/file.foo",
74                    "/contains/foo/any.txt",
75                    "./file.foo",
76                    "./baz/bar/file.foo",
77                    "./contains/foo/any.txt",
78                ],
79                no: &[
80                    "file.bar",
81                    "/baz/bar/file.bar",
82                    "./file.bar",
83                    "./baz/bar/file.bar",
84                ],
85            },
86            TestSet {
87                globs: &["/foo/**/*"],
88                yes: &["/foo/file.go", "/foo/bar/baz/file.go"],
89                no: &["/bar/file.go"],
90            },
91            TestSet {
92                globs: &["/foo/**/*", "!/foo/bar/baz.*"],
93                yes: &["/foo/file.go", "/foo/bar/quux/file.go"],
94                no: &["/bar/file.go", "/foo/bar/baz.txt"],
95            },
96        ];
97
98        for t in tests {
99            let globs = t.globs.join(" ");
100            let m = MatcherBuilder::new("/").with(t.globs)?.build()?;
101            for y in t.yes {
102                assert!(m.path_matches(Path::new(y), false), "{y} matches [{globs}]");
103            }
104            for n in t.no {
105                assert!(
106                    !m.path_matches(Path::new(n), false),
107                    "{n} does not match [{globs}]",
108                );
109            }
110        }
111
112        Ok(())
113    }
114}