Repository: mitsuhiko/minijinja
Pull request #548 (2024-07-31 15:49:32)

Implemented &Object and Arc<Object> as ArgType
This simplifies working with objects in filters and functions.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a4bea91..5b28261 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@ All notable changes to MiniJinja are documented here.
 - Added `indent` parameter to `tojson` filter.  #546
 - Added `randrange`, `lipsum`, `random`, `cycler` and `joiner` to
   `minijinja-contrib`.  #547
+- Added the ability to use `&T` and `Arc<T>` as parameters
+  to filters and functions if `T` is an `Object`.  #548
 
 ## 2.1.0
 
diff --git a/examples/dsl/src/main.rs b/examples/dsl/src/main.rs
index f1d0fb1..13d770d 100644
--- a/examples/dsl/src/main.rs
+++ b/examples/dsl/src/main.rs
@@ -3,7 +3,7 @@ use std::fmt;
 use std::sync::Arc;
 
 use minijinja::value::{from_args, Kwargs, Object, Value};
-use minijinja::{Environment, Error, ErrorKind};
+use minijinja::{Environment, Error};
 
 /// A copy-on-write object that holds an assembled query.
 #[derive(Debug, Clone)]
@@ -85,29 +85,19 @@ fn query(table: String) -> Value {
     Value::from_object(Query::new(table))
 }
 
-/// Utility function to extract a [`Query`] out of a [`Value`].
-fn value_as_query(obj: &Value) -> Result<&Query, Error> {
-    obj.downcast_object_ref::<Query>().ok_or_else(|| {
-        Error::new(
-            ErrorKind::InvalidOperation,
-            "filter must be applied to query",
-        )
-    })
-}
-
 /// Filters a query by some keyword arguments as filter function.
-fn filter_filter(obj: &Value, kwargs: Kwargs) -> Result<Value, Error> {
-    Ok(Value::from_object(value_as_query(obj)?.filter(kwargs)))
+fn filter_filter(obj: &Query, kwargs: Kwargs) -> Result<Value, Error> {
+    Ok(Value::from_object(obj.filter(kwargs)))
 }
 
 /// Applies a limit to a query as filter function.
-fn limit_filter(obj: &Value, limit: usize) -> Result<Value, Error> {
-    Ok(Value::from_object(value_as_query(obj)?.limit(limit)))
+fn limit_filter(obj: &Query, limit: usize) -> Result<Value, Error> {
+    Ok(Value::from_object(obj.limit(limit)))
 }
 
 /// Applies an offset to a query as filter function.
-fn offset_filter(obj: &Value, offset: usize) -> Result<Value, Error> {
-    Ok(Value::from_object(value_as_query(obj)?.offset(offset)))
+fn offset_filter(obj: &Query, offset: usize) -> Result<Value, Error> {
+    Ok(Value::from_object(obj.offset(offset)))
 }
 
 fn main() {
diff --git a/minijinja/src/value/argtypes.rs b/minijinja/src/value/argtypes.rs
index 90eaefb..dd96abc 100644
--- a/minijinja/src/value/argtypes.rs
+++ b/minijinja/src/value/argtypes.rs
@@ -111,7 +111,7 @@ where
 /// * bytes: [`&[u8]`][`slice`]
 /// * values: [`Value`], `&Value`
 /// * vectors: [`Vec<T>`]
-/// * objects: [`DynObject`]
+/// * objects: [`DynObject`], [`Arc<T>`], `&T` (where `T` is an [`Object`])
 /// * serde deserializable: [`ViaDeserialize<T>`](crate::value::deserialize::ViaDeserialize)
 /// * keyword arguments: [`Kwargs`]
 /// * leftover arguments: [`Rest<T>`]
@@ -599,6 +599,34 @@ impl<'a> ArgType<'a> for &[Value] {
     }
 }
 
+impl<'a, T: Object + 'static> ArgType<'a> for &T {
+    type Output = &'a T;
+
+    #[inline(always)]
+    fn from_value(value: Option<&'a Value>) -> Result<Self::Output, Error> {
+        match value {
+            Some(value) => value
+                .downcast_object_ref()
+                .ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "expected object")),
+            None => Err(Error::from(ErrorKind::MissingArgument)),
+        }
+    }
+}
+
+impl<'a, T: Object + 'static> ArgType<'a> for Arc<T> {
+    type Output = Arc<T>;
+
+    #[inline(always)]
+    fn from_value(value: Option<&'a Value>) -> Result<Self::Output, Error> {
+        match value {
+            Some(value) => value
+                .downcast_object()
+                .ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "expected object")),
+            None => Err(Error::from(ErrorKind::MissingArgument)),
+        }
+    }
+}
+
 /// Utility type to capture remaining arguments.
 ///
 /// In some cases you might want to have a variadic function.  In that case


Test patch:
diff --git a/minijinja/tests/test_value.rs b/minijinja/tests/test_value.rs
index fe850f4..a8d887f 100644
--- a/minijinja/tests/test_value.rs
+++ b/minijinja/tests/test_value.rs
@@ -961,3 +961,29 @@ fn test_object_btree_map() {
     );
     assert_eq!(value.to_string(), "{true: 1}");
 }
+
+#[test]
+fn test_downcast_arg() {
+    #[derive(Debug)]
+    struct A;
+
+    #[derive(Debug)]
+    struct B;
+
+    fn my_func(a: &A, b: Arc<B>) -> String {
+        format!("{:?}|{:?}", a, b)
+    }
+
+    impl Object for A {}
+    impl Object for B {}
+
+    let mut env = Environment::new();
+    env.add_function("my_func", my_func);
+
+    assert_eq!(
+        render!(in env, "{{ my_func(a, b) }}",
+        a => Value::from_object(A),
+        b => Value::from_object(B)),
+        "A|B"
+    );
+}