rustapi_view/
templates.rs1use crate::ViewError;
4use std::sync::Arc;
5use tera::Tera;
6use tokio::sync::RwLock;
7
8#[derive(Debug, Clone)]
10pub struct TemplatesConfig {
11 pub glob: String,
13 pub auto_reload: bool,
15 pub strict_mode: bool,
17}
18
19impl Default for TemplatesConfig {
20 fn default() -> Self {
21 Self {
22 glob: "templates/**/*.html".to_string(),
23 auto_reload: cfg!(debug_assertions),
24 strict_mode: false,
25 }
26 }
27}
28
29impl TemplatesConfig {
30 pub fn new(glob: impl Into<String>) -> Self {
32 Self {
33 glob: glob.into(),
34 ..Default::default()
35 }
36 }
37
38 pub fn auto_reload(mut self, enabled: bool) -> Self {
40 self.auto_reload = enabled;
41 self
42 }
43
44 pub fn strict_mode(mut self, enabled: bool) -> Self {
46 self.strict_mode = enabled;
47 self
48 }
49}
50
51#[derive(Clone)]
64pub struct Templates {
65 inner: Arc<RwLock<Tera>>,
66 config: TemplatesConfig,
67}
68
69impl Templates {
70 pub fn new(glob: impl Into<String>) -> Result<Self, ViewError> {
81 let config = TemplatesConfig::new(glob);
82 Self::with_config(config)
83 }
84
85 pub fn with_config(config: TemplatesConfig) -> Result<Self, ViewError> {
87 let mut tera = Tera::new(&config.glob)?;
88
89 register_builtin_filters(&mut tera);
91
92 Ok(Self {
93 inner: Arc::new(RwLock::new(tera)),
94 config,
95 })
96 }
97
98 pub fn empty() -> Self {
100 Self {
101 inner: Arc::new(RwLock::new(Tera::default())),
102 config: TemplatesConfig::default(),
103 }
104 }
105
106 pub async fn add_template(
108 &self,
109 name: impl Into<String>,
110 content: impl Into<String>,
111 ) -> Result<(), ViewError> {
112 let mut tera = self.inner.write().await;
113 tera.add_raw_template(&name.into(), &content.into())?;
114 Ok(())
115 }
116
117 pub async fn render(
119 &self,
120 template: &str,
121 context: &tera::Context,
122 ) -> Result<String, ViewError> {
123 #[cfg(debug_assertions)]
125 if self.config.auto_reload {
126 let mut tera = self.inner.write().await;
127 if let Err(e) = tera.full_reload() {
128 tracing::warn!("Template reload failed: {}", e);
129 }
130 }
131
132 let tera = self.inner.read().await;
133 tera.render(template, context).map_err(ViewError::from)
134 }
135
136 pub async fn render_with<T: serde::Serialize>(
138 &self,
139 template: &str,
140 data: &T,
141 ) -> Result<String, ViewError> {
142 let context = tera::Context::from_serialize(data)
143 .map_err(|e| ViewError::serialization_error(e.to_string()))?;
144 self.render(template, &context).await
145 }
146
147 pub async fn has_template(&self, name: &str) -> bool {
149 let tera = self.inner.read().await;
150 let result = tera.get_template_names().any(|n| n == name);
151 result
152 }
153
154 pub async fn template_names(&self) -> Vec<String> {
156 let tera = self.inner.read().await;
157 tera.get_template_names().map(String::from).collect()
158 }
159
160 pub async fn reload(&self) -> Result<(), ViewError> {
162 let mut tera = self.inner.write().await;
163 tera.full_reload()?;
164 Ok(())
165 }
166
167 pub fn config(&self) -> &TemplatesConfig {
169 &self.config
170 }
171}
172
173fn register_builtin_filters(tera: &mut Tera) {
175 tera.register_filter(
177 "json_pretty",
178 |value: &tera::Value, _: &std::collections::HashMap<String, tera::Value>| {
179 serde_json::to_string_pretty(value)
180 .map(tera::Value::String)
181 .map_err(|e| tera::Error::msg(e.to_string()))
182 },
183 );
184
185 tera.register_filter(
187 "truncate_words",
188 |value: &tera::Value, args: &std::collections::HashMap<String, tera::Value>| {
189 let s = tera::try_get_value!("truncate_words", "value", String, value);
190 let length = match args.get("length") {
191 Some(val) => tera::try_get_value!("truncate_words", "length", usize, val),
192 None => 50,
193 };
194 let end = match args.get("end") {
195 Some(val) => tera::try_get_value!("truncate_words", "end", String, val),
196 None => "...".to_string(),
197 };
198
199 let words: Vec<&str> = s.split_whitespace().collect();
200 if words.len() <= length {
201 Ok(tera::Value::String(s))
202 } else {
203 let truncated: String = words[..length].join(" ");
204 Ok(tera::Value::String(format!("{}{}", truncated, end)))
205 }
206 },
207 );
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[tokio::test]
215 async fn test_empty_templates() {
216 let templates = Templates::empty();
217 templates
218 .add_template("test", "Hello, {{ name }}!")
219 .await
220 .unwrap();
221
222 let mut ctx = tera::Context::new();
223 ctx.insert("name", "World");
224
225 let result = templates.render("test", &ctx).await.unwrap();
226 assert_eq!(result, "Hello, World!");
227 }
228
229 #[tokio::test]
230 async fn test_render_with_struct() {
231 #[derive(serde::Serialize)]
232 struct Data {
233 name: String,
234 }
235
236 let templates = Templates::empty();
237 templates
238 .add_template("test", "Hello, {{ name }}!")
239 .await
240 .unwrap();
241
242 let data = Data {
243 name: "Alice".to_string(),
244 };
245 let result = templates.render_with("test", &data).await.unwrap();
246 assert_eq!(result, "Hello, Alice!");
247 }
248}