Skip to content

Commit

Permalink
Merge pull request #311 from GuillaumeGomez/runtime-values
Browse files Browse the repository at this point in the history
Add support for "runtime" values
  • Loading branch information
GuillaumeGomez authored Jan 23, 2025
2 parents 8253624 + 5257838 commit cae09c5
Show file tree
Hide file tree
Showing 15 changed files with 671 additions and 60 deletions.
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
38 changes: 38 additions & 0 deletions book/src/runtime.md
Original file line number Diff line number Diff line change
@@ -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<dyn Any>> = 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::<u32>("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)`.
1 change: 1 addition & 0 deletions rinja/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
10 changes: 10 additions & 0 deletions rinja/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ pub type Result<I, E = Error> = core::result::Result<I, E>;
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<dyn StdError + Send + Sync>),
Expand All @@ -41,6 +45,8 @@ impl Error {
pub fn into_box(self) -> Box<dyn StdError + Send + Sync> {
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(),
Expand All @@ -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")]
Expand All @@ -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")]
Expand Down
35 changes: 30 additions & 5 deletions rinja/src/filters/escape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()>;
fn rinja_write<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()>;
}

/// Used internally by rinja to speed up writing some types.
Expand Down Expand Up @@ -604,16 +610,35 @@ const _: () = {
}
}

impl<S: FastWritable + ?Sized> WriteWritable for &Writable<'_, S> {
impl<S: crate::Template + ?Sized> WriteWritable for &Writable<'_, S> {
#[inline]
fn rinja_write<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
self.0.render_into_with_values(dest, values)
}
}

impl<S: FastWritable + ?Sized> WriteWritable for &&Writable<'_, S> {
#[inline]
fn rinja_write<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
fn rinja_write<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
_: &dyn Values,
) -> crate::Result<()> {
self.0.write_into(dest)
}
}

impl<S: fmt::Display + ?Sized> WriteWritable for &&Writable<'_, S> {
impl<S: fmt::Display + ?Sized> WriteWritable for &&&Writable<'_, S> {
#[inline]
fn rinja_write<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
fn rinja_write<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
_: &dyn Values,
) -> crate::Result<()> {
Ok(write!(dest, "{}", self.0)?)
}
}
Expand Down
1 change: 1 addition & 0 deletions rinja/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<I>
where
Expand Down
131 changes: 116 additions & 15 deletions rinja/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub mod filters;
#[doc(hidden)]
pub mod helpers;
mod html;
mod values;

#[cfg(feature = "alloc")]
use alloc::string::String;
Expand All @@ -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
///
Expand All @@ -106,21 +108,50 @@ pub use crate::helpers::PrimitiveType;
///
/// [dynamic methods calls]: <https://doc.rust-lang.org/stable/std/keyword.dyn.html>
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<String> {
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<String> {
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<W: fmt::Write + ?Sized>(&self, writer: &mut W) -> Result<()>;
/// Renders the template to the given `writer` fmt buffer.
#[inline]
fn render_into<W: fmt::Write + ?Sized>(&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<W: fmt::Write + ?Sized>(
&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<W: io::Write + ?Sized>(&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<W: io::Write + ?Sized>(
&self,
writer: &mut W,
values: &dyn Values,
) -> io::Result<()> {
struct Wrapped<W: io::Write> {
writer: W,
err: Option<io::Error>,
Expand All @@ -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();
Expand All @@ -160,15 +191,30 @@ pub trait Template: fmt::Display + filters::FastWritable {
}

impl<T: Template + ?Sized> Template for &T {
#[inline]
#[cfg(feature = "alloc")]
fn render(&self) -> Result<String> {
<T as Template>::render(self)
}

#[inline]
#[cfg(feature = "alloc")]
fn render_with_values(&self, values: &dyn Values) -> Result<String> {
<T as Template>::render_with_values(self, values)
}

#[inline]
fn render_into<W: fmt::Write + ?Sized>(&self, writer: &mut W) -> Result<()> {
<T as Template>::render_into(self, writer)
}

#[inline]
#[cfg(feature = "alloc")]
fn render(&self) -> Result<String> {
<T as Template>::render(self)
fn render_into_with_values<W: fmt::Write + ?Sized>(
&self,
writer: &mut W,
values: &dyn Values,
) -> Result<()> {
<T as Template>::render_into_with_values(self, writer, values)
}

#[inline]
Expand All @@ -177,6 +223,16 @@ impl<T: Template + ?Sized> Template for &T {
<T as Template>::write_into(self, writer)
}

#[inline]
#[cfg(feature = "std")]
fn write_into_with_values<W: io::Write + ?Sized>(
&self,
writer: &mut W,
values: &dyn Values,
) -> io::Result<()> {
<T as Template>::write_into_with_values(self, writer, values)
}

const SIZE_HINT: usize = T::SIZE_HINT;
}

Expand All @@ -186,18 +242,37 @@ impl<T: Template + ?Sized> 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<String>;

/// 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<String>;

/// 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;
}

Expand All @@ -208,17 +283,39 @@ impl<T: Template> DynTemplate for T {
<Self as Template>::render(self)
}

#[cfg(feature = "alloc")]
fn dyn_render_with_values(&self, values: &dyn Values) -> Result<String> {
<Self as Template>::render_with_values(self, values)
}

#[inline]
fn dyn_render_into(&self, writer: &mut dyn fmt::Write) -> Result<()> {
<Self as Template>::render_into(self, writer)
}

#[inline]
fn dyn_render_into_with_values(
&self,
writer: &mut dyn fmt::Write,
values: &dyn Values,
) -> Result<()> {
<Self as Template>::render_into_with_values(self, writer, values)
}

#[cfg(feature = "std")]
fn dyn_write_into(&self, writer: &mut dyn io::Write) -> io::Result<()> {
<Self as Template>::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<()> {
<Self as Template>::write_into_with_values(self, writer, values)
}

#[inline]
fn size_hint(&self) -> usize {
<Self as Template>::SIZE_HINT
Expand Down Expand Up @@ -287,7 +384,11 @@ mod tests {
struct Test;

impl Template for Test {
fn render_into<W: fmt::Write + ?Sized>(&self, writer: &mut W) -> Result<()> {
fn render_into_with_values<W: fmt::Write + ?Sized>(
&self,
writer: &mut W,
_values: &dyn Values,
) -> Result<()> {
Ok(writer.write_str("test")?)
}

Expand Down
Loading

0 comments on commit cae09c5

Please sign in to comment.