use crate::cache::{TemplateLoader, TemplateSource};
use crate::error::{Result, TemplateError};
use encoding_rs::Encoding;
use std::collections::HashMap;
pub struct ByteArrayTemplateLoader {
templates: std::sync::Mutex<HashMap<String, Vec<u8>>>,
last_modified_times: std::sync::Mutex<HashMap<String, i64>>,
}
impl ByteArrayTemplateLoader {
pub fn new() -> Self {
ByteArrayTemplateLoader {
templates: std::sync::Mutex::new(HashMap::new()),
last_modified_times: std::sync::Mutex::new(HashMap::new()),
}
}
pub fn put(&self, name: &str, bytes: &[u8]) {
let now = current_time_millis();
self.put_with_time(name, bytes, now);
}
pub fn put_with_time(&self, name: &str, bytes: &[u8], last_modified: i64) {
let mut t = self.templates.lock().unwrap();
t.insert(name.to_string(), bytes.to_vec());
let mut m = self.last_modified_times.lock().unwrap();
m.insert(name.to_string(), last_modified);
}
pub fn remove(&self, name: &str) {
let mut t = self.templates.lock().unwrap();
t.remove(name);
let mut m = self.last_modified_times.lock().unwrap();
m.remove(name);
}
}
impl Default for ByteArrayTemplateLoader {
fn default() -> Self {
Self::new()
}
}
impl TemplateLoader for ByteArrayTemplateLoader {
fn find(&self, name: &str) -> Result<Option<Box<dyn TemplateSource>>> {
let t = self.templates.lock().unwrap();
Ok(t.get(name).map(|_| {
Box::new(ByteArrayTemplateSource(name.to_string())) as Box<dyn TemplateSource>
}))
}
fn read(&self, src: &dyn TemplateSource) -> Result<String> {
self.read_encoded(src, "UTF-8")
}
fn read_encoded(&self, src: &dyn TemplateSource, encoding: &str) -> Result<String> {
let t = self.templates.lock().unwrap();
let bytes = t
.get(&src.name())
.ok_or_else(|| TemplateError::NotFound { name: src.name() })?;
let enc = Encoding::for_label(encoding.as_bytes()).ok_or_else(|| {
TemplateError::misc(format!(
"Unknown encoding: \"{encoding}\". Did you mean to use an IANA character set name?"
))
})?;
let (text, _, _) = enc.decode(bytes);
Ok(text.into_owned())
}
fn last_modified(&self, src: &dyn TemplateSource) -> Result<i64> {
let m = self.last_modified_times.lock().unwrap();
Ok(m.get(&src.name()).copied().unwrap_or(0))
}
}
pub struct ByteArrayTemplateSource(String);
impl TemplateSource for ByteArrayTemplateSource {
fn name(&self) -> String {
self.0.clone()
}
}
fn current_time_millis() -> i64 {
use std::time::SystemTime;
SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn put_and_read_utf8() {
let loader = ByteArrayTemplateLoader::new();
loader.put("hello.ftl", "Hello, World!".as_bytes());
let src = loader.find("hello.ftl").unwrap().expect("应命中");
assert_eq!(src.name(), "hello.ftl");
assert_eq!(loader.read(&*src).unwrap(), "Hello, World!");
}
#[test]
fn put_with_time_and_last_modified() {
let loader = ByteArrayTemplateLoader::new();
let ts = 1_700_000_000_000i64;
loader.put_with_time("a.ftl", b"a", ts);
let src = loader.find("a.ftl").unwrap().expect("应命中");
assert_eq!(loader.last_modified(&*src).unwrap(), ts);
}
#[test]
fn put_without_time_gets_auto_timestamp() {
let loader = ByteArrayTemplateLoader::new();
loader.put("b.ftl", b"b");
let src = loader.find("b.ftl").unwrap().expect("应命中");
let ts = loader.last_modified(&*src).unwrap();
assert!(ts > 0, "自动时间戳应 > 0,实际 {}", ts);
}
#[test]
fn find_missing_returns_none() {
let loader = ByteArrayTemplateLoader::new();
assert!(loader.find("nope.ftl").unwrap().is_none());
}
#[test]
fn remove_template() {
let loader = ByteArrayTemplateLoader::new();
loader.put("x.ftl", b"x");
assert!(loader.find("x.ftl").unwrap().is_some());
loader.remove("x.ftl");
assert!(loader.find("x.ftl").unwrap().is_none());
}
#[test]
fn read_encoded_non_utf8() {
let loader = ByteArrayTemplateLoader::new();
let bytes: Vec<u8> = vec![0x63, 0x61, 0x66, 0xE9];
loader.put("latin1.ftl", &bytes);
let src = loader.find("latin1.ftl").unwrap().expect("应命中");
let text = loader.read_encoded(&*src, "ISO-8859-1").unwrap();
assert_eq!(text, "café");
}
#[test]
fn read_encoded_unknown_encoding_errors() {
let loader = ByteArrayTemplateLoader::new();
loader.put("x.ftl", b"x");
let src = loader.find("x.ftl").unwrap().expect("应命中");
let err = loader.read_encoded(&*src, "BOGUS-CHARSET").unwrap_err();
assert!(
err.to_user_message().contains("Unknown encoding"),
"{}",
err.to_user_message()
);
}
#[test]
fn same_name_overwrites() {
let loader = ByteArrayTemplateLoader::new();
loader.put("a.ftl", b"first");
loader.put("a.ftl", b"second");
let src = loader.find("a.ftl").unwrap().expect("应命中");
assert_eq!(loader.read(&*src).unwrap(), "second");
}
}