femstache 0.1.2

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::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 TemplateLoaderInner {
    root: PathBuf,
    cache: BTreeMap<String, (Instant, Arc<Template>)>,
}

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

impl TemplateLoader {
    pub fn new(root: impl Into<PathBuf>) -> Self {
        Self {
            inner: Arc::new(RwLock::new(TemplateLoaderInner {
                root: root.into(),
                cache: BTreeMap::new(),
            })),
        }
    }

    pub(crate) fn load(&self, path: &str) -> Result<Arc<Template>, LoaderError> {
        let full_path = {
            // 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());
            }
            inner.root.join(path)
        };
        // TODO: path traversal safety!!
        let source = fs::read_to_string(&full_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)
    }
}