use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use rand::{distributions::Alphanumeric, rngs::StdRng, Rng, SeedableRng};
use tera::{Context, Tera};
use crate::settings::Settings;
const TEMPLATE_EXTENSION: &str = "t";
fn generate_random_string<R: Rng>(rng: &mut R, length: u64) -> String {
(0..length)
.map(|_| rng.sample(Alphanumeric) as char)
.collect()
}
#[derive(Debug, Clone)]
pub struct Template {
rng: Arc<Mutex<StdRng>>,
}
impl Default for Template {
fn default() -> Self {
#[cfg(test)]
let rng = StdRng::seed_from_u64(42);
#[cfg(not(test))]
let rng = StdRng::from_entropy();
Self {
rng: Arc::new(Mutex::new(rng)),
}
}
}
impl Template {
#[must_use]
pub fn new(rng: StdRng) -> Self {
Self {
rng: Arc::new(Mutex::new(rng)),
}
}
#[must_use]
pub fn is_template(&self, path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.filter(|&ext| ext == TEMPLATE_EXTENSION)
.is_some()
}
fn register_filters(&self, tera_instance: &mut tera::Tera) {
let rng_clone = Arc::clone(&self.rng);
tera_instance.register_filter(
"random_string",
move |value: &tera::Value, _args: &HashMap<String, tera::Value>| {
if let tera::Value::Number(length) = value {
if let Some(length) = length.as_u64() {
let rand_str: String = rng_clone.lock().map_or_else(
|_| {
let mut r = StdRng::from_entropy();
generate_random_string(&mut r, length)
},
|mut rng| generate_random_string(&mut *rng, length),
);
return Ok(tera::Value::String(rand_str));
}
}
Err(tera::Error::msg("arg must be a number"))
},
);
}
pub fn render(&self, template_content: &str, settings: &Settings) -> tera::Result<String> {
tracing::trace!(
template_content,
settings = format!("{settings:#?}"),
"render template"
);
let mut tera_instance = Tera::default();
self.register_filters(&mut tera_instance);
let mut context = Context::new();
context.insert("settings", &settings);
let rendered_output = tera_instance.render_str(template_content, &context)?;
Ok(rendered_output)
}
pub fn strip_template_extension(&self, path: &Path) -> std::io::Result<PathBuf> {
path.file_stem().map_or_else(
|| {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Failed to retrieve file stem",
))
},
|stem| {
let mut path_without_extension = path.to_path_buf();
path_without_extension.set_file_name(stem);
if let Some(parent_dir) = path.parent() {
path_without_extension = parent_dir.join(stem.to_string_lossy().to_string());
}
Ok(path_without_extension)
},
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_template() {
let template = Template::default();
let path = Path::new("example.t");
assert!(template.is_template(path));
let path = Path::new("example.txt");
assert!(!template.is_template(path));
let path = Path::new("directory/");
assert!(!template.is_template(path));
}
#[test]
fn test_render_template() {
let template = Template::default();
let template_content = "crate: {{ settings.package_name }}";
let mock_settings = Settings {
package_name: "loco-app".to_string(),
..Default::default()
};
let result = template.render(template_content, &mock_settings);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "crate: loco-app");
}
#[test]
fn test_strip_template_extension() {
let template = Template::default();
let path = Path::new("example.t");
let result = template.strip_template_extension(path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), Path::new("example"));
let path = Path::new("example");
let result = template.strip_template_extension(path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), Path::new("example"));
let path = Path::new("");
let result = template.strip_template_extension(path);
assert!(result.is_err());
}
#[test]
fn can_create_random_string() {
let template = Template::default();
let template_content = "rand: {{20 | random_string }}";
let mock_settings = Settings {
package_name: "loco-app".to_string(),
..Default::default()
};
let result = template.render(template_content, &mock_settings);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "rand: IhPi3oZCnaWvL2oIeA07");
let result = template.render(template_content, &mock_settings);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "rand: mg3ZtJzh0NoAKhdDqpQ2");
}
}