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}