maimai 0.1.1

Markup-based meme generator
Documentation
use std::borrow::Cow;

use camino::{Utf8Path, Utf8PathBuf};

/// Any result returned by this library.
pub type Result<T> = std::result::Result<T, Error>;

#[derive(Debug)]
/// Any error returned by this library.
pub struct Error {
    kind: Box<ErrorKind>,
    path: Option<Utf8PathBuf>,
}

impl Error {
    pub fn kind(&self) -> &ErrorKind {
        &self.kind
    }
    pub fn path(&self) -> Option<&Utf8Path> {
        self.path.as_deref()
    }
}

impl Error {
    #[track_caller]
    #[doc(hidden)]
    pub fn new(kind: ErrorKind) -> Self {
        Self {
            kind: Box::new(kind),
            path: None,
        }
    }
    #[track_caller]
    #[doc(hidden)]
    pub fn other(e: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
        Error::new(ErrorKind::Other(e.into()))
    }
    pub(crate) fn with_path(mut self, path: impl Into<Utf8PathBuf>) -> Self {
        self.path = Some(path.into());
        self
    }
}

impl From<std::fmt::Error> for Error {
    #[track_caller]
    fn from(_: std::fmt::Error) -> Self {
        panic!()
    }
}

impl From<std::io::Error> for Error {
    #[track_caller]
    fn from(e: std::io::Error) -> Self {
        Self::new(ErrorKind::FileRead(e))
    }
}

#[cfg(feature = "read")]
impl From<toml::de::Error> for Error {
    #[track_caller]
    fn from(e: toml::de::Error) -> Self {
        Self::new(ErrorKind::Parse(e))
    }
}

#[cfg(feature = "render")]
impl From<image::ImageError> for Error {
    #[track_caller]
    fn from(e: image::ImageError) -> Self {
        Self::new(ErrorKind::Image(e))
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.kind() {
            ErrorKind::FileRead(_) => match self.path() {
                Some(path) => write!(f, "failed to read file `{path}`"),
                None => write!(f, "failed to read file"),
            },
            #[cfg(feature = "read")]
            ErrorKind::Parse(_) => f.write_str("failed to deserialize file"),
            ErrorKind::Incomplete(_) => f.write_str("incomplete meme definition"),
            #[cfg(feature = "render")]
            ErrorKind::Image(e) => std::fmt::Display::fmt(e, f),
            ErrorKind::Other(e) => std::fmt::Display::fmt(e, f),
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &*self.kind {
            ErrorKind::FileRead(e) => Some(e),
            #[cfg(feature = "read")]
            ErrorKind::Parse(e) => Some(e),
            ErrorKind::Incomplete(_) => None,
            #[cfg(feature = "render")]
            ErrorKind::Image(e) => Some(e),
            ErrorKind::Other(_) => None,
        }
    }
}

/// The kind of error.
#[non_exhaustive]
#[derive(Debug)]
pub enum ErrorKind {
    FileRead(std::io::Error),
    #[cfg(feature = "read")]
    Parse(toml::de::Error),
    Incomplete(Vec<IncompleteMemeDefinition>),
    #[cfg(feature = "render")]
    Image(image::ImageError),
    Other(Box<dyn std::error::Error + Send + Sync>),
}

/// Describes missing information to build a [`Meme`](crate::Meme)
/// from one or multiple [`PartialMeme`](crate::partial::PartialMeme)s.
#[non_exhaustive]
#[derive(Debug)]
pub enum IncompleteMemeDefinition {
    MissingField(Vec<Cow<'static, str>>),
    MissingParameters(Vec<Cow<'static, str>>),
}

impl IncompleteMemeDefinition {
    #[doc(hidden)]
    #[cfg(feature = "cli")]
    pub fn keys(&self) -> impl IntoIterator<Item = impl AsRef<str>> {
        match self {
            Self::MissingField(keys) | Self::MissingParameters(keys) => keys,
        }
    }
}

impl std::fmt::Display for IncompleteMemeDefinition {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        fn write_path(
            f: &mut std::fmt::Formatter<'_>,
            path: &[Cow<'static, str>],
        ) -> std::fmt::Result {
            for (i, name) in path.iter().enumerate() {
                if i > 0 {
                    write!(f, ".")?;
                }
                match name {
                    Cow::Borrowed(static_name) => write!(f, "{static_name}")?,
                    Cow::Owned(dynamic_name) => write!(f, "{dynamic_name:?}")?,
                }
            }

            Ok(())
        }

        match self {
            Self::MissingField(path) => {
                write!(f, "missing field: ")?;
                write_path(f, path)?;
                Ok(())
            }
            Self::MissingParameters(path) => {
                write!(f, "missing parameters for ")?;
                write_path(f, path)?;
                Ok(())
            }
        }
    }
}

impl std::error::Error for IncompleteMemeDefinition {}

// based on `ariadne::FileCache` but takes `&Utf8Path` instead of `Path`.
// see also https://github.com/zesterer/ariadne/issues/40
#[cfg(feature = "cli")]
#[derive(Default, Debug, Clone)]

pub struct FileCache {
    files: std::collections::HashMap<Utf8PathBuf, ariadne::Source>,
}

#[cfg(feature = "cli")]
impl ariadne::Cache<&Utf8Path> for FileCache {
    type Storage = String;

    fn fetch(
        &mut self,
        path: &&Utf8Path,
    ) -> std::result::Result<&ariadne::Source, impl std::fmt::Debug> {
        use std::collections::hash_map::Entry;

        Ok::<_, Error>(match self.files.entry((*path).to_owned()) {
            Entry::Occupied(entry) => entry.into_mut(),
            Entry::Vacant(entry) => entry.insert(ariadne::Source::from(
                std::fs::read_to_string(path).with_path(path)?,
            )),
        })
    }

    fn display<'a>(&self, path: &'a &Utf8Path) -> Option<impl std::fmt::Display + 'a> {
        Some(path)
    }
}

pub(crate) trait ResultExt<T, E>
where
    Self: Sized,
    E: Into<Error>,
{
    fn with_path(self, path: impl Into<Utf8PathBuf>) -> Result<T>;
}

impl<T, E> ResultExt<T, E> for std::result::Result<T, E>
where
    Self: Sized,
    E: Into<Error>,
{
    #[track_caller]
    fn with_path(self, path: impl Into<Utf8PathBuf>) -> Result<T> {
        match self {
            Ok(v) => Ok(v),
            Err(e) => Err(e.into().with_path(path)),
        }
    }
}