use crate::cache::{TemplateLoader, TemplateSource};
use crate::error::{Result, TemplateError};
use std::collections::HashMap;
pub struct ClassTemplateLoader {
templates: std::sync::Mutex<HashMap<String, &'static str>>,
base_path: std::sync::Mutex<Option<String>>,
}
impl ClassTemplateLoader {
pub fn new() -> Self {
ClassTemplateLoader {
templates: std::sync::Mutex::new(HashMap::new()),
base_path: std::sync::Mutex::new(None),
}
}
pub fn put(&self, name: &str, source: &'static str) {
let mut t = self.templates.lock().unwrap();
t.insert(name.to_string(), source);
}
pub fn set_base_path(&self, path: &str) {
let mut bp = self.base_path.lock().unwrap();
*bp = Some(normalize_base_path(path));
}
fn resolve_name(&self, raw_name: &str) -> String {
let name = normalize_slash(raw_name);
let bp = self.base_path.lock().unwrap();
match &*bp {
Some(prefix) if name.starts_with(prefix) => name[prefix.len()..].to_string(),
_ => name,
}
}
}
impl Default for ClassTemplateLoader {
fn default() -> Self {
Self::new()
}
}
impl TemplateLoader for ClassTemplateLoader {
fn find(&self, name: &str) -> Result<Option<Box<dyn TemplateSource>>> {
let resolved = self.resolve_name(name);
let t = self.templates.lock().unwrap();
Ok(t.get(&resolved)
.map(|_| Box::new(ClassTemplateSource(resolved)) as Box<dyn TemplateSource>))
}
fn read(&self, src: &dyn TemplateSource) -> Result<String> {
let t = self.templates.lock().unwrap();
let source = t
.get(&src.name())
.ok_or_else(|| TemplateError::NotFound { name: src.name() })?;
Ok((*source).to_string())
}
}
pub struct ClassTemplateSource(String);
impl TemplateSource for ClassTemplateSource {
fn name(&self) -> String {
self.0.clone()
}
}
fn normalize_slash(path: &str) -> String {
path.replace('\\', "/")
}
fn normalize_base_path(path: &str) -> String {
let normalized = normalize_slash(path);
let trimmed = normalized.trim_matches('/');
if trimmed.is_empty() {
String::new()
} else {
format!("{}/", trimmed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_and_read_without_base_path() {
let loader = ClassTemplateLoader::new();
loader.put("hello.ftl", "Hello, World!");
let src = loader.find("hello.ftl").unwrap().expect("应命中");
assert_eq!(src.name(), "hello.ftl");
assert_eq!(loader.read(&*src).unwrap(), "Hello, World!");
}
#[test]
fn find_with_base_path() {
let loader = ClassTemplateLoader::new();
loader.put("hello.ftl", "Hello, World!");
loader.set_base_path("/templates/");
let src = loader.find("templates/hello.ftl").unwrap().expect("应命中");
assert_eq!(src.name(), "hello.ftl");
assert_eq!(loader.read(&*src).unwrap(), "Hello, World!");
let src2 = loader.find("hello.ftl").unwrap().expect("应命中");
assert_eq!(src2.name(), "hello.ftl");
}
#[test]
fn find_missing_returns_none() {
let loader = ClassTemplateLoader::new();
assert!(loader.find("nope.ftl").unwrap().is_none());
}
#[test]
fn base_path_with_backslash_normalized() {
let loader = ClassTemplateLoader::new();
loader.put("sub/foo.ftl", "foo content");
loader.set_base_path("\\templates\\");
let src = loader
.find("templates/sub/foo.ftl")
.unwrap()
.expect("应命中");
assert_eq!(src.name(), "sub/foo.ftl");
assert_eq!(loader.read(&*src).unwrap(), "foo content");
}
#[test]
fn base_path_without_slashes() {
let loader = ClassTemplateLoader::new();
loader.put("foo.ftl", "foo");
loader.set_base_path("prefix");
let src = loader.find("prefix/foo.ftl").unwrap().expect("应命中");
assert_eq!(src.name(), "foo.ftl");
}
#[test]
fn same_name_overwrites() {
let loader = ClassTemplateLoader::new();
loader.put("a.ftl", "first");
loader.put("a.ftl", "second");
let src = loader.find("a.ftl").unwrap().expect("应命中");
assert_eq!(loader.read(&*src).unwrap(), "second");
}
}