Skip to main content

modo/template/
context.rs

1use std::collections::BTreeMap;
2
3/// Per-request template context shared between middleware and handlers.
4///
5/// [`TemplateContextLayer`](super::TemplateContextLayer) populates an instance with
6/// request-scoped values (`locale`, `current_url`, `is_htmx`, `csrf_token`,
7/// `flash_messages`) and inserts it into request extensions before the handler runs.
8///
9/// Handlers access the merged context through the [`Renderer`](super::Renderer)
10/// extractor. Values supplied by a handler override middleware-set values for the
11/// same key (handler context wins on conflicts).
12#[derive(Debug, Clone, Default)]
13pub struct TemplateContext {
14    values: BTreeMap<String, minijinja::Value>,
15}
16
17impl TemplateContext {
18    /// Inserts or replaces the value stored under `key`.
19    ///
20    /// Used by [`TemplateContextLayer`](super::TemplateContextLayer) to populate
21    /// request-scoped data, and available to custom middleware that wants to
22    /// expose values to templates. Note that values supplied by a handler at
23    /// render time still override values set here.
24    pub fn set(&mut self, key: impl Into<String>, value: minijinja::Value) {
25        self.values.insert(key.into(), value);
26    }
27
28    /// Returns a reference to the value stored under `key`, or `None` if the
29    /// key is absent.
30    pub fn get(&self, key: &str) -> Option<&minijinja::Value> {
31        self.values.get(key)
32    }
33
34    /// Merges this context with a handler-supplied MiniJinja map.
35    ///
36    /// Handler values take precedence over values already stored in `self`.
37    /// If `handler_context` is not a map, the middleware values are returned unchanged
38    /// and a warning is logged.
39    pub(crate) fn merge(&self, handler_context: minijinja::Value) -> minijinja::Value {
40        let mut merged = BTreeMap::new();
41
42        // Middleware values first (base)
43        for (k, v) in &self.values {
44            merged.insert(k.clone(), v.clone());
45        }
46
47        // Handler values override (if handler_context is a map)
48        if let Ok(keys) = handler_context.try_iter() {
49            for key in keys {
50                if let Ok(val) = handler_context.get_attr(&key.to_string()) {
51                    merged.insert(key.to_string(), val);
52                }
53            }
54        } else if !handler_context.is_none() && !handler_context.is_undefined() {
55            tracing::warn!(
56                "Handler context is not a map — handler values ignored. Use context! {{ ... }}"
57            );
58        }
59
60        minijinja::Value::from(merged)
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use minijinja::context;
68
69    #[test]
70    fn set_and_get_value() {
71        let mut ctx = TemplateContext::default();
72        ctx.set("name", minijinja::Value::from("Dmytro"));
73        let val = ctx.get("name").unwrap();
74        assert_eq!(val.to_string(), "Dmytro");
75    }
76
77    #[test]
78    fn get_missing_key_returns_none() {
79        let ctx = TemplateContext::default();
80        assert!(ctx.get("missing").is_none());
81    }
82
83    #[test]
84    fn set_overwrites_existing_value() {
85        let mut ctx = TemplateContext::default();
86        ctx.set("key", minijinja::Value::from("old"));
87        ctx.set("key", minijinja::Value::from("new"));
88        assert_eq!(ctx.get("key").unwrap().to_string(), "new");
89    }
90
91    #[test]
92    fn merge_combines_middleware_and_handler_context() {
93        let mut ctx = TemplateContext::default();
94        ctx.set("locale", minijinja::Value::from("en"));
95        ctx.set("name", minijinja::Value::from("middleware"));
96
97        let handler_ctx = context! { name => "handler", items => vec![1, 2, 3] };
98        let merged = ctx.merge(handler_ctx);
99
100        // Handler values win on conflict
101        assert_eq!(merged.get_attr("name").unwrap().to_string(), "handler");
102        // Middleware values preserved when no conflict
103        assert_eq!(merged.get_attr("locale").unwrap().to_string(), "en");
104        // Handler-only values present
105        assert!(merged.get_attr("items").is_ok());
106    }
107
108    #[test]
109    fn default_context_is_empty() {
110        let ctx = TemplateContext::default();
111        assert!(ctx.get("anything").is_none());
112    }
113
114    #[test]
115    fn context_is_clone() {
116        let mut ctx = TemplateContext::default();
117        ctx.set("key", minijinja::Value::from("value"));
118        let cloned = ctx.clone();
119        assert_eq!(cloned.get("key").unwrap().to_string(), "value");
120    }
121
122    #[test]
123    fn merge_with_non_map_ignores_handler_values() {
124        let mut ctx = TemplateContext::default();
125        ctx.set("locale", minijinja::Value::from("en"));
126        ctx.set("name", minijinja::Value::from("middleware"));
127
128        // Pass a non-map value as handler context
129        let merged = ctx.merge(minijinja::Value::from("not a map"));
130
131        // Only middleware values should survive
132        assert_eq!(merged.get_attr("locale").unwrap().to_string(), "en");
133        assert_eq!(merged.get_attr("name").unwrap().to_string(), "middleware");
134    }
135}