Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Make Context a user-implementable Trait #829

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
54 changes: 54 additions & 0 deletions examples/dynamic/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use serde_json::Value;
use std::{borrow::Cow, env};
use tera::{Context, ContextProvider, Tera};

#[derive(Clone)]
struct MyContext {
upper_layer: Context, // for overrides
}

impl MyContext {
pub fn new() -> Self {
Self { upper_layer: Context::new() }
}
}

impl ContextProvider for MyContext {
fn try_insert<T: serde::Serialize + ?Sized, S: Into<String>>(
&mut self,
key: S,
val: &T,
) -> tera::Result<()> {
self.upper_layer.try_insert(key, val)
}

fn find_value(&self, key: &str) -> Option<Cow<Value>> {
if let Some(val) = self.upper_layer.find_value(key) {
return Some(val);
}

env::var(key.to_uppercase()).map(Value::String).map(Cow::Owned).ok()
}

fn find_value_by_dotted_pointer(&self, pointer: &str) -> Option<Cow<Value>> {
env::var(pointer.to_uppercase().replace('.', "_"))
.map(Value::String)
.map(Cow::Owned)
.ok()
.or_else(|| self.upper_layer.find_value_by_dotted_pointer(pointer))
}

fn into_json(self) -> Value {
let Value::Object(map) = self.upper_layer.into_json() else { unreachable!() };
Value::Object(map)
}
}

fn main() {
env::set_var("SETTINGS_FOO", "bar");
let ctx = MyContext::new();

let output = Tera::one_off("Hello {{ user }}! foo={{ settings.foo }}", &ctx, false).unwrap();

println!("{output}");
}
1 change: 1 addition & 0 deletions src/builtins/filters/array.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
/// Filters operating on array
use std::collections::HashMap;

Expand Down
55 changes: 54 additions & 1 deletion src/context.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,63 @@
use std::collections::BTreeMap;
use std::io::Write;
use std::{borrow::Cow, collections::BTreeMap};

use serde::ser::Serialize;
use serde_json::value::{to_value, Map, Value};

use crate::errors::{Error, Result as TeraResult};
use crate::renderer::stack_frame::value_by_pointer;

/// The interface trait of a Context towards Tera
///
/// Implement this if you want to provide your own custom context implementation.
pub trait ContextProvider {
/// Converts the `val` parameter to `Value` and insert it into the context.
///
/// Panics if the serialization fails.
fn insert<T: Serialize + ?Sized, S: Into<String>>(&mut self, key: S, val: &T) {
self.try_insert(key, val).unwrap();
}

/// Converts the `val` parameter to `Value` and insert it into the context.
///
/// Returns an error if the serialization fails.
fn try_insert<T: Serialize + ?Sized, S: Into<String>>(
&mut self,
key: S,
val: &T,
) -> TeraResult<()>;

/// Return a value for a given key.
fn find_value(&self, key: &str) -> Option<Cow<Value>>;

/// Return a value given a dotted pointer path.
fn find_value_by_dotted_pointer(&self, pointer: &str) -> Option<Cow<Value>> {
let root = pointer.split('.').next().unwrap().replace("~1", "/").replace("~0", "~");
let rest = &pointer[root.len() + 1..];
self.find_value(&root).and_then(|val| value_by_pointer(rest, &val))
}

/// Convert the context into JSON.
fn into_json(self) -> Value;
}

impl ContextProvider for Context {
fn try_insert<T: Serialize + ?Sized, S: Into<String>>(
&mut self,
key: S,
val: &T,
) -> TeraResult<()> {
self.try_insert(key, val)
}

fn find_value(&self, key: &str) -> Option<Cow<Value>> {
self.get(key).map(Cow::Borrowed)
}

fn into_json(self) -> Value {
self.into_json()
}
}

/// The struct that holds the context of a template rendering.
///
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ mod utils;
pub use crate::builtins::filters::Filter;
pub use crate::builtins::functions::Function;
pub use crate::builtins::testers::Test;
pub use crate::context::Context;
pub use crate::context::{Context, ContextProvider};
pub use crate::errors::{Error, ErrorKind, Result};
// Template, dotted_pointer and get_json_pointer are meant to be used internally only but is exported for test/bench.
#[doc(hidden)]
Expand Down
49 changes: 12 additions & 37 deletions src/renderer/call_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,28 @@ use std::collections::HashMap;

use serde_json::{to_value, Value};

use crate::context::dotted_pointer;
use crate::context::ContextProvider;
use crate::errors::{Error, Result};
use crate::renderer::for_loop::{ForLoop, ForLoopState};
use crate::renderer::stack_frame::{FrameContext, FrameType, StackFrame, Val};
use crate::template::Template;
use crate::Context;

/// Contains the user data and allows no mutation
#[derive(Debug)]
pub struct UserContext<'a> {
/// Read-only context
inner: &'a Context,
}

impl<'a> UserContext<'a> {
/// Create an immutable user context to be used in the call stack
pub fn new(context: &'a Context) -> Self {
UserContext { inner: context }
}

pub fn find_value(&self, key: &str) -> Option<&'a Value> {
self.inner.get(key)
}

pub fn find_value_by_dotted_pointer(&self, pointer: &str) -> Option<&'a Value> {
let root = pointer.split('.').next().unwrap().replace("~1", "/").replace("~0", "~");
let rest = &pointer[root.len() + 1..];
self.inner.get(&root).and_then(|val| dotted_pointer(val, rest))
}
}

/// Contains the stack of frames
#[derive(Debug)]
pub struct CallStack<'a> {
pub struct CallStack<'a, C: ContextProvider + Clone> {
/// The stack of frames
stack: Vec<StackFrame<'a>>,
/// User supplied context for the render
context: UserContext<'a>,
context: &'a C,
}

impl<'a> CallStack<'a> {
impl<'a, C> CallStack<'a, C>
where
C: ContextProvider + Clone,
{
/// Create the initial call stack
pub fn new(context: &'a Context, template: &'a Template) -> CallStack<'a> {
CallStack {
stack: vec![StackFrame::new(FrameType::Origin, "ORIGIN", template)],
context: UserContext::new(context),
}
pub fn new(context: &'a C, template: &'a Template) -> CallStack<'a, C> {
CallStack { stack: vec![StackFrame::new(FrameType::Origin, "ORIGIN", template)], context }
}

pub fn push_for_loop_frame(&mut self, name: &'a str, for_loop: ForLoop<'a>) {
Expand Down Expand Up @@ -119,9 +94,9 @@ impl<'a> CallStack<'a> {

// Not in stack frame, look in user supplied context
if key.contains('.') {
return self.context.find_value_by_dotted_pointer(key).map(Cow::Borrowed);
return self.context.find_value_by_dotted_pointer(key);
} else if let Some(value) = self.context.find_value(key) {
return Some(Cow::Borrowed(value));
return Some(value);
}

None
Expand Down Expand Up @@ -221,7 +196,7 @@ impl<'a> CallStack<'a> {
// If we are here we take the user context
// and add the values found in the stack to it.
// We do it this way as we can override global variable temporarily in forloops
let mut new_ctx = self.context.inner.clone();
let mut new_ctx = self.context.clone();
for (key, val) in context {
new_ctx.insert(key, &val)
}
Expand Down
12 changes: 6 additions & 6 deletions src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod call_stack;
mod for_loop;
mod macros;
mod processor;
mod stack_frame;
pub(crate) mod stack_frame;

use std::io::Write;

Expand All @@ -15,25 +15,25 @@ use crate::errors::Result;
use crate::template::Template;
use crate::tera::Tera;
use crate::utils::buffer_to_string;
use crate::Context;
use crate::ContextProvider;

/// Given a `Tera` and reference to `Template` and a `Context`, renders text
#[derive(Debug)]
pub struct Renderer<'a> {
pub struct Renderer<'a, C: ContextProvider + Clone> {
/// Template to render
template: &'a Template,
/// Houses other templates, filters, global functions, etc
tera: &'a Tera,
/// Read-only context to be bound to template˝
context: &'a Context,
context: &'a C,
/// If set rendering should be escaped
should_escape: bool,
}

impl<'a> Renderer<'a> {
impl<'a, C: ContextProvider + Clone> Renderer<'a, C> {
/// Create a new `Renderer`
#[inline]
pub fn new(template: &'a Template, tera: &'a Tera, context: &'a Context) -> Renderer<'a> {
pub fn new(template: &'a Template, tera: &'a Tera, context: &'a C) -> Renderer<'a, C> {
let should_escape = tera.autoescape_suffixes.iter().any(|ext| {
// We prefer a `path` if set, otherwise use the `name`
if let Some(ref p) = template.path {
Expand Down
21 changes: 13 additions & 8 deletions src/renderer/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::io::Write;

use serde_json::{to_string_pretty, to_value, Number, Value};

use crate::context::{ValueRender, ValueTruthy};
use crate::context::{ContextProvider, ValueRender, ValueTruthy};
use crate::errors::{Error, Result};
use crate::parser::ast::*;
use crate::renderer::call_stack::CallStack;
Expand All @@ -15,14 +15,16 @@ use crate::renderer::stack_frame::{FrameContext, FrameType, Val};
use crate::template::Template;
use crate::tera::Tera;
use crate::utils::render_to_string;
use crate::Context;

/// Special string indicating request to dump context
static MAGICAL_DUMP_VAR: &str = "__tera_context";

/// This will convert a Tera variable to a json pointer if it is possible by replacing
/// the index with their evaluated stringified value
fn evaluate_sub_variables(key: &str, call_stack: &CallStack) -> Result<String> {
fn evaluate_sub_variables<C: ContextProvider + Clone>(
key: &str,
call_stack: &CallStack<C>,
) -> Result<String> {
let sub_vars_to_calc = pull_out_square_bracket(key);
let mut new_key = key.to_string();

Expand Down Expand Up @@ -71,7 +73,10 @@ fn evaluate_sub_variables(key: &str, call_stack: &CallStack) -> Result<String> {
.replace(']', ""))
}

fn process_path<'a>(path: &str, call_stack: &CallStack<'a>) -> Result<Val<'a>> {
fn process_path<'a, C>(path: &str, call_stack: &CallStack<'a, C>) -> Result<Val<'a>>
where
C: ContextProvider + Clone,
{
if !path.contains('[') {
match call_stack.lookup(path) {
Some(v) => Ok(v),
Expand All @@ -98,7 +103,7 @@ fn process_path<'a>(path: &str, call_stack: &CallStack<'a>) -> Result<Val<'a>> {
}

/// Processes the ast and renders the output
pub struct Processor<'a> {
pub struct Processor<'a, C: ContextProvider + Clone> {
/// The template we're trying to render
template: &'a Template,
/// Root template of template to render - contains ast to use for rendering
Expand All @@ -107,7 +112,7 @@ pub struct Processor<'a> {
/// The Tera object with template details
tera: &'a Tera,
/// The call stack for processing
call_stack: CallStack<'a>,
call_stack: CallStack<'a, C>,
/// The macros organised by template and namespaces
macros: MacroCollection<'a>,
/// If set, rendering should be escaped
Expand All @@ -118,12 +123,12 @@ pub struct Processor<'a> {
blocks: Vec<(&'a str, &'a str, usize)>,
}

impl<'a> Processor<'a> {
impl<'a, C: ContextProvider + Clone> Processor<'a, C> {
/// Create a new `Processor` that will do the rendering
pub fn new(
template: &'a Template,
tera: &'a Tera,
context: &'a Context,
context: &'a C,
should_escape: bool,
) -> Self {
// Gets the root template if we are rendering something with inheritance or just return
Expand Down
24 changes: 18 additions & 6 deletions src/tera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use globwalk::glob_builder;
use crate::builtins::filters::{array, common, number, object, string, Filter};
use crate::builtins::functions::{self, Function};
use crate::builtins::testers::{self, Test};
use crate::context::Context;
use crate::context::{Context, ContextProvider};
use crate::errors::{Error, Result};
use crate::renderer::Renderer;
use crate::template::Template;
Expand Down Expand Up @@ -386,7 +386,11 @@ impl Tera {
/// let output = tera.render("hello.html", &Context::new()).unwrap();
/// assert_eq!(output, "<h1>Hello</h1>");
/// ```
pub fn render(&self, template_name: &str, context: &Context) -> Result<String> {
pub fn render<C: ContextProvider + Clone>(
&self,
template_name: &str,
context: &C,
) -> Result<String> {
let template = self.get_template(template_name)?;
let renderer = Renderer::new(template, self, context);
renderer.render()
Expand Down Expand Up @@ -416,10 +420,10 @@ impl Tera {
/// tera.render_to("index.html", &context, &mut buffer).unwrap();
/// assert_eq!(buffer, b"<p>John Wick</p>");
/// ```
pub fn render_to(
pub fn render_to<C: ContextProvider + Clone>(
&self,
template_name: &str,
context: &Context,
context: &C,
write: impl Write,
) -> Result<()> {
let template = self.get_template(template_name)?;
Expand All @@ -440,7 +444,11 @@ impl Tera {
/// let string = tera.render_str("{{ greeting }} World!", &context)?;
/// assert_eq!(string, "Hello World!");
/// ```
pub fn render_str(&mut self, input: &str, context: &Context) -> Result<String> {
pub fn render_str<C: ContextProvider + Clone>(
&mut self,
input: &str,
context: &C,
) -> Result<String> {
self.add_raw_template(ONE_OFF_TEMPLATE_NAME, input)?;
let result = self.render(ONE_OFF_TEMPLATE_NAME, context);
self.templates.remove(ONE_OFF_TEMPLATE_NAME);
Expand All @@ -459,7 +467,11 @@ impl Tera {
/// context.insert("greeting", &"hello");
/// Tera::one_off("{{ greeting }} world", &context, true);
/// ```
pub fn one_off(input: &str, context: &Context, autoescape: bool) -> Result<String> {
pub fn one_off<C: ContextProvider + Clone>(
input: &str,
context: &C,
autoescape: bool,
) -> Result<String> {
let mut tera = Tera::default();

if autoescape {
Expand Down