Repository: mitsuhiko/minijinja (language: rust)

Issue:
Iterator-based SeqObject
Since version 0.27 this crate provides a method to adapt an arbitrary object into a conceptual sequence, using the `SeqObject` trait which can then be used to iterate over. This can then be turned into a `Value` and used in the template.

One thing it doesn't allow you to do, is iterate over objects in a streaming fashion. The requirement of random access, baked into the trait, invalidates doing as much. You have to know ahead of time how many objects you have, and you have to have random access on the underlying object.

In Python on the other hand, it's possible to use generators to lazily load values and iterate over them as you go.

This mainly comes into play when writing multi-gigabyte files based on millions of iterable items.

Something like `Value::from_iterator(val: impl<Iterator> + 'static)` would solve this. Not to be confused with the current `FromIterator` impl, which materializes the list into a Vec. This is explicitly undesirable.


Discussion / hints:
To add some context to what @bertptrs said:

`jinja2` has a [`template.stream(...)`](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Template.stream) method which can take an iterator of `items` and then return a [`TemplateStream`](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.environment.TemplateStream). Example:

```python
 generated_stream = jj2_env.from_string(some_template).stream(
      items=item_generator, **new_template_context
  )
```

This is very useful when working with a large number of items that need to be rendered, since they don't all need to be loaded into memory first. Is there a way to achieve this in `minijinja` as well?

We looked at [SeqObject](https://docs.rs/minijinja/latest/minijinja/value/trait.SeqObject.html) to replicate this, however it requires random access in its `get_item` method:

```rust
fn get_item(&self, idx: usize) -> Option<Value>;
```



This seems to conflate two very different problems. One is iterating over something that has no known length, the other is streaming a large template. The latter (eg: what `stream` does in Jinja2 on the template) you can do today just fine by using `render_to_write`.

Iterators in MiniJinja don't really exist, they are just vectors or sequences internally. Since there is no other loop equivalent it's quite likely it won't be possible to accomplish this without abusing recursion. I will look into what it takes to add iterator support but it's not something that is super trivial.
I looked into this a bit now. Today the iterator types you can get from `Value` are all `ExactSizeIterator`. That is also leverage by the iterator system for calculating the reverse indexes. Unfortunately also `ValueKind` is exhaustive which means that an `Iterator` type cannot really be added. This might require backwards incompatible changes.

Maybe this is something to look at together with #400
In order for this to be changed it most likely will either require some hacks or at least to do the changes here: https://github.com/mitsuhiko/minijinja/issues/418

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4fbbc83f..b92931f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,19 +6,17 @@ All notable changes to MiniJinja are documented here.
 
 - `minijinja-cli` now supports an `-o` or `--output` parameter to write
   into a target file.  #405
-
 - `minijinja-cli` now accepts the `--safe-path` parameter to disallow
   includes or extends from paths not explicitly allowlisted.  #432
-
 - Added support for `Error::display_debug_info` which displays just the
   debug info, same way as alternative display on the error does.  #420
-
 - Added the `namspace()` function from Jinja2 and the ability to assign
   to it via `{% set %}`.  #422
-
 - `minijinja-autoreload` now supports `on_should_reload_callback` which
   lets one register a callback to be called just before an auto reload
   should be performed.  #424
+- Added support for `Value::from_iterator`, `IteratorObject` and
+  `ObjectKind::Iterator`.  #426
 
 ## 1.0.12
 
diff --git a/examples/dynamic-objects/src/main.rs b/examples/dynamic-objects/src/main.rs
index 7103aba6..378d5b87 100644
--- a/examples/dynamic-objects/src/main.rs
+++ b/examples/dynamic-objects/src/main.rs
@@ -74,6 +74,7 @@ fn main() {
     env.add_function("cycler", make_cycler);
     env.add_global("magic", Value::from_object(Magic));
     env.add_global("seq", Value::from_seq_object(SimpleDynamicSeq));
+    env.add_global("real_iter", Value::from_iterator((0..10).chain(20..30)));
     env.add_template("template.html", include_str!("template.html"))
         .unwrap();
 
diff --git a/examples/dynamic-objects/src/template.html b/examples/dynamic-objects/src/template.html
index 47241fbe..98eda4a1 100644
--- a/examples/dynamic-objects/src/template.html
+++ b/examples/dynamic-objects/src/template.html
@@ -4,4 +4,8 @@
     <li class={{ next_class() }}>{{ char }}</li>
   {%- endfor %}
   </ul>
-{%- endwith %}
\ No newline at end of file
+{%- endwith %}
+
+{%- for item in real_iter %}
+  - {{ item }} ({{ loop.index }} from {{ loop.length|default("?") }})
+{%- endfor %}
\ No newline at end of file
diff --git a/minijinja/src/functions.rs b/minijinja/src/functions.rs
index caea85e5..554a557a 100644
--- a/minijinja/src/functions.rs
+++ b/minijinja/src/functions.rs
@@ -340,7 +340,9 @@ mod builtins {
                 ValueRepr::Map(map, _) => map,
                 ValueRepr::Dynamic(ref dynamic) => match dynamic.kind() {
                     ObjectKind::Plain => Arc::new(ValueMap::default()),
-                    ObjectKind::Seq(_) => return Err(Error::from(ErrorKind::InvalidOperation)),
+                    ObjectKind::Seq(_) | ObjectKind::Iterator(_) => {
+                        return Err(Error::from(ErrorKind::InvalidOperation))
+                    }
                     ObjectKind::Struct(s) => {
                         let mut rv = ValueMap::default();
                         for field in s.fields() {
diff --git a/minijinja/src/syntax.rs b/minijinja/src/syntax.rs
index eae94f1f..01dc51a7 100644
--- a/minijinja/src/syntax.rs
+++ b/minijinja/src/syntax.rs
@@ -211,6 +211,11 @@
 //! - `loop.changed(...args)`: Returns true if the passed values have changed since the last time it was called with the same arguments.
 //! - `loop.cycle(...args)`: Returns a value from the passed sequence in a cycle.
 //!
+//! A special note on iterators: in the current version of MiniJinja, some sequences are actually
+//! lazy iterators.  They behave a bit like sequences not not entirely.  They can be iterated over,
+//! will happily serialize once into a a list etc.  However when iterating over an actual iterator,
+//! `last`, `revindex` and `revindex0` will always be undefined.
+//!
 //! Within a for-loop, it’s possible to cycle among a list of strings/variables each time through
 //! the loop by using the special `loop.cycle` helper:
 //!
diff --git a/minijinja/src/value/deserialize.rs b/minijinja/src/value/deserialize.rs
index 27c0e041..8c58b0b7 100644
--- a/minijinja/src/value/deserialize.rs
+++ b/minijinja/src/value/deserialize.rs
@@ -449,6 +449,7 @@ fn value_to_unexpected(value: &Value) -> de::Unexpected {
             super::ObjectKind::Plain => de::Unexpected::Other("plain object"),
             super::ObjectKind::Seq(_) => de::Unexpected::Seq,
             super::ObjectKind::Struct(_) => de::Unexpected::Map,
+            super::ObjectKind::Iterator(_) => de::Unexpected::Other("iterator"),
         },
     }
 }
diff --git a/minijinja/src/value/mod.rs b/minijinja/src/value/mod.rs
index ce985b0f..4105e354 100644
--- a/minijinja/src/value/mod.rs
+++ b/minijinja/src/value/mod.rs
@@ -24,6 +24,7 @@
 //!
 //! ```
 //! # use minijinja::value::Value;
+//!
 //! // collection into a sequence
 //! let value: Value = (1..10).into_iter().collect();
 //!
@@ -31,6 +32,15 @@
 //! let value: Value = [("key", "value")].into_iter().collect();
 //! ```
 //!
+//! For certain types of iterators (`Send` + `Sync` + `'static`) it's also
+//! possible to make the value lazily iterate over the value by using the
+//! `Value::from_iterator` function instead:
+//!
+//! ```
+//! # use minijinja::value::Value;
+//! let value: Value = Value::from_iterator(1..10);
+//! ```
+//!
 //! To to into the inverse directly the various [`TryFrom`](std::convert::TryFrom)
 //! implementations can be used:
 //!
@@ -132,20 +142,22 @@ use std::convert::TryFrom;
 use std::fmt;
 use std::hash::{Hash, Hasher};
 use std::marker::PhantomData;
-use std::sync::Arc;
+use std::sync::{Arc, Mutex};
 
 use serde::ser::{Serialize, Serializer};
 
 use crate::error::{Error, ErrorKind};
 use crate::functions;
 use crate::utils::OnDrop;
-use crate::value::object::{SimpleSeqObject, SimpleStructObject};
+use crate::value::object::{SimpleIteratorObject, SimpleSeqObject, SimpleStructObject};
 use crate::value::ops::as_f64;
 use crate::value::serialize::transform;
 use crate::vm::State;
 
 pub use crate::value::argtypes::{from_args, ArgType, FunctionArgs, FunctionResult, Kwargs, Rest};
-pub use crate::value::object::{Object, ObjectKind, SeqObject, SeqObjectIter, StructObject};
+pub use crate::value::object::{
+    IteratorObject, Object, ObjectKind, SeqObject, SeqObjectIter, StructObject,
+};
 
 mod argtypes;
 #[cfg(feature = "deserialization")]
@@ -362,7 +374,7 @@ impl Hash for Value {
                 v.hash(state);
             }),
             ValueRepr::Dynamic(d) => match d.kind() {
-                ObjectKind::Plain => 0u8.hash(state),
+                ObjectKind::Plain | ObjectKind::Iterator(_) => 0u8.hash(state),
                 ObjectKind::Seq(s) => s.iter().for_each(|x| x.hash(state)),
                 ObjectKind::Struct(s) => {
                     if let Some(fields) = s.static_fields() {
@@ -600,6 +612,31 @@ impl Value {
         transform(value)
     }
 
+    /// Creates a value from an iterator.
+    ///
+    /// This takes an iterator (yielding values that can be turned into a [`Value`])
+    /// and returns a value that can be iterated over.  Today this value looks a bit like
+    /// a sequence (and will pretend to be one) but this is misleading.  Such values are
+    /// actually objects implementing [`IteratorObject`] but due to backwards
+    /// compatibility reasons it's not possible to give them a distinct type.
+    ///
+    /// Iterators that implement [`ExactSizeIterator`] or have a matching lower and upper
+    /// bound on the [`Iterator::size_hint`] report a known `loop.length`.  Iterators that
+    /// do not fulfill these requirements will not.  The same is true for `revindex` and
+    /// similar properties.
+    ///
+    /// ```
+    /// # use minijinja::value::Value;
+    /// let val = Value::from_iterator(0..10);
+    /// ```
+    pub fn from_iterator<I, T>(iter: I) -> Value
+    where
+        I: Iterator<Item = T> + Send + Sync + 'static,
+        T: Into<Value> + 'static,
+    {
+        Value::from_object(SimpleIteratorObject(Mutex::new(iter.fuse())))
+    }
+
     /// Creates a value from a safe string.
     ///
     /// A safe string is one that will bypass auto escaping.  For instance if you
@@ -710,9 +747,11 @@ impl Value {
             // XXX: invalid values report themselves as maps which is a lie
             ValueRepr::Invalid(_) => ValueKind::Map,
             ValueRepr::Dynamic(ref dy) => match dy.kind() {
-                // XXX: basic objects should probably not report as map
+                // XXX: basic objects should probably not report as map and
+                // iterators should report as a new iterator value type.
                 ObjectKind::Plain => ValueKind::Map,
                 ObjectKind::Seq(_) => ValueKind::Seq,
+                ObjectKind::Iterator(_) => ValueKind::Seq,
                 ObjectKind::Struct(_) => ValueKind::Map,
             },
         }
@@ -752,7 +791,7 @@ impl Value {
             ValueRepr::Seq(ref x) => !x.is_empty(),
             ValueRepr::Map(ref x, _) => !x.is_empty(),
             ValueRepr::Dynamic(ref x) => match x.kind() {
-                ObjectKind::Plain => true,
+                ObjectKind::Plain | ObjectKind::Iterator(_) => true,
                 ObjectKind::Seq(s) => s.item_count() != 0,
                 ObjectKind::Struct(s) => s.field_count() != 0,
             },
@@ -838,7 +877,7 @@ impl Value {
             ValueRepr::Map(ref items, _) => Some(items.len()),
             ValueRepr::Seq(ref items) => Some(items.len()),
             ValueRepr::Dynamic(ref dy) => match dy.kind() {
-                ObjectKind::Plain => None,
+                ObjectKind::Plain | ObjectKind::Iterator(_) => None,
                 ObjectKind::Seq(s) => Some(s.item_count()),
                 ObjectKind::Struct(s) => Some(s.field_count()),
             },
@@ -868,7 +907,7 @@ impl Value {
             ValueRepr::Map(ref items, _) => items.get(&KeyRef::Str(key)).cloned(),
             ValueRepr::Dynamic(ref dy) => match dy.kind() {
                 ObjectKind::Struct(s) => s.get_field(key),
-                ObjectKind::Plain | ObjectKind::Seq(_) => None,
+                ObjectKind::Plain | ObjectKind::Seq(_) | ObjectKind::Iterator(_) => None,
             },
             _ => None,
         }
@@ -886,7 +925,7 @@ impl Value {
             ValueRepr::Map(ref items, _) => items.get(&KeyRef::Str(key)).cloned(),
             ValueRepr::Dynamic(ref dy) => match dy.kind() {
                 ObjectKind::Struct(s) => s.get_field(key),
-                ObjectKind::Plain | ObjectKind::Seq(_) => None,
+                ObjectKind::Plain | ObjectKind::Seq(_) | ObjectKind::Iterator(_) => None,
             },
             _ => None,
         }
@@ -1024,7 +1063,7 @@ impl Value {
             ValueRepr::Map(ref items, _) => return items.get(&key).cloned(),
             ValueRepr::Seq(ref items) => &**items as &dyn SeqObject,
             ValueRepr::Dynamic(ref dy) => match dy.kind() {
-                ObjectKind::Plain => return None,
+                ObjectKind::Plain | ObjectKind::Iterator(_) => return None,
                 ObjectKind::Seq(s) => s,
                 ObjectKind::Struct(s) => {
                     return if let Some(key) = key.as_str() {
@@ -1145,43 +1184,49 @@ impl Value {
     /// Iterates over the value without holding a reference.
     pub(crate) fn try_iter_owned(&self) -> Result<OwnedValueIterator, Error> {
         let (iter_state, len) = match self.0 {
-            ValueRepr::None | ValueRepr::Undefined => (ValueIteratorState::Empty, 0),
+            ValueRepr::None | ValueRepr::Undefined => (ValueIteratorState::Empty, Some(0)),
             ValueRepr::String(ref s, _) => (
                 ValueIteratorState::Chars(0, Arc::clone(s)),
-                s.chars().count(),
+                Some(s.chars().count()),
             ),
-            ValueRepr::Seq(ref seq) => (ValueIteratorState::Seq(0, Arc::clone(seq)), seq.len()),
-            #[cfg(feature = "preserve_order")]
-            ValueRepr::Map(ref items, _) => {
-                (ValueIteratorState::Map(0, Arc::clone(items)), items.len())
+            ValueRepr::Seq(ref seq) => {
+                (ValueIteratorState::Seq(0, Arc::clone(seq)), Some(seq.len()))
             }
+            #[cfg(feature = "preserve_order")]
+            ValueRepr::Map(ref items, _) => (
+                ValueIteratorState::Map(0, Arc::clone(items)),
+                Some(items.len()),
+            ),
             #[cfg(not(feature = "preserve_order"))]
             ValueRepr::Map(ref items, _) => (
                 ValueIteratorState::Map(
                     items.iter().next().map(|x| x.0.clone()),
                     Arc::clone(items),
                 ),
-                items.len(),
+                Some(items.len()),
             ),
             ValueRepr::Dynamic(ref obj) => {
                 match obj.kind() {
-                    ObjectKind::Plain => (ValueIteratorState::Empty, 0),
+                    ObjectKind::Plain => (ValueIteratorState::Empty, Some(0)),
                     ObjectKind::Seq(s) => (
                         ValueIteratorState::DynSeq(0, Arc::clone(obj)),
-                        s.item_count(),
+                        Some(s.item_count()),
                     ),
                     ObjectKind::Struct(s) => {
                         // the assumption is that structs don't have excessive field counts
                         // and that most iterations go over all fields, so creating a
                         // temporary vector here is acceptable.
                         if let Some(fields) = s.static_fields() {
-                            (ValueIteratorState::StaticStr(0, fields), fields.len())
+                            (ValueIteratorState::StaticStr(0, fields), Some(fields.len()))
                         } else {
                             let attrs = s.fields();
                             let attr_count = attrs.len();
-                            (ValueIteratorState::ArcStr(0, attrs), attr_count)
+                            (ValueIteratorState::ArcStr(0, attrs), Some(attr_count))
                         }
                     }
+                    ObjectKind::Iterator(_) => {
+                        (ValueIteratorState::Iterator(Arc::clone(obj)), None)
+                    }
                 }
             }
             _ => {
@@ -1279,6 +1324,14 @@ impl Serialize for Value {
                     }
                     map.end()
                 }
+                ObjectKind::Iterator(i) => {
+                    use serde::ser::SerializeSeq;
+                    let mut seq = ok!(serializer.serialize_seq(None));
+                    while let Some(value) = i.next_value() {
+                        ok!(seq.serialize_element(&value));
+                    }
+                    seq.end()
+                }
             },
         }
     }
@@ -1301,7 +1354,7 @@ impl<'a> Iterator for ValueIter<'a> {
 
 pub(crate) struct OwnedValueIterator {
     iter_state: ValueIteratorState,
-    len: usize,
+    len: Option<usize>,
 }
 
 impl Iterator for OwnedValueIterator {
@@ -1309,13 +1362,22 @@ impl Iterator for OwnedValueIterator {
 
     fn next(&mut self) -> Option<Self::Item> {
         self.iter_state.advance_state().map(|x| {
-            self.len -= 1;
+            if let Some(ref mut len) = self.len {
+                *len -= 1;
+            }
             x
         })
     }
 
     fn size_hint(&self) -> (usize, Option<usize>) {
-        (self.len, Some(self.len))
+        if let ValueIteratorState::Iterator(ref obj) = self.iter_state {
+            if let ObjectKind::Iterator(iter) = obj.kind() {
+                if let Some(len) = iter.iterator_len() {
+                    return (len, Some(len));
+                }
+            }
+        }
+        (self.len.unwrap_or(0), self.len)
     }
 }
 
@@ -1336,6 +1398,7 @@ enum ValueIteratorState {
     Map(Option<KeyRef<'static>>, Arc<ValueMap>),
     #[cfg(feature = "preserve_order")]
     Map(usize, Arc<ValueMap>),
+    Iterator(Arc<dyn Object>),
 }
 
 impl ValueIteratorState {
@@ -1389,6 +1452,13 @@ impl ValueIteratorState {
                     None
                 }
             }
+            ValueIteratorState::Iterator(obj) => {
+                if let ObjectKind::Iterator(iter) = obj.kind() {
+                    iter.next_value()
+                } else {
+                    unreachable!()
+                }
+            }
         }
     }
 }
diff --git a/minijinja/src/value/object.rs b/minijinja/src/value/object.rs
index 9968547c..b8293eba 100644
--- a/minijinja/src/value/object.rs
+++ b/minijinja/src/value/object.rs
@@ -1,7 +1,7 @@
 use std::any::{Any, TypeId};
 use std::fmt;
 use std::ops::Range;
-use std::sync::Arc;
+use std::sync::{Arc, Mutex};
 
 use crate::error::{Error, ErrorKind};
 use crate::value::{intern, Value};
@@ -208,6 +208,13 @@ pub enum ObjectKind<'a> {
     ///
     /// Requires that the object implements [`StructObject`].
     Struct(&'a dyn StructObject),
+
+    /// This object is an iterator that yields new values.
+    ///
+    /// Requires that the object implements [`IteratorObject`].  It's not
+    /// recommended to implement this, instead one should directly pass
+    /// iterators to [`Value::from_iterator`].
+    Iterator(&'a dyn IteratorObject),
 }
 
 /// Provides the behavior of an [`Object`] holding sequence of values.
@@ -675,3 +682,73 @@ impl<T: StructObject + 'static> Object for SimpleStructObject<T> {
         ObjectKind::Struct(&self.0)
     }
 }
+
+/// Represents a dynamic iterable.
+///
+/// Iterators need to use interior mutability to function.
+pub trait IteratorObject: Send + Sync {
+    /// Produces the next value from the iterator.
+    fn next_value(&self) -> Option<Value>;
+
+    /// Returns the exact size of the iterator if known.
+    ///
+    /// An iterator must only return the length if it's known and correct.
+    /// The default implementation returns `None`.  If the length is
+    /// provided then `loop.revindex` and `loop.length` will return the
+    /// correct information.
+    fn iterator_len(&self) -> Option<usize> {
+        None
+    }
+}
+
+pub(crate) struct SimpleIteratorObject<I, T>(pub Mutex<I>)
+where
+    I: Iterator<Item = T> + Send + Sync + 'static,
+    T: Into<Value> + 'static;
+
+impl<I, T> fmt::Debug for SimpleIteratorObject<I, T>
+where
+    I: Iterator<Item = T> + Send + Sync + 'static,
+    T: Into<Value> + 'static,
+{
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_tuple("Iterator").finish()
+    }
+}
+
+impl<I, T> fmt::Display for SimpleIteratorObject<I, T>
+where
+    I: Iterator<Item = T> + Send + Sync + 'static,
+    T: Into<Value> + 'static,
+{
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "<iterator>")
+    }
+}
+
+impl<I, T> Object for SimpleIteratorObject<I, T>
+where
+    I: Iterator<Item = T> + Send + Sync + 'static,
+    T: Into<Value> + 'static,
+{
+    fn kind(&self) -> ObjectKind<'_> {
+        ObjectKind::Iterator(self)
+    }
+}
+
+impl<I, T> IteratorObject for SimpleIteratorObject<I, T>
+where
+    I: Iterator<Item = T> + Send + Sync,
+    T: Into<Value>,
+{
+    fn next_value(&self) -> Option<Value> {
+        self.0.lock().unwrap().next().map(Into::into)
+    }
+
+    fn iterator_len(&self) -> Option<usize> {
+        match self.0.lock().unwrap().size_hint() {
+            (lower, Some(upper)) if lower == upper => Some(lower),
+            _ => None,
+        }
+    }
+}


Test patch:
diff --git a/minijinja/tests/test_value.rs b/minijinja/tests/test_value.rs
index 0a899927..33a1ca75 100644
--- a/minijinja/tests/test_value.rs
+++ b/minijinja/tests/test_value.rs
@@ -4,7 +4,7 @@ use insta::assert_snapshot;
 use similar_asserts::assert_eq;
 
 use minijinja::value::{Kwargs, Object, ObjectKind, Rest, SeqObject, StructObject, Value};
-use minijinja::{args, Environment, Error};
+use minijinja::{args, context, render, Environment, Error};
 
 #[test]
 fn test_sort() {
@@ -511,6 +511,41 @@ fn test_seq_object_borrow() {
     );
 }
 
+#[test]
+fn test_iterator() {
+    let value = Value::from_iterator(0..10);
+    assert_eq!(value.to_string(), "<iterator>");
+    let rv = render!(
+        "{% for item in iter %}[{{ item }}]{% endfor %}",
+        iter => value
+    );
+    assert_snapshot!(rv, @"[0][1][2][3][4][5][6][7][8][9]");
+
+    let rv = render!(
+        "{% for item in iter %}- {{ item }}: {{ loop.index }} / {{ loop.length }}\n{% endfor %}",
+        iter => Value::from_iterator('a'..'f')
+    );
+    assert_snapshot!(rv, @r###"
+    - a: 1 / 5
+    - b: 2 / 5
+    - c: 3 / 5
+    - d: 4 / 5
+    - e: 5 / 5
+    "###);
+
+    let rv = render!(
+        "{% for item in iter %}- {{ item }}: {{ loop.index }} / {{ loop.length|default('?') }}\n{% endfor %}",
+     iter => Value::from_iterator((0..10).filter(|x| x % 2 == 0))
+    );
+    assert_snapshot!(rv, @r###"
+    - 0: 1 / ?
+    - 2: 2 / ?
+    - 4: 3 / ?
+    - 6: 4 / ?
+    - 8: 5 / ?
+    "###);
+}
+
 #[test]
 fn test_complex_key() {
     let value = Value::from_iter([