femstache 0.1.4

Embrace the feminine mustache to unlock your templating dreams! Or something, I don't know. :3
Documentation
use core::fmt;
use std::{
    collections::BTreeMap,
    error::Error as StdError,
    fs, io,
    path::{Path, PathBuf},
    sync::{Arc, RwLock},
    time::{Duration, Instant},
};

use femstache_parser::ParseError;

use crate::template::Template;

#[derive(Debug)]
pub enum LoaderError {
    Io(io::Error),
    NoLoader,
    ParseError(ParseError),
}

impl fmt::Display for LoaderError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            LoaderError::Io(err) => write!(f, "io error: {err}"),
            LoaderError::NoLoader => write!(f, "no loader set"),
            LoaderError::ParseError(err) => write!(f, "parse error: {err}"),
        }
    }
}

impl StdError for LoaderError {}

impl From<io::Error> for LoaderError {
    fn from(err: io::Error) -> Self {
        Self::Io(err)
    }
}

impl From<ParseError> for LoaderError {
    fn from(err: ParseError) -> Self {
        Self::ParseError(err)
    }
}

#[derive(Debug, Clone)]
pub struct FilesystemSource {
    root: PathBuf,
}

impl FilesystemSource {
    fn load(&self, path: impl AsRef<Path>) -> Result<String, LoaderError> {
        // TODO: path traversal safety!!
        fs::read_to_string(self.root.join(path)).map_err(Into::into)
    }
}

#[derive(Debug, Clone)]
pub struct MapSource {
    map: BTreeMap<PathBuf, String>,
}

impl MapSource {
    pub fn load(&self, path: impl AsRef<Path>) -> Result<String, LoaderError> {
        self.map
            .get(path.as_ref())
            .map(|s| s.clone())
            .ok_or_else(|| {
                LoaderError::Io(io::Error::new(
                    io::ErrorKind::NotFound,
                    "template not present in map",
                ))
            })
    }
}

/// Where templates should be loaded from
#[derive(Debug, Clone)]
pub enum TemplateSource {
    #[doc(hidden)]
    Filesystem(FilesystemSource),
    #[doc(hidden)]
    Map(MapSource),
}

impl TemplateSource {
    /// Load from the filesystem at root `root`
    pub fn filesystem(root: impl Into<PathBuf>) -> Self {
        Self::Filesystem(FilesystemSource { root: root.into() })
    }

    /// Load from a [BTreeMap]
    pub fn map(map: BTreeMap<PathBuf, String>) -> Self {
        Self::Map(MapSource { map })
    }

    fn load(&self, path: impl AsRef<Path>) -> Result<String, LoaderError> {
        match self {
            TemplateSource::Filesystem(source) => source.load(path),
            TemplateSource::Map(source) => source.load(path),
        }
    }
}

#[derive(Debug, Clone)]
pub struct TemplateLoaderInner {
    cache: BTreeMap<String, (Instant, Arc<Template>)>,
}

#[derive(Debug, Clone)]
pub struct TemplateLoader {
    source: TemplateSource,
    inner: Arc<RwLock<TemplateLoaderInner>>,
}

impl TemplateLoader {
    pub fn new(source: TemplateSource) -> Self {
        Self {
            source,
            inner: Arc::new(RwLock::new(TemplateLoaderInner {
                cache: BTreeMap::new(),
            })),
        }
    }

    pub(crate) fn load(&self, path: &str) -> Result<Arc<Template>, LoaderError> {
        {
            // TODO: do not the unwrap?
            let inner = self.inner.read().unwrap();
            if let Some((created, template)) = inner.cache.get(path)
                && created.elapsed() < Duration::from_secs(1)
            {
                return Ok(template.clone());
            }
        }
        let source = self.source.load(path)?;
        let template = Arc::new(Template::parse(&source)?);
        {
            // TODO: do not the unwrap?
            let mut inner = self.inner.write().unwrap();
            inner
                .cache
                .insert(path.to_owned(), (Instant::now(), template.clone()));
            Ok(template)
        }
    }
}