1use std::collections::HashMap;
87use std::error::Error as StdError;
88
89use minijinja::{Environment, Error as MjError};
90
91use crate::value::QuillValue;
92
93#[derive(thiserror::Error, Debug)]
95pub enum TemplateError {
96 #[error("{0}")]
98 RenderError(#[from] minijinja::Error),
99 #[error("{0}")]
101 InvalidTemplate(String, #[source] Box<dyn StdError + Send + Sync>),
102 #[error("{0}")]
104 FilterError(String),
105}
106
107pub mod filter_api {
109 pub use minijinja::value::{Kwargs, Value};
110 pub use minijinja::{Error, ErrorKind, State};
111
112 pub trait DynFilter: Send + Sync + 'static {}
114 impl<T> DynFilter for T where T: Send + Sync + 'static {}
115}
116
117type FilterFn = fn(
119 &filter_api::State,
120 filter_api::Value,
121 filter_api::Kwargs,
122) -> Result<filter_api::Value, MjError>;
123
124pub trait GlueEngine {
126 fn register_filter(&mut self, name: &str, func: FilterFn);
128
129 fn compose(&mut self, context: HashMap<String, QuillValue>) -> Result<String, TemplateError>;
131}
132
133pub struct TemplateGlue {
135 template: String,
136 filters: HashMap<String, FilterFn>,
137}
138
139pub struct AutoGlue {
141 filters: HashMap<String, FilterFn>,
142}
143
144pub enum Glue {
146 Template(TemplateGlue),
148 Auto(AutoGlue),
150}
151
152impl TemplateGlue {
153 pub fn new(template: String) -> Self {
155 Self {
156 template,
157 filters: HashMap::new(),
158 }
159 }
160}
161
162impl GlueEngine for TemplateGlue {
163 fn register_filter(&mut self, name: &str, func: FilterFn) {
165 self.filters.insert(name.to_string(), func);
166 }
167
168 fn compose(&mut self, context: HashMap<String, QuillValue>) -> Result<String, TemplateError> {
170 let context = convert_quillvalue_to_minijinja(context)?;
172
173 let mut env = Environment::new();
175
176 for (name, filter_fn) in &self.filters {
178 let filter_fn = *filter_fn; env.add_filter(name, filter_fn);
180 }
181
182 env.add_template("main", &self.template).map_err(|e| {
183 TemplateError::InvalidTemplate("Failed to add template".to_string(), Box::new(e))
184 })?;
185
186 let tmpl = env.get_template("main").map_err(|e| {
188 TemplateError::InvalidTemplate("Failed to get template".to_string(), Box::new(e))
189 })?;
190
191 let result = tmpl.render(&context)?;
192
193 if result.len() > crate::error::MAX_TEMPLATE_OUTPUT {
195 return Err(TemplateError::FilterError(format!(
196 "Template output too large: {} bytes (max: {} bytes)",
197 result.len(),
198 crate::error::MAX_TEMPLATE_OUTPUT
199 )));
200 }
201
202 Ok(result)
203 }
204}
205
206impl AutoGlue {
207 pub fn new() -> Self {
209 Self {
210 filters: HashMap::new(),
211 }
212 }
213}
214
215impl GlueEngine for AutoGlue {
216 fn register_filter(&mut self, name: &str, func: FilterFn) {
218 self.filters.insert(name.to_string(), func);
221 }
222
223 fn compose(&mut self, context: HashMap<String, QuillValue>) -> Result<String, TemplateError> {
225 let mut json_map = serde_json::Map::new();
227 for (key, value) in context {
228 json_map.insert(key, value.as_json().clone());
229 }
230
231 let json_value = serde_json::Value::Object(json_map);
232 let result = serde_json::to_string_pretty(&json_value).map_err(|e| {
233 TemplateError::FilterError(format!("Failed to serialize to JSON: {}", e))
234 })?;
235
236 if result.len() > crate::error::MAX_TEMPLATE_OUTPUT {
238 return Err(TemplateError::FilterError(format!(
239 "JSON output too large: {} bytes (max: {} bytes)",
240 result.len(),
241 crate::error::MAX_TEMPLATE_OUTPUT
242 )));
243 }
244
245 Ok(result)
246 }
247}
248
249impl Glue {
250 pub fn new(template: String) -> Self {
252 Glue::Template(TemplateGlue::new(template))
253 }
254
255 pub fn new_auto() -> Self {
257 Glue::Auto(AutoGlue::new())
258 }
259
260 pub fn register_filter(&mut self, name: &str, func: FilterFn) {
262 match self {
263 Glue::Template(engine) => engine.register_filter(name, func),
264 Glue::Auto(engine) => engine.register_filter(name, func),
265 }
266 }
267
268 pub fn compose(
270 &mut self,
271 context: HashMap<String, QuillValue>,
272 ) -> Result<String, TemplateError> {
273 match self {
274 Glue::Template(engine) => engine.compose(context),
275 Glue::Auto(engine) => engine.compose(context),
276 }
277 }
278}
279
280fn convert_quillvalue_to_minijinja(
282 fields: HashMap<String, QuillValue>,
283) -> Result<HashMap<String, minijinja::value::Value>, TemplateError> {
284 let mut result = HashMap::new();
285
286 for (key, value) in fields {
287 let minijinja_value = value.to_minijinja().map_err(|e| {
288 TemplateError::FilterError(format!("Failed to convert QuillValue to MiniJinja: {}", e))
289 })?;
290 result.insert(key, minijinja_value);
291 }
292
293 Ok(result)
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use std::collections::HashMap;
300
301 #[test]
302 fn test_glue_creation() {
303 let _glue = Glue::new("Hello {{ name }}".to_string());
304 assert!(true);
305 }
306
307 #[test]
308 fn test_compose_simple_template() {
309 let mut glue = Glue::new("Hello {{ name }}! Body: {{ body }}".to_string());
310 let mut context = HashMap::new();
311 context.insert(
312 "name".to_string(),
313 QuillValue::from_json(serde_json::Value::String("World".to_string())),
314 );
315 context.insert(
316 "body".to_string(),
317 QuillValue::from_json(serde_json::Value::String("Hello content".to_string())),
318 );
319
320 let result = glue.compose(context).unwrap();
321 assert!(result.contains("Hello World!"));
322 assert!(result.contains("Body: Hello content"));
323 }
324
325 #[test]
326 fn test_field_with_dash() {
327 let mut glue = Glue::new("Field: {{ letterhead_title }}".to_string());
328 let mut context = HashMap::new();
329 context.insert(
330 "letterhead_title".to_string(),
331 QuillValue::from_json(serde_json::Value::String("TEST VALUE".to_string())),
332 );
333 context.insert(
334 "body".to_string(),
335 QuillValue::from_json(serde_json::Value::String("body".to_string())),
336 );
337
338 let result = glue.compose(context).unwrap();
339 assert!(result.contains("TEST VALUE"));
340 }
341
342 #[test]
343 fn test_compose_with_dash_in_template() {
344 let mut glue = Glue::new("Field: {{ letterhead_title }}".to_string());
346 let mut context = HashMap::new();
347 context.insert(
348 "letterhead_title".to_string(),
349 QuillValue::from_json(serde_json::Value::String("DASHED".to_string())),
350 );
351 context.insert(
352 "body".to_string(),
353 QuillValue::from_json(serde_json::Value::String("body".to_string())),
354 );
355
356 let result = glue.compose(context).unwrap();
357 assert!(result.contains("DASHED"));
358 }
359
360 #[test]
361 fn test_template_output_size_limit() {
362 let template = "{{ content }}".to_string();
366 let mut glue = Glue::new(template);
367
368 let mut context = HashMap::new();
369 context.insert(
373 "content".to_string(),
374 QuillValue::from_json(serde_json::Value::String("test".to_string())),
375 );
376
377 let result = glue.compose(context);
378 assert!(result.is_ok());
380 }
381
382 #[test]
383 fn test_auto_glue_basic() {
384 let mut glue = Glue::new_auto();
385 let mut context = HashMap::new();
386 context.insert(
387 "name".to_string(),
388 QuillValue::from_json(serde_json::Value::String("World".to_string())),
389 );
390 context.insert(
391 "body".to_string(),
392 QuillValue::from_json(serde_json::Value::String("Hello content".to_string())),
393 );
394
395 let result = glue.compose(context).unwrap();
396
397 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
399 assert_eq!(json["name"], "World");
400 assert_eq!(json["body"], "Hello content");
401 }
402
403 #[test]
404 fn test_auto_glue_with_nested_data() {
405 let mut glue = Glue::new_auto();
406 let mut context = HashMap::new();
407
408 let nested_obj = serde_json::json!({
410 "first": "John",
411 "last": "Doe"
412 });
413 context.insert("author".to_string(), QuillValue::from_json(nested_obj));
414
415 let tags = serde_json::json!(["tag1", "tag2", "tag3"]);
417 context.insert("tags".to_string(), QuillValue::from_json(tags));
418
419 let result = glue.compose(context).unwrap();
420
421 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
423 assert_eq!(json["author"]["first"], "John");
424 assert_eq!(json["author"]["last"], "Doe");
425 assert_eq!(json["tags"][0], "tag1");
426 assert_eq!(json["tags"].as_array().unwrap().len(), 3);
427 }
428
429 #[test]
430 fn test_auto_glue_filter_registration() {
431 let mut glue = Glue::new_auto();
433
434 fn dummy_filter(
435 _state: &filter_api::State,
436 value: filter_api::Value,
437 _kwargs: filter_api::Kwargs,
438 ) -> Result<filter_api::Value, MjError> {
439 Ok(value)
440 }
441
442 glue.register_filter("dummy", dummy_filter);
444
445 let mut context = HashMap::new();
446 context.insert(
447 "test".to_string(),
448 QuillValue::from_json(serde_json::Value::String("value".to_string())),
449 );
450
451 let result = glue.compose(context).unwrap();
452 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
453 assert_eq!(json["test"], "value");
454 }
455}