Skip to main content

rig_core/loaders/
file.rs

1use std::{fs, path::PathBuf, string::FromUtf8Error};
2
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum FileLoaderError {
7    #[error("Invalid glob pattern: {0}")]
8    InvalidGlobPattern(String),
9
10    #[error("IO error: {0}")]
11    IoError(#[from] std::io::Error),
12
13    #[error("Pattern error: {0}")]
14    PatternError(#[from] glob::PatternError),
15
16    #[error("Glob error: {0}")]
17    GlobError(#[from] glob::GlobError),
18
19    #[error("String conversion error: {0}")]
20    StringUtf8Error(#[from] FromUtf8Error),
21}
22
23// ================================================================
24// Implementing Readable trait for reading file contents
25// ================================================================
26loadable_trait!(Readable, FileLoaderError, String, read, read_with_path);
27
28impl Readable for PathBuf {
29    fn read(self) -> Result<String, FileLoaderError> {
30        fs::read_to_string(self).map_err(FileLoaderError::IoError)
31    }
32    fn read_with_path(self) -> Result<(PathBuf, String), FileLoaderError> {
33        let contents = fs::read_to_string(&self);
34        Ok((self, contents?))
35    }
36}
37
38impl Readable for Vec<u8> {
39    fn read(self) -> Result<String, FileLoaderError> {
40        Ok(String::from_utf8(self)?)
41    }
42
43    fn read_with_path(self) -> Result<(PathBuf, String), FileLoaderError> {
44        let res = String::from_utf8(self)?;
45
46        Ok((PathBuf::from("<memory>"), res))
47    }
48}
49
50// ================================================================
51// FileLoader definitions and implementations
52// ================================================================
53
54/// [FileLoader] is a utility for loading files from the filesystem using glob patterns or directory
55///  paths. It provides methods to read file contents and handle errors gracefully.
56///
57/// # Errors
58///
59/// This module defines a custom error type [FileLoaderError] which can represent various errors
60///  that might occur during file loading operations, such as invalid glob patterns, IO errors, and
61///  glob errors.
62///
63/// # Example Usage
64///
65/// ```no_run
66/// use rig_core::loaders::FileLoader;
67///
68/// fn main() -> Result<(), Box<dyn std::error::Error>> {
69///     // Create a FileLoader using a glob pattern
70///     let loader = FileLoader::with_glob("path/to/files/*.txt")?;
71///
72///     // Read file contents, ignoring any errors
73///     let contents: Vec<String> = loader
74///         .read()
75///         .ignore_errors()
76///         .into_iter()
77///         .collect();
78///
79///     for content in contents {
80///         println!("{}", content);
81///     }
82///
83///     Ok(())
84/// }
85/// ```
86///
87/// [FileLoader] uses strict typing between the iterator methods to ensure that transitions between
88///   different implementations of the loaders and it's methods are handled properly by the compiler.
89pub struct FileLoader<'a, T> {
90    iterator: Box<dyn Iterator<Item = T> + 'a>,
91}
92
93#[allow(private_bounds)] // `Readable` deliberately seals which states expose these methods
94impl<'a, T: Readable + 'a> FileLoader<'a, T> {
95    /// Reads the contents of the files within the iterator returned by [FileLoader::with_glob] or
96    ///  [FileLoader::with_dir].
97    ///
98    /// # Example
99    /// Read files in directory "files/*.txt" and print the content for each file
100    ///
101    /// ```no_run
102    /// # use rig_core::loaders::FileLoader;
103    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
104    /// let content = FileLoader::with_glob("files/*.txt")?.read();
105    /// for result in content {
106    ///     match result {
107    ///         Ok(content) => println!("{}", content),
108    ///         Err(e) => eprintln!("Error reading file: {}", e),
109    ///     }
110    /// }
111    /// # Ok(())
112    /// # }
113    /// ```
114    pub fn read(self) -> FileLoader<'a, Result<String, FileLoaderError>> {
115        FileLoader {
116            iterator: Box::new(self.iterator.map(|res| res.read())),
117        }
118    }
119    /// Reads the contents of the files within the iterator returned by [FileLoader::with_glob] or
120    ///  [FileLoader::with_dir] and returns the path along with the content.
121    ///
122    /// # Example
123    /// Read files in directory "files/*.txt" and print the content for corresponding path for each
124    ///  file.
125    ///
126    /// ```no_run
127    /// # use rig_core::loaders::FileLoader;
128    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
129    /// let content = FileLoader::with_glob("files/*.txt")?.read_with_path();
130    /// for result in content {
131    ///     match result {
132    ///         Ok((path, content)) => println!("{:?} {}", path, content),
133    ///         Err(e) => eprintln!("Error reading file: {}", e),
134    ///     }
135    /// }
136    /// # Ok(())
137    /// # }
138    /// ```
139    pub fn read_with_path(self) -> FileLoader<'a, Result<(PathBuf, String), FileLoaderError>> {
140        FileLoader {
141            iterator: Box::new(self.iterator.map(|res| res.read_with_path())),
142        }
143    }
144}
145
146loader_scaffold!(FileLoader, FileLoaderError, dir: files_only);
147loader_from_bytes!(FileLoader);
148
149#[cfg(test)]
150mod tests {
151    use assert_fs::prelude::{FileTouch, FileWriteStr, PathChild};
152
153    use super::FileLoader;
154
155    #[test]
156    fn test_file_loader() {
157        let temp = assert_fs::TempDir::new().expect("Failed to create temp dir");
158        let foo_file = temp.child("foo.txt");
159        let bar_file = temp.child("bar.txt");
160
161        foo_file.touch().expect("Failed to create foo.txt");
162        bar_file.touch().expect("Failed to create bar.txt");
163
164        foo_file.write_str("foo").expect("Failed to write to foo");
165        bar_file.write_str("bar").expect("Failed to write to bar");
166
167        let glob = temp.path().to_string_lossy().to_string() + "/*.txt";
168
169        let loader = FileLoader::with_glob(&glob).unwrap();
170        let mut actual = loader
171            .ignore_errors()
172            .read()
173            .ignore_errors()
174            .into_iter()
175            .collect::<Vec<_>>();
176        let mut expected = vec!["foo".to_string(), "bar".to_string()];
177
178        actual.sort();
179        expected.sort();
180
181        assert!(!actual.is_empty());
182        assert!(expected == actual)
183    }
184
185    #[test]
186    fn test_file_loader_bytes() {
187        let temp = assert_fs::TempDir::new().expect("Failed to create temp dir");
188        let foo_file = temp.child("foo.txt");
189        let bar_file = temp.child("bar.txt");
190
191        foo_file.touch().expect("Failed to create foo.txt");
192        bar_file.touch().expect("Failed to create bar.txt");
193
194        foo_file.write_str("foo").expect("Failed to write to foo");
195        bar_file.write_str("bar").expect("Failed to write to bar");
196
197        let foo_bytes = std::fs::read(foo_file.path()).unwrap();
198        let bar_bytes = std::fs::read(bar_file.path()).unwrap();
199
200        let loader = FileLoader::from_bytes_multi(vec![foo_bytes, bar_bytes]);
201        let mut actual = loader
202            .read()
203            .ignore_errors()
204            .into_iter()
205            .collect::<Vec<_>>();
206        let mut expected = vec!["foo".to_string(), "bar".to_string()];
207
208        actual.sort();
209        expected.sort();
210
211        assert!(!actual.is_empty());
212        assert!(expected == actual)
213    }
214}