up_finder/
lib.rs

1use rustc_hash::FxHashMap;
2use std::path::{Path, PathBuf};
3use typed_builder::TypedBuilder;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum FindUpKind {
7  File,
8  Dir,
9}
10
11pub enum FindUpResult {
12  Saved(PathBuf),
13  Continue,
14  Stop,
15}
16
17/// A builder for the `find_up` function.
18///
19/// # Example
20///
21/// ```rust
22/// use up_finder::{UpFinder, FindUpKind};
23///
24/// let find_up = UpFinder::builder().cwd(".").kind(FindUpKind::File).build();
25/// let paths = find_up.find_up("package.json");
26///
27/// println!("{:#?}", paths);
28/// ```
29#[derive(Debug, PartialEq, TypedBuilder)]
30pub struct UpFinder<P: AsRef<Path>> {
31  /// The current working directory.
32  cwd: P,
33  /// The kind of file to search for.
34  #[builder(default = FindUpKind::File)]
35  kind: FindUpKind,
36}
37
38impl<P: AsRef<Path>> UpFinder<P> {
39  /// Find a file in the current working directory and all parent directories.
40  ///
41  /// # Example
42  ///
43  /// ```rust
44  /// use up_finder::{UpFinder, FindUpKind};
45  ///
46  /// let find_up = UpFinder::builder().cwd(".").kind(FindUpKind::File).build();
47  /// let paths = find_up.find_up("package.json");
48  ///
49  /// println!("{:#?}", paths);
50  /// ```
51  pub fn find_up(&self, name: &str) -> Vec<PathBuf> {
52    let paths = self.find_up_multi(&[name]);
53
54    if let Some(paths) = paths.get(name) {
55      paths.clone()
56    } else {
57      vec![]
58    }
59  }
60
61  /// Find multiple files in the current working directory and all parent directories.
62  ///
63  /// # Example
64  ///
65  /// ```rust
66  /// use up_finder::{UpFinder, FindUpKind};
67  ///
68  /// let find_up = UpFinder::builder().cwd(".").kind(FindUpKind::File).build();
69  /// let paths = find_up.find_up_multi(&["package.json", ".node-version"]);
70  ///
71  /// println!("{:#?}", paths);
72  /// ```
73  pub fn find_up_multi(&self, names: &[&str]) -> FxHashMap<String, Vec<PathBuf>> {
74    self.find_up_with_impl(self.cwd.as_ref().to_path_buf(), names, FindUpResult::Saved)
75  }
76
77  fn find_up_with_impl<F>(
78    &self,
79    cwd: PathBuf,
80    names: &[&str],
81    matcher: F,
82  ) -> FxHashMap<String, Vec<PathBuf>>
83  where
84    F: Fn(PathBuf) -> FindUpResult,
85  {
86    let mut paths: FxHashMap<&str, Vec<PathBuf>> = FxHashMap::default();
87
88    let mut cwd = cwd;
89
90    loop {
91      for &name in names {
92        let vecs = paths.entry(name).or_default();
93
94        let file = cwd.join(name);
95
96        if !file.exists() {
97          continue;
98        }
99
100        let matches_criteria = match self.kind {
101          FindUpKind::File => file.is_file(),
102          FindUpKind::Dir => file.is_dir(),
103        };
104
105        if !matches_criteria {
106          continue;
107        }
108
109        match matcher(file) {
110          FindUpResult::Saved(path) => {
111            vecs.push(path);
112          }
113          FindUpResult::Continue => {
114            continue;
115          }
116          FindUpResult::Stop => {
117            break;
118          }
119        }
120      }
121
122      let Some(parent) = cwd.parent() else {
123        break;
124      };
125
126      cwd = parent.to_path_buf();
127    }
128
129    paths
130      .into_iter()
131      .map(|(name, paths)| (name.to_string(), paths))
132      .collect()
133  }
134}
135
136#[cfg(test)]
137mod tests {
138  use insta::assert_debug_snapshot;
139
140  use super::*;
141
142  #[test]
143  fn should_find_files_when_searching_upward() {
144    let up_finder = UpFinder::builder()
145      .cwd("fixtures/a/b/c/d")
146      .kind(FindUpKind::File)
147      .build();
148
149    let paths = up_finder.find_up("package.json");
150
151    assert_eq!(paths.len(), 4);
152
153    assert_debug_snapshot!(paths);
154  }
155
156  #[test]
157  fn should_find_multiple_files_when_searching_upward() {
158    let package_json_name = "package.json";
159    let node_version_name = ".node-version";
160
161    let up_finder = UpFinder::builder()
162      .cwd("fixtures/a/b/c/d")
163      .kind(FindUpKind::File)
164      .build();
165
166    let paths = up_finder.find_up_multi(&[package_json_name, node_version_name]);
167
168    println!("{:#?}", paths);
169
170    assert_eq!(paths.len(), 2);
171
172    if let Some(paths) = paths.get(package_json_name) {
173      assert_eq!(paths.len(), 4);
174    }
175
176    if let Some(paths) = paths.get(node_version_name) {
177      assert_eq!(paths.len(), 1);
178    }
179
180    assert_debug_snapshot!(paths);
181  }
182
183  #[test]
184  fn should_not_find_files_when_searching_for_directories() {
185    let package_json_name = "package.json";
186    let node_version_name = ".node-version";
187
188    let up_finder = UpFinder::builder()
189      .cwd("fixtures/a/b/c/d")
190      .kind(FindUpKind::Dir)
191      .build();
192
193    let paths = up_finder.find_up_multi(&[package_json_name, node_version_name]);
194
195    println!("{:#?}", paths);
196
197    assert_eq!(paths.len(), 2);
198
199    if let Some(paths) = paths.get(package_json_name) {
200      assert_eq!(paths.len(), 0);
201    }
202
203    if let Some(paths) = paths.get(node_version_name) {
204      assert_eq!(paths.len(), 0);
205    }
206
207    assert_debug_snapshot!(paths);
208  }
209
210  #[test]
211  fn should_find_directory_in_parent_path() {
212    let dir_name = "a";
213
214    let up_finder = UpFinder::builder()
215      .cwd("fixtures/a/b/c/d")
216      .kind(FindUpKind::Dir)
217      .build();
218
219    let paths = up_finder.find_up_multi(&[dir_name]);
220
221    println!("{:#?}", paths);
222
223    assert_eq!(paths.len(), 1);
224
225    if let Some(paths) = paths.get(dir_name) {
226      assert_eq!(paths.len(), 1);
227    }
228
229    assert_debug_snapshot!(paths);
230  }
231}