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#[derive(Debug, PartialEq, TypedBuilder)]
30pub struct UpFinder<P: AsRef<Path>> {
31 cwd: P,
33 #[builder(default = FindUpKind::File)]
35 kind: FindUpKind,
36}
37
38impl<P: AsRef<Path>> UpFinder<P> {
39 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 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}