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("json_pretty", |value: &tera::Value, _: &std::collections::HashMap<String, tera::Value>| {
177 serde_json::to_string_pretty(value)
178 .map(tera::Value::String)
179 .map_err(|e| tera::Error::msg(e.to_string()))
180 });
181
182 tera.register_filter("truncate_words", |value: &tera::Value, args: &std::collections::HashMap<String, tera::Value>| {
184 let s = tera::try_get_value!("truncate_words", "value", String, value);
185 let length = match args.get("length") {
186 Some(val) => tera::try_get_value!("truncate_words", "length", usize, val),
187 None => 50,
188 };
189 let end = match args.get("end") {
190 Some(val) => tera::try_get_value!("truncate_words", "end", String, val),
191 None => "...".to_string(),
192 };
193
194 let words: Vec<&str> = s.split_whitespace().collect();
195 if words.len() <= length {
196 Ok(tera::Value::String(s))
197 } else {
198 let truncated: String = words[..length].join(" ");
199 Ok(tera::Value::String(format!("{}{}", truncated, end)))
200 }
201 });
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[tokio::test]
209 async fn test_empty_templates() {
210 let templates = Templates::empty();
211 templates.add_template("test", "Hello, {{ name }}!").await.unwrap();
212
213 let mut ctx = tera::Context::new();
214 ctx.insert("name", "World");
215
216 let result = templates.render("test", &ctx).await.unwrap();
217 assert_eq!(result, "Hello, World!");
218 }
219
220 #[tokio::test]
221 async fn test_render_with_struct() {
222 #[derive(serde::Serialize)]
223 struct Data {
224 name: String,
225 }
226
227 let templates = Templates::empty();
228 templates.add_template("test", "Hello, {{ name }}!").await.unwrap();
229
230 let data = Data { name: "Alice".to_string() };
231 let result = templates.render_with("test", &data).await.unwrap();
232 assert_eq!(result, "Hello, Alice!");
233 }
234}