From f819d28a83276fa0ac039716b97161d82518d0ed Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 19 Nov 2022 11:00:06 +0100 Subject: [PATCH] Added simplified object creation API for sequences and structs --- examples/dynamic/src/main.rs | 19 +++- examples/dynamic/src/template.html | 6 +- minijinja/src/value/mod.rs | 51 ++++++--- minijinja/src/value/object.rs | 164 ++++++++++++++++++++++++++--- 4 files changed, 211 insertions(+), 29 deletions(-) diff --git a/examples/dynamic/src/main.rs b/examples/dynamic/src/main.rs index 9c1431ba..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)] @@ -57,10 +57,27 @@ impl Object for Magic { } } +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/minijinja/src/value/mod.rs b/minijinja/src/value/mod.rs index 865874d8..74afad43 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,6 +118,7 @@ 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; @@ -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. /// /// ``` @@ -526,7 +551,7 @@ impl Value { ValueRepr::Map(..) => ValueKind::Map, ValueRepr::Dynamic(ref dy) => match dy.kind() { // XXX: basic objects should probably not report as map - ObjectKind::Basic => ValueKind::Map, + ObjectKind::Plain => ValueKind::Map, ObjectKind::Seq(_) => ValueKind::Seq, ObjectKind::Struct(_) => ValueKind::Map, }, @@ -554,7 +579,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::Basic => true, + ObjectKind::Plain => true, ObjectKind::Seq(s) => s.item_count() != 0, ObjectKind::Struct(s) => s.field_count() != 0, }, @@ -651,7 +676,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::Basic => None, + ObjectKind::Plain => None, ObjectKind::Seq(s) => Some(s.item_count()), ObjectKind::Struct(s) => Some(s.field_count()), }, @@ -682,7 +707,7 @@ impl Value { items.get(&lookup_key).cloned() } ValueRepr::Dynamic(ref dy) => match dy.kind() { - ObjectKind::Basic | ObjectKind::Seq(_) => None, + ObjectKind::Plain | ObjectKind::Seq(_) => None, ObjectKind::Struct(s) => s.get_field(key), }, ValueRepr::Undefined => { @@ -807,7 +832,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::Basic => return None, + ObjectKind::Plain => return None, ObjectKind::Seq(s) => s, ObjectKind::Struct(s) => match key { Key::String(ref key) => return s.get_field(key), @@ -900,7 +925,7 @@ impl Value { .filter_map(|(k, v)| k.as_str().map(move |k| (k, v.clone()))), ) as Box>, ValueRepr::Dynamic(ref obj) => match obj.kind() { - ObjectKind::Basic | ObjectKind::Seq(_) => { + ObjectKind::Plain | ObjectKind::Seq(_) => { Box::new(None.into_iter()) as Box> } ObjectKind::Struct(s) => Box::new( @@ -931,7 +956,7 @@ impl Value { ), ValueRepr::Dynamic(ref obj) => { match obj.kind() { - ObjectKind::Basic => (ValueIteratorState::Empty, 0), + ObjectKind::Plain => (ValueIteratorState::Empty, 0), ObjectKind::Seq(s) => ( ValueIteratorState::DynSeq(0, Arc::clone(obj)), s.item_count(), @@ -994,7 +1019,7 @@ impl Serialize for Value { map.end() } ValueRepr::Dynamic(ref dy) => match dy.kind() { - ObjectKind::Basic => serializer.serialize_str(&dy.to_string()), + 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()))); diff --git a/minijinja/src/value/object.rs b/minijinja/src/value/object.rs index 1a788887..272e28ee 100644 --- a/minijinja/src/value/object.rs +++ b/minijinja/src/value/object.rs @@ -33,13 +33,13 @@ use crate::vm::State; pub trait Object: fmt::Display + fmt::Debug + Any + Sync + Send { /// Describes the kind of an object. /// - /// If not implemented behavior for an object is [`ObjectKind::Basic`] + /// 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. /// /// For more information see [`ObjectKind`]. fn kind(&self) -> ObjectKind<'_> { - ObjectKind::Basic + ObjectKind::Plain } /// Called when the engine tries to call a method on the object. @@ -92,7 +92,7 @@ impl Object for std::sync::Arc { /// 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 [`Basic`](Self::Basic) object which +/// 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. @@ -102,12 +102,12 @@ impl Object for std::sync::Arc { /// be represented by objects. #[non_exhaustive] pub enum ObjectKind<'a> { - /// This object is a basic object. + /// 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. - Basic, + Plain, /// This object is a sequence. /// @@ -120,12 +120,48 @@ pub enum ObjectKind<'a> { Struct(&'a dyn StructObject), } -/// Views an [`Object`] as sequence of values. +/// Provides the behavior of an [`Object`] holding sequence of values. /// -/// # Example +/// An object holding a sequence of values (tuple, list etc.) can be +/// represented by this trait. /// -/// The following example shows how to implement a dynamic object which -/// represents a sequence of three items: +/// # 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; @@ -163,7 +199,7 @@ pub enum ObjectKind<'a> { /// /// let value = Value::from_object(Point(1.0, 2.5, 3.0)); /// ``` -pub trait SeqObject { +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` @@ -242,12 +278,48 @@ impl<'a> DoubleEndedIterator for SeqObjectIter<'a> { impl<'a> ExactSizeIterator for SeqObjectIter<'a> {} -/// Views an [`Object`] as a struct. +/// 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, +/// } +/// } /// -/// # Example +/// 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: +/// 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; @@ -285,7 +357,7 @@ impl<'a> ExactSizeIterator for SeqObjectIter<'a> {} /// /// let value = Value::from_object(Point(1.0, 2.5, 3.0)); /// ``` -pub trait StructObject { +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 @@ -315,3 +387,67 @@ pub trait StructObject { 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) + } +}