bep/loaders/file.rs
1use std::{fs, path::PathBuf};
2
3use glob::glob;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
7pub enum FileLoaderError {
8 #[error("Invalid glob pattern: {0}")]
9 InvalidGlobPattern(String),
10
11 #[error("IO error: {0}")]
12 IoError(#[from] std::io::Error),
13
14 #[error("Pattern error: {0}")]
15 PatternError(#[from] glob::PatternError),
16
17 #[error("Glob error: {0}")]
18 GlobError(#[from] glob::GlobError),
19}
20
21// ================================================================
22// Implementing Readable trait for reading file contents
23// ================================================================
24pub(crate) trait Readable {
25 fn read(self) -> Result<String, FileLoaderError>;
26 fn read_with_path(self) -> Result<(PathBuf, String), FileLoaderError>;
27}
28
29impl<'a> FileLoader<'a, PathBuf> {
30 pub fn read(self) -> FileLoader<'a, Result<String, FileLoaderError>> {
31 FileLoader {
32 iterator: Box::new(self.iterator.map(|res| res.read())),
33 }
34 }
35 pub fn read_with_path(self) -> FileLoader<'a, Result<(PathBuf, String), FileLoaderError>> {
36 FileLoader {
37 iterator: Box::new(self.iterator.map(|res| res.read_with_path())),
38 }
39 }
40}
41
42impl Readable for PathBuf {
43 fn read(self) -> Result<String, FileLoaderError> {
44 fs::read_to_string(self).map_err(FileLoaderError::IoError)
45 }
46 fn read_with_path(self) -> Result<(PathBuf, String), FileLoaderError> {
47 let contents = fs::read_to_string(&self);
48 Ok((self, contents?))
49 }
50}
51impl<T: Readable> Readable for Result<T, FileLoaderError> {
52 fn read(self) -> Result<String, FileLoaderError> {
53 self.map(|t| t.read())?
54 }
55 fn read_with_path(self) -> Result<(PathBuf, String), FileLoaderError> {
56 self.map(|t| t.read_with_path())?
57 }
58}
59
60// ================================================================
61// FileLoader definitions and implementations
62// ================================================================
63
64/// [FileLoader] is a utility for loading files from the filesystem using glob patterns or directory
65/// paths. It provides methods to read file contents and handle errors gracefully.
66///
67/// # Errors
68///
69/// This module defines a custom error type [FileLoaderError] which can represent various errors
70/// that might occur during file loading operations, such as invalid glob patterns, IO errors, and
71/// glob errors.
72///
73/// # Example Usage
74///
75/// ```rust
76/// use bep:loaders::FileLoader;
77///
78/// fn main() -> Result<(), Box<dyn std::error::Error>> {
79/// // Create a FileLoader using a glob pattern
80/// let loader = FileLoader::with_glob("path/to/files/*.txt")?;
81///
82/// // Read file contents, ignoring any errors
83/// let contents: Vec<String> = loader
84/// .read()
85/// .ignore_errors()
86///
87/// for content in contents {
88/// println!("{}", content);
89/// }
90///
91/// Ok(())
92/// }
93/// ```
94///
95/// [FileLoader] uses strict typing between the iterator methods to ensure that transitions between
96/// different implementations of the loaders and it's methods are handled properly by the compiler.
97pub struct FileLoader<'a, T> {
98 iterator: Box<dyn Iterator<Item = T> + 'a>,
99}
100
101impl<'a> FileLoader<'a, Result<PathBuf, FileLoaderError>> {
102 /// Reads the contents of the files within the iterator returned by [FileLoader::with_glob] or
103 /// [FileLoader::with_dir].
104 ///
105 /// # Example
106 /// Read files in directory "files/*.txt" and print the content for each file
107 ///
108 /// ```rust
109 /// let content = FileLoader::with_glob(...)?.read();
110 /// for result in content {
111 /// match result {
112 /// Ok(content) => println!("{}", content),
113 /// Err(e) => eprintln!("Error reading file: {}", e),
114 /// }
115 /// }
116 /// ```
117 pub fn read(self) -> FileLoader<'a, Result<String, FileLoaderError>> {
118 FileLoader {
119 iterator: Box::new(self.iterator.map(|res| res.read())),
120 }
121 }
122 /// Reads the contents of the files within the iterator returned by [FileLoader::with_glob] or
123 /// [FileLoader::with_dir] and returns the path along with the content.
124 ///
125 /// # Example
126 /// Read files in directory "files/*.txt" and print the content for cooresponding path for each
127 /// file.
128 ///
129 /// ```rust
130 /// let content = FileLoader::with_glob("files/*.txt")?.read();
131 /// for (path, result) in content {
132 /// match result {
133 /// Ok((path, content)) => println!("{:?} {}", path, content),
134 /// Err(e) => eprintln!("Error reading file: {}", e),
135 /// }
136 /// }
137 /// ```
138 pub fn read_with_path(self) -> FileLoader<'a, Result<(PathBuf, String), FileLoaderError>> {
139 FileLoader {
140 iterator: Box::new(self.iterator.map(|res| res.read_with_path())),
141 }
142 }
143}
144
145impl<'a, T: 'a> FileLoader<'a, Result<T, FileLoaderError>> {
146 /// Ignores errors in the iterator, returning only successful results. This can be used on any
147 /// [FileLoader] state of iterator whose items are results.
148 ///
149 /// # Example
150 /// Read files in directory "files/*.txt" and ignore errors from unreadable files.
151 ///
152 /// ```rust
153 /// let content = FileLoader::with_glob("files/*.txt")?.read().ignore_errors();
154 /// for result in content {
155 /// println!("{}", content)
156 /// }
157 /// ```
158 pub fn ignore_errors(self) -> FileLoader<'a, T> {
159 FileLoader {
160 iterator: Box::new(self.iterator.filter_map(|res| res.ok())),
161 }
162 }
163}
164
165impl FileLoader<'_, Result<PathBuf, FileLoaderError>> {
166 /// Creates a new [FileLoader] using a glob pattern to match files.
167 ///
168 /// # Example
169 /// Create a [FileLoader] for all `.txt` files that match the glob "files/*.txt".
170 ///
171 /// ```rust
172 /// let loader = FileLoader::with_glob("files/*.txt")?;
173 /// ```
174 pub fn with_glob(
175 pattern: &str,
176 ) -> Result<FileLoader<Result<PathBuf, FileLoaderError>>, FileLoaderError> {
177 let paths = glob(pattern)?;
178 Ok(FileLoader {
179 iterator: Box::new(
180 paths
181 .into_iter()
182 .map(|path| path.map_err(FileLoaderError::GlobError)),
183 ),
184 })
185 }
186
187 /// Creates a new [FileLoader] on all files within a directory.
188 ///
189 /// # Example
190 /// Create a [FileLoader] for all files that are in the directory "files" (ignores subdirectories).
191 ///
192 /// ```rust
193 /// let loader = FileLoader::with_dir("files")?;
194 /// ```
195 pub fn with_dir(
196 directory: &str,
197 ) -> Result<FileLoader<Result<PathBuf, FileLoaderError>>, FileLoaderError> {
198 Ok(FileLoader {
199 iterator: Box::new(fs::read_dir(directory)?.filter_map(|entry| {
200 let path = entry.ok()?.path();
201 if path.is_file() {
202 Some(Ok(path))
203 } else {
204 None
205 }
206 })),
207 })
208 }
209}
210
211// ================================================================
212// Iterators for FileLoader
213// ================================================================
214
215pub struct IntoIter<'a, T> {
216 iterator: Box<dyn Iterator<Item = T> + 'a>,
217}
218
219impl<'a, T> IntoIterator for FileLoader<'a, T> {
220 type Item = T;
221 type IntoIter = IntoIter<'a, T>;
222
223 fn into_iter(self) -> Self::IntoIter {
224 IntoIter {
225 iterator: self.iterator,
226 }
227 }
228}
229
230impl<T> Iterator for IntoIter<'_, T> {
231 type Item = T;
232
233 fn next(&mut self) -> Option<Self::Item> {
234 self.iterator.next()
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use assert_fs::prelude::{FileTouch, FileWriteStr, PathChild};
241
242 use super::FileLoader;
243
244 #[test]
245 fn test_file_loader() {
246 let temp = assert_fs::TempDir::new().expect("Failed to create temp dir");
247 let foo_file = temp.child("foo.txt");
248 let bar_file = temp.child("bar.txt");
249
250 foo_file.touch().expect("Failed to create foo.txt");
251 bar_file.touch().expect("Failed to create bar.txt");
252
253 foo_file.write_str("foo").expect("Failed to write to foo");
254 bar_file.write_str("bar").expect("Failed to write to bar");
255
256 let glob = temp.path().to_string_lossy().to_string() + "/*.txt";
257
258 let loader = FileLoader::with_glob(&glob).unwrap();
259 let mut actual = loader
260 .ignore_errors()
261 .read()
262 .ignore_errors()
263 .into_iter()
264 .collect::<Vec<_>>();
265 let mut expected = vec!["foo".to_string(), "bar".to_string()];
266
267 actual.sort();
268 expected.sort();
269
270 assert!(!actual.is_empty());
271 assert!(expected == actual)
272 }
273}