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}