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 a value in the context.
19    pub fn set(&mut self, key: impl Into<String>, value: minijinja::Value) {
20        self.values.insert(key.into(), value);
21    }
22
23    /// Returns a reference to a value by key, or `None` if the key is absent.
24    pub fn get(&self, key: &str) -> Option<&minijinja::Value> {
25        self.values.get(key)
26    }
27
28    /// Merges this context with a handler-supplied MiniJinja map.
29    ///
30    /// Handler values take precedence over values already stored in `self`.
31    /// If `handler_context` is not a map, the middleware values are returned unchanged
32    /// and a warning is logged.
33    pub(crate) fn merge(&self, handler_context: minijinja::Value) -> minijinja::Value {
34        let mut merged = BTreeMap::new();
35
36        // Middleware values first (base)
37        for (k, v) in &self.values {
38            merged.insert(k.clone(), v.clone());
39        }
40
41        // Handler values override (if handler_context is a map)
42        if let Ok(keys) = handler_context.try_iter() {
43            for key in keys {
44                if let Ok(val) = handler_context.get_attr(&key.to_string()) {
45                    merged.insert(key.to_string(), val);
46                }
47            }
48        } else if !handler_context.is_none() && !handler_context.is_undefined() {
49            tracing::warn!(
50                "Handler context is not a map — handler values ignored. Use context! {{ ... }}"
51            );
52        }
53
54        minijinja::Value::from(merged)
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use minijinja::context;
62
63    #[test]
64    fn set_and_get_value() {
65        let mut ctx = TemplateContext::default();
66        ctx.set("name", minijinja::Value::from("Dmytro"));
67        let val = ctx.get("name").unwrap();
68        assert_eq!(val.to_string(), "Dmytro");
69    }
70
71    #[test]
72    fn get_missing_key_returns_none() {
73        let ctx = TemplateContext::default();
74        assert!(ctx.get("missing").is_none());
75    }
76
77    #[test]
78    fn set_overwrites_existing_value() {
79        let mut ctx = TemplateContext::default();
80        ctx.set("key", minijinja::Value::from("old"));
81        ctx.set("key", minijinja::Value::from("new"));
82        assert_eq!(ctx.get("key").unwrap().to_string(), "new");
83    }
84
85    #[test]
86    fn merge_combines_middleware_and_handler_context() {
87        let mut ctx = TemplateContext::default();
88        ctx.set("locale", minijinja::Value::from("en"));
89        ctx.set("name", minijinja::Value::from("middleware"));
90
91        let handler_ctx = context! { name => "handler", items => vec![1, 2, 3] };
92        let merged = ctx.merge(handler_ctx);
93
94        // Handler values win on conflict
95        assert_eq!(merged.get_attr("name").unwrap().to_string(), "handler");
96        // Middleware values preserved when no conflict
97        assert_eq!(merged.get_attr("locale").unwrap().to_string(), "en");
98        // Handler-only values present
99        assert!(merged.get_attr("items").is_ok());
100    }
101
102    #[test]
103    fn default_context_is_empty() {
104        let ctx = TemplateContext::default();
105        assert!(ctx.get("anything").is_none());
106    }
107
108    #[test]
109    fn context_is_clone() {
110        let mut ctx = TemplateContext::default();
111        ctx.set("key", minijinja::Value::from("value"));
112        let cloned = ctx.clone();
113        assert_eq!(cloned.get("key").unwrap().to_string(), "value");
114    }
115
116    #[test]
117    fn merge_with_non_map_ignores_handler_values() {
118        let mut ctx = TemplateContext::default();
119        ctx.set("locale", minijinja::Value::from("en"));
120        ctx.set("name", minijinja::Value::from("middleware"));
121
122        // Pass a non-map value as handler context
123        let merged = ctx.merge(minijinja::Value::from("not a map"));
124
125        // Only middleware values should survive
126        assert_eq!(merged.get_attr("locale").unwrap().to_string(), "en");
127        assert_eq!(merged.get_attr("name").unwrap().to_string(), "middleware");
128    }
129}