Merge branch 'walk_up_context' into render

This commit is contained in:
Tom Alexander 2020-05-05 20:47:58 -04:00
commit 35f1ba8447
Signed by: talexander
GPG Key ID: D3A179C9A53C0EDE
7 changed files with 313 additions and 149 deletions

View File

@ -0,0 +1,90 @@
Through experimentation it seems that you can walk up to access higher levels in the context. Interestingly enough, it seems that walking up to a higher context does not unwind the context stack but instead seems to add the higher level context element to the bottom. For example:
```js
{
"foo": {
"f1": "f",
"f2": "ff"
},
"bar": {
"b1": "b",
"b2": "bb"
}
}
```
if we walk down into bar and then into foo then our variable look ups appear to follow this pattern:
```
(attempts to read from the context in-order starting with the first line)
Starting access context:
{"foo":{"f1":"f","f2":"ff"},"bar":{"b1":"b","b2":"bb"}}
After walk "bar":
{"b1":"b","b2":"bb"}
{"foo":{"f1":"f","f2":"ff"},"bar":{"b1":"b","b2":"bb"}}
After walk "foo":
{"f1":"f","f2":"ff"}
{"b1":"b","b2":"bb"}
{"foo":{"f1":"f","f2":"ff"},"bar":{"b1":"b","b2":"bb"}}
```
Scoping
-------
This appears to be using dynamic scoping instead of lexical scoping. For example, in lexical scoping a read of "b1" would fail after that final walk because you're inside the "foo" context which does not have any "b1" in or above it, however, since this is using dynamic scoping its using the invocations to build a scope tree rather than their original position.
Itermediate scopes appear to not be added. For example:
```js
{
"globals": {
"item": "pencil",
"things": {"color": "purple"}
},
"people": [
{"name": "Dave"},
{"name": "Emily", "item": "pen"}
]
}
```
If we walk into people and then into globals.things in one step, globals will not be added to the dynamic scope:
```
(attempts to read from the context in-order starting with the first line)
Starting access context:
{"globals":{"item":"pencil","things":{"color":"purple"}},"people":[{"name":"Dave"},{"name":"Emily","item":"pen"}]}
After walk "people":
[{"name":"Dave"},{"name":"Emily","item":"pen"}]
{"globals":{"item":"pencil","things":{"color":"purple"}},"people":[{"name":"Dave"},{"name":"Emily","item":"pen"}]}
After walk globals.things
{"color":"purple"}
[{"name":"Dave"},{"name":"Emily","item":"pen"}]
{"globals":{"item":"pencil","things":{"color":"purple"}},"people":[{"name":"Dave"},{"name":"Emily","item":"pen"}]}
```
So if we were on the "Dave" iteration in people and I attempted to read "item" it would not find a value despite "item" being a key in the lexical context above `globals.things`.
Backtracking
------------
Item resolution appears to be greedy. For example if we have:
```js
{
"clothes": {
"shirt": "t-shirt",
"pants": "jeans"
},
"alice": {
"clothes": {
"shirt": "tank top"
}
},
"bob": {},
}
```
If we walked into `alice` and then attempted to read `clothes.pants` it will return nothing because `alice` has a `clothes` block but no `pants` element inside that. However, if we walked into `bob` and attempted to read `clothes.pants` it would return `jeans` because `bob` does not have a `clothes` block so it would walk up to the global `clothes` block.

View File

@ -0,0 +1,19 @@
{
"company": "The Pendulum",
"globals": {
"email": "email hidden"
},
"people": [
{"name": "Alice", "job": "Chief Swinger"},
{"name": "Bob", "job": "Chief Swayer"},
{"name": "Chris", "job": "Barista", "company": "GenericCoffee", "email": "thecoffeeguy@generic.coffee"}
],
"deep_globals": {
"item": "pencil",
"things": {"color": "purple", "deeper_item": {"style": "number 2"}}
},
"deep_people": [
{"name": "Dave"},
{"name": "Emily", "item": "pen", "deeper_item": {"style": "ballpoint", "material": "plastic"}}
]
}

View File

@ -0,0 +1,13 @@
Directory for {company}:{~n}
{#people}
{name}: {job} at {company} (email: {globals.email}){~n}
Testing walking after entering a parent context {#globals}job: {job}, email: {email}, company: {company}{/globals}{~n}
{/people}
Doing a deep walk to see if intermediate steps are added to the dynamic context.{~n}
{#deep_people}
{#deep_globals.things}
{name} has a {color} {item} which is {deeper_item.style} and made out of {deeper_item.material}
{/deep_globals.things}
but everyone shares one that is {deeper_item.style} and made out of {deeper_item.material}{~n}
{/deep_people}

View File

@ -44,10 +44,11 @@ fn main() {
.first()
.expect("There should be more than 1 template")
.name;
let breadcrumbs = vec![&context as &dyn ContextElement];
println!(
"{}",
dust_renderer
.render(main_template_name, &context)
.render(main_template_name, &breadcrumbs)
.expect("Failed to render")
);
}

View File

@ -1,9 +1,8 @@
use crate::parser::Filter;
use crate::renderer::errors::RenderError;
use crate::renderer::renderer::RenderWrapper;
use std::fmt::Debug;
pub trait ContextElement: Debug + RenderWrapper + Walkable + Renderable + Loopable {}
pub trait ContextElement: Debug + Walkable + Renderable + Loopable {}
pub trait Walkable {
fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError>;

View File

@ -14,6 +14,10 @@ pub enum RenderError<'a> {
segment: String,
elem: &'a dyn ContextElement,
},
NotFound {
path: &'a Vec<&'a str>,
breadcrumbs: Vec<&'a dyn ContextElement>,
},
/// Attempting to render and unrenderable type (for example, an object without any filters)
CantRender {
elem: &'a dyn ContextElement,
@ -36,6 +40,9 @@ impl fmt::Display for RenderError<'_> {
write!(f, "Failed to walk to {} from {:?}", segment, elem)
}
RenderError::CantRender { elem } => write!(f, "Cant render {:?}", elem),
RenderError::NotFound { path, breadcrumbs } => {
write!(f, "Could not find {:?} in {:?}", path, breadcrumbs)
}
}
}
}
@ -51,6 +58,9 @@ impl fmt::Debug for RenderError<'_> {
write!(f, "Failed to walk to {} from {:?}", segment, elem)
}
RenderError::CantRender { elem } => write!(f, "Cant render {:?}", elem),
RenderError::NotFound { path, breadcrumbs } => {
write!(f, "Could not find {:?} in {:?}", path, breadcrumbs)
}
}
}
}

View File

@ -20,24 +20,6 @@ pub struct DustRenderer<'a> {
templates: HashMap<String, &'a Template<'a>>,
}
pub trait RenderWrapper {
fn render_body<'a>(
&'a self,
renderer: &'a DustRenderer,
body: &Body,
) -> Result<String, RenderError>;
}
impl<C: ContextElement> RenderWrapper for C {
fn render_body<'a>(
&'a self,
renderer: &'a DustRenderer,
body: &Body,
) -> Result<String, RenderError<'a>> {
renderer.render_body(body, self)
}
}
pub fn compile_template<'a>(
source: &'a str,
name: String,
@ -63,10 +45,11 @@ impl<'a> DustRenderer<'a> {
.insert(template.name.clone(), &template.template);
}
pub fn render<C>(&'a self, name: &str, context: &'a C) -> Result<String, RenderError<'a>>
where
C: ContextElement,
{
pub fn render(
&'a self,
name: &str,
breadcrumbs: &Vec<&'a dyn ContextElement>,
) -> Result<String, RenderError<'a>> {
let main_template = match self.templates.get(name) {
Some(tmpl) => tmpl,
None => {
@ -76,46 +59,55 @@ impl<'a> DustRenderer<'a> {
)));
}
};
self.render_body(&main_template.contents, context)
self.render_body(&main_template.contents, breadcrumbs)
}
fn render_body<C>(&'a self, body: &Body, context: &'a C) -> Result<String, RenderError<'a>>
where
C: ContextElement,
{
fn render_body(
&'a self,
body: &'a Body,
breadcrumbs: &Vec<&'a dyn ContextElement>,
) -> Result<String, RenderError<'a>> {
let mut output = String::new();
for elem in &body.elements {
match elem {
TemplateElement::TEIgnoredWhitespace(_) => {}
TemplateElement::TESpan(span) => output.push_str(span.contents),
TemplateElement::TETag(dt) => {
output.push_str(&self.render_tag(dt, context)?);
output.push_str(&self.render_tag(dt, breadcrumbs)?);
}
}
}
Ok(output)
}
fn render_tag<C>(&'a self, tag: &DustTag, context: &'a C) -> Result<String, RenderError<'a>>
where
C: ContextElement,
{
fn render_tag(
&'a self,
tag: &'a DustTag,
breadcrumbs: &Vec<&'a dyn ContextElement>,
) -> Result<String, RenderError<'a>> {
match tag {
DustTag::DTComment(_comment) => (),
DustTag::DTSpecial(special) => {
return Ok(match special {
Special::Space => " ",
Special::NewLine => "\n",
Special::CarriageReturn => "\r",
Special::LeftCurlyBrace => "{",
Special::RightCurlyBrace => "}",
}
.to_owned())
}
DustTag::DTReference(reference) => {
let val = walk_path(context, &reference.path.keys);
if let Err(RenderError::WontWalk { .. }) = val {
let val = walk_path(breadcrumbs, &reference.path.keys);
if let Err(RenderError::NotFound { .. }) = val {
// If reference does not exist in the context, it becomes an empty string
return Ok("".to_owned());
} else if let Err(RenderError::CantWalk { .. }) = val {
// If the context type does not support walking, it becomes an empty string
return Ok("".to_owned());
} else {
return val?.render(&reference.filters);
}
}
DustTag::DTSection(container) => {
let val = walk_path(context, &container.path.keys);
let val = walk_path(breadcrumbs, &container.path.keys);
let loop_elements: Vec<&dyn ContextElement> = self.get_loop_elements(val)?;
if loop_elements.is_empty() {
// Oddly enough if the value is falsey (like
@ -126,7 +118,7 @@ impl<'a> DustRenderer<'a> {
// TODO: do filters apply? I don't think so
// but I should test
return match &container.else_contents {
Some(body) => self.render_body(&body, context),
Some(body) => self.render_body(&body, breadcrumbs),
None => Ok("".to_owned()),
};
} else {
@ -135,23 +127,17 @@ impl<'a> DustRenderer<'a> {
Some(body) => {
let rendered_results: Result<Vec<String>, RenderError> = loop_elements
.into_iter()
.map(|array_elem| array_elem.render_body(self, &body))
.map(|array_elem| {
let mut new_breadcumbs = breadcrumbs.clone();
new_breadcumbs.push(array_elem);
self.render_body(&body, &new_breadcumbs)
})
.collect();
let rendered_slice: &[String] = &rendered_results?;
return Ok(rendered_slice.join(""));
}
};
}
}
DustTag::DTSpecial(special) => {
return Ok(match special {
Special::Space => " ",
Special::NewLine => "\n",
Special::CarriageReturn => "\r",
Special::LeftCurlyBrace => "{",
Special::RightCurlyBrace => "}",
}
.to_owned())
}
_ => (), // TODO: Implement the rest
}
@ -166,29 +152,68 @@ impl<'a> DustRenderer<'a> {
&'a self,
walk_result: Result<&'b dyn ContextElement, RenderError<'b>>,
) -> Result<Vec<&'b dyn ContextElement>, RenderError<'b>> {
if let Err(RenderError::WontWalk { .. }) = walk_result {
if let Err(RenderError::NotFound { .. }) = walk_result {
// If reference does not exist in the context, render the else block
Ok(vec![])
} else if let Err(RenderError::CantWalk { .. }) = walk_result {
// If the context type does not support walking, render the else block
Ok(vec![])
} else {
Ok(walk_result?.get_loop_elements()?)
}
}
}
fn walk_path<'a>(
context: &'a dyn ContextElement,
path: &Vec<&str>,
) -> Result<&'a dyn ContextElement, RenderError<'a>> {
let mut output = context;
for elem in path.iter() {
output = output.walk(elem)?;
enum WalkResult<'a> {
NoWalk,
PartialWalk,
FullyWalked(&'a dyn ContextElement),
}
Ok(output)
fn walk_path_from_single_level<'a>(
context: &'a dyn ContextElement,
path: &Vec<&str>,
) -> Result<WalkResult<'a>, RenderError<'a>> {
if path.is_empty() {
return Ok(WalkResult::FullyWalked(context));
}
let mut walk_failure = WalkResult::NoWalk;
let mut output = context;
for elem in path.iter() {
let new_val = output.walk(elem);
if let Err(RenderError::WontWalk { .. }) = new_val {
return Ok(walk_failure);
} else if let Err(RenderError::CantWalk { .. }) = new_val {
return Ok(walk_failure);
}
walk_failure = WalkResult::PartialWalk;
output = new_val?;
}
Ok(WalkResult::FullyWalked(output))
}
fn walk_path<'a>(
breadcrumbs: &Vec<&'a dyn ContextElement>,
path: &'a Vec<&str>,
) -> Result<&'a dyn ContextElement, RenderError<'a>> {
for context in breadcrumbs.iter().rev() {
match walk_path_from_single_level(*context, path)? {
// If no walking was done at all, keep looping
WalkResult::NoWalk => {}
// If we partially walked then stop trying to find
// anything
WalkResult::PartialWalk => {
return Err(RenderError::NotFound {
path: path,
breadcrumbs: breadcrumbs.clone(),
});
}
WalkResult::FullyWalked(new_context) => return Ok(new_context),
}
}
Err(RenderError::NotFound {
path: path,
breadcrumbs: breadcrumbs.clone(),
})
}
#[cfg(test)]
@ -199,8 +224,6 @@ mod tests {
use crate::renderer::context_element::Renderable;
use crate::renderer::context_element::Walkable;
#[test]
fn test_walk_path() {
impl ContextElement for u32 {}
impl ContextElement for &str {}
impl<I: ContextElement> ContextElement for HashMap<&str, I> {}
@ -276,6 +299,8 @@ mod tests {
}
}
#[test]
fn test_walk_path() {
let context: HashMap<&str, &str> =
[("cat", "kitty"), ("dog", "doggy"), ("tiger", "murderkitty")]
.iter()
@ -293,22 +318,29 @@ mod tests {
.iter()
.cloned()
.collect();
assert_eq!(
walk_path(&context, &vec!["cat"])
walk_path(&vec![&context as &dyn ContextElement], &vec!["cat"])
.unwrap()
.render(&Vec::new())
.unwrap(),
"kitty".to_owned()
);
assert_eq!(
walk_path(&number_context, &vec!["tiger"])
walk_path(
&vec![&number_context as &dyn ContextElement],
&vec!["tiger"]
)
.unwrap()
.render(&Vec::new())
.unwrap(),
"3".to_owned()
);
assert_eq!(
walk_path(&deep_context, &vec!["tiger", "food"])
walk_path(
&vec![&deep_context as &dyn ContextElement],
&vec!["tiger", "food"]
)
.unwrap()
.render(&Vec::new())
.unwrap(),