use std::{borrow::Borrow, collections::HashMap, hash::Hash};
use camino::Utf8Path;
use conflate::Merge;
use indexmap::IndexMap;
use crate::{Meme, MemeBase, PartialMeme, error::ResultExt, partial::PartialMemeBase};
impl Meme {
pub fn from_files<'f>(
provider: &'f impl FileProvider<'f>,
path: &Utf8Path,
) -> crate::Result<Self> {
let mut working_dir = path.parent().unwrap_or(Utf8Path::new("")).to_owned();
let mut partial_meme = PartialMeme::from_file(provider, path)?;
while let PartialMemeBase::Extends(ref path) = partial_meme.base {
let path = working_dir.join(path);
working_dir = path.parent().unwrap_or(Utf8Path::new("")).to_owned();
partial_meme.merge(PartialMeme::from_file(provider, path)?)
}
let mut meme = Meme::from_partial(partial_meme).with_path(path)?;
if let MemeBase::Image(ref mut path) = meme.base {
*path = working_dir.join(&*path);
}
Ok(meme)
}
}
impl PartialMeme {
pub(crate) fn from_file<'f>(
provider: &'f impl FileProvider<'f>,
path: impl AsRef<Utf8Path>,
) -> crate::Result<Self> {
let path = path.as_ref();
let src = provider.read_string(path).with_path(path)?;
toml::from_str::<Self>(src.as_ref()).with_path(path)
}
}
pub trait FileProvider<'d> {
type Bytes: AsRef<[u8]> + 'd;
type String: AsRef<str> + Into<String> + 'd;
type BufReader: std::io::BufRead + std::io::Seek;
fn read(&'d self, path: impl AsRef<Utf8Path>) -> std::io::Result<Self::Bytes>;
fn read_string(&'d self, path: impl AsRef<Utf8Path>) -> std::io::Result<Self::String>;
fn buf_read(&'d self, path: impl AsRef<Utf8Path>) -> std::io::Result<Self::BufReader>;
}
pub(crate) trait FileProviderExt<'d>: FileProvider<'d> {
#[cfg(feature = "render")]
fn load_image(&'d self, path: impl AsRef<Utf8Path>) -> crate::Result<image::RgbaImage> {
let path = path.as_ref();
let image = image::ImageReader::with_format(
self.buf_read(path).with_path(path)?,
image::ImageFormat::from_path(path)?,
)
.with_guessed_format()?
.decode()?
.to_rgba8();
Ok(image)
}
}
impl<'d, T: FileProvider<'d>> FileProviderExt<'d> for T {}
#[derive(Clone, Copy, Debug)]
pub struct FsFileProvider;
impl<'d> FileProvider<'d> for FsFileProvider {
type Bytes = Vec<u8>;
type String = String;
type BufReader = std::io::BufReader<std::fs::File>;
fn read(&'d self, path: impl AsRef<Utf8Path>) -> std::io::Result<Self::Bytes> {
std::fs::read(path.as_ref())
}
fn read_string(&'d self, path: impl AsRef<Utf8Path>) -> std::io::Result<Self::String> {
std::fs::read_to_string(path.as_ref())
}
fn buf_read(&'d self, path: impl AsRef<Utf8Path>) -> std::io::Result<Self::BufReader> {
std::fs::File::open(path.as_ref()).map(std::io::BufReader::new)
}
}
impl<'d, K, V> FileProvider<'d> for HashMap<K, V>
where
K: Eq + Hash + Borrow<Utf8Path>,
V: AsRef<[u8]>,
{
type Bytes = &'d [u8];
type String = &'d str;
type BufReader = std::io::Cursor<Self::Bytes>;
fn read(&'d self, path: impl AsRef<Utf8Path>) -> std::io::Result<Self::Bytes> {
let path = path.as_ref();
self.get(path).map(AsRef::as_ref).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("file `{path}` not found in map"),
)
})
}
fn read_string(&'d self, path: impl AsRef<Utf8Path>) -> std::io::Result<Self::String> {
self.read(path).and_then(|d| {
std::str::from_utf8(d)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
})
}
fn buf_read(&'d self, path: impl AsRef<Utf8Path>) -> std::io::Result<Self::BufReader> {
self.read(path).map(std::io::Cursor::new)
}
}
impl<'d, K, V> FileProvider<'d> for IndexMap<K, V>
where
K: Eq + Hash,
Utf8Path: indexmap::Equivalent<K>,
V: AsRef<[u8]>,
{
type Bytes = &'d [u8];
type String = &'d str;
type BufReader = std::io::Cursor<Self::Bytes>;
fn read(&'d self, path: impl AsRef<Utf8Path>) -> std::io::Result<Self::Bytes> {
let path = path.as_ref();
self.get(path).map(AsRef::as_ref).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("file `{path}` not found in map"),
)
})
}
fn read_string(&'d self, path: impl AsRef<Utf8Path>) -> std::io::Result<Self::String> {
self.read(path).and_then(|d| {
std::str::from_utf8(d)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
})
}
fn buf_read(&'d self, path: impl AsRef<Utf8Path>) -> std::io::Result<Self::BufReader> {
self.read(path).map(std::io::Cursor::new)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fs() -> std::io::Result<()> {
let provider = FsFileProvider;
let path = Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/reuse.toml");
provider.read(&path)?;
provider.read_string(&path)?;
provider.read_string(&path)?;
assert!(provider.read(path.join("doesn't exist")).is_err());
Ok(())
}
#[test]
fn hashmap() -> std::io::Result<()> {
let path = Utf8Path::new("foo");
let provider: HashMap<camino::Utf8PathBuf, String> =
HashMap::from([(path.to_owned(), "bar".to_owned())]);
provider.read(path)?;
provider.read_string(path)?;
provider.read_string(path)?;
assert!(provider.read(path.join("doesn't exist")).is_err());
Ok(())
}
#[test]
fn indexmap() -> std::io::Result<()> {
let path = Utf8Path::new("foo");
let provider: IndexMap<camino::Utf8PathBuf, String> =
IndexMap::from([(path.to_owned(), "bar".to_owned())]);
provider.read(path)?;
provider.read_string(path)?;
provider.read_string(path)?;
assert!(provider.read(path.join("doesn't exist")).is_err());
Ok(())
}
}