diff --git a/examples/dynamic/src/main.rs b/examples/dynamic/src/main.rs index c0689363..253e6adb 100644 --- a/examples/dynamic/src/main.rs +++ b/examples/dynamic/src/main.rs @@ -2,7 +2,7 @@ use std::fmt; use std::sync::atomic::{AtomicUsize, Ordering}; -use minijinja::value::{from_args, Object, Value}; +use minijinja::value::{from_args, Object, SeqObject, Value}; use minijinja::{Environment, Error, State}; #[derive(Debug)] @@ -50,17 +50,34 @@ impl Object for Magic { Ok(Value::from(format!("magic-{}", tag))) } else { Err(Error::new( - minijinja::ErrorKind::InvalidOperation, + minijinja::ErrorKind::UnknownMethod, format!("object has no method named {}", name), )) } } } +struct SimpleDynamicSeq; + +impl SeqObject for SimpleDynamicSeq { + fn get_item(&self, idx: usize) -> Option { + if idx < 3 { + Some(Value::from(idx * 2)) + } else { + None + } + } + + fn item_count(&self) -> usize { + 3 + } +} + fn main() { let mut env = Environment::new(); 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_template("template.html", include_str!("template.html")) .unwrap(); diff --git a/examples/dynamic/src/template.html b/examples/dynamic/src/template.html index 0e702f9d..9b33b1ab 100644 --- a/examples/dynamic/src/template.html +++ b/examples/dynamic/src/template.html @@ -4,4 +4,8 @@
  • {{ char }}
  • {%- endfor %} -{%- endwith %} \ No newline at end of file +{%- endwith %} + +{% for item in seq %} + [{{ item }}] +{% endfor %} \ No newline at end of file diff --git a/examples/load-lazy/src/main.rs b/examples/load-lazy/src/main.rs index c4066fa3..94e4b827 100644 --- a/examples/load-lazy/src/main.rs +++ b/examples/load-lazy/src/main.rs @@ -4,6 +4,8 @@ use std::fmt; use std::fs; use std::sync::Mutex; +use minijinja::value::ObjectKind; +use minijinja::value::StructObject; use minijinja::value::{Object, Value}; use minijinja::Environment; @@ -19,13 +21,19 @@ impl fmt::Display for Site { } impl Object for Site { + fn kind(&self) -> ObjectKind<'_> { + ObjectKind::Struct(self) + } +} + +impl StructObject for Site { /// This loads a file on attribute access. Note that attribute access /// can neither access the state nor return failures as such it can at /// max turn into an undefined object. /// /// If that is necessary, use `call_method()` instead which is able to /// both access interpreter state and fail. - fn get_attr(&self, name: &str) -> Option { + fn get_field(&self, name: &str) -> Option { let mut cache = self.cache.lock().unwrap(); if let Some(rv) = cache.get(name) { return Some(rv.clone()); diff --git a/minijinja/src/error.rs b/minijinja/src/error.rs index b6e0f82b..18883d70 100644 --- a/minijinja/src/error.rs +++ b/minijinja/src/error.rs @@ -118,6 +118,8 @@ pub enum ErrorKind { UnknownTest, /// A function is unknown UnknownFunction, + /// Un unknown method was called + UnknownMethod, /// A bad escape sequence in a string was encountered. BadEscape, /// An operation on an undefined value was attempted. @@ -147,6 +149,7 @@ impl ErrorKind { ErrorKind::UnknownFilter => "unknown filter", ErrorKind::UnknownFunction => "unknown function", ErrorKind::UnknownTest => "unknown test", + ErrorKind::UnknownMethod => "unknown method", ErrorKind::BadEscape => "bad string escape", ErrorKind::UndefinedError => "undefined value", ErrorKind::BadSerialization => "could not serialize to internal format", diff --git a/minijinja/src/filters.rs b/minijinja/src/filters.rs index 47c362c8..4c2f7235 100644 --- a/minijinja/src/filters.rs +++ b/minijinja/src/filters.rs @@ -250,7 +250,7 @@ mod builtins { use super::*; use crate::error::ErrorKind; - use crate::value::{ValueKind, ValueRepr}; + use crate::value::ValueRepr; use std::borrow::Cow; use std::fmt::Write; use std::mem; @@ -404,10 +404,8 @@ mod builtins { pub fn reverse(v: Value) -> Result { if let Some(s) = v.as_str() { Ok(Value::from(s.chars().rev().collect::())) - } else if matches!(v.kind(), ValueKind::Seq) { - Ok(Value::from( - ok!(v.as_slice()).iter().rev().cloned().collect::>(), - )) + } else if let Some(seq) = v.as_seq() { + Ok(Value::from(seq.iter().rev().collect::>())) } else { Err(Error::new( ErrorKind::InvalidOperation, @@ -446,9 +444,9 @@ mod builtins { rv.push(c); } Ok(rv) - } else if matches!(val.kind(), ValueKind::Seq) { + } else if let Some(seq) = val.as_seq() { let mut rv = String::new(); - for item in ok!(val.as_slice()) { + for item in seq.iter() { if !rv.is_empty() { rv.push_str(joiner); } @@ -537,13 +535,15 @@ mod builtins { /// ``` #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))] pub fn first(value: Value) -> Result { - match value.0 { - ValueRepr::String(s, _) => Ok(s.chars().next().map_or(Value::UNDEFINED, Value::from)), - ValueRepr::Seq(ref s) => Ok(s.first().cloned().unwrap_or(Value::UNDEFINED)), - _ => Err(Error::new( + if let Some(s) = value.as_str() { + Ok(s.chars().next().map_or(Value::UNDEFINED, Value::from)) + } else if let Some(s) = value.as_seq() { + Ok(s.get_item(0).unwrap_or(Value::UNDEFINED)) + } else { + Err(Error::new( ErrorKind::InvalidOperation, "cannot get first item from value", - )), + )) } } @@ -564,15 +564,15 @@ mod builtins { /// ``` #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))] pub fn last(value: Value) -> Result { - match value.0 { - ValueRepr::String(s, _) => { - Ok(s.chars().rev().next().map_or(Value::UNDEFINED, Value::from)) - } - ValueRepr::Seq(ref s) => Ok(s.last().cloned().unwrap_or(Value::UNDEFINED)), - _ => Err(Error::new( + if let Some(s) = value.as_str() { + Ok(s.chars().rev().next().map_or(Value::UNDEFINED, Value::from)) + } else if let Some(seq) = value.as_seq() { + Ok(seq.iter().last().unwrap_or(Value::UNDEFINED)) + } else { + Err(Error::new( ErrorKind::InvalidOperation, "cannot get last item from value", - )), + )) } } @@ -584,21 +584,14 @@ mod builtins { /// an empty list is returned. #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))] pub fn list(value: Value) -> Result { - match &value.0 { - ValueRepr::Undefined => Ok(Value::from(Vec::::new())), - ValueRepr::String(ref s, _) => { - Ok(Value::from(s.chars().map(Value::from).collect::>())) - } - ValueRepr::Seq(_) => Ok(value.clone()), - ValueRepr::Map(ref m, _) => Ok(Value::from( - m.iter() - .map(|x| Value::from(x.0.clone())) - .collect::>(), - )), - _ => Err(Error::new( - ErrorKind::InvalidOperation, - "cannot convert value to list", - )), + if let Some(s) = value.as_str() { + Ok(Value::from(s.chars().map(Value::from).collect::>())) + } else { + let iter = ok!(value.try_iter().map_err(|err| { + Error::new(ErrorKind::InvalidOperation, "cannot convert value to list") + .with_source(err) + })); + Ok(Value::from(iter.collect::>())) } } @@ -876,6 +869,36 @@ mod builtins { ); }); } + + #[test] + fn test_values_in_vec() { + fn upper(value: &str) -> String { + value.to_uppercase() + } + + fn sum(value: Vec) -> i64 { + value.into_iter().sum::() + } + + let upper = BoxedFilter::new(upper); + let sum = BoxedFilter::new(sum); + + let env = crate::Environment::new(); + State::with_dummy(&env, |state| { + assert_eq!( + upper + .apply_to(state, &[Value::from("Hello World!")]) + .unwrap(), + Value::from("HELLO WORLD!") + ); + + assert_eq!( + sum.apply_to(state, &[Value::from(vec![Value::from(1), Value::from(2)])]) + .unwrap(), + Value::from(3) + ); + }); + } } #[cfg(feature = "builtins")] diff --git a/minijinja/src/value/argtypes.rs b/minijinja/src/value/argtypes.rs index 67842e34..08b99c05 100644 --- a/minijinja/src/value/argtypes.rs +++ b/minijinja/src/value/argtypes.rs @@ -5,7 +5,9 @@ use std::ops::{Deref, DerefMut}; use crate::error::{Error, ErrorKind}; use crate::key::{Key, StaticKey}; -use crate::value::{Arc, MapType, Object, Packed, StringType, Value, ValueKind, ValueRepr}; +use crate::value::{ + Arc, MapType, Object, Packed, SeqObject, StringType, Value, ValueKind, ValueRepr, +}; use crate::vm::State; /// A utility trait that represents the return value of functions and filters. @@ -90,10 +92,11 @@ where /// * signed integers: [`i8`], [`i16`], [`i32`], [`i64`], [`i128`] /// * floats: [`f32`], [`f64`] /// * bool: [`bool`] -/// * string: [`String`], [`&str`], `Cow<'_, str>` ([`char`]) +/// * string: [`String`], [`&str`], `Cow<'_, str>`, [`char`] /// * bytes: [`&[u8]`][`slice`] /// * values: [`Value`], `&Value` -/// * vectors: [`Vec`], `&[Value]` +/// * vectors: [`Vec`] +/// * sequences: [`&dyn SeqObject`](crate::value::SeqObject) /// /// The type is also implemented for optional values (`Option`) which is used /// to encode optional parameters to filters, functions or tests. Additionally @@ -111,6 +114,10 @@ where /// Byte slices will borrow out of values carrying bytes or strings. In the latter /// case the utf-8 bytes are returned. /// +/// There are also further restrictions imposed on borrowing in some situations. +/// For instance you cannot implicitly borrow out of sequences which means that +/// for instance `Vec<&str>` is not a legal argument. +/// /// ## Notes on State /// /// When `&State` is used, it does not consume a passed parameter. This means that @@ -123,6 +130,14 @@ pub trait ArgType<'a> { #[doc(hidden)] fn from_value(value: Option<&'a Value>) -> Result; + #[doc(hidden)] + fn from_value_owned(_value: Value) -> Result { + Err(Error::new( + ErrorKind::InvalidOperation, + "type conversion is not legal in this situation (implicit borrow)", + )) + } + #[doc(hidden)] fn from_state_and_value( _state: Option<&'a State>, @@ -351,6 +366,10 @@ macro_rules! primitive_try_from { None => Err(Error::from(ErrorKind::MissingArgument)) } } + + fn from_value_owned(value: Value) -> Result { + TryFrom::try_from(value) + } } } } @@ -422,6 +441,20 @@ impl<'a> ArgType<'a> for &[u8] { } } +impl<'a> ArgType<'a> for &dyn SeqObject { + type Output = &'a dyn SeqObject; + + #[inline(always)] + fn from_value(value: Option<&'a Value>) -> Result { + match value { + Some(value) => value + .as_seq() + .ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "value is not a sequence")), + None => Err(Error::from(ErrorKind::MissingArgument)), + } + } +} + impl<'a, T: ArgType<'a>> ArgType<'a> for Option { type Output = Option; @@ -437,6 +470,14 @@ impl<'a, T: ArgType<'a>> ArgType<'a> for Option { None => Ok(None), } } + + fn from_value_owned(value: Value) -> Result { + if value.is_undefined() || value.is_none() { + Ok(None) + } else { + T::from_value_owned(value).map(Some) + } + } } impl<'a> ArgType<'a> for Cow<'_, str> { @@ -463,18 +504,6 @@ impl<'a> ArgType<'a> for &Value { } } -impl<'a> ArgType<'a> for &[Value] { - type Output = &'a [Value]; - - #[inline(always)] - fn from_value(value: Option<&'a Value>) -> Result<&'a [Value], Error> { - match value { - Some(value) => Ok(ok!(value.as_slice())), - 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 @@ -547,6 +576,10 @@ impl<'a> ArgType<'a> for Value { None => Err(Error::from(ErrorKind::MissingArgument)), } } + + fn from_value_owned(value: Value) -> Result { + Ok(value) + } } impl<'a> ArgType<'a> for String { @@ -558,34 +591,51 @@ impl<'a> ArgType<'a> for String { None => Err(Error::from(ErrorKind::MissingArgument)), } } -} -impl From for String { - fn from(val: Value) -> Self { - val.to_string() - } -} - -impl From for Value { - fn from(val: usize) -> Self { - Value::from(val as u64) + fn from_value_owned(value: Value) -> Result { + Ok(value.to_string()) } } impl<'a, T: ArgType<'a, Output = T>> ArgType<'a> for Vec { - type Output = Self; + type Output = Vec; fn from_value(value: Option<&'a Value>) -> Result { match value { None => Ok(Vec::new()), - Some(values) => { - let values = ok!(values.as_slice()); + Some(value) => { + let seq = ok!(value + .as_seq() + .ok_or_else(|| { Error::new(ErrorKind::InvalidOperation, "not a sequence") })); let mut rv = Vec::new(); - for value in values { - rv.push(ok!(T::from_value(Some(value)))); + for value in seq.iter() { + rv.push(ok!(T::from_value_owned(value))); } Ok(rv) } } } + + fn from_value_owned(value: Value) -> Result { + let seq = ok!(value + .as_seq() + .ok_or_else(|| { Error::new(ErrorKind::InvalidOperation, "not a sequence") })); + let mut rv = Vec::new(); + for value in seq.iter() { + rv.push(ok!(T::from_value_owned(value))); + } + Ok(rv) + } +} + +impl From for String { + fn from(val: Value) -> Self { + val.to_string() + } +} + +impl From for Value { + fn from(val: usize) -> Self { + Value::from(val as u64) + } } diff --git a/minijinja/src/value/mod.rs b/minijinja/src/value/mod.rs index 656d12f9..f6b0e6b4 100644 --- a/minijinja/src/value/mod.rs +++ b/minijinja/src/value/mod.rs @@ -70,12 +70,14 @@ //! # Dynamic Objects //! //! Values can also hold "dynamic" objects. These are objects which implement the -//! [`Object`] trait. These can be used to implement dynamic functionality such as -//! stateful values and more. Dynamic objects are internally also used to implement -//! the special `loop` variable or macros. +//! [`Object`] trait and optionally [`SeqObject`] or [`StructObject`] These can +//! be used to implement dynamic functionality such as stateful values and more. +//! Dynamic objects are internally also used to implement the special `loop` +//! variable or macros. //! -//! To create a dynamic `Value` object, use [`Value::from_object()`] or the -//! `From>` implementations for `Value`: +//! To create a dynamic `Value` object, use [`Value::from_object`], +//! [`Value::from_seq_object`], [`Value::from_struct_object`] or the `From>` implementations for `Value`: //! //! ```rust //! # use std::sync::Arc; @@ -116,11 +118,12 @@ use crate::error::{Error, ErrorKind}; use crate::functions; use crate::key::{Key, StaticKey}; use crate::utils::OnDrop; +use crate::value::object::{SimpleSeqObject, SimpleStructObject}; use crate::value::serialize::ValueSerializer; use crate::vm::State; pub use crate::value::argtypes::{from_args, ArgType, FunctionArgs, FunctionResult, Rest}; -pub use crate::value::object::Object; +pub use crate::value::object::{Object, ObjectKind, SeqObject, SeqObjectIter, StructObject}; mod argtypes; #[cfg(feature = "deserialization")] @@ -490,6 +493,28 @@ impl Value { Value::from(Arc::new(value) as Arc) } + /// Creates a value from an owned [`SeqObject`]. + /// + /// This is a simplified API for creating dynamic sequences + /// without having to implement the entire [`Object`] protocol. + /// + /// **Note:** objects created this way cannot be downcasted via + /// [`downcast_object_ref`](Self::downcast_object_ref). + pub fn from_seq_object(value: T) -> Value { + Value::from_object(SimpleSeqObject(value)) + } + + /// Creates a value from an owned [`StructObject`]. + /// + /// This is a simplified API for creating dynamic structs + /// without having to implement the entire [`Object`] protocol. + /// + /// **Note:** objects created this way cannot be downcasted via + /// [`downcast_object_ref`](Self::downcast_object_ref). + pub fn from_struct_object(value: T) -> Value { + Value::from_object(SimpleStructObject(value)) + } + /// Creates a callable value from a function. /// /// ``` @@ -523,7 +548,13 @@ impl Value { ValueRepr::Bytes(_) => ValueKind::Bytes, ValueRepr::U128(_) => ValueKind::Number, ValueRepr::Seq(_) => ValueKind::Seq, - ValueRepr::Map(..) | ValueRepr::Dynamic(_) => ValueKind::Map, + ValueRepr::Map(..) => ValueKind::Map, + ValueRepr::Dynamic(ref dy) => match dy.kind() { + // XXX: basic objects should probably not report as map + ObjectKind::Plain => ValueKind::Map, + ObjectKind::Seq(_) => ValueKind::Seq, + ObjectKind::Struct(_) => ValueKind::Map, + }, } } @@ -547,7 +578,11 @@ impl Value { ValueRepr::None | ValueRepr::Undefined => false, ValueRepr::Seq(ref x) => !x.is_empty(), ValueRepr::Map(ref x, _) => !x.is_empty(), - ValueRepr::Dynamic(_) => true, + ValueRepr::Dynamic(ref x) => match x.kind() { + ObjectKind::Plain => true, + ObjectKind::Seq(s) => s.item_count() != 0, + ObjectKind::Struct(s) => s.field_count() != 0, + }, } } @@ -583,23 +618,28 @@ impl Value { } } - /// If the value is a sequence it's returned as slice. - /// - /// ``` - /// # use minijinja::value::Value; - /// let seq = Value::from(vec![1u32, 2, 3, 4]); - /// let slice = seq.as_slice().unwrap(); - /// assert_eq!(slice.len(), 4); - /// ``` - pub fn as_slice(&self) -> Result<&[Value], Error> { + /// If the value is a sequence it's returned as [`SeqObject`]. + pub fn as_seq(&self) -> Option<&dyn SeqObject> { match self.0 { - ValueRepr::Undefined | ValueRepr::None => Ok(&[][..]), - ValueRepr::Seq(ref v) => Ok(&v[..]), - _ => Err(Error::new( - ErrorKind::InvalidOperation, - format!("value of type {} is not a sequence", self.kind()), - )), + ValueRepr::Seq(ref v) => return Some(&**v as &dyn SeqObject), + ValueRepr::Dynamic(ref dy) => { + if let ObjectKind::Seq(seq) = dy.kind() { + return Some(seq); + } + } + _ => {} } + None + } + + /// If the value is a struct, return it as [`StructObject`]. + pub fn as_struct(&self) -> Option<&dyn StructObject> { + if let ValueRepr::Dynamic(ref dy) = self.0 { + if let ObjectKind::Struct(s) = dy.kind() { + return Some(s); + } + } + None } /// Returns the length of the contained value. @@ -616,7 +656,11 @@ impl Value { ValueRepr::String(ref s, _) => Some(s.chars().count()), ValueRepr::Map(ref items, _) => Some(items.len()), ValueRepr::Seq(ref items) => Some(items.len()), - ValueRepr::Dynamic(ref dy) => Some(dy.attributes().count()), + ValueRepr::Dynamic(ref dy) => match dy.kind() { + ObjectKind::Plain => None, + ObjectKind::Seq(s) => Some(s.item_count()), + ObjectKind::Struct(s) => Some(s.field_count()), + }, _ => None, } } @@ -643,7 +687,10 @@ impl Value { let lookup_key = Key::Str(key); items.get(&lookup_key).cloned() } - ValueRepr::Dynamic(ref dy) => dy.get_attr(key), + ValueRepr::Dynamic(ref dy) => match dy.kind() { + ObjectKind::Plain | ObjectKind::Seq(_) => None, + ObjectKind::Struct(s) => s.get_field(key), + }, ValueRepr::Undefined => { return Err(Error::from(ErrorKind::UndefinedError)); } @@ -762,27 +809,32 @@ impl Value { fn get_item_opt(&self, key: &Value) -> Option { let key = some!(Key::from_borrowed_value(key).ok()); - match self.0 { + let seq = match self.0 { ValueRepr::Map(ref items, _) => return items.get(&key).cloned(), - ValueRepr::Seq(ref items) => { - if let Key::I64(idx) = key { - let idx = some!(isize::try_from(idx).ok()); - let idx = if idx < 0 { - some!(items.len().checked_sub(-idx as usize)) - } else { - idx as usize - }; - return items.get(idx).cloned(); - } - } - ValueRepr::Dynamic(ref dy) => match key { - Key::String(ref key) => return dy.get_attr(key), - Key::Str(key) => return dy.get_attr(key), - _ => {} + ValueRepr::Seq(ref items) => &**items as &dyn SeqObject, + ValueRepr::Dynamic(ref dy) => match dy.kind() { + ObjectKind::Plain => return None, + ObjectKind::Seq(s) => s, + ObjectKind::Struct(s) => match key { + Key::String(ref key) => return s.get_field(key), + Key::Str(key) => return s.get_field(key), + _ => return None, + }, }, - _ => {} + _ => return None, + }; + + if let Key::I64(idx) = key { + let idx = some!(isize::try_from(idx).ok()); + let idx = if idx < 0 { + some!(seq.item_count().checked_sub(-idx as usize)) + } else { + idx as usize + }; + seq.get_item(idx) + } else { + None } - None } /// Calls the value directly. @@ -853,10 +905,15 @@ impl Value { m.iter() .filter_map(|(k, v)| k.as_str().map(move |k| (k, v.clone()))), ) as Box>, - ValueRepr::Dynamic(ref obj) => Box::new( - obj.attributes() - .filter_map(move |attr| Some((attr, some!(obj.get_attr(attr))))), - ) as Box>, + ValueRepr::Dynamic(ref obj) => match obj.kind() { + ObjectKind::Plain | ObjectKind::Seq(_) => { + Box::new(None.into_iter()) as Box> + } + ObjectKind::Struct(s) => Box::new( + s.fields() + .filter_map(move |attr| Some((attr, some!(s.get_field(attr))))), + ) as Box>, + }, _ => Box::new(None.into_iter()) as Box>, } } @@ -879,14 +936,26 @@ impl Value { items.len(), ), ValueRepr::Dynamic(ref obj) => { - let attrs = obj.attributes().map(Value::from).collect::>(); - let attr_count = attrs.len(); - (ValueIteratorState::Seq(0, Arc::new(attrs)), attr_count) + match obj.kind() { + ObjectKind::Plain => (ValueIteratorState::Empty, 0), + ObjectKind::Seq(s) => ( + ValueIteratorState::DynSeq(0, Arc::clone(obj)), + 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. + let attrs = s.fields().map(Value::from).collect::>(); + let attr_count = s.field_count(); + (ValueIteratorState::Seq(0, Arc::new(attrs)), attr_count) + } + } } _ => { return Err(Error::new( ErrorKind::InvalidOperation, - "object is not iterable", + format!("{} is not iterable", self.kind()), )) } }; @@ -930,15 +999,26 @@ impl Serialize for Value { } map.end() } - ValueRepr::Dynamic(ref n) => { - use serde::ser::SerializeMap; - let mut s = ok!(serializer.serialize_map(None)); - for k in n.attributes() { - let v = n.get_attr(k).unwrap_or(Value::UNDEFINED); - ok!(s.serialize_entry(&k, &v)); + ValueRepr::Dynamic(ref dy) => match dy.kind() { + ObjectKind::Plain => serializer.serialize_str(&dy.to_string()), + ObjectKind::Seq(s) => { + use serde::ser::SerializeSeq; + let mut seq = ok!(serializer.serialize_seq(Some(s.item_count()))); + for item in s.iter() { + ok!(seq.serialize_element(&item)); + } + seq.end() } - s.end() - } + ObjectKind::Struct(s) => { + use serde::ser::SerializeMap; + let mut map = ok!(serializer.serialize_map(None)); + for k in s.fields() { + let v = s.get_field(k).unwrap_or(Value::UNDEFINED); + ok!(map.serialize_entry(&k, &v)); + } + map.end() + } + }, } } } @@ -989,6 +1069,7 @@ impl fmt::Debug for OwnedValueIterator { enum ValueIteratorState { Empty, Seq(usize, Arc>), + DynSeq(usize, Arc), #[cfg(not(feature = "preserve_order"))] Map(Option, Arc), #[cfg(feature = "preserve_order")] @@ -1006,6 +1087,16 @@ impl ValueIteratorState { x }) .cloned(), + ValueIteratorState::DynSeq(idx, obj) => { + if let ObjectKind::Seq(seq) = obj.kind() { + seq.get_item(*idx).map(|x| { + *idx += 1; + x + }) + } else { + unreachable!() + } + } #[cfg(feature = "preserve_order")] ValueIteratorState::Map(idx, map) => map.get_index(*idx).map(|x| { *idx += 1; @@ -1038,14 +1129,20 @@ fn test_dynamic_object_roundtrip() { } impl Object for X { - fn get_attr(&self, name: &str) -> Option { + fn kind(&self) -> ObjectKind<'_> { + ObjectKind::Struct(self) + } + } + + impl crate::value::object::StructObject for X { + fn get_field(&self, name: &str) -> Option { match name { "value" => Some(Value::from(self.0.load(atomic::Ordering::Relaxed))), _ => None, } } - fn attributes(&self) -> Box + '_> { + fn fields(&self) -> Box + '_> { Box::new(["value"].into_iter()) } } diff --git a/minijinja/src/value/object.rs b/minijinja/src/value/object.rs index 1ed7fc22..272e28ee 100644 --- a/minijinja/src/value/object.rs +++ b/minijinja/src/value/object.rs @@ -1,5 +1,6 @@ use std::any::Any; use std::fmt; +use std::ops::Range; use crate::error::{Error, ErrorKind}; use crate::value::Value; @@ -22,33 +23,23 @@ use crate::vm::State; /// Objects need to implement [`Display`](std::fmt::Display) which is used by /// the engine to convert the object into a string if needed. Additionally /// [`Debug`](std::fmt::Debug) is required as well. +/// +/// The exact runtime characteristics of the object are influenced by the +/// [`kind`](Self::kind) of the object. By default an object can just be +/// stringified and methods can be called. +/// +/// For examples of how to implement objects refer to [`SeqObject`] and +/// [`StructObject`]. pub trait Object: fmt::Display + fmt::Debug + Any + Sync + Send { - /// Invoked by the engine to get the attribute of an object. - /// - /// Where possible it's a good idea for this to align with the return value - /// of [`attributes`](Self::attributes) but it's not necessary. + /// Describes the kind of an object. /// - /// If an attribute does not exist, `None` shall be returned. - /// - /// A note should be made here on side effects: unlike calling objects or - /// calling methods on objects, accessing attributes is not supposed to - /// have side effects. Neither does this API get access to the interpreter - /// [`State`] nor is there a channel to send out failures as only an option - /// can be returned. If you do plan on doing something in attribute access - /// that is fallible, instead use a method call. - fn get_attr(&self, name: &str) -> Option { - let _name = name; - None - } - - /// An enumeration of attributes that are known to exist on this object. + /// If not implemented behavior for an object is [`ObjectKind::Plain`] + /// which just means that it's stringifyable and potentially can be + /// called or has methods. /// - /// The default implementation returns an empty iterator. If it's not possible - /// to implement this, it's fine for the implementation to be omitted. The - /// enumeration here is used by the `for` loop to iterate over the attributes - /// on the value. - fn attributes(&self) -> Box + '_> { - Box::new(None.into_iter()) + /// For more information see [`ObjectKind`]. + fn kind(&self) -> ObjectKind<'_> { + ObjectKind::Plain } /// Called when the engine tries to call a method on the object. @@ -62,7 +53,7 @@ pub trait Object: fmt::Display + fmt::Debug + Any + Sync + Send { let _state = state; let _args = args; Err(Error::new( - ErrorKind::InvalidOperation, + ErrorKind::UnknownMethod, format!("object has no method named {}", name), )) } @@ -85,12 +76,8 @@ pub trait Object: fmt::Display + fmt::Debug + Any + Sync + Send { } impl Object for std::sync::Arc { - fn get_attr(&self, name: &str) -> Option { - T::get_attr(self, name) - } - - fn attributes(&self) -> Box + '_> { - T::attributes(self) + fn kind(&self) -> ObjectKind<'_> { + T::kind(self) } fn call_method(&self, state: &State, name: &str, args: &[Value]) -> Result { @@ -101,3 +88,366 @@ impl Object for std::sync::Arc { T::call(self, state, args) } } + +/// A kind defines the object's behavior. +/// +/// When a dynamic [`Object`] is implemented, it can be of one of the kinds +/// here. The default behavior will be a [`Plain`](Self::Plain) object which +/// doesn't do much other than that it can be printed. For an object to turn +/// into a [struct](Self::Struct) or [sequence](Self::Seq) the necessary kind +/// has to be returned with a pointer to itself. +/// +/// Today object's can have the behavior of structs and sequences but this +/// might expand in the future. It does mean that not all types of values can +/// be represented by objects. +#[non_exhaustive] +pub enum ObjectKind<'a> { + /// This object is a plain object. + /// + /// Such an object has no attributes but it might be callable and it + /// can be stringified. When serialized it's serialized in it's + /// stringified form. + Plain, + + /// This object is a sequence. + /// + /// Requires that the object implements [`SeqObject`]. + Seq(&'a dyn SeqObject), + + /// This object is a struct (map with string keys). + /// + /// Requires that the object implements [`StructObject`]. + Struct(&'a dyn StructObject), +} + +/// Provides the behavior of an [`Object`] holding sequence of values. +/// +/// An object holding a sequence of values (tuple, list etc.) can be +/// represented by this trait. +/// +/// # Simplified Example +/// +/// For sequences which do not need any special method behavior, the [`Value`] +/// type is capable of automatically constructing a wrapper [`Object`] by using +/// [`Value::from_seq_object`]. In that case only [`SeqObject`] needs to be +/// implemented and the value will provide default implementations for +/// stringification and debug printing. +/// +/// ``` +/// use minijinja::value::{Value, SeqObject}; +/// +/// struct Point(f32, f32, f32); +/// +/// impl SeqObject for Point { +/// fn get_item(&self, idx: usize) -> Option { +/// match idx { +/// 0 => Some(Value::from(self.0)), +/// 1 => Some(Value::from(self.1)), +/// 2 => Some(Value::from(self.2)), +/// _ => None, +/// } +/// } +/// +/// fn item_count(&self) -> usize { +/// 3 +/// } +/// } +/// +/// let value = Value::from_seq_object(Point(1.0, 2.5, 3.0)); +/// ``` +/// +/// # Full Example +/// +/// This example shows how one can use [`SeqObject`] in conjunction +/// with a fully customized [`Object`]. Note that in this case not +/// only [`Object`] needs to be implemented, but also [`Debug`] and +/// [`Display`](std::fmt::Display) no longer come for free. +/// +/// ``` +/// use std::fmt; +/// use minijinja::value::{Value, Object, ObjectKind, SeqObject}; +/// +/// #[derive(Debug, Clone)] +/// struct Point(f32, f32, f32); +/// +/// impl fmt::Display for Point { +/// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +/// write!(f, "({}, {}, {})", self.0, self.1, self.2) +/// } +/// } +/// +/// impl Object for Point { +/// fn kind(&self) -> ObjectKind<'_> { +/// ObjectKind::Seq(self) +/// } +/// } +/// +/// impl SeqObject for Point { +/// fn get_item(&self, idx: usize) -> Option { +/// match idx { +/// 0 => Some(Value::from(self.0)), +/// 1 => Some(Value::from(self.1)), +/// 2 => Some(Value::from(self.2)), +/// _ => None, +/// } +/// } +/// +/// fn item_count(&self) -> usize { +/// 3 +/// } +/// } +/// +/// let value = Value::from_object(Point(1.0, 2.5, 3.0)); +/// ``` +pub trait SeqObject: Send + Sync { + /// Looks up an item by index. + /// + /// Sequences should provide a value for all items in the range of `0..item_count` + /// but the engine will assume that items within the range are `Undefined` + /// if `None` is returned. + fn get_item(&self, idx: usize) -> Option; + + /// Returns the number of items in the sequence. + fn item_count(&self) -> usize; +} + +impl dyn SeqObject + '_ { + /// Convenient iterator over a [`SeqObject`]. + pub fn iter(&self) -> SeqObjectIter<'_> { + SeqObjectIter { + seq: self, + range: 0..self.item_count(), + } + } +} + +impl<'a> SeqObject for &'a [Value] { + #[inline(always)] + fn get_item(&self, idx: usize) -> Option { + self.get(idx).cloned().map(Into::into) + } + + #[inline(always)] + fn item_count(&self) -> usize { + self.len() + } +} + +impl SeqObject for Vec { + #[inline(always)] + fn get_item(&self, idx: usize) -> Option { + self.get(idx).cloned().map(Into::into) + } + + #[inline(always)] + fn item_count(&self) -> usize { + self.len() + } +} + +/// Iterates over [`SeqObject`] +pub struct SeqObjectIter<'a> { + seq: &'a dyn SeqObject, + range: Range, +} + +impl<'a> Iterator for SeqObjectIter<'a> { + type Item = Value; + + #[inline(always)] + fn next(&mut self) -> Option { + self.range + .next() + .map(|idx| self.seq.get_item(idx).unwrap_or(Value::UNDEFINED)) + } + + #[inline(always)] + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } +} + +impl<'a> DoubleEndedIterator for SeqObjectIter<'a> { + #[inline(always)] + fn next_back(&mut self) -> Option { + self.range + .next_back() + .map(|idx| self.seq.get_item(idx).unwrap_or(Value::UNDEFINED)) + } +} + +impl<'a> ExactSizeIterator for SeqObjectIter<'a> {} + +/// Provides the behavior of an [`Object`] holding a struct. +/// +/// An basic object with the shape and behavior of a struct (that means a +/// map with string keys) can be represented by this trait. +/// +/// # Simplified Example +/// +/// For structs which do not need any special method behavior or methods, the +/// [`Value`] type is capable of automatically constructing a wrapper [`Object`] +/// by using [`Value::from_struct_object`]. In that case only [`StructObject`] +/// needs to be implemented and the value will provide default implementations +/// for stringification and debug printing. +/// +/// ``` +/// use minijinja::value::{Value, StructObject}; +/// +/// struct Point(f32, f32, f32); +/// +/// impl StructObject for Point { +/// fn get_field(&self, name: &str) -> Option { +/// match name { +/// "x" => Some(Value::from(self.0)), +/// "y" => Some(Value::from(self.1)), +/// "z" => Some(Value::from(self.2)), +/// _ => None, +/// } +/// } +/// +/// fn fields(&self) -> Box + '_> { +/// Box::new(["x", "y", "z"].into_iter()) +/// } +/// } +/// +/// let value = Value::from_struct_object(Point(1.0, 2.5, 3.0)); +/// ``` +/// +/// # Full Example +/// +/// The following example shows how to implement a dynamic object which +/// represents a struct. Note that in this case not only [`Object`] needs to be +/// implemented, but also [`Debug`] and [`Display`](std::fmt::Display) no longer +/// come for free. +/// +/// ``` +/// use std::fmt; +/// use minijinja::value::{Value, Object, ObjectKind, StructObject}; +/// +/// #[derive(Debug, Clone)] +/// struct Point(f32, f32, f32); +/// +/// impl fmt::Display for Point { +/// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +/// write!(f, "({}, {}, {})", self.0, self.1, self.2) +/// } +/// } +/// +/// impl Object for Point { +/// fn kind(&self) -> ObjectKind<'_> { +/// ObjectKind::Struct(self) +/// } +/// } +/// +/// impl StructObject for Point { +/// fn get_field(&self, name: &str) -> Option { +/// match name { +/// "x" => Some(Value::from(self.0)), +/// "y" => Some(Value::from(self.1)), +/// "z" => Some(Value::from(self.2)), +/// _ => None, +/// } +/// } +/// +/// fn fields(&self) -> Box + '_> { +/// Box::new(["x", "y", "z"].into_iter()) +/// } +/// } +/// +/// let value = Value::from_object(Point(1.0, 2.5, 3.0)); +/// ``` +pub trait StructObject: Send + Sync { + /// Invoked by the engine to get a field of a struct. + /// + /// Where possible it's a good idea for this to align with the return value + /// of [`fields`](Self::fields) but it's not necessary. + /// + /// If an field does not exist, `None` shall be returned. + /// + /// A note should be made here on side effects: unlike calling objects or + /// calling methods on objects, accessing fields is not supposed to + /// have side effects. Neither does this API get access to the interpreter + /// [`State`] nor is there a channel to send out failures as only an option + /// can be returned. If you do plan on doing something in field access + /// that is fallible, instead use a method call. + fn get_field(&self, idx: &str) -> Option; + + /// Iterates over the fields. + /// + /// The default implementation returns an empty iterator. + fn fields(&self) -> Box + '_> { + Box::new(None.into_iter()) + } + + /// Returns the number of fields in the struct. + /// + /// The default implementation returns the number of fields. + fn field_count(&self) -> usize { + self.fields().count() + } +} + +#[repr(transparent)] +pub struct SimpleSeqObject(pub T); + +impl fmt::Display for SimpleSeqObject { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ok!(write!(f, "[")); + for (idx, val) in (&self.0 as &dyn SeqObject).iter().enumerate() { + if idx > 0 { + ok!(write!(f, ", ")); + } + ok!(write!(f, "{:?}", val)); + } + write!(f, "]") + } +} + +impl fmt::Debug for SimpleSeqObject { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list() + .entries((&self.0 as &dyn SeqObject).iter()) + .finish() + } +} + +impl Object for SimpleSeqObject { + fn kind(&self) -> ObjectKind<'_> { + ObjectKind::Seq(&self.0) + } +} + +#[repr(transparent)] +pub struct SimpleStructObject(pub T); + +impl fmt::Display for SimpleStructObject { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ok!(write!(f, "[")); + for (idx, field) in self.0.fields().enumerate() { + if idx > 0 { + ok!(write!(f, ", ")); + } + let val = self.0.get_field(field).unwrap_or(Value::UNDEFINED); + ok!(write!(f, "{:?}: {:?}", field, val)); + } + write!(f, "]") + } +} + +impl fmt::Debug for SimpleStructObject { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut m = f.debug_map(); + for field in self.0.fields() { + let value = self.0.get_field(field).unwrap_or(Value::UNDEFINED); + m.entry(&field, &value); + } + m.finish() + } +} + +impl Object for SimpleStructObject { + fn kind(&self) -> ObjectKind<'_> { + ObjectKind::Struct(&self.0) + } +} diff --git a/minijinja/src/value/ops.rs b/minijinja/src/value/ops.rs index 30599dc3..fa320969 100644 --- a/minijinja/src/value/ops.rs +++ b/minijinja/src/value/ops.rs @@ -2,7 +2,7 @@ use std::convert::{TryFrom, TryInto}; use std::fmt::Write; use crate::error::{Error, ErrorKind}; -use crate::value::{Arc, Value, ValueKind, ValueRepr}; +use crate::value::{Arc, ObjectKind, SeqObject, Value, ValueKind, ValueRepr}; pub enum CoerceResult { I128(i128, i128), @@ -97,28 +97,45 @@ pub fn slice(value: Value, start: Value, stop: Value, step: Value) -> Result(), - )); - } + let maybe_seq = match value.0 { + ValueRepr::String(ref s, _) => { + let (start, len) = get_offset_and_len(start, stop, || s.chars().count()); + return Ok(Value::from( + s.chars() + .skip(start) + .take(len) + .step_by(step) + .collect::(), + )); + } + ValueRepr::Undefined | ValueRepr::None => return Ok(Value::from(Vec::::new())), + ValueRepr::Seq(ref s) => Some(&**s as &dyn SeqObject), + ValueRepr::Dynamic(ref dy) => { + if let ObjectKind::Seq(seq) = dy.kind() { + Some(seq) + } else { + None + } + } + _ => None, + }; - let slice = ok!(value.as_slice()); - let (start, len) = get_offset_and_len(start, stop, || slice.len()); - Ok(Value::from( - slice - .iter() - .skip(start) - .take(len) - .step_by(step) - .cloned() - .collect::>(), - )) + match maybe_seq { + Some(seq) => { + let (start, len) = get_offset_and_len(start, stop, || seq.item_count()); + Ok(Value::from( + seq.iter() + .skip(start) + .take(len) + .step_by(step) + .collect::>(), + )) + } + None => Err(Error::new( + ErrorKind::InvalidOperation, + format!("value of type {} cannot be sliced", value.kind()), + )), + } } fn int_as_value(val: i128) -> Value { @@ -247,27 +264,27 @@ pub fn string_concat(mut left: Value, right: &Value) -> Value { /// Implements a containment operation on values. pub fn contains(container: &Value, value: &Value) -> Result { - match container.0 { - ValueRepr::Seq(ref values) => Ok(Value::from(values.contains(value))), - ValueRepr::Map(ref map, _) => { - let key = match value.clone().try_into_key() { - Ok(key) => key, - Err(_) => return Ok(Value::from(false)), - }; - return Ok(Value::from(map.get(&key).is_some())); - } - ValueRepr::String(ref s, _) => { - return Ok(Value::from(if let Some(s2) = value.as_str() { - s.contains(s2) - } else { - s.contains(&value.to_string()) - })); + let rv = if let Some(s) = container.as_str() { + if let Some(s2) = value.as_str() { + s.contains(s2) + } else { + s.contains(&value.to_string()) } - _ => Err(Error::new( + } else if let Some(seq) = container.as_seq() { + seq.iter().any(|item| &item == value) + } else if let ValueRepr::Map(ref map, _) = container.0 { + let key = match value.clone().try_into_key() { + Ok(key) => key, + Err(_) => return Ok(Value::from(false)), + }; + map.get(&key).is_some() + } else { + return Err(Error::new( ErrorKind::InvalidOperation, "cannot perform a containment check on this value", - )), - } + )); + }; + Ok(Value::from(rv)) } #[test] diff --git a/minijinja/src/vm/loop_object.rs b/minijinja/src/vm/loop_object.rs index 95cfd4cb..7e9c70ef 100644 --- a/minijinja/src/vm/loop_object.rs +++ b/minijinja/src/vm/loop_object.rs @@ -3,7 +3,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Mutex; use crate::error::{Error, ErrorKind}; -use crate::value::{Object, Value}; +use crate::value::{Object, ObjectKind, StructObject, Value}; use crate::vm::state::State; pub(crate) struct Loop { @@ -16,46 +16,16 @@ pub(crate) struct Loop { impl fmt::Debug for Loop { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut s = f.debug_struct("Loop"); - for attr in self.attributes() { - s.field(attr, &self.get_attr(attr).unwrap()); + for attr in self.fields() { + s.field(attr, &self.get_field(attr).unwrap()); } s.finish() } } impl Object for Loop { - fn attributes(&self) -> Box + '_> { - Box::new( - [ - "index0", - "index", - "length", - "revindex", - "revindex0", - "first", - "last", - "depth", - "depth0", - ] - .into_iter(), - ) - } - - fn get_attr(&self, name: &str) -> Option { - let idx = self.idx.load(Ordering::Relaxed) as u64; - let len = self.len as u64; - match name { - "index0" => Some(Value::from(idx)), - "index" => Some(Value::from(idx + 1)), - "length" => Some(Value::from(len)), - "revindex" => Some(Value::from(len.saturating_sub(idx))), - "revindex0" => Some(Value::from(len.saturating_sub(idx).saturating_sub(1))), - "first" => Some(Value::from(idx == 0)), - "last" => Some(Value::from(len == 0 || idx == len - 1)), - "depth" => Some(Value::from(self.depth + 1)), - "depth0" => Some(Value::from(self.depth)), - _ => None, - } + fn kind(&self) -> ObjectKind<'_> { + ObjectKind::Struct(self) } fn call(&self, _state: &State, _args: &[Value]) -> Result { @@ -84,13 +54,49 @@ impl Object for Loop { } } else { Err(Error::new( - ErrorKind::InvalidOperation, + ErrorKind::UnknownMethod, format!("loop object has no method named {}", name), )) } } } +impl StructObject for Loop { + fn fields(&self) -> Box + '_> { + Box::new( + [ + "index0", + "index", + "length", + "revindex", + "revindex0", + "first", + "last", + "depth", + "depth0", + ] + .into_iter(), + ) + } + + fn get_field(&self, name: &str) -> Option { + let idx = self.idx.load(Ordering::Relaxed) as u64; + let len = self.len as u64; + match name { + "index0" => Some(Value::from(idx)), + "index" => Some(Value::from(idx + 1)), + "length" => Some(Value::from(len)), + "revindex" => Some(Value::from(len.saturating_sub(idx))), + "revindex0" => Some(Value::from(len.saturating_sub(idx).saturating_sub(1))), + "first" => Some(Value::from(idx == 0)), + "last" => Some(Value::from(len == 0 || idx == len - 1)), + "depth" => Some(Value::from(self.depth + 1)), + "depth0" => Some(Value::from(self.depth)), + _ => None, + } + } +} + impl fmt::Display for Loop { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( diff --git a/minijinja/src/vm/macro_object.rs b/minijinja/src/vm/macro_object.rs index 0735464a..46607225 100644 --- a/minijinja/src/vm/macro_object.rs +++ b/minijinja/src/vm/macro_object.rs @@ -6,7 +6,7 @@ use crate::error::{Error, ErrorKind}; use crate::key::Key; use crate::output::Output; use crate::utils::AutoEscape; -use crate::value::{MapType, Object, StringType, Value, ValueRepr}; +use crate::value::{MapType, Object, ObjectKind, StringType, StructObject, Value, ValueRepr}; use crate::vm::state::State; use crate::vm::Vm; @@ -41,25 +41,8 @@ impl fmt::Display for Macro { } impl Object for Macro { - fn attributes(&self) -> Box + '_> { - Box::new(["name", "arguments"].into_iter()) - } - - fn get_attr(&self, name: &str) -> Option { - match name { - "name" => Some(Value(ValueRepr::String( - self.data.name.clone(), - StringType::Normal, - ))), - "arguments" => Some(Value::from( - self.data - .arg_spec - .iter() - .map(|x| Value(ValueRepr::String(x.clone(), StringType::Normal))) - .collect::>(), - )), - _ => None, - } + fn kind(&self) -> ObjectKind<'_> { + ObjectKind::Struct(self) } fn call(&self, state: &State, args: &[Value]) -> Result { @@ -150,3 +133,26 @@ impl Object for Macro { }) } } + +impl StructObject for Macro { + fn fields(&self) -> Box + '_> { + Box::new(["name", "arguments"].into_iter()) + } + + fn get_field(&self, name: &str) -> Option { + match name { + "name" => Some(Value(ValueRepr::String( + self.data.name.clone(), + StringType::Normal, + ))), + "arguments" => Some(Value::from( + self.data + .arg_spec + .iter() + .map(|x| Value(ValueRepr::String(x.clone(), StringType::Normal))) + .collect::>(), + )), + _ => None, + } + } +} diff --git a/minijinja/src/vm/mod.rs b/minijinja/src/vm/mod.rs index c4e8cb6a..fd7e3c26 100644 --- a/minijinja/src/vm/mod.rs +++ b/minijinja/src/vm/mod.rs @@ -283,6 +283,7 @@ impl<'env> Vm<'env> { } Instruction::ListAppend => { a = stack.pop(); + // this intentionally only works with actual sequences if let ValueRepr::Seq(mut v) = stack.pop().0 { Arc::make_mut(&mut v).push(a); stack.push(Value(ValueRepr::Seq(v))) @@ -600,14 +601,16 @@ impl<'env> Vm<'env> { out: &mut Output, ignore_missing: bool, ) -> Result<(), Error> { - let choices = if let ValueRepr::Seq(ref choices) = name.0 { - &choices[..] - } else { - std::slice::from_ref(&name) - }; + use crate::value::SeqObject; + + let single_name_slice = std::slice::from_ref(&name); + let choices = name + .as_seq() + .unwrap_or(&single_name_slice as &dyn SeqObject); + let mut templates_tried = vec![]; - for name in choices { - let name = ok!(name.as_str().ok_or_else(|| { + for choice in choices.iter() { + let name = ok!(choice.as_str().ok_or_else(|| { Error::new( ErrorKind::InvalidOperation, "template name was not a string", @@ -617,7 +620,7 @@ impl<'env> Vm<'env> { Ok(tmpl) => tmpl, Err(err) => { if err.kind() == ErrorKind::TemplateNotFound { - templates_tried.push(name); + templates_tried.push(choice); } else { return Err(err); } @@ -652,8 +655,8 @@ impl<'env> Vm<'env> { ) } else { format!( - "tried to include one of multiple templates, none of which existed {:?}", - templates_tried + "tried to include one of multiple templates, none of which existed {}", + Value::from(templates_tried) ) }, )) @@ -814,22 +817,21 @@ impl<'env> Vm<'env> { fn unpack_list(&self, stack: &mut Stack, count: &usize) -> Result<(), Error> { let top = stack.pop(); - let v = - ok!(top - .as_slice() - .map_err(|e| Error::new(ErrorKind::CannotUnpack, "not a sequence").with_source(e))); - if v.len() != *count { + let seq = ok!(top + .as_seq() + .ok_or_else(|| Error::new(ErrorKind::CannotUnpack, "not a sequence"))); + if seq.item_count() != *count { return Err(Error::new( ErrorKind::CannotUnpack, format!( "sequence of wrong length (expected {}, got {})", *count, - v.len() + seq.item_count() ), )); } - for value in v.iter().rev() { - stack.push(value.clone()); + for item in seq.iter().rev() { + stack.push(item); } Ok(()) } diff --git a/minijinja/tests/snapshots/test_templates__vm@err_bad_filter.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_bad_filter.txt.snap index f93502ea..033c5824 100644 --- a/minijinja/tests/snapshots/test_templates__vm@err_bad_filter.txt.snap +++ b/minijinja/tests/snapshots/test_templates__vm@err_bad_filter.txt.snap @@ -8,12 +8,12 @@ input_file: minijinja/tests/inputs/err_bad_filter.txt Error { kind: InvalidOperation, - detail: "object is not iterable", + detail: "number is not iterable", name: "err_bad_filter.txt", line: 1, } -invalid operation: object is not iterable (in err_bad_filter.txt:1) +invalid operation: number is not iterable (in err_bad_filter.txt:1) ----------------------------- err_bad_filter.txt ------------------------------ 1 > {% for item in 42|slice(4) %} i ^^^^^^^^ invalid operation diff --git a/minijinja/tests/snapshots/test_templates__vm@loop_bad_unpacking.txt.snap b/minijinja/tests/snapshots/test_templates__vm@loop_bad_unpacking.txt.snap index a3aba674..f6eb726a 100644 --- a/minijinja/tests/snapshots/test_templates__vm@loop_bad_unpacking.txt.snap +++ b/minijinja/tests/snapshots/test_templates__vm@loop_bad_unpacking.txt.snap @@ -15,10 +15,6 @@ Error { detail: "not a sequence", name: "loop_bad_unpacking.txt", line: 2, - source: Error { - kind: InvalidOperation, - detail: "value of type number is not a sequence", - }, } cannot unpack: not a sequence (in loop_bad_unpacking.txt:2) @@ -50,5 +46,3 @@ Referenced variables: { } ------------------------------------------------------------------------------- -caused by: invalid operation: value of type number is not a sequence - diff --git a/minijinja/tests/snapshots/test_templates__vm@loop_over_non_iterable.txt.snap b/minijinja/tests/snapshots/test_templates__vm@loop_over_non_iterable.txt.snap index 902a1c62..f42cd053 100644 --- a/minijinja/tests/snapshots/test_templates__vm@loop_over_non_iterable.txt.snap +++ b/minijinja/tests/snapshots/test_templates__vm@loop_over_non_iterable.txt.snap @@ -9,12 +9,12 @@ input_file: minijinja/tests/inputs/loop_over_non_iterable.txt Error { kind: InvalidOperation, - detail: "object is not iterable", + detail: "number is not iterable", name: "loop_over_non_iterable.txt", line: 1, } -invalid operation: object is not iterable (in loop_over_non_iterable.txt:1) +invalid operation: number is not iterable (in loop_over_non_iterable.txt:1) ------------------------- loop_over_non_iterable.txt -------------------------- 1 > [{% for item in seq %}{% endfor %}] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/minijinja/tests/test_value.rs b/minijinja/tests/test_value.rs index 048b32aa..78f95c95 100644 --- a/minijinja/tests/test_value.rs +++ b/minijinja/tests/test_value.rs @@ -2,8 +2,7 @@ use std::cmp::Ordering; use std::fmt; use insta::assert_snapshot; -use minijinja::value::{Object, Value}; -use minijinja::ErrorKind; +use minijinja::value::{Object, ObjectKind, SeqObject, StructObject, Value}; #[test] fn test_sort() { @@ -63,21 +62,6 @@ fn test_float_to_string() { assert_eq!(Value::from(42.0f32).to_string(), "42.0"); } -#[test] -fn test_value_as_slice() { - let val = Value::from(vec![1u32, 2, 3]); - assert_eq!( - val.as_slice().unwrap(), - &[Value::from(1), Value::from(2), Value::from(3)] - ); - assert_eq!(Value::UNDEFINED.as_slice().unwrap(), &[]); - assert_eq!(Value::from(()).as_slice().unwrap(), &[]); - assert_eq!( - Value::from("foo").as_slice().unwrap_err().kind(), - ErrorKind::InvalidOperation - ); -} - #[test] fn test_value_as_bytes() { assert_eq!(Value::from("foo").as_bytes(), Some(&b"foo"[..])); @@ -92,7 +76,7 @@ fn test_value_by_index() { } #[test] -fn test_object_iteration() { +fn test_map_object_iteration_and_indexing() { #[derive(Debug, Clone)] struct Point(i32, i32, i32); @@ -103,7 +87,13 @@ fn test_object_iteration() { } impl Object for Point { - fn get_attr(&self, name: &str) -> Option { + fn kind(&self) -> ObjectKind<'_> { + ObjectKind::Struct(self) + } + } + + impl StructObject for Point { + fn get_field(&self, name: &str) -> Option { match name { "x" => Some(Value::from(self.0)), "y" => Some(Value::from(self.1)), @@ -112,19 +102,73 @@ fn test_object_iteration() { } } - fn attributes(&self) -> Box + '_> { + fn fields(&self) -> Box + '_> { Box::new(["x", "y", "z"].into_iter()) } } - let point = Point(1, 2, 3); let rv = minijinja::render!( "{% for key in point %}{{ key }}: {{ point[key] }}\n{% endfor %}", - point => Value::from_object(point) + point => Value::from_object(Point(1, 2, 3)) ); assert_snapshot!(rv, @r###" x: 1 y: 2 z: 3 "###); + + let rv = minijinja::render!( + "{{ [point.x, point.z, point.missing_attribute] }}", + point => Value::from_object(Point(1, 2, 3)) + ); + assert_snapshot!(rv, @r###"[1, 3, Undefined]"###); +} + +#[test] +fn test_seq_object_iteration_and_indexing() { + #[derive(Debug, Clone)] + struct Point(i32, i32, i32); + + impl fmt::Display for Point { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}, {}", self.0, self.1, self.2) + } + } + + impl Object for Point { + fn kind(&self) -> ObjectKind<'_> { + ObjectKind::Seq(self) + } + } + + impl SeqObject for Point { + fn get_item(&self, index: usize) -> Option { + match index { + 0 => Some(Value::from(self.0)), + 1 => Some(Value::from(self.1)), + 2 => Some(Value::from(self.2)), + _ => None, + } + } + + fn item_count(&self) -> usize { + 3 + } + } + + let rv = minijinja::render!( + "{% for value in point %}{{ loop.index0 }}: {{ value }}\n{% endfor %}", + point => Value::from_object(Point(1, 2, 3)) + ); + assert_snapshot!(rv, @r###" + 0: 1 + 1: 2 + 2: 3 + "###); + + let rv = minijinja::render!( + "{{ [point[0], point[2], point[42]] }}", + point => Value::from_object(Point(1, 2, 3)) + ); + assert_snapshot!(rv, @r###"[1, 3, Undefined]"###); }