pipi/generator/
template.rs1use std::{
4 collections::HashMap,
5 path::{Path, PathBuf},
6 sync::{Arc, Mutex},
7};
8
9use rand::{distributions::Alphanumeric, rngs::StdRng, Rng, SeedableRng};
10use tera::{Context, Tera};
11
12use crate::settings::Settings;
13
14const TEMPLATE_EXTENSION: &str = "t";
15
16fn generate_random_string<R: Rng>(rng: &mut R, length: u64) -> String {
17 (0..length)
18 .map(|_| rng.sample(Alphanumeric) as char)
19 .collect()
20}
21
22#[derive(Debug, Clone)]
24pub struct Template {
25 rng: Arc<Mutex<StdRng>>,
26}
27
28impl Default for Template {
29 fn default() -> Self {
30 #[cfg(test)]
31 let rng = StdRng::seed_from_u64(42);
32 #[cfg(not(test))]
33 let rng = StdRng::from_entropy();
34 Self {
35 rng: Arc::new(Mutex::new(rng)),
36 }
37 }
38}
39
40impl Template {
41 #[must_use]
42 pub fn new(rng: StdRng) -> Self {
43 Self {
44 rng: Arc::new(Mutex::new(rng)),
45 }
46 }
47 #[must_use]
52 pub fn is_template(&self, path: &Path) -> bool {
53 path.extension()
54 .and_then(|ext| ext.to_str())
55 .filter(|&ext| ext == TEMPLATE_EXTENSION)
56 .is_some()
57 }
58
59 fn register_filters(&self, tera_instance: &mut tera::Tera) {
61 let rng_clone = Arc::clone(&self.rng);
63
64 tera_instance.register_filter(
65 "random_string",
66 move |value: &tera::Value, _args: &HashMap<String, tera::Value>| {
67 if let tera::Value::Number(length) = value {
68 if let Some(length) = length.as_u64() {
69 let rand_str: String = rng_clone.lock().map_or_else(
70 |_| {
71 let mut r = StdRng::from_entropy();
72 generate_random_string(&mut r, length)
73 },
74 |mut rng| generate_random_string(&mut *rng, length),
75 );
76 return Ok(tera::Value::String(rand_str));
77 }
78 }
79 Err(tera::Error::msg("arg must be a number"))
81 },
82 );
83 }
84
85 pub fn render(&self, template_content: &str, settings: &Settings) -> tera::Result<String> {
90 tracing::trace!(
91 template_content,
92 settings = format!("{settings:#?}"),
93 "render template"
94 );
95
96 let mut tera_instance = Tera::default();
97 self.register_filters(&mut tera_instance);
98
99 let mut context = Context::new();
100 context.insert("settings", &settings);
101
102 let rendered_output = tera_instance.render_str(template_content, &context)?;
103
104 Ok(rendered_output)
105 }
106
107 pub fn strip_template_extension(&self, path: &Path) -> std::io::Result<PathBuf> {
112 path.file_stem().map_or_else(
113 || {
114 Err(std::io::Error::new(
115 std::io::ErrorKind::InvalidInput,
116 "Failed to retrieve file stem",
117 ))
118 },
119 |stem| {
120 let mut path_without_extension = path.to_path_buf();
121 path_without_extension.set_file_name(stem);
122 if let Some(parent_dir) = path.parent() {
123 path_without_extension = parent_dir.join(stem.to_string_lossy().to_string());
124 }
125 Ok(path_without_extension)
126 },
127 )
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_is_template() {
137 let template = Template::default();
138
139 let path = Path::new("example.t");
140 assert!(template.is_template(path));
141
142 let path = Path::new("example.txt");
143 assert!(!template.is_template(path));
144
145 let path = Path::new("directory/");
146 assert!(!template.is_template(path));
147 }
148
149 #[test]
150 fn test_render_template() {
151 let template = Template::default();
152 let template_content = "crate: {{ settings.package_name }}";
153
154 let mock_settings = Settings {
155 package_name: "pipi-app".to_string(),
156 ..Default::default()
157 };
158
159 let result = template.render(template_content, &mock_settings);
160 assert!(result.is_ok());
161 assert_eq!(result.unwrap(), "crate: pipi-app");
162 }
163
164 #[test]
165 fn test_strip_template_extension() {
166 let template = Template::default();
167
168 let path = Path::new("example.t");
169 let result = template.strip_template_extension(path);
170 assert!(result.is_ok());
171 assert_eq!(result.unwrap(), Path::new("example"));
172
173 let path = Path::new("example");
174 let result = template.strip_template_extension(path);
175 assert!(result.is_ok());
176 assert_eq!(result.unwrap(), Path::new("example"));
177
178 let path = Path::new("");
179 let result = template.strip_template_extension(path);
180 assert!(result.is_err());
181 }
182
183 #[test]
184 fn can_create_random_string() {
185 let template = Template::default();
186 let template_content = "rand: {{20 | random_string }}";
187
188 let mock_settings = Settings {
189 package_name: "pipi-app".to_string(),
190 ..Default::default()
191 };
192
193 let result = template.render(template_content, &mock_settings);
194 assert!(result.is_ok());
195 assert_eq!(result.unwrap(), "rand: IhPi3oZCnaWvL2oIeA07");
196 let result = template.render(template_content, &mock_settings);
197 assert!(result.is_ok());
198 assert_eq!(result.unwrap(), "rand: mg3ZtJzh0NoAKhdDqpQ2");
199 }
200}