Repository: mitsuhiko/minijinja
Pull request #697 (2025-02-03 15:03:44)

Added temps feature to state
This adds a way for functions and filters to stash away state during rendering.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1166682..637d31e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ All notable changes to MiniJinja are documented here.
 - Raise MSRV to 1.70.
 - The contrib crate now uses a basic xorrand implementation instead
   of depending on all of the `rand` module.  #696
+- Added temps, a way to stash away temporary state during rendering.  #697
 
 ## 2.8.0
 
diff --git a/Cargo.lock b/Cargo.lock
index 7d5e390..bf05c48 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2942,6 +2942,13 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "state-temps"
+version = "0.1.0"
+dependencies = [
+ "minijinja",
+]
+
 [[package]]
 name = "static_assertions"
 version = "1.1.0"
diff --git a/examples/README.md b/examples/README.md
index 5cf42b1..9986771 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -42,6 +42,7 @@ the `cargo run` command.  Alternatively you can do `cargo run -p example-name`.
 * [render-template](render-template): CLI app that renders templates from string.
 * [render-value](render-value): Demonstrates how `Value` can be passed as `Serialize` as context.
 * [self-referential-context](self-referential-context): Shows a helper that allows self-referential contexts.
+* [state-temps](state-temps): Shows how temporaries can be used for hidden state.
 * [streaming](streaming): Demonstrates how a one-shot iterator can be used to stream results in.
 * [syntax-highlighting](syntax-highlighting): Shows how to implement syntax highlighting with syntect.
 * [undefined-tracking](undefined-tracking): Shows how you can track undefined values.
diff --git a/minijinja/src/vm/mod.rs b/minijinja/src/vm/mod.rs
index d0ab8b0..0f381e9 100644
--- a/minijinja/src/vm/mod.rs
+++ b/minijinja/src/vm/mod.rs
@@ -128,6 +128,7 @@ impl<'env> Vm<'env> {
                 auto_escape: state.auto_escape(),
                 instructions,
                 blocks: BTreeMap::default(),
+                temps: state.temps.clone(),
                 loaded_templates: Default::default(),
                 #[cfg(feature = "macros")]
                 id: state.id,
diff --git a/minijinja/src/vm/state.rs b/minijinja/src/vm/state.rs
index ba153c9..bcd9bad 100644
--- a/minijinja/src/vm/state.rs
+++ b/minijinja/src/vm/state.rs
@@ -1,5 +1,6 @@
 use std::collections::{BTreeMap, BTreeSet};
 use std::fmt;
+use std::sync::{Arc, Mutex};
 
 use crate::compiler::instructions::Instructions;
 use crate::environment::Environment;
@@ -7,7 +8,7 @@ use crate::error::{Error, ErrorKind};
 use crate::output::Output;
 use crate::template::Template;
 use crate::utils::{AutoEscape, UndefinedBehavior};
-use crate::value::{ArgType, Value};
+use crate::value::{ArgType, Object, Value};
 use crate::vm::context::Context;
 
 #[cfg(feature = "fuel")]
@@ -44,6 +45,7 @@ pub struct State<'template, 'env> {
     pub(crate) current_block: Option<&'env str>,
     pub(crate) auto_escape: AutoEscape,
     pub(crate) instructions: &'template Instructions<'env>,
+    pub(crate) temps: Arc<Mutex<BTreeMap<Box<str>, Value>>>,
     pub(crate) blocks: BTreeMap<&'env str, BlockStack<'template, 'env>>,
     #[allow(unused)]
     pub(crate) loaded_templates: BTreeSet<&'env str>,
@@ -87,6 +89,7 @@ impl<'template, 'env> State<'template, 'env> {
             auto_escape,
             instructions,
             blocks,
+            temps: Default::default(),
             loaded_templates: BTreeSet::new(),
             #[cfg(feature = "macros")]
             macros: Default::default(),
@@ -299,6 +302,90 @@ impl<'template, 'env> State<'template, 'env> {
             .map(|x| (x.consumed(), x.remaining()))
     }
 
+    /// Looks up a temp and returns it.
+    ///
+    /// Temps are similar to context values but the engine never looks them up
+    /// on their own and they are not scoped.  The lifetime of temps is limited
+    /// to the rendering process of a template.  Temps are useful so that
+    /// filters and other things can temporary stash away state without having
+    /// to resort to thread locals which are hard to manage.  Unlike context
+    /// variables, temps can also be modified during evaluation by filters and
+    /// functions.
+    ///
+    /// Temps are values but if you want to hold complex state you can store a
+    /// custom object there.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use minijinja::{Value, State};
+    ///
+    /// fn inc(state: &State) -> Value {
+    ///     let old = state
+    ///         .get_temp("my_counter")
+    ///         .unwrap_or_else(|| Value::from(0i64));
+    ///     let new = Value::from(i64::try_from(old).unwrap() + 1);
+    ///     state.set_temp("my_counter", new.clone());
+    ///     new
+    /// }
+    /// ```
+    pub fn get_temp(&self, name: &str) -> Option<Value> {
+        self.temps.lock().unwrap().get(name).cloned()
+    }
+
+    /// Inserts a temp and returns the old temp.
+    ///
+    /// For more information see [`get_temp`](Self::get_temp).
+    pub fn set_temp(&self, name: &str, value: Value) -> Option<Value> {
+        self.temps
+            .lock()
+            .unwrap()
+            .insert(name.to_owned().into(), value)
+    }
+
+    /// Shortcut for registering an object as a temp.
+    ///
+    /// If the value is already there, it's returned as object, if it's
+    /// not there yet, the function is invoked to create it.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use std::sync::atomic::{AtomicUsize, Ordering};
+    /// use minijinja::{Value, State};
+    /// use minijinja::value::Object;
+    ///
+    /// #[derive(Debug, Default)]
+    /// struct MyObject(AtomicUsize);
+    ///
+    /// impl Object for MyObject {}
+    ///
+    /// fn inc(state: &State) -> Value {
+    ///     let obj = state.get_or_set_temp_object("my_counter", MyObject::default);
+    ///     let old = obj.0.fetch_add(1, Ordering::AcqRel);
+    ///     Value::from(old + 1)
+    /// }
+    /// ```
+    ///
+    /// # Panics
+    ///
+    /// This will panick if the value registered under that name is not
+    /// the object expected.
+    pub fn get_or_set_temp_object<O, F>(&self, name: &str, f: F) -> Arc<O>
+    where
+        O: Object + 'static,
+        F: FnOnce() -> O,
+    {
+        self.get_temp(name)
+            .unwrap_or_else(|| {
+                let rv = Value::from_object(f());
+                self.set_temp(name, rv.clone());
+                rv
+            })
+            .downcast_object()
+            .expect("downcast unexpectedly failed. Name conflict?")
+    }
+
     #[cfg(feature = "debug")]
     pub(crate) fn make_debug_info(
         &self,


Test patch:
diff --git a/minijinja/tests/test_state.rs b/minijinja/tests/test_state.rs
index eed72d7..ffa5ef9 100644
--- a/minijinja/tests/test_state.rs
+++ b/minijinja/tests/test_state.rs
@@ -1,4 +1,6 @@
-use minijinja::value::Value;
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+use minijinja::value::{Object, Value};
 use minijinja::{Environment, State};
 
 #[test]
@@ -11,3 +13,51 @@ fn test_state_lookup_global() {
     let rv = env.render_str("[{{ lookup_global() }}]", ()).unwrap();
     assert_eq!(rv, "[true]");
 }
+
+#[test]
+fn test_state_temps() {
+    fn inc(state: &State) -> Value {
+        let old = state
+            .get_temp("my_counter")
+            .unwrap_or_else(|| Value::from(0i64));
+        let new = Value::from(i64::try_from(old).unwrap() + 1);
+        state.set_temp("my_counter", new.clone());
+        new
+    }
+
+    let mut env = Environment::new();
+    env.add_function("inc", inc);
+    env.add_template("inc.txt", "{{ inc() }}").unwrap();
+    let rv = env
+        .render_str(
+            "{{ inc() }}|{% include 'inc.txt' %}|{% if true %}{{ inc() }}{% endif %}",
+            (),
+        )
+        .unwrap();
+    assert_eq!(rv, "1|2|3");
+}
+
+#[test]
+fn test_state_object_temps() {
+    #[derive(Debug, Default)]
+    struct MyObject(AtomicUsize);
+
+    impl Object for MyObject {}
+
+    fn inc(state: &State) -> Value {
+        let obj = state.get_or_set_temp_object("my_counter", MyObject::default);
+        let old = obj.0.fetch_add(1, Ordering::AcqRel);
+        Value::from(old + 1)
+    }
+
+    let mut env = Environment::new();
+    env.add_function("inc", inc);
+    env.add_template("inc.txt", "{{ inc() }}").unwrap();
+    let rv = env
+        .render_str(
+            "{{ inc() }}|{% include 'inc.txt' %}|{% if true %}{{ inc() }}{% endif %}",
+            (),
+        )
+        .unwrap();
+    assert_eq!(rv, "1|2|3");
+}