maimai 0.1.1

Markup-based meme generator
Documentation
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)
    }
}

/// An abstraction over methods of providing readable files.
///
/// For regular file system access, [`FsFileProvider`] can be used. The trait
/// is also implemented for [`HashMap`]s and  [`IndexMap`]s storing in-memory
/// data mapped to a virtual file 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 {}

/// A [`FileProvider`] that uses [`std::fs`] to read file from the filesystem.
#[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(())
    }
}