use std::{fs, path::PathBuf, string::FromUtf8Error};
use glob::glob;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum FileLoaderError {
#[error("Invalid glob pattern: {0}")]
InvalidGlobPattern(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Pattern error: {0}")]
PatternError(#[from] glob::PatternError),
#[error("Glob error: {0}")]
GlobError(#[from] glob::GlobError),
#[error("String conversion error: {0}")]
StringUtf8Error(#[from] FromUtf8Error),
}
pub(crate) trait Readable {
fn read(self) -> Result<String, FileLoaderError>;
fn read_with_path(self) -> Result<(PathBuf, String), FileLoaderError>;
}
impl<'a> FileLoader<'a, PathBuf> {
pub fn read(self) -> FileLoader<'a, Result<String, FileLoaderError>> {
FileLoader {
iterator: Box::new(self.iterator.map(|res| res.read())),
}
}
pub fn read_with_path(self) -> FileLoader<'a, Result<(PathBuf, String), FileLoaderError>> {
FileLoader {
iterator: Box::new(self.iterator.map(|res| res.read_with_path())),
}
}
}
impl Readable for PathBuf {
fn read(self) -> Result<String, FileLoaderError> {
fs::read_to_string(self).map_err(FileLoaderError::IoError)
}
fn read_with_path(self) -> Result<(PathBuf, String), FileLoaderError> {
let contents = fs::read_to_string(&self);
Ok((self, contents?))
}
}
impl Readable for Vec<u8> {
fn read(self) -> Result<String, FileLoaderError> {
Ok(String::from_utf8(self)?)
}
fn read_with_path(self) -> Result<(PathBuf, String), FileLoaderError> {
let res = String::from_utf8(self)?;
Ok((PathBuf::from("<memory>"), res))
}
}
impl<T: Readable> Readable for Result<T, FileLoaderError> {
fn read(self) -> Result<String, FileLoaderError> {
self.map(|t| t.read())?
}
fn read_with_path(self) -> Result<(PathBuf, String), FileLoaderError> {
self.map(|t| t.read_with_path())?
}
}
pub struct FileLoader<'a, T> {
iterator: Box<dyn Iterator<Item = T> + 'a>,
}
impl<'a> FileLoader<'a, Result<PathBuf, FileLoaderError>> {
pub fn read(self) -> FileLoader<'a, Result<String, FileLoaderError>> {
FileLoader {
iterator: Box::new(self.iterator.map(|res| res.read())),
}
}
pub fn read_with_path(self) -> FileLoader<'a, Result<(PathBuf, String), FileLoaderError>> {
FileLoader {
iterator: Box::new(self.iterator.map(|res| res.read_with_path())),
}
}
}
impl<'a, T> FileLoader<'a, Result<T, FileLoaderError>>
where
T: 'a,
{
pub fn ignore_errors(self) -> FileLoader<'a, T> {
FileLoader {
iterator: Box::new(self.iterator.filter_map(|res| res.ok())),
}
}
}
impl FileLoader<'_, Result<PathBuf, FileLoaderError>> {
pub fn with_glob(
pattern: &str,
) -> Result<FileLoader<'_, Result<PathBuf, FileLoaderError>>, FileLoaderError> {
let paths = glob(pattern)?;
Ok(FileLoader {
iterator: Box::new(
paths
.into_iter()
.map(|path| path.map_err(FileLoaderError::GlobError)),
),
})
}
pub fn with_dir(
directory: &str,
) -> Result<FileLoader<'_, Result<PathBuf, FileLoaderError>>, FileLoaderError> {
Ok(FileLoader {
iterator: Box::new(fs::read_dir(directory)?.filter_map(|entry| {
let path = entry.ok()?.path();
if path.is_file() { Some(Ok(path)) } else { None }
})),
})
}
}
impl<'a> FileLoader<'a, Vec<u8>> {
pub fn from_bytes(bytes: Vec<u8>) -> FileLoader<'a, Vec<u8>> {
FileLoader {
iterator: Box::new(vec![bytes].into_iter()),
}
}
pub fn from_bytes_multi(bytes_vec: Vec<Vec<u8>>) -> FileLoader<'a, Vec<u8>> {
FileLoader {
iterator: Box::new(bytes_vec.into_iter()),
}
}
pub fn read(self) -> FileLoader<'a, Result<String, FileLoaderError>> {
FileLoader {
iterator: Box::new(self.iterator.map(|res| res.read())),
}
}
pub fn read_with_path(self) -> FileLoader<'a, Result<(PathBuf, String), FileLoaderError>> {
FileLoader {
iterator: Box::new(self.iterator.map(|res| res.read_with_path())),
}
}
}
pub struct IntoIter<'a, T> {
iterator: Box<dyn Iterator<Item = T> + 'a>,
}
impl<'a, T> IntoIterator for FileLoader<'a, T> {
type Item = T;
type IntoIter = IntoIter<'a, T>;
fn into_iter(self) -> Self::IntoIter {
IntoIter {
iterator: self.iterator,
}
}
}
impl<T> Iterator for IntoIter<'_, T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
self.iterator.next()
}
}
#[cfg(test)]
mod tests {
use assert_fs::prelude::{FileTouch, FileWriteStr, PathChild};
use super::FileLoader;
#[test]
fn test_file_loader() {
let temp = assert_fs::TempDir::new().expect("Failed to create temp dir");
let foo_file = temp.child("foo.txt");
let bar_file = temp.child("bar.txt");
foo_file.touch().expect("Failed to create foo.txt");
bar_file.touch().expect("Failed to create bar.txt");
foo_file.write_str("foo").expect("Failed to write to foo");
bar_file.write_str("bar").expect("Failed to write to bar");
let glob = temp.path().to_string_lossy().to_string() + "/*.txt";
let loader = FileLoader::with_glob(&glob).unwrap();
let mut actual = loader
.ignore_errors()
.read()
.ignore_errors()
.into_iter()
.collect::<Vec<_>>();
let mut expected = vec!["foo".to_string(), "bar".to_string()];
actual.sort();
expected.sort();
assert!(!actual.is_empty());
assert!(expected == actual)
}
#[test]
fn test_file_loader_bytes() {
let temp = assert_fs::TempDir::new().expect("Failed to create temp dir");
let foo_file = temp.child("foo.txt");
let bar_file = temp.child("bar.txt");
foo_file.touch().expect("Failed to create foo.txt");
bar_file.touch().expect("Failed to create bar.txt");
foo_file.write_str("foo").expect("Failed to write to foo");
bar_file.write_str("bar").expect("Failed to write to bar");
let foo_bytes = std::fs::read(foo_file.path()).unwrap();
let bar_bytes = std::fs::read(bar_file.path()).unwrap();
let loader = FileLoader::from_bytes_multi(vec![foo_bytes, bar_bytes]);
let mut actual = loader
.read()
.ignore_errors()
.into_iter()
.collect::<Vec<_>>();
let mut expected = vec!["foo".to_string(), "bar".to_string()];
actual.sort();
expected.sort();
assert!(!actual.is_empty());
assert!(expected == actual)
}
}