From b72622120d95e4440159d90cc00b05d79c96df50 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 23 Jan 2025 15:23:25 +0100 Subject: [PATCH 1/2] Add support for "runtime" values --- rinja/Cargo.toml | 1 + rinja/src/error.rs | 10 ++ rinja/src/filters/escape.rs | 35 ++++- rinja/src/helpers.rs | 1 + rinja/src/lib.rs | 131 ++++++++++++++++--- rinja/src/values.rs | 200 +++++++++++++++++++++++++++++ rinja_derive/src/generator.rs | 8 +- rinja_derive/src/generator/expr.rs | 43 +++++++ rinja_derive/src/generator/node.rs | 3 +- rinja_derive/src/tests.rs | 78 +++++------ testing/tests/ui/values.rs | 45 +++++++ testing/tests/ui/values.stderr | 47 +++++++ testing/tests/values.rs | 90 +++++++++++++ 13 files changed, 632 insertions(+), 60 deletions(-) create mode 100644 rinja/src/values.rs create mode 100644 testing/tests/ui/values.rs create mode 100644 testing/tests/ui/values.stderr create mode 100644 testing/tests/values.rs diff --git a/rinja/Cargo.toml b/rinja/Cargo.toml index 556446ab8..1d14f0979 100644 --- a/rinja/Cargo.toml +++ b/rinja/Cargo.toml @@ -33,6 +33,7 @@ serde_json = { version = "1.0", optional = true, default-features = false, featu itoa = "1.0.11" [dev-dependencies] +assert_matches = "1.5.0" criterion = "0.5" [badges] diff --git a/rinja/src/error.rs b/rinja/src/error.rs index 49e81c0df..89748632a 100644 --- a/rinja/src/error.rs +++ b/rinja/src/error.rs @@ -19,6 +19,10 @@ pub type Result = core::result::Result; pub enum Error { /// Generic, unspecified formatting error Fmt, + /// Key not present in [`Values`][crate::Values] + ValueMissing, + /// Incompatible value type for key in [`Values`][crate::Values] + ValueType, /// An error raised by using `?` in a template #[cfg(feature = "alloc")] Custom(Box), @@ -41,6 +45,8 @@ impl Error { pub fn into_box(self) -> Box { match self { Error::Fmt => fmt::Error.into(), + Error::ValueMissing => Box::new(Error::ValueMissing), + Error::ValueType => Box::new(Error::ValueType), Error::Custom(err) => err, #[cfg(feature = "serde_json")] Error::Json(err) => err.into(), @@ -66,6 +72,8 @@ impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { Error::Fmt => Some(&fmt::Error), + Error::ValueMissing => None, + Error::ValueType => None, #[cfg(feature = "alloc")] Error::Custom(err) => Some(err.as_ref()), #[cfg(feature = "serde_json")] @@ -78,6 +86,8 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::Fmt => fmt::Error.fmt(f), + Error::ValueMissing => f.write_str("key missing in values"), + Error::ValueType => f.write_str("value has wrong type"), #[cfg(feature = "alloc")] Error::Custom(err) => err.fmt(f), #[cfg(feature = "serde_json")] diff --git a/rinja/src/filters/escape.rs b/rinja/src/filters/escape.rs index 8a249aac5..e0cb40fda 100644 --- a/rinja/src/filters/escape.rs +++ b/rinja/src/filters/escape.rs @@ -4,6 +4,8 @@ use core::ops::Deref; use core::pin::Pin; use core::str; +use crate::Values; + /// Marks a string (or other `Display` type) as safe /// /// Use this if you want to allow markup in an expression, or if you know @@ -489,7 +491,11 @@ pub struct Writable<'a, S: ?Sized>(pub &'a S); /// Used internally by rinja to select the appropriate [`write!()`] mechanism pub trait WriteWritable { /// Used internally by rinja to select the appropriate [`write!()`] mechanism - fn rinja_write(&self, dest: &mut W) -> crate::Result<()>; + fn rinja_write( + &self, + dest: &mut W, + values: &dyn Values, + ) -> crate::Result<()>; } /// Used internally by rinja to speed up writing some types. @@ -604,16 +610,35 @@ const _: () = { } } - impl WriteWritable for &Writable<'_, S> { + impl WriteWritable for &Writable<'_, S> { + #[inline] + fn rinja_write( + &self, + dest: &mut W, + values: &dyn Values, + ) -> crate::Result<()> { + self.0.render_into_with_values(dest, values) + } + } + + impl WriteWritable for &&Writable<'_, S> { #[inline] - fn rinja_write(&self, dest: &mut W) -> crate::Result<()> { + fn rinja_write( + &self, + dest: &mut W, + _: &dyn Values, + ) -> crate::Result<()> { self.0.write_into(dest) } } - impl WriteWritable for &&Writable<'_, S> { + impl WriteWritable for &&&Writable<'_, S> { #[inline] - fn rinja_write(&self, dest: &mut W) -> crate::Result<()> { + fn rinja_write( + &self, + dest: &mut W, + _: &dyn Values, + ) -> crate::Result<()> { Ok(write!(dest, "{}", self.0)?) } } diff --git a/rinja/src/helpers.rs b/rinja/src/helpers.rs index 7a3df76ab..0d2045333 100644 --- a/rinja/src/helpers.rs +++ b/rinja/src/helpers.rs @@ -14,6 +14,7 @@ use core::pin::Pin; pub use crate::error::{ErrorMarker, ResultConverter}; use crate::filters::FastWritable; +pub use crate::values::get_value; pub struct TemplateLoop where diff --git a/rinja/src/lib.rs b/rinja/src/lib.rs index ecc4273bc..eecca307c 100644 --- a/rinja/src/lib.rs +++ b/rinja/src/lib.rs @@ -70,6 +70,7 @@ pub mod filters; #[doc(hidden)] pub mod helpers; mod html; +mod values; #[cfg(feature = "alloc")] use alloc::string::String; @@ -83,6 +84,7 @@ pub use rinja_derive::Template; pub use crate as shared; pub use crate::error::{Error, Result}; pub use crate::helpers::PrimitiveType; +pub use crate::values::{NO_VALUES, Value, Values, get_value}; /// Main `Template` trait; implementations are generally derived /// @@ -106,21 +108,50 @@ pub use crate::helpers::PrimitiveType; /// /// [dynamic methods calls]: pub trait Template: fmt::Display + filters::FastWritable { - /// Helper method which allocates a new `String` and renders into it + /// Helper method which allocates a new `String` and renders into it. + #[inline] #[cfg(feature = "alloc")] fn render(&self) -> Result { + self.render_with_values(NO_VALUES) + } + + /// Helper method which allocates a new `String` and renders into it with provided [`Values`]. + #[inline] + #[cfg(feature = "alloc")] + fn render_with_values(&self, values: &dyn Values) -> Result { let mut buf = String::new(); let _ = buf.try_reserve(Self::SIZE_HINT); - self.render_into(&mut buf)?; + self.render_into_with_values(&mut buf, values)?; Ok(buf) } - /// Renders the template to the given `writer` fmt buffer - fn render_into(&self, writer: &mut W) -> Result<()>; + /// Renders the template to the given `writer` fmt buffer. + #[inline] + fn render_into(&self, writer: &mut W) -> Result<()> { + self.render_into_with_values(writer, NO_VALUES) + } + + /// Renders the template to the given `writer` fmt buffer with provided [`Values`]. + fn render_into_with_values( + &self, + writer: &mut W, + values: &dyn Values, + ) -> Result<()>; - /// Renders the template to the given `writer` io buffer + /// Renders the template to the given `writer` io buffer. + #[inline] #[cfg(feature = "std")] fn write_into(&self, writer: &mut W) -> io::Result<()> { + self.write_into_with_values(writer, NO_VALUES) + } + + /// Renders the template to the given `writer` io buffer with provided [`Values`]. + #[cfg(feature = "std")] + fn write_into_with_values( + &self, + writer: &mut W, + values: &dyn Values, + ) -> io::Result<()> { struct Wrapped { writer: W, err: Option, @@ -138,7 +169,7 @@ pub trait Template: fmt::Display + filters::FastWritable { } let mut wrapped = Wrapped { writer, err: None }; - if self.render_into(&mut wrapped).is_ok() { + if self.render_into_with_values(&mut wrapped, values).is_ok() { Ok(()) } else { let err = wrapped.err.take(); @@ -160,15 +191,30 @@ pub trait Template: fmt::Display + filters::FastWritable { } impl Template for &T { + #[inline] + #[cfg(feature = "alloc")] + fn render(&self) -> Result { + ::render(self) + } + + #[inline] + #[cfg(feature = "alloc")] + fn render_with_values(&self, values: &dyn Values) -> Result { + ::render_with_values(self, values) + } + #[inline] fn render_into(&self, writer: &mut W) -> Result<()> { ::render_into(self, writer) } #[inline] - #[cfg(feature = "alloc")] - fn render(&self) -> Result { - ::render(self) + fn render_into_with_values( + &self, + writer: &mut W, + values: &dyn Values, + ) -> Result<()> { + ::render_into_with_values(self, writer, values) } #[inline] @@ -177,6 +223,16 @@ impl Template for &T { ::write_into(self, writer) } + #[inline] + #[cfg(feature = "std")] + fn write_into_with_values( + &self, + writer: &mut W, + values: &dyn Values, + ) -> io::Result<()> { + ::write_into_with_values(self, writer, values) + } + const SIZE_HINT: usize = T::SIZE_HINT; } @@ -186,18 +242,37 @@ impl Template for &T { /// /// [`dyn`-compatible]: https://doc.rust-lang.org/stable/reference/items/traits.html#dyn-compatibility pub trait DynTemplate { - /// Helper method which allocates a new `String` and renders into it + /// Helper method which allocates a new `String` and renders into it. #[cfg(feature = "alloc")] fn dyn_render(&self) -> Result; - /// Renders the template to the given `writer` fmt buffer + /// Helper method which allocates a new `String` and renders into it with provided [`Values`]. + #[cfg(feature = "alloc")] + fn dyn_render_with_values(&self, values: &dyn Values) -> Result; + + /// Renders the template to the given `writer` fmt buffer. fn dyn_render_into(&self, writer: &mut dyn fmt::Write) -> Result<()>; - /// Renders the template to the given `writer` io buffer + /// Renders the template to the given `writer` fmt buffer with provided [`Values`]. + fn dyn_render_into_with_values( + &self, + writer: &mut dyn fmt::Write, + values: &dyn Values, + ) -> Result<()>; + + /// Renders the template to the given `writer` io buffer. #[cfg(feature = "std")] fn dyn_write_into(&self, writer: &mut dyn io::Write) -> io::Result<()>; - /// Provides a conservative estimate of the expanded length of the rendered template + /// Renders the template to the given `writer` io buffer with provided [`Values`]. + #[cfg(feature = "std")] + fn dyn_write_into_with_values( + &self, + writer: &mut dyn io::Write, + values: &dyn Values, + ) -> io::Result<()>; + + /// Provides a conservative estimate of the expanded length of the rendered template. fn size_hint(&self) -> usize; } @@ -208,17 +283,39 @@ impl DynTemplate for T { ::render(self) } + #[cfg(feature = "alloc")] + fn dyn_render_with_values(&self, values: &dyn Values) -> Result { + ::render_with_values(self, values) + } + #[inline] fn dyn_render_into(&self, writer: &mut dyn fmt::Write) -> Result<()> { ::render_into(self, writer) } - #[inline] + fn dyn_render_into_with_values( + &self, + writer: &mut dyn fmt::Write, + values: &dyn Values, + ) -> Result<()> { + ::render_into_with_values(self, writer, values) + } + #[cfg(feature = "std")] fn dyn_write_into(&self, writer: &mut dyn io::Write) -> io::Result<()> { ::write_into(self, writer) } + #[inline] + #[cfg(feature = "std")] + fn dyn_write_into_with_values( + &self, + writer: &mut dyn io::Write, + values: &dyn Values, + ) -> io::Result<()> { + ::write_into_with_values(self, writer, values) + } + #[inline] fn size_hint(&self) -> usize { ::SIZE_HINT @@ -287,7 +384,11 @@ mod tests { struct Test; impl Template for Test { - fn render_into(&self, writer: &mut W) -> Result<()> { + fn render_into_with_values( + &self, + writer: &mut W, + _values: &dyn Values, + ) -> Result<()> { Ok(writer.write_str("test")?) } diff --git a/rinja/src/values.rs b/rinja/src/values.rs new file mode 100644 index 000000000..bb7667f94 --- /dev/null +++ b/rinja/src/values.rs @@ -0,0 +1,200 @@ +use core::any::Any; +use core::borrow::Borrow; + +use crate::Error; + +/// No runtime values provided. +pub const NO_VALUES: &dyn Values = &(); + +/// Try to find `key` in `values` and then to convert it to `T`. +pub fn get_value(values: &dyn Values, key: impl AsRef) -> Result<&T, Error> { + let Some(src) = values.get_value(key.as_ref()) else { + return Err(Error::ValueMissing); + }; + + if let Some(value) = src.downcast_ref::() { + return Ok(value); + } else if let Some(value) = src.downcast_ref::<&T>() { + return Ok(value); + } + + #[cfg(feature = "alloc")] + if let Some(value) = src.downcast_ref::>() { + return Ok(value); + } else if let Some(value) = src.downcast_ref::>() { + return Ok(value); + } else if let Some(value) = src.downcast_ref::>() { + return Ok(value); + } + + Err(Error::ValueType) +} + +/// A runtime value store for [`Template::render_with_values()`][crate::Template::render_with_values]. +pub trait Values { + /// Try to find `key` in this store. + fn get_value<'a>(&'a self, key: &str) -> Option<&'a dyn Any>; +} + +crate::impl_for_ref! { + impl Values for T { + #[inline] + fn get_value<'a>(&'a self, key: &str) -> Option<&'a dyn Any> { + T::get_value(self, key) + } + } +} + +impl Values for () { + #[inline] + fn get_value<'a>(&'a self, _: &str) -> Option<&'a dyn Any> { + None + } +} + +impl Values for Option { + #[inline] + fn get_value<'a>(&'a self, key: &str) -> Option<&'a dyn Any> { + self.as_ref()?.get_value(key) + } +} + +impl Values for [(K, V); N] +where + K: Borrow, + V: Value, +{ + #[inline] + fn get_value<'a>(&'a self, key: &str) -> Option<&'a dyn Any> { + self.as_slice().get_value(key) + } +} + +impl Values for [(K, V)] +where + K: Borrow, + V: Value, +{ + fn get_value<'a>(&'a self, key: &str) -> Option<&'a dyn Any> { + for (k, v) in self { + if k.borrow() == key { + return v.ref_any(); + } + } + None + } +} + +#[cfg(feature = "alloc")] +impl Values for alloc::collections::BTreeMap +where + K: Borrow + core::cmp::Ord, + V: Value, +{ + #[inline] + fn get_value<'a>(&'a self, key: &str) -> Option<&'a dyn Any> { + self.get(key)?.ref_any() + } +} + +#[cfg(feature = "std")] +impl Values for std::collections::HashMap +where + K: Borrow + Eq + core::hash::Hash, + V: Value, + S: core::hash::BuildHasher, +{ + #[inline] + fn get_value<'a>(&'a self, key: &str) -> Option<&'a dyn Any> { + self.get(key)?.ref_any() + } +} + +/// A value in a [`Values`] collection. +/// +/// This is [dyn](https://doc.rust-lang.org/stable/std/keyword.dyn.html) [Any], +/// [Option]<dyn Any>, or a reference to either. +pub trait Value { + /// Returns a reference to this value unless it is `None`. + fn ref_any(&self) -> Option<&dyn Any>; +} + +crate::impl_for_ref! { + impl Value for T { + #[inline] + fn ref_any(&self) -> Option<&dyn Any> { + T::ref_any(self) + } + } +} + +impl Value for dyn Any { + #[inline] + fn ref_any(&self) -> Option<&dyn Any> { + Some(self) + } +} + +impl Value for Option { + #[inline] + fn ref_any(&self) -> Option<&dyn Any> { + T::ref_any(self.as_ref()?) + } +} + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use super::*; + + #[cfg(feature = "std")] + #[test] + fn values_on_hashmap() { + use alloc::boxed::Box; + use alloc::string::String; + use std::collections::HashMap; + + let mut values: HashMap> = HashMap::new(); + values.insert("a".into(), Box::new(10u32)); + values.insert("c".into(), Box::new("blam")); + assert_matches!(get_value::(&values, "a"), Ok(&10u32)); + assert_matches!(get_value::<&str>(&values, "c"), Ok(&"blam")); + assert_matches!(get_value::(&values, "a"), Err(Error::ValueType)); + assert_matches!(get_value::(&values, "d"), Err(Error::ValueMissing)); + + let mut values: HashMap<&str, Box> = HashMap::new(); + values.insert("a", Box::new(10u32)); + assert_matches!(get_value::(&values, "a"), Ok(&10u32)); + } + + #[cfg(feature = "alloc")] + #[test] + fn values_on_btreemap() { + use alloc::boxed::Box; + use alloc::collections::BTreeMap; + use alloc::string::String; + + let mut values: BTreeMap> = BTreeMap::new(); + values.insert("a".into(), Box::new(10u32)); + values.insert("c".into(), Box::new("blam")); + + assert_matches!(get_value::(&values, "a"), Ok(&10u32)); + assert_matches!(get_value::<&str>(&values, "c"), Ok(&"blam")); + assert_matches!(get_value::(&values, "a"), Err(Error::ValueType)); + assert_matches!(get_value::(&values, "d"), Err(Error::ValueMissing)); + + let mut values: BTreeMap<&str, Box> = BTreeMap::new(); + values.insert("a", Box::new(10u32)); + assert_matches!(get_value::(&values, "a"), Ok(&10u32)); + } + + #[test] + fn values_on_slice() { + let values: &[(&str, &dyn Any)] = &[("a", &12u32), ("c", &"blam")]; + assert_matches!(get_value::(&values, "a"), Ok(12u32)); + assert_matches!(get_value::<&str>(&values, "c"), Ok(&"blam")); + assert_matches!(get_value::(&values, "a"), Err(Error::ValueType)); + assert_matches!(get_value::(&values, "d"), Err(Error::ValueMissing)); + } +} diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs index 96d0aca6b..3318b45a9 100644 --- a/rinja_derive/src/generator.rs +++ b/rinja_derive/src/generator.rs @@ -124,12 +124,16 @@ impl<'a, 'h> Generator<'a, 'h> { ) -> Result { write_header(self.input.ast, buf, target); buf.write( - "fn render_into(&self, __rinja_writer: &mut RinjaW) -> rinja::Result<()>\ + "fn render_into_with_values(\ + &self,\ + __rinja_writer: &mut RinjaW,\ + __rinja_values: &dyn rinja::Values\ + ) -> rinja::Result<()>\ where \ RinjaW: rinja::helpers::core::fmt::Write + ?rinja::helpers::core::marker::Sized\ {\ use rinja::filters::{AutoEscape as _, WriteWritable as _};\ - use rinja::helpers::ResultConverter as _; + use rinja::helpers::ResultConverter as _;\ use rinja::helpers::core::fmt::Write as _;", ); diff --git a/rinja_derive/src/generator/expr.rs b/rinja_derive/src/generator/expr.rs index 87428cd99..cd03bbcf9 100644 --- a/rinja_derive/src/generator/expr.rs +++ b/rinja_derive/src/generator/expr.rs @@ -264,6 +264,7 @@ impl<'a> Generator<'a, '_> { "safe" => Self::_visit_safe_filter, "urlencode" => Self::_visit_urlencode_filter, "urlencode_strict" => Self::_visit_urlencode_strict_filter, + "value" => return self._visit_value(ctx, buf, args, generics, node, "`value` filter"), name if BUILTIN_FILTERS.contains(&name) => { return self._visit_builtin_filter(ctx, buf, name, args, generics, node); } @@ -676,6 +677,37 @@ impl<'a> Generator<'a, '_> { Ok(DisplayWrap::Unwrapped) } + fn _visit_value( + &mut self, + ctx: &Context<'_>, + buf: &mut Buffer, + args: &[WithSpan<'_, Expr<'a>>], + generics: &[WithSpan<'_, TyGenerics<'_>>], + node: Span<'_>, + kind: &str, + ) -> Result { + let [key] = args else { + return Err(ctx.generate_error( + format_args!("{kind} only takes one argument, found {}", args.len()), + node, + )); + }; + let [gen] = generics else { + return Err(ctx.generate_error( + format_args!("{kind} expects one generic, found {}", generics.len()), + node, + )); + }; + buf.write("rinja::helpers::get_value"); + buf.write("::<"); + self.visit_ty_generic(buf, gen); + buf.write('>'); + buf.write("(&__rinja_values, &("); + self._visit_arg(ctx, buf, key)?; + buf.write("))"); + Ok(DisplayWrap::Unwrapped) + } + fn _visit_args( &mut self, ctx: &Context<'_>, @@ -893,6 +925,17 @@ impl<'a> Generator<'a, '_> { } } } + // We special-case "rinja::get_value". + Expr::Path(path) if path == &["rinja", "get_value"] => { + self._visit_value( + ctx, + buf, + args, + generics, + left.span(), + "`get_value` function", + )?; + } sub_left => { match sub_left { Expr::Var(name) => match self.locals.resolve(name) { diff --git a/rinja_derive/src/generator/node.rs b/rinja_derive/src/generator/node.rs index ce42477f7..d3417b8d3 100644 --- a/rinja_derive/src/generator/node.rs +++ b/rinja_derive/src/generator/node.rs @@ -1168,7 +1168,8 @@ impl<'a> Generator<'a, '_> { idx }; lines.write(format_args!( - "(&&rinja::filters::Writable(expr{idx})).rinja_write(__rinja_writer)?;", + "(&&&rinja::filters::Writable(expr{idx})).\ + rinja_write(__rinja_writer, __rinja_values)?;", )); } } diff --git a/rinja_derive/src/tests.rs b/rinja_derive/src/tests.rs index d144e6673..0c1833839 100644 --- a/rinja_derive/src/tests.rs +++ b/rinja_derive/src/tests.rs @@ -27,7 +27,11 @@ fn compare(jinja: &str, expected: &str, fields: &[(&str, &str)], size_hint: usiz extern crate rinja as rinja; impl rinja::Template for Foo { - fn render_into(&self, __rinja_writer: &mut RinjaW) -> rinja::Result<()> + fn render_into_with_values( + &self, + __rinja_writer: &mut RinjaW, + __rinja_values: &dyn rinja::Values, + ) -> rinja::Result<()> where RinjaW: rinja::helpers::core::fmt::Write + ?rinja::helpers::core::marker::Sized, { @@ -153,7 +157,7 @@ fn check_if_let() { &((&&rinja::filters::AutoEscaper::new(&(query), rinja::filters::Text)).rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } }", @@ -170,7 +174,7 @@ fn check_if_let() { &((&&rinja::filters::AutoEscaper::new(&(s), rinja::filters::Text)).rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } }", @@ -187,7 +191,7 @@ fn check_if_let() { &((&&rinja::filters::AutoEscaper::new(&(s), rinja::filters::Text)).rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } }", @@ -211,9 +215,9 @@ fn check_if_let_chain() { .rinja_auto_escape()?), ) { (expr0, expr2) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; __rinja_writer.write_str(" ")?; - (&&rinja::filters::Writable(expr2)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr2)).rinja_write(__rinja_writer, __rinja_values)?; } } }"#, @@ -236,9 +240,9 @@ fn check_if_let_chain() { .rinja_auto_escape()?), ) { (expr0, expr2) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; __rinja_writer.write_str(" ")?; - (&&rinja::filters::Writable(expr2)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr2)).rinja_write(__rinja_writer, __rinja_values)?; } } }"#, @@ -265,9 +269,9 @@ fn check_if_let_chain() { .rinja_auto_escape()?), ) { (expr0, expr2) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; __rinja_writer.write_str(" ")?; - (&&rinja::filters::Writable(expr2)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr2)).rinja_write(__rinja_writer, __rinja_values)?; } } }"#, @@ -292,9 +296,9 @@ fn check_if_let_chain() { .rinja_auto_escape()?), ) { (expr0, expr2) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; __rinja_writer.write_str(" ")?; - (&&rinja::filters::Writable(expr2)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr2)).rinja_write(__rinja_writer, __rinja_values)?; } } }"#, @@ -429,7 +433,7 @@ __rinja_writer.write_str("12")?; &((&&rinja::filters::AutoEscaper::new(&(self.x), rinja::filters::Text)).rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } } @@ -443,7 +447,7 @@ __rinja_writer.write_str("12")?; &((&&rinja::filters::AutoEscaper::new(&(self.x), rinja::filters::Text)).rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } ", @@ -474,7 +478,7 @@ if rinja::helpers::as_bool(&(self.y == 12)) { .rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } } else { @@ -496,7 +500,7 @@ match ( .rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } ", @@ -616,7 +620,7 @@ fn check_bool_conditions() { &((&&rinja::filters::AutoEscaper::new(&(self.x), rinja::filters::Text)).rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } ", @@ -634,7 +638,7 @@ fn check_bool_conditions() { .rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } } @@ -658,7 +662,7 @@ fn check_bool_conditions() { .rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } } @@ -678,7 +682,7 @@ fn check_bool_conditions() { .rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } ", @@ -699,7 +703,7 @@ if rinja::helpers::as_bool(&(self.y == 3)) .rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } } @@ -899,9 +903,9 @@ fn test_pluralize() { )?), ) { (expr0, expr3) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; __rinja_writer.write_str(" dog")?; - (&&rinja::filters::Writable(expr3)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr3)).rinja_write(__rinja_writer, __rinja_values)?; } }"#, &[("dogs", "i8")], @@ -923,9 +927,9 @@ fn test_pluralize() { )?), ) { (expr0, expr3) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; __rinja_writer.write_str(" dog")?; - (&&rinja::filters::Writable(expr3)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr3)).rinja_write(__rinja_writer, __rinja_values)?; } }"#, &[("dogs", "i8")], @@ -947,9 +951,9 @@ fn test_pluralize() { )?), ) { (expr0, expr2) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; __rinja_writer.write_str(" ")?; - (&&rinja::filters::Writable(expr2)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr2)).rinja_write(__rinja_writer, __rinja_values)?; } }"#, &[("dogs", "i8")], @@ -975,7 +979,7 @@ fn test_pluralize() { )?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } ", @@ -991,7 +995,7 @@ fn test_pluralize() { .rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } ", @@ -1006,7 +1010,7 @@ fn test_pluralize() { .rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } ", @@ -1019,7 +1023,7 @@ fn test_pluralize() { r#" match (&(rinja::filters::Safe("pl")),) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } "#, @@ -1031,7 +1035,7 @@ fn test_pluralize() { r#" match (&(rinja::filters::Safe("sg")),) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } "#, @@ -1044,7 +1048,7 @@ fn test_pluralize() { r#" match (&(rinja::filters::Safe("s")),) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } "#, @@ -1056,7 +1060,7 @@ fn test_pluralize() { r" match (&(rinja::helpers::Empty),) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } ", @@ -1078,9 +1082,9 @@ fn test_concat() { .rinja_auto_escape()?), ) { (expr1, expr3) => { - (&&rinja::filters::Writable(expr1)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr1)).rinja_write(__rinja_writer, __rinja_values)?; __rinja_writer.write_str("|")?; - (&&rinja::filters::Writable(expr3)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr3)).rinja_write(__rinja_writer, __rinja_values)?; } } __rinja_writer.write_str(">")?; @@ -1105,7 +1109,7 @@ fn test_concat() { .rinja_auto_escape()?), ) { (expr0,) => { - (&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer)?; + (&&&rinja::filters::Writable(expr0)).rinja_write(__rinja_writer, __rinja_values)?; } } "#, diff --git a/testing/tests/ui/values.rs b/testing/tests/ui/values.rs new file mode 100644 index 000000000..47915def8 --- /dev/null +++ b/testing/tests/ui/values.rs @@ -0,0 +1,45 @@ +use rinja::Template; + +#[derive(Template)] +#[template( + ext = "html", + source = r#"{% if let Ok(x) = "a"|value %}{% endif %}"#, +)] +struct A; + +#[derive(Template)] +#[template( + ext = "html", + source = r#"{% if let Ok(x) = "a"|value:: %}{% endif %}"#, +)] +struct B; + +#[derive(Template)] +#[template( + ext = "html", + source = r#"{% if let Ok(x) = rinja::get_value("a") %}{% endif %}"#, +)] +struct C; + +#[derive(Template)] +#[template( + ext = "html", + source = r#"{% if let Ok(x) = rinja::get_value::("a") %}{% endif %}"#, +)] +struct D; + +#[derive(Template)] +#[template( + ext = "html", + source = r#"{% if let Ok(x) = rinja::get_value::() %}{% endif %}"#, +)] +struct E; + +#[derive(Template)] +#[template( + ext = "html", + source = r#"{% if let Ok(x) = rinja::get_value::("a", "b") %}{% endif %}"#, +)] +struct F; + +fn main() {} diff --git a/testing/tests/ui/values.stderr b/testing/tests/ui/values.stderr new file mode 100644 index 000000000..0fad8154e --- /dev/null +++ b/testing/tests/ui/values.stderr @@ -0,0 +1,47 @@ +error: `value` filter expects one generic, found 0 + --> A.html:1:18 + "\"a\"|value %}{% endif %}" + --> tests/ui/values.rs:6:14 + | +6 | source = r#"{% if let Ok(x) = "a"|value %}{% endif %}"#, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: `value` filter expects one generic, found 2 + --> B.html:1:18 + "\"a\"|value:: %}{% endif %}" + --> tests/ui/values.rs:13:14 + | +13 | source = r#"{% if let Ok(x) = "a"|value:: %}{% endif %}"#, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: `get_value` function expects one generic, found 0 + --> C.html:1:18 + "rinja::get_value(\"a\") %}{% endif %}" + --> tests/ui/values.rs:20:14 + | +20 | source = r#"{% if let Ok(x) = rinja::get_value("a") %}{% endif %}"#, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: `get_value` function expects one generic, found 2 + --> D.html:1:18 + "rinja::get_value::(\"a\") %}{% endif %}" + --> tests/ui/values.rs:27:14 + | +27 | source = r#"{% if let Ok(x) = rinja::get_value::("a") %}{% endif %}"#, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: `get_value` function only takes one argument, found 0 + --> E.html:1:18 + "rinja::get_value::() %}{% endif %}" + --> tests/ui/values.rs:34:14 + | +34 | source = r#"{% if let Ok(x) = rinja::get_value::() %}{% endif %}"#, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: `get_value` function only takes one argument, found 2 + --> F.html:1:18 + "rinja::get_value::(\"a\", \"b\") %}{% endif %}" + --> tests/ui/values.rs:41:14 + | +41 | source = r#"{% if let Ok(x) = rinja::get_value::("a", "b") %}{% endif %}"#, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/testing/tests/values.rs b/testing/tests/values.rs new file mode 100644 index 000000000..844927419 --- /dev/null +++ b/testing/tests/values.rs @@ -0,0 +1,90 @@ +use std::any::Any; +use std::collections::HashMap; + +use rinja::Template; + +#[test] +fn test_values() { + #[derive(Template)] + #[template( + source = r#"{% if let Ok(bla) = "a" | value:: %}{{bla}}{% endif %}"#, + ext = "txt" + )] + struct V; + + let mut values: HashMap> = HashMap::default(); + assert_eq!(V.render_with_values(&values).unwrap(), ""); + values.insert("a".to_string(), Box::new(12u32)); + assert_eq!(V.render_with_values(&values).unwrap(), "12"); + values.insert("a".to_string(), Box::new(false)); + assert_eq!(V.render_with_values(&values).unwrap(), ""); +} + +#[test] +fn test_values2() { + #[derive(Template)] + #[template( + source = r#" + {%- if let Ok(bla) = "a" | value::<&str> %}{{bla}}{% endif -%} + {%- if let Ok(bla) = "b" | value:: %} {{bla}}{% endif -%} + "#, + ext = "txt" + )] + struct V; + + let mut values: HashMap> = HashMap::default(); + assert_eq!(V.render_with_values(&values).unwrap(), ""); + values.insert("a".to_string(), Box::new("hey")); + assert_eq!(V.render_with_values(&values).unwrap(), "hey"); + values.insert("b".to_string(), Box::new(false)); + assert_eq!(V.render_with_values(&values).unwrap(), "hey false"); + values.remove("a"); + assert_eq!(V.render_with_values(&values).unwrap(), " false"); +} + +#[test] +fn test_values3() { + #[derive(Template)] + #[template( + source = r#" + {%- match "data" | value::<&str> -%} + {%- when Ok(data) -%} ok={{ data }} + {%- when Err(err) -%} err={{ err }} + {%- endmatch -%} + "#, + ext = "txt" + )] + struct V; + + let mut values: HashMap> = HashMap::default(); + assert_eq!( + V.render_with_values(&values).unwrap(), + "err=key missing in values" + ); + values.insert("data".to_string(), Box::new(false)); + assert_eq!( + V.render_with_values(&values).unwrap(), + "err=value has wrong type" + ); + values.insert("data".to_string(), Box::new("hey")); + assert_eq!(V.render_with_values(&values).unwrap(), "ok=hey"); + values.insert("data".to_string(), Box::new(Box::new("hey"))); + assert_eq!(V.render_with_values(&values).unwrap(), "ok=hey"); +} + +#[test] +fn test_value_function_getter() { + #[derive(Template)] + #[template( + source = r#"{% if let Ok(bla) = rinja::get_value::("a") %}{{bla}}{% endif %}"#, + ext = "txt" + )] + struct V; + + let mut values: HashMap> = HashMap::default(); + assert_eq!(V.render_with_values(&values).unwrap(), ""); + values.insert("a".to_string(), Box::new(12u32)); + assert_eq!(V.render_with_values(&values).unwrap(), "12"); + values.insert("a".to_string(), Box::new(false)); + assert_eq!(V.render_with_values(&values).unwrap(), ""); +} From 5257838127f581931bd074a26d9ecd0ed20db6ae Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 23 Jan 2025 15:24:07 +0100 Subject: [PATCH 2/2] Add new chapter for runtime values --- book/src/SUMMARY.md | 1 + book/src/runtime.md | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 book/src/runtime.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index f7da11eea..68a2041fc 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -2,6 +2,7 @@ - [Introduction](./introduction.md) - [Creating templates](./creating_templates.md) +- [Runtime values](./runtime.md) - [Debugging](./debugging.md) - [Configuration](./configuration.md) - [Template syntax](./template_syntax.md) diff --git a/book/src/runtime.md b/book/src/runtime.md new file mode 100644 index 000000000..4eb1544f5 --- /dev/null +++ b/book/src/runtime.md @@ -0,0 +1,38 @@ +# Runtime values + +It is possible to define variables at runtime and to use them in the templates using the `values` +filter or the `rinja::get_value` function and to call the `_with_values` variants of the `render` +methods. It expects an extra argument implementing the `Values` trait. This trait is implemented on +a few types provided by the `std`, like `HashMap`: + +```rust +use std::collections::HashMap; + +let mut values: HashMap<&str, Box> = HashMap::new(); +// We add a new value named "name" with the value "Bibop". +values.insert("name", Box::new("Bibop")); +values.insert("age", Box::new(12u32)); +``` + +The `Values` trait is expecting types storing data with the `Any` trait, allowing to store any type. + +Then to render with these values: + +```rust +template_struct.render_with_values(&values).unwrap(); +``` + +There are two ways to get the values from the template, either by using the `value` filter +or by calling directly the `rinja::get_value` function: + +```jinja +{% if let Ok(name) = "name"|value::<&str> %} + name is {{ name }} +{% endif %} +{% if let Ok(age) = rinja::get_value::("age") %} + age is {{ age }} +{% endif %} +``` + +If you try to retrieve a value with the wrong type or that you didn't set, you will get an +`Err(ValueError)`.