precious_core/paths/
matcher.rs1use 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}