Skip to main content

rig_core/loaders/epub/
loader.rs

1use epub::doc::EpubDoc;
2
3use std::fs::File;
4use std::io::BufReader;
5use std::marker::PhantomData;
6use std::path::PathBuf;
7
8use super::RawTextProcessor;
9use super::errors::EpubLoaderError;
10use super::text_processors::TextProcessor;
11
12// ================================================================
13// Implementing Loadable trait for loading epubs
14// ================================================================
15
16loadable_trait!(
17    Loadable,
18    EpubLoaderError,
19    EpubDoc<BufReader<File>>,
20    load,
21    load_with_path
22);
23
24impl Loadable for PathBuf {
25    fn load(self) -> Result<EpubDoc<BufReader<File>>, EpubLoaderError> {
26        EpubDoc::new(self).map_err(EpubLoaderError::EpubError)
27    }
28
29    fn load_with_path(self) -> Result<(PathBuf, EpubDoc<BufReader<File>>), EpubLoaderError> {
30        let contents = EpubDoc::new(&self).map_err(EpubLoaderError::EpubError);
31        Ok((self, contents?))
32    }
33}
34
35// ================================================================
36// EpubFileLoader definitions and implementations
37// ================================================================
38
39/// [EpubFileLoader] is a utility for loading epub files from the filesystem using glob patterns or
40///  directory paths. It provides methods to read file contents and handle errors gracefully.
41///
42/// # Errors
43///
44/// This module defines a custom error type [EpubLoaderError] which can represent various errors
45///  that might occur during file loading operations, such as any [FileLoaderError](crate::loaders::file::FileLoaderError) alongside
46///  specific EPUB-related errors.
47///
48/// # Example Usage
49///
50/// ```no_run
51/// use rig_core::loaders::{EpubFileLoader, RawTextProcessor, StripXmlProcessor};
52///
53/// fn main() -> Result<(), Box<dyn std::error::Error>> {
54///     // Create a FileLoader using a glob pattern
55///     let loader = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?;
56///
57///     // Load epub file contents by chapter, ignoring any errors
58///     let contents = loader
59///         .load_with_path()
60///         .ignore_errors()
61///         .by_chapter()
62///         .ignore_errors();
63///
64///     for (path, chapters) in contents {
65///         println!("{}", path.display());
66///         for (idx, chapter) in chapters {
67///             println!("Chapter {} begins", idx);
68///             println!("{}", chapter);
69///             println!("Chapter {} ends", idx);
70///         }
71///     }
72///
73///     // Create a FileLoader using a glob pattern with stripping xml
74///     let loader = EpubFileLoader::<_, StripXmlProcessor>::with_glob("tests/data/*.epub")?;
75///
76///     // Load epub file contents by chapter, ignoring any errors
77///     let contents = loader
78///         .load_with_path()
79///         .ignore_errors()
80///         .by_chapter()
81///         .ignore_errors();
82///
83///     for (path, chapters) in contents {
84///         println!("{}", path.display());
85///         for (idx, chapter) in chapters {
86///             println!("Chapter {} begins", idx);
87///             println!("{}", chapter);
88///             println!("Chapter {} ends", idx);
89///         }
90///     }
91///
92///     Ok(())
93/// }
94/// ```
95///
96/// [EpubFileLoader] uses strict typing between the iterator methods to ensure that transitions
97///  between different implementations of the loaders and it's methods are handled properly by
98///  the compiler.
99pub struct EpubFileLoader<'a, T, P = RawTextProcessor> {
100    iterator: Box<dyn Iterator<Item = T> + 'a>,
101    _processor: PhantomData<P>,
102}
103
104type EpubLoaded = Result<(PathBuf, EpubDoc<BufReader<File>>), EpubLoaderError>;
105
106impl<'a, P> EpubFileLoader<'a, Result<PathBuf, EpubLoaderError>, P> {
107    /// Loads the contents of the epub files within the iterator returned by [EpubFileLoader::with_glob]
108    ///  or [EpubFileLoader::with_dir]. Loaded EPUB documents are raw EPUB instances that can be
109    ///  further processed (by chapter, etc).
110    ///
111    /// # Example
112    /// Load epub files in directory "tests/data/*.epub" and return the loaded documents
113    ///
114    /// ```no_run
115    /// use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
116    ///
117    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
118    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?.load().into_iter();
119    /// for result in content {
120    ///     match result {
121    ///         Ok(doc) => println!("{:?}", doc),
122    ///         Err(e) => eprintln!("Error reading epub: {}", e),
123    ///     }
124    /// }
125    /// # Ok(())
126    /// # }
127    /// ```
128    pub fn load(self) -> EpubFileLoader<'a, Result<EpubDoc<BufReader<File>>, EpubLoaderError>, P> {
129        EpubFileLoader {
130            iterator: Box::new(self.iterator.map(|res| res.load())),
131            _processor: PhantomData,
132        }
133    }
134
135    /// Loads the contents of the epub files within the iterator returned by [EpubFileLoader::with_glob]
136    ///  or [EpubFileLoader::with_dir]. Loaded EPUB documents are raw EPUB instances with their path
137    ///  that can be further processed.
138    ///
139    /// # Example
140    /// Load epub files in directory "tests/data/*.epub" and return the loaded documents
141    ///
142    /// ```no_run
143    /// use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
144    ///
145    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
146    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?.load_with_path().into_iter();
147    /// for result in content {
148    ///     match result {
149    ///         Ok((path, doc)) => println!("{:?} {:?}", path, doc),
150    ///         Err(e) => eprintln!("Error reading epub: {}", e),
151    ///     }
152    /// }
153    /// # Ok(())
154    /// # }
155    /// ```
156    pub fn load_with_path(self) -> EpubFileLoader<'a, EpubLoaded, P> {
157        EpubFileLoader {
158            iterator: Box::new(self.iterator.map(|res| res.load_with_path())),
159            _processor: PhantomData,
160        }
161    }
162}
163
164impl<'a, P> EpubFileLoader<'a, Result<PathBuf, EpubLoaderError>, P>
165where
166    P: TextProcessor,
167{
168    /// Directly reads the contents of the epub files within the iterator returned by
169    ///  [EpubFileLoader::with_glob] or [EpubFileLoader::with_dir].
170    ///
171    /// # Example
172    /// Read epub files in directory "tests/data/*.epub" and return the contents of the documents.
173    ///
174    /// ```no_run
175    /// # use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
176    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
177    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?.read().into_iter();
178    /// for result in content {
179    ///     match result {
180    ///         Ok(content) => println!("{}", content),
181    ///         Err(e) => eprintln!("Error reading epub: {}", e),
182    ///     }
183    /// }
184    /// # Ok(())
185    /// # }
186    /// ```
187    pub fn read(self) -> EpubFileLoader<'a, Result<String, EpubLoaderError>, P> {
188        EpubFileLoader {
189            iterator: Box::new(self.iterator.map(|res| {
190                let doc = res.load().map(EpubChapterIterator::<P>::from)?;
191
192                Ok(doc
193                    .into_iter()
194                    .collect::<Result<Vec<String>, EpubLoaderError>>()?
195                    .into_iter()
196                    .collect::<String>())
197            })),
198            _processor: PhantomData,
199        }
200    }
201
202    /// Directly reads the contents of the epub files within the iterator returned by
203    ///  [EpubFileLoader::with_glob] or [EpubFileLoader::with_dir] and returns the path along with
204    ///  the content.
205    ///
206    /// # Example
207    /// Read epub files in directory "tests/data/*.epub" and return the content and paths of the documents.
208    ///
209    /// ```no_run
210    /// # use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
211    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
212    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?.read_with_path().into_iter();
213    /// for result in content {
214    ///     match result {
215    ///         Ok((path, content)) => println!("{:?} {}", path, content),
216    ///         Err(e) => eprintln!("Error reading epub: {}", e),
217    ///     }
218    /// }
219    /// # Ok(())
220    /// # }
221    /// ```
222    pub fn read_with_path(
223        self,
224    ) -> EpubFileLoader<'a, Result<(PathBuf, String), EpubLoaderError>, P> {
225        EpubFileLoader {
226            iterator: Box::new(self.iterator.map(|res| {
227                let (path, doc) = res.load_with_path()?;
228
229                let content = EpubChapterIterator::<P>::from(doc)
230                    .collect::<Result<Vec<String>, EpubLoaderError>>()?
231                    .into_iter()
232                    .collect::<String>();
233                Ok((path, content))
234            })),
235            _processor: PhantomData,
236        }
237    }
238}
239
240impl<'a, P> EpubFileLoader<'a, EpubDoc<BufReader<File>>, P>
241where
242    P: TextProcessor + 'a,
243{
244    /// Chunks the chapters of a loaded document by chapter, flattened as a single vector.
245    ///
246    /// # Example
247    /// Load epub files in directory "tests/data/*.epub" and chunk all document into it's chapters.
248    ///
249    /// ```no_run
250    /// # use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
251    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
252    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?
253    ///     .load()
254    ///     .ignore_errors()
255    ///     .by_chapter()
256    ///     .into_iter();
257    /// for result in content {
258    ///     match result {
259    ///         Ok(chapter) => println!("{}", chapter),
260    ///         Err(e) => eprintln!("Error reading chapter: {}", e),
261    ///     }
262    /// }
263    /// # Ok(())
264    /// # }
265    /// ```
266    pub fn by_chapter(self) -> EpubFileLoader<'a, Result<String, EpubLoaderError>, P> {
267        EpubFileLoader {
268            iterator: Box::new(self.iterator.flat_map(EpubChapterIterator::<P>::from)),
269            _processor: PhantomData,
270        }
271    }
272}
273
274type ByChapter = (PathBuf, Vec<(usize, Result<String, EpubLoaderError>)>);
275impl<'a, P: TextProcessor> EpubFileLoader<'a, (PathBuf, EpubDoc<BufReader<File>>), P> {
276    /// Chunks the chapters of a loaded document by chapter, processed as a vector of documents by path
277    ///  which each document container an inner vector of chapters by chapter number.
278    ///
279    /// # Example
280    /// Read epub files in directory "tests/data/*.epub" and chunk all documents by path by it's chapters.
281    ///
282    /// ```no_run
283    /// # use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
284    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
285    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?
286    ///     .load_with_path()
287    ///     .ignore_errors()
288    ///     .by_chapter()
289    ///     .ignore_errors()
290    ///     .into_iter();
291    ///
292    /// for result in content {
293    ///     println!("{:?}", result);
294    /// }
295    /// # Ok(())
296    /// # }
297    /// ```
298    pub fn by_chapter(self) -> EpubFileLoader<'a, ByChapter, P> {
299        EpubFileLoader {
300            iterator: Box::new(self.iterator.map(|doc| {
301                let (path, doc) = doc;
302
303                (
304                    path,
305                    EpubChapterIterator::<P>::from(doc)
306                        .enumerate()
307                        .collect::<Vec<_>>(),
308                )
309            })),
310            _processor: PhantomData,
311        }
312    }
313}
314
315impl<'a, P> EpubFileLoader<'a, ByChapter, P>
316where
317    P: TextProcessor,
318{
319    /// Ignores errors in the iterator, returning only successful results. This can be used on any
320    ///  [EpubFileLoader] state of iterator whose items are results.
321    ///
322    /// # Example
323    /// Read files in directory "tests/data/*.epub" and ignore errors from unreadable files.
324    ///
325    /// ```no_run
326    /// # use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
327    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
328    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?
329    ///     .load_with_path()
330    ///     .ignore_errors()
331    ///     .by_chapter()
332    ///     .ignore_errors();
333    /// for (_path, chapters) in content {
334    ///     println!("{}", chapters.len())
335    /// }
336    /// # Ok(())
337    /// # }
338    /// ```
339    pub fn ignore_errors(self) -> EpubFileLoader<'a, (PathBuf, Vec<(usize, String)>), P> {
340        EpubFileLoader {
341            iterator: Box::new(self.iterator.map(|(path, chapters)| {
342                let chapters = chapters
343                    .into_iter()
344                    .filter_map(|(idx, res)| res.ok().map(|content| (idx, content)))
345                    .collect::<Vec<_>>();
346                (path, chapters)
347            })),
348            _processor: PhantomData,
349        }
350    }
351}
352
353loader_scaffold!(EpubFileLoader, EpubLoaderError, dir: all_entries, extra: P);
354
355// ================================================================
356// EpubChapterIterator definitions and implementations
357// ================================================================
358
359struct EpubChapterIterator<P> {
360    epub: EpubDoc<BufReader<File>>,
361    finished: bool,
362    _processor: PhantomData<P>,
363}
364
365impl<P> From<EpubDoc<BufReader<File>>> for EpubChapterIterator<P> {
366    fn from(epub: EpubDoc<BufReader<File>>) -> Self {
367        Self::new(epub)
368    }
369}
370
371impl<P> EpubChapterIterator<P> {
372    fn new(epub: EpubDoc<BufReader<File>>) -> Self {
373        Self {
374            epub,
375            finished: false,
376            _processor: PhantomData,
377        }
378    }
379}
380
381impl<P> Iterator for EpubChapterIterator<P>
382where
383    P: TextProcessor,
384{
385    type Item = Result<String, EpubLoaderError>;
386
387    fn next(&mut self) -> Option<Self::Item> {
388        if self.finished {
389            return None;
390        }
391
392        // ignore empty chapters if they exist
393        while !self.finished {
394            let chapter = self.epub.get_current_str();
395
396            if !self.epub.go_next() {
397                self.finished = true;
398            }
399
400            if let Some((text, _)) = chapter {
401                return Some(
402                    P::process(&text)
403                        .map_err(|err| EpubLoaderError::TextProcessorError(Box::new(err))),
404                );
405            }
406        }
407
408        None
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use crate::loaders::epub::RawTextProcessor;
415    use crate::loaders::test_fixtures::{fixture_glob, fixture_path};
416
417    use super::EpubFileLoader;
418
419    #[test]
420    fn test_epub_loader_with_errors() {
421        let glob = fixture_glob("*.epub");
422        let loader = EpubFileLoader::<_, RawTextProcessor>::with_glob(&glob).unwrap();
423        let actual = loader
424            .load_with_path()
425            .ignore_errors()
426            .by_chapter()
427            .into_iter()
428            .collect::<Vec<_>>();
429
430        assert_eq!(actual.len(), 1);
431
432        let (_, chapters) = &actual[0];
433        assert_eq!(chapters.len(), 3);
434
435        for chapter in chapters {
436            assert!(chapter.1.is_ok());
437        }
438    }
439
440    #[test]
441    fn test_epub_loader_with_ignoring_errors() {
442        let glob = fixture_glob("*.epub");
443        let loader = EpubFileLoader::<_, RawTextProcessor>::with_glob(&glob).unwrap();
444        let actual = loader
445            .load_with_path()
446            .ignore_errors()
447            .by_chapter()
448            .ignore_errors()
449            .into_iter()
450            .collect::<Vec<_>>();
451
452        assert_eq!(actual.len(), 1);
453
454        let (_, chapters) = &actual[0];
455        assert_eq!(chapters.len(), 3);
456    }
457
458    #[test]
459    fn test_single_file() {
460        let glob = fixture_glob("*.epub");
461        let loader = EpubFileLoader::<_, RawTextProcessor>::with_glob(&glob).unwrap();
462
463        let actual = loader
464            .read()
465            .ignore_errors()
466            .into_iter()
467            .collect::<Vec<_>>();
468
469        assert_eq!(actual.len(), 1);
470    }
471
472    #[test]
473    fn test_single_file_with_path() {
474        let glob = fixture_glob("*.epub");
475        let loader = EpubFileLoader::<_, RawTextProcessor>::with_glob(&glob).unwrap();
476
477        let actual = loader
478            .read_with_path()
479            .ignore_errors()
480            .into_iter()
481            .collect::<Vec<_>>();
482
483        assert_eq!(actual.len(), 1);
484
485        let (path, _) = &actual[0];
486        assert_eq!(path, &fixture_path("dummy.epub"));
487    }
488}