Repository: Keats/tera
Pull request #65 (2016-10-09 01:06:05)

Expose renderer directly to allow non-Context JSON object values.
It would be nice to render JSON values directly without first converting into a `Context` object. This is the way other Rust templating libraries work, and I think it makes the most sense. One way to do this is to export the Renderer type so that someone can call `Renderer::new_with_json` and then `render` directly. This is what this PR contributes.

Keats: I'm not convinced about the `Renderer::new_with_json` as that allows invalid calls, `render_with_json(..,..,to_value(&1)` would be valid. An alternative would be a `render` method that takes `BTreeMap<String, Value>` as arg but i'm not sure it's valuable at all.

Another thing against the `render_with_json` is that json is a temporary format for Tera, hopefully we will be able to move to something else (#45)

Also if it exists it should be on the `Tera` instance like the `render`
SergioBenitez: Restricting rendering to `Context` (or any non-polymorphic type, to be clear) is a considerable usability problem. Imagine a library that wants to expose an interface to Tera. With the current design, and the one you've proposed, the library is shoehorned into making one of two choices: 1) it must copy everything into a `Context`, or 2) it has to re-export `Context` and force their users into using that type.

What's the issue with allowing something that's not a `Value::Object` to be passed in? If that happens, we should simply return an error to the user. That is, `new_with_json ` should return a `Result` object if the Value isn't an object. I didn't do that here because later code (currently, though you've removed it in a PR) checks if the value is indeed a `Value::Object`.

Alternatively, you can define your own serializer and deserializer for Serde and ship that along with Tera. But this is functionally identical to what I've proposed, except it moves the `Result` up a level to the serialization step.
Keats: The last paragraph is the optimal path as it allows more information than what JSON offers (dates etc) but it is not an immediate goal.

You can change the type of the `Renderer` constructor to take `context` as a value and call `to_json` in `Tera::render`. This way `Tera::render_with_json` is easier as it only needs to check if the `Value` is of type `Value::Object` and then only calls the renderer with correct input.

SergioBenitez: Sounds good to me. Added `Tera::render_value`, which I thought sounded a bit better than `Tera::render_with_json`.
SergioBenitez: Actually, that sounds like it's rendering the value itself, so I changed it to `value_render`.
Keats: Only missing integrations tests for render failures and render ok  for the new `render` method + some docs on README and it's ready to go

Patch:
diff --git a/README.md b/README.md
index 9641177..f0f9863 100644
--- a/README.md
+++ b/README.md
@@ -200,6 +200,13 @@ Again, straight from jinja2 docs:
 
 When trying to render that template, Tera will see that it depends on a parent template and will render it first, filling the blocks as it encounters them in the base template.
 
+#### Include
+You can include a template to be rendered using the current context with the `include` tag.
+
+```jinja
+{% include "included.html" %}
+```
+
 ### Tests
 
 Tests can be used against a variable to check some condition on the variable.
diff --git a/src/errors.rs b/src/errors.rs
index 0a4039b..eb2fd23 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -23,6 +23,10 @@ quick_error! {
             display("Template `{}` wasn't found", name)
             description("template not found")
         }
+        InvalidValue(name: String) {
+            display("Expected the value to be an Object while rendering `{}`.", name)
+            description("invalid value")
+        }
         FilterNotFound(name: String) {
             display("Filter `{}` was not found in the context.", name)
             description("filter not found")
diff --git a/src/lib.rs b/src/lib.rs
index 0caef02..4df7b08 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,7 +3,6 @@
 // Needed by pest
 #![recursion_limit = "200"]
 
-
 #![cfg_attr(feature = "dev", feature(plugin))]
 #![cfg_attr(feature = "dev", plugin(clippy))]
 #![cfg_attr(feature = "dev", allow(block_in_if_condition_stmt, linkedlist))]
@@ -33,10 +32,10 @@ mod filters;
 mod testers;
 
 
-// Library exports
-// Template is not meant to be used in your code, only there for bench/test of
-// tera itself
-pub use template::Template;
+// Library exports.
+
+// Template is meant to be used internally only but is exported for test/bench.
+#[doc(hidden)] pub use template::Template;
 pub use context::Context;
 pub use tera::Tera;
 pub use errors::{TeraResult, TeraError};
diff --git a/src/parser.rs b/src/parser.rs
index 9853ad4..5963177 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -35,6 +35,7 @@ pub enum Node {
     Raw(String),
     Extends(String),
     VariableBlock(Box<Node>),
+    Include(String),
 }
 
 impl Node {
@@ -135,6 +136,7 @@ impl_rdp! {
         block_start    = _{ variable_start | tag_start | comment_start }
 
         // Actual tags
+        include_tag     = !@{ tag_start ~ ["include"] ~ string ~ tag_end }
         extends_tag     = !@{ tag_start ~ ["extends"] ~ string ~ tag_end }
         variable_tag    = !@{ variable_start ~ expression ~ variable_end }
         comment_tag     = !@{ comment_start ~ (!comment_end ~ any )* ~ comment_end }
@@ -154,6 +156,7 @@ impl_rdp! {
         text       = { (!(block_start) ~ any )+ }
 
         content = @{
+            include_tag |
             variable_tag |
             comment_tag |
             block_tag ~ content* ~ endblock_tag |
@@ -204,6 +207,9 @@ impl_rdp! {
             (&head: text) => {
                 Ok(Some(Node::Text(head.to_string())))
             },
+            (_: include_tag, &name: string) => {
+                Ok(Some(Node::Include(name.replace("\"", "").to_string())))
+            },
             (_: variable_tag, exp: _expression()) => {
                 Ok(Some(Node::VariableBlock(Box::new(try!(exp)))))
             },
@@ -622,6 +628,13 @@ mod tests {
         assert!(parser.end());
     }
 
+    #[test]
+    fn test_include_tag() {
+        let mut parser = Rdp::new(StringInput::new("{% include \"component.html\" %}"));
+        assert!(parser.include_tag());
+        assert!(parser.end());
+    }
+
     #[test]
     fn test_comment_tag() {
         let mut parser = Rdp::new(StringInput::new("{# some text {{}} #}"));
diff --git a/src/render.rs b/src/render.rs
index aeb9167..51f30a3 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -2,7 +2,7 @@ use std::collections::LinkedList;
 
 use serde_json::value::{Value, to_value};
 
-use context::{Context, ValueRender, ValueNumber, ValueTruthy, get_json_pointer};
+use context::{ValueRender, ValueNumber, ValueTruthy, get_json_pointer};
 use template::Template;
 use errors::TeraResult;
 use errors::TeraError::*;
@@ -56,11 +56,11 @@ pub struct Renderer<'a> {
 }
 
 impl<'a> Renderer<'a> {
-    pub fn new(tpl: &'a Template, tera: &'a Tera, context: Context) -> Renderer<'a> {
+    pub fn new(tpl: &'a Template, tera: &'a Tera, context: Value) -> Renderer<'a> {
         Renderer {
             template: tpl,
             tera: tera,
-            context: context.as_json(),
+            context: context,
             for_loops: vec![],
         }
     }
@@ -346,6 +346,15 @@ impl<'a> Renderer<'a> {
 
     pub fn render_node(&mut self, node: Node) -> TeraResult<String> {
         match node {
+            Include(p) => {
+                let ast = try!(self.tera.get_template(&p)).ast.get_children();
+                let mut output = String::new();
+                for node in ast {
+                    output.push_str(&try!(self.render_node(node)));
+                }
+
+                Ok(output)
+            },
             Text(s) => Ok(s),
             Raw(s) => Ok(s.trim().to_string()),
             VariableBlock(exp) => self.render_variable_block(*exp),
@@ -412,6 +421,15 @@ mod tests {
         tera.render("hello", context)
     }
 
+    #[test]
+    fn test_render_include() {
+        let mut tera = Tera::default();
+        tera.add_template("world", "world");
+        tera.add_template("hello", "<h1>Hello {% include \"world\" %}</h1>");
+        let result = tera.render("hello", Context::new());
+        assert_eq!(result.unwrap(), "<h1>Hello world</h1>".to_owned());
+    }
+
     #[test]
     fn test_render_simple_string() {
         let result = render_template("<h1>Hello world</h1>", Context::new());
diff --git a/src/tera.rs b/src/tera.rs
index 437f559..97a33d3 100644
--- a/src/tera.rs
+++ b/src/tera.rs
@@ -4,6 +4,8 @@ use std::fs::File;
 use std::fmt;
 
 use glob::glob;
+use serde::Serialize;
+use serde_json::value::to_value;
 
 use template::Template;
 use filters::{FilterFn, string, array, common};
@@ -57,13 +59,28 @@ impl Tera {
         tera
     }
 
+    /// Renders a Tera template given a `Context`.
     pub fn render(&self, template_name: &str, data: Context) -> TeraResult<String> {
         let template = try!(self.get_template(template_name));
-        let mut renderer = Renderer::new(template, self, data);
+        let mut renderer = Renderer::new(template, self, data.as_json());
 
         renderer.render()
     }
 
+    /// Renders a Tera template given a `Serializeable` object.
+    pub fn value_render<T>(&self, template_name: &str, data: &T) -> TeraResult<String>
+        where T: Serialize
+    {
+        let value = to_value(data);
+        if !value.is_object() {
+            return Err(TeraError::InvalidValue(template_name.to_string()))
+        }
+
+        let template = try!(self.get_template(template_name));
+        let mut renderer = Renderer::new(template, self, value);
+        renderer.render()
+    }
+
     pub fn get_template(&self, template_name: &str) -> TeraResult<&Template> {
         match self.templates.get(template_name) {
             Some(tmpl) => Ok(tmpl),


Test patch:
diff --git a/tests/expected/include.html b/tests/expected/include.html
new file mode 100755
index 0000000..5ae6416
--- /dev/null
+++ b/tests/expected/include.html
@@ -0,0 +1,17 @@
+<html>
+  <head>
+    <title>Products</title>
+  </head>
+  <body>
+    <div>
+      <table>
+        <thead>
+          <th>User</th><th>Number of reviews</th>
+        </thead>
+        <tr>
+          <td>bob</td><td>2</td>
+        </tr>
+      </table>
+    </div>
+  </body>
+</html>
diff --git a/tests/render-failures/inexisting_include.html b/tests/render-failures/inexisting_include.html
new file mode 100755
index 0000000..bbbb8bd
--- /dev/null
+++ b/tests/render-failures/inexisting_include.html
@@ -0,0 +1,1 @@
+{% include "hola" %}
diff --git a/tests/render_fails.rs b/tests/render_fails.rs
index 54d1959..cb15c97 100644
--- a/tests/render_fails.rs
+++ b/tests/render_fails.rs
@@ -59,3 +59,11 @@ fn test_error_render_iterate_non_array() {
     assert_eq!(result.is_err(), true);
     assert_eq!(result.unwrap_err().description(), "field is not an array".to_string());
 }
+
+#[test]
+fn test_error_render_include_inexistent() {
+    let result = render_tpl("inexisting_include.html");
+
+    assert_eq!(result.is_err(), true);
+    assert_eq!(result.unwrap_err().description(), "template not found".to_string());
+}
diff --git a/tests/render_ok.rs b/tests/render_ok.rs
index f2bb144..3d46bdd 100644
--- a/tests/render_ok.rs
+++ b/tests/render_ok.rs
@@ -109,3 +109,8 @@ fn test_ok_variable_tests() {
 fn test_ok_indexing() {
     assert_template_ok("tests/templates/indexing.html", None);
 }
+
+#[test]
+fn test_ok_include_template() {
+    assert_template_ok("tests/templates/include.html", Some("tests/templates/included.html"));
+}
diff --git a/tests/templates/include.html b/tests/templates/include.html
new file mode 100755
index 0000000..bb1e246
--- /dev/null
+++ b/tests/templates/include.html
@@ -0,0 +1,10 @@
+<html>
+  <head>
+    <title>Products</title>
+  </head>
+  <body>
+    <div>
+      {% include "included.html" %}
+    </div>
+  </body>
+</html>
diff --git a/tests/templates/included.html b/tests/templates/included.html
new file mode 100755
index 0000000..064cf02
--- /dev/null
+++ b/tests/templates/included.html
@@ -0,0 +1,8 @@
+<table>
+        <thead>
+          <th>User</th><th>Number of reviews</th>
+        </thead>
+        <tr>
+          <td>{{ username }}</td><td>{{ number_reviews }}</td>
+        </tr>
+      </table>
\ No newline at end of file