Repository: mitsuhiko/minijinja (language: rust)

Issue:
Ability to add entries to a Value
I am in the position that I want to add a new entry to a Value (returned by the `context!()` macro).

I want to create a `render` function that takes the regular context value and adds more fields to it.
Using globals is not possible in this case, because those Values are request specific.
I was thinking about a function, but those can't have states.
Then I read about "abusing" an object variable, but I sadly cant get access to the request session that way, so I want to enhance the regular context values by adding the required values by calling a wrapper function.

Problem being: I cannot get the inner ValueMap, and I prefer not to serialize into serde and back.
Is there any (future) possibility to get a function analog to `get_attr` that allows to set an attribute on the map?

I know that most/all other ValueKinds won't be able to support that function, but it would help me a lot in this case.

I will for now deserialize into serde and then back to minijinja Value.
I hope this suggestion will get some thought anyway.


Discussion / hints:
Regular values are immutable. However your requirement is real and it has come up before. One way to solve this today is to use a custom object that looks up from multiple other values: https://github.com/mitsuhiko/minijinja/blob/main/examples/merge-context/src/main.rs
That looks more to my use case, thank you!

I would love to have an inbuilt value merging system available, but this helps a ton already!
I definitely want to have something as part of the engine but I haven't found a good solution yet I like.
this is what i use now:

```rs
use std::{collections::BTreeSet, sync::Arc};

use minijinja::value::{StructObject, Value};

/// A struct that looks up from multiple values.
#[derive(Debug, Default)]
struct MergeContext(Vec<Value>);

impl MergeContext {
    pub fn new(values: Vec<Value>) -> Self {
        Self(values)
    }

    pub fn to_value(self) -> Value {
        Value::from_struct_object(self)
    }
}

impl StructObject for MergeContext {
    fn get_field(&self, field: &str) -> Option<Value> {
        for val in &self.0 {
            match val.get_attr(field) {
                Ok(val) if !val.is_undefined() => return Some(val),
                _ => {}
            }
        }
        None
    }

    fn fields(&self) -> Vec<Arc<str>> {
        let mut rv = BTreeSet::new();
        for val in &self.0 {
            if let Ok(iter) = val.try_iter() {
                for item in iter {
                    if let Some(s) = item.as_str() {
                        if !rv.contains(s) {
                            rv.insert(Arc::from(s.to_string()));
                        }
                    }
                }
            }
        }
        rv.into_iter().collect()
    }
}

macro_rules! merge {
    ($($val:expr),*) => {
        MergeContext::new(vec![$($val),*]).to_value()
    };
}
```

and an example:
```rs
    fn working_with_contexts(val: Value) -> Value {
        merge!(val, context!( more => "data" ))
    }
```

the `merge!` macro makes it easy to use (and understand IMO).
maybe this can help in your inspiration
I think what could be trivially added is a second syntax to `context!` that merges:

```rust
let ctx = context![..ctx, ..context! {
    more => "data"
}];
```
Oh, that would be awesome as well.
I'm liking this one even more. Would remove the need for an extra macro as well

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a6854b0..950156e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
 
 All notable changes to MiniJinja are documented here.
 
+## 1.0.5
+
+- Added the ability to merge multiple values with the `context!`
+  macro.  (#317)
+
 ## 1.0.4
 
 - Added the `args!` macro which can be used to create an argument
diff --git a/examples/merge-context/src/main.rs b/examples/merge-context/src/main.rs
index fde8134a..e159a391 100644
--- a/examples/merge-context/src/main.rs
+++ b/examples/merge-context/src/main.rs
@@ -1,51 +1,8 @@
-use std::collections::BTreeSet;
-use std::sync::Arc;
-
-use minijinja::value::{StructObject, Value};
 use minijinja::{context, Environment};
 
-/// A struct that looks up from multiple values.
-struct MergeContext(Vec<Value>);
-
-impl StructObject for MergeContext {
-    fn get_field(&self, field: &str) -> Option<Value> {
-        for val in &self.0 {
-            match val.get_attr(field) {
-                Ok(val) if !val.is_undefined() => return Some(val),
-                _ => {}
-            }
-        }
-        None
-    }
-
-    fn fields(&self) -> Vec<Arc<str>> {
-        let mut rv = BTreeSet::new();
-        for val in &self.0 {
-            if let Ok(iter) = val.try_iter() {
-                for item in iter {
-                    if let Some(s) = item.as_str() {
-                        if !rv.contains(s) {
-                            rv.insert(Arc::from(s.to_string()));
-                        }
-                    }
-                }
-            }
-        }
-        rv.into_iter().collect()
-    }
-}
-
-/// Merges one or more contexts.
-pub fn merge_contexts<I>(i: I) -> Value
-where
-    I: Iterator<Item = Value>,
-{
-    Value::from_struct_object(MergeContext(i.into_iter().collect()))
-}
-
 fn main() {
     let env = Environment::new();
-    let ctx = merge_contexts([context! { a => "A" }, context! { b => "B" }].into_iter());
+    let ctx = context! { a => "A", ..context! { b => "B" } };
     println!(
         "{}",
         env.render_str("Two variables: {{ a }} and {{ b }}!", ctx)
diff --git a/minijinja/src/filters.rs b/minijinja/src/filters.rs
index bb2f586d..655b6205 100644
--- a/minijinja/src/filters.rs
+++ b/minijinja/src/filters.rs
@@ -642,7 +642,7 @@ mod builtins {
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
     pub fn last(value: Value) -> Result<Value, Error> {
         if let Some(s) = value.as_str() {
-            Ok(s.chars().rev().next().map_or(Value::UNDEFINED, Value::from))
+            Ok(s.chars().next_back().map_or(Value::UNDEFINED, Value::from))
         } else if let Some(seq) = value.as_seq() {
             Ok(seq.iter().last().unwrap_or(Value::UNDEFINED))
         } else {
diff --git a/minijinja/src/macros.rs b/minijinja/src/macros.rs
index ebca3ef2..48aad235 100644
--- a/minijinja/src/macros.rs
+++ b/minijinja/src/macros.rs
@@ -23,6 +23,7 @@ macro_rules! some {
 /// Hidden utility module for the [`context!`](crate::context!) macro.
 #[doc(hidden)]
 pub mod __context {
+    pub use crate::value::merge_object::MergeObject;
     use crate::value::{KeyRef, MapType, Value, ValueMap, ValueRepr};
     use crate::Environment;
     use std::rc::Rc;
@@ -56,11 +57,11 @@ pub mod __context {
     }
 }
 
-/// Creates a template context with keys and values.
+/// Creates a template context from keys and values or merging in another value.
 ///
 /// ```rust
 /// # use minijinja::context;
-/// let ctx = context! {
+/// let ctx = context!{
 ///     name => "Peter",
 ///     location => "World",
 /// };
@@ -72,7 +73,7 @@ pub mod __context {
 /// ```rust
 /// # use minijinja::context;
 /// let name = "Peter";
-/// let ctx = context! { name };
+/// let ctx = context!{ name };
 /// ```
 ///
 /// The return value is a [`Value`](crate::value::Value).
@@ -90,30 +91,78 @@ pub mod __context {
 ///     ]
 /// };
 /// ```
+///
+/// Additionally the macro supports a second syntax that can merge other
+/// contexts or values.  In that case one or more values need to be
+/// passed with a leading `..` operator.  This is useful to supply extra
+/// values into render in a common place.  The order of precedence is
+/// left to right:
+///
+/// ```rust
+/// # use minijinja::context;
+/// let ctx = context! { a => "A" };
+/// let ctx = context! { ..ctx, ..context! {
+///     b => "B"
+/// }};
+///
+/// // or
+///
+/// let ctx = context! {
+///     a => "A",
+///     ..context! {
+///         b => "B"
+///     }
+/// };
+/// ```
+///
+/// The merge works with an value, not just values created by the `context!`
+/// macro and is performed lazy.  This means it also works with dynamic
+/// [`StructObject`](crate::value::StructObject)s.
 #[macro_export]
 macro_rules! context {
     () => {
         $crate::__context::build($crate::__context::make())
     };
     (
-        $($key:ident $(=> $value:expr)?),* $(,)?
+        $($key:ident $(=> $value:expr)?),*
+        $(, .. $ctx:expr),* $(,)?
     ) => {{
         let _guard = $crate::__context::value_optimization();
         let mut ctx = $crate::__context::make();
         $(
-            $crate::__context_pair!(ctx, $key $(, $value)?);
+            $crate::__context_pair!(ctx, $key $(=> $value)?);
         )*
-        $crate::__context::build(ctx)
-    }}
+        let ctx = $crate::__context::build(ctx);
+        let mut merged_ctx = ::std::vec::Vec::new();
+        $(
+            merged_ctx.push($crate::value::Value::from($ctx));
+        )*;
+        if merged_ctx.is_empty() {
+            ctx
+        } else {
+            merged_ctx.insert(0, ctx);
+            $crate::value::Value::from_struct_object($crate::__context::MergeObject(merged_ctx))
+        }
+    }};
+    (
+        $(.. $ctx:expr),* $(,)?
+    ) => {{
+        let _guard = $crate::__context::value_optimization();
+        let mut ctx = ::std::vec::Vec::new();
+        $(
+            ctx.push($crate::value::Value::from($ctx));
+        )*;
+        $crate::value::Value::from_struct_object($crate::__context::MergeObject(ctx))
+    }};
 }
 
 #[macro_export]
 #[doc(hidden)]
 macro_rules! __context_pair {
     ($ctx:ident, $key:ident) => {{
-        $crate::__context_pair!($ctx, $key, $key);
+        $crate::__context_pair!($ctx, $key => $key);
     }};
-    ($ctx:ident, $key:ident, $value:expr) => {
+    ($ctx:ident, $key:ident => $value:expr) => {
         $crate::__context::add(
             &mut $ctx,
             stringify!($key),
diff --git a/minijinja/src/value/merge_object.rs b/minijinja/src/value/merge_object.rs
new file mode 100644
index 00000000..30425a7a
--- /dev/null
+++ b/minijinja/src/value/merge_object.rs
@@ -0,0 +1,40 @@
+use std::collections::BTreeSet;
+use std::sync::Arc;
+
+use crate::value::object::StructObject;
+use crate::value::Value;
+
+/// Utility struct used by [`context!`](crate::context) to merge
+/// multiple values.
+pub struct MergeObject(pub Vec<Value>);
+
+impl StructObject for MergeObject {
+    fn get_field(&self, field: &str) -> Option<Value> {
+        for val in &self.0 {
+            match val.get_attr(field) {
+                Ok(val) if !val.is_undefined() => return Some(val),
+                _ => {}
+            }
+        }
+        None
+    }
+
+    fn fields(&self) -> Vec<Arc<str>> {
+        let mut seen = BTreeSet::new();
+        let mut rv = Vec::new();
+        for val in &self.0 {
+            if let Ok(iter) = val.try_iter() {
+                for item in iter {
+                    let s: Result<Arc<str>, _> = item.try_into();
+                    if let Ok(s) = s {
+                        if !seen.contains(&s) {
+                            seen.insert(s.clone());
+                            rv.push(s);
+                        }
+                    }
+                }
+            }
+        }
+        rv
+    }
+}
diff --git a/minijinja/src/value/mod.rs b/minijinja/src/value/mod.rs
index ecbd9cad..a09db5b0 100644
--- a/minijinja/src/value/mod.rs
+++ b/minijinja/src/value/mod.rs
@@ -133,6 +133,7 @@ mod argtypes;
 #[cfg(feature = "deserialization")]
 mod deserialize;
 mod keyref;
+pub(crate) mod merge_object;
 mod object;
 pub(crate) mod ops;
 mod serialize;


Test patch:
diff --git a/minijinja/tests/test_macros.rs b/minijinja/tests/test_macros.rs
index 794fc521..198c945a 100644
--- a/minijinja/tests/test_macros.rs
+++ b/minijinja/tests/test_macros.rs
@@ -1,6 +1,6 @@
 use similar_asserts::assert_eq;
 
-use minijinja::value::{Kwargs, Value};
+use minijinja::value::{Kwargs, StructObject, Value};
 use minijinja::{args, context, render, Environment};
 
 #[test]
@@ -11,6 +11,40 @@ fn test_context() {
     assert_eq!(ctx.get_attr("var2").unwrap(), Value::from(42));
 }
 
+#[test]
+fn test_context_merge() {
+    let one = context!(a => 1);
+    let two = context!(b => 2, a => 42);
+    let ctx = context![..one, ..two];
+    assert_eq!(ctx.get_attr("a").unwrap(), Value::from(1));
+    assert_eq!(ctx.get_attr("b").unwrap(), Value::from(2));
+
+    let two = context!(b => 2, a => 42);
+    let ctx = context!(a => 1, ..two);
+    assert_eq!(ctx.get_attr("a").unwrap(), Value::from(1));
+    assert_eq!(ctx.get_attr("b").unwrap(), Value::from(2));
+}
+
+#[test]
+fn test_context_merge_custom() {
+    struct X;
+    impl StructObject for X {
+        fn get_field(&self, name: &str) -> Option<Value> {
+            match name {
+                "a" => Some(Value::from(1)),
+                "b" => Some(Value::from(2)),
+                _ => None,
+            }
+        }
+    }
+
+    let x = Value::from_struct_object(X);
+    let ctx = context! { a => 42, ..x };
+
+    assert_eq!(ctx.get_attr("a").unwrap(), Value::from(42));
+    assert_eq!(ctx.get_attr("b").unwrap(), Value::from(2));
+}
+
 #[test]
 fn test_render() {
     let env = Environment::new();