Merge branch 'walk_up_context' into render
This commit is contained in:
commit
35f1ba8447
90
js/test_cases/walk_up/README.md
Normal file
90
js/test_cases/walk_up/README.md
Normal 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.
|
19
js/test_cases/walk_up/input1.json
Normal file
19
js/test_cases/walk_up/input1.json
Normal 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"}}
|
||||
]
|
||||
}
|
13
js/test_cases/walk_up/main.dust
Normal file
13
js/test_cases/walk_up/main.dust
Normal 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}
|
@ -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")
|
||||
);
|
||||
}
|
||||
|
@ -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>;
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user