aprender_rag/loader/
mod.rs1#[cfg(feature = "ocr")]
25mod image;
26mod subtitle;
27mod text;
28
29#[cfg(feature = "ocr")]
30pub use image::ImageLoader;
31pub use subtitle::SubtitleLoader;
32pub use text::TextLoader;
33
34use crate::{Document, Error, Result};
35use std::path::Path;
36
37pub trait DocumentLoader: Send + Sync {
43 fn supported_extensions(&self) -> Vec<&str>;
45
46 fn can_load(&self, path: &Path) -> bool {
51 path.extension()
52 .and_then(|ext| ext.to_str())
53 .map(|ext| {
54 let lower = ext.to_lowercase();
55 self.supported_extensions().iter().any(|s| *s == lower)
56 })
57 .unwrap_or(false)
58 }
59
60 fn load(&self, path: &Path) -> Result<Document>;
68}
69
70pub struct LoaderRegistry {
75 loaders: Vec<Box<dyn DocumentLoader>>,
76}
77
78impl LoaderRegistry {
79 #[must_use]
81 pub fn new() -> Self {
82 let mut registry = Self { loaders: Vec::new() };
83 registry.register(Box::new(TextLoader));
84 registry.register(Box::new(SubtitleLoader));
85 #[cfg(feature = "ocr")]
86 registry.register(Box::new(ImageLoader));
87 registry
88 }
89
90 pub fn register(&mut self, loader: Box<dyn DocumentLoader>) {
92 self.loaders.push(loader);
93 }
94
95 #[must_use]
97 pub fn loader_for(&self, path: &Path) -> Option<&dyn DocumentLoader> {
98 self.loaders.iter().find(|l| l.can_load(path)).map(|l| l.as_ref())
99 }
100
101 pub fn load(&self, path: &Path) -> Result<Document> {
103 let loader = self.loader_for(path).ok_or_else(|| {
104 Error::InvalidInput(format!("No loader registered for: {}", path.display()))
105 })?;
106 loader.load(path)
107 }
108
109 #[must_use]
113 pub fn find_sidecar(media_path: &Path) -> Option<std::path::PathBuf> {
114 for ext in &["srt", "vtt"] {
115 let sidecar = media_path.with_extension(ext);
116 if sidecar.exists() {
117 return Some(sidecar);
118 }
119 }
120 None
121 }
122
123 #[must_use]
125 pub fn supported_extensions(&self) -> Vec<&str> {
126 self.loaders.iter().flat_map(|l| l.supported_extensions()).collect()
127 }
128}
129
130impl Default for LoaderRegistry {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136impl std::fmt::Debug for LoaderRegistry {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 f.debug_struct("LoaderRegistry")
139 .field("loader_count", &self.loaders.len())
140 .field("extensions", &self.supported_extensions())
141 .finish()
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_registry_default_loaders() {
151 let registry = LoaderRegistry::new();
152 let exts = registry.supported_extensions();
153 assert!(exts.contains(&"txt"));
154 assert!(exts.contains(&"md"));
155 assert!(exts.contains(&"srt"));
156 assert!(exts.contains(&"vtt"));
157 }
158
159 #[test]
160 fn test_registry_loader_for_txt() {
161 let registry = LoaderRegistry::new();
162 assert!(registry.loader_for(Path::new("file.txt")).is_some());
163 assert!(registry.loader_for(Path::new("file.TXT")).is_some());
164 }
165
166 #[test]
167 fn test_registry_loader_for_srt() {
168 let registry = LoaderRegistry::new();
169 assert!(registry.loader_for(Path::new("file.srt")).is_some());
170 }
171
172 #[test]
173 fn test_registry_no_loader_for_unknown() {
174 let registry = LoaderRegistry::new();
175 assert!(registry.loader_for(Path::new("file.xyz")).is_none());
176 }
177
178 #[test]
179 fn test_registry_load_missing_file() {
180 let registry = LoaderRegistry::new();
181 let result = registry.load(Path::new("/nonexistent/file.txt"));
182 assert!(result.is_err());
183 }
184
185 #[test]
186 fn test_registry_load_unsupported_format() {
187 let registry = LoaderRegistry::new();
188 let result = registry.load(Path::new("file.mp4"));
189 assert!(result.is_err());
190 }
191
192 #[test]
193 fn test_find_sidecar_none() {
194 assert!(
196 LoaderRegistry::find_sidecar(Path::new("/tmp/nonexistent_video_12345.mp4")).is_none()
197 );
198 }
199
200 #[test]
201 fn test_registry_custom_loader() {
202 struct DummyLoader;
203 impl DocumentLoader for DummyLoader {
204 fn supported_extensions(&self) -> Vec<&str> {
205 vec!["xyz"]
206 }
207 fn load(&self, path: &Path) -> Result<Document> {
208 Ok(Document::new("dummy").with_source(path.to_string_lossy()))
209 }
210 }
211
212 let mut registry = LoaderRegistry::new();
213 registry.register(Box::new(DummyLoader));
214 assert!(registry.loader_for(Path::new("test.xyz")).is_some());
215 }
216
217 #[test]
218 fn test_registry_debug() {
219 let registry = LoaderRegistry::new();
220 let debug = format!("{registry:?}");
221 assert!(debug.contains("LoaderRegistry"));
222 assert!(debug.contains("loader_count"));
223 }
224
225 #[test]
226 fn test_registry_default() {
227 let registry = LoaderRegistry::default();
228 assert!(!registry.supported_extensions().is_empty());
229 }
230
231 #[test]
232 fn test_find_sidecar_srt_preferred() {
233 let dir = std::env::temp_dir().join("trueno_rag_test_sidecar");
235 let _ = std::fs::create_dir_all(&dir);
236 let video = dir.join("lecture.mp4");
237 let srt = dir.join("lecture.srt");
238 let vtt = dir.join("lecture.vtt");
239 std::fs::write(&video, b"").unwrap();
240 std::fs::write(&srt, b"").unwrap();
241 std::fs::write(&vtt, b"").unwrap();
242
243 let found = LoaderRegistry::find_sidecar(&video);
244 assert!(found.is_some());
245 assert_eq!(found.unwrap().extension().unwrap(), "srt");
247
248 let _ = std::fs::remove_dir_all(&dir);
250 }
251
252 #[test]
253 fn test_can_load_no_extension() {
254 let loader = TextLoader;
255 assert!(!loader.can_load(Path::new("Makefile")));
256 }
257
258 #[test]
259 fn test_load_real_txt_file() {
260 let dir = std::env::temp_dir().join("trueno_rag_test_load_txt");
261 let _ = std::fs::create_dir_all(&dir);
262 let file = dir.join("test.txt");
263 std::fs::write(&file, "Hello from test file.").unwrap();
264
265 let registry = LoaderRegistry::new();
266 let doc = registry.load(&file).unwrap();
267 assert_eq!(doc.content, "Hello from test file.");
268 assert!(doc.title.is_some());
269
270 let _ = std::fs::remove_dir_all(&dir);
271 }
272
273 #[test]
274 fn test_load_real_srt_file() {
275 let dir = std::env::temp_dir().join("trueno_rag_test_load_srt");
276 let _ = std::fs::create_dir_all(&dir);
277 let file = dir.join("test.srt");
278 std::fs::write(&file, "1\n00:00:01,000 --> 00:00:04,500\nHello from subtitle.\n").unwrap();
279
280 let registry = LoaderRegistry::new();
281 let doc = registry.load(&file).unwrap();
282 assert!(doc.content.contains("Hello from subtitle"));
283 assert!(doc.metadata.contains_key("subtitle_cues"));
284
285 let _ = std::fs::remove_dir_all(&dir);
286 }
287}