From 9adb88d13261354f0a33841a4210453723e3cfd9 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sun, 3 May 2020 15:47:21 -0400 Subject: [PATCH 01/13] Added a test case for walking up the context --- js/test_cases/walk_up/input1.json | 11 +++++++++++ js/test_cases/walk_up/main.dust | 4 ++++ 2 files changed, 15 insertions(+) create mode 100644 js/test_cases/walk_up/input1.json create mode 100644 js/test_cases/walk_up/main.dust diff --git a/js/test_cases/walk_up/input1.json b/js/test_cases/walk_up/input1.json new file mode 100644 index 0000000..993ee52 --- /dev/null +++ b/js/test_cases/walk_up/input1.json @@ -0,0 +1,11 @@ +{ + "company": "The Pendulum", + "globals": { + "email": "email hidden" + }, + "people": [ + {"name": "Alice", "job": "Chief Swinger"}, + {"name": "Bob", "job": "Chief Swayer"}, + {"name": "Chris", "job": "Barista", "company": "GenericCoffee"} + ] +} diff --git a/js/test_cases/walk_up/main.dust b/js/test_cases/walk_up/main.dust new file mode 100644 index 0000000..af4f875 --- /dev/null +++ b/js/test_cases/walk_up/main.dust @@ -0,0 +1,4 @@ +Directory for {company}:{~n} +{#people} +{name}: {job} at {company} (email: {globals.email}){~n} +{/people} From 45facfed0df2d61a82fc205780d88d9b09b6bf5e Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sun, 3 May 2020 16:13:29 -0400 Subject: [PATCH 02/13] Improve the walk up test to prove that DustJS is doing dynamic scoping, not lexical scoping. --- js/test_cases/walk_up/README.md | 33 +++++++++++++++++++++++++++++++ js/test_cases/walk_up/input1.json | 2 +- js/test_cases/walk_up/main.dust | 1 + src/renderer/renderer.rs | 1 + 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 js/test_cases/walk_up/README.md diff --git a/js/test_cases/walk_up/README.md b/js/test_cases/walk_up/README.md new file mode 100644 index 0000000..f30e7cc --- /dev/null +++ b/js/test_cases/walk_up/README.md @@ -0,0 +1,33 @@ +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"}} +``` + +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. diff --git a/js/test_cases/walk_up/input1.json b/js/test_cases/walk_up/input1.json index 993ee52..1d96d10 100644 --- a/js/test_cases/walk_up/input1.json +++ b/js/test_cases/walk_up/input1.json @@ -6,6 +6,6 @@ "people": [ {"name": "Alice", "job": "Chief Swinger"}, {"name": "Bob", "job": "Chief Swayer"}, - {"name": "Chris", "job": "Barista", "company": "GenericCoffee"} + {"name": "Chris", "job": "Barista", "company": "GenericCoffee", "email": "thecoffeeguy@generic.coffee"} ] } diff --git a/js/test_cases/walk_up/main.dust b/js/test_cases/walk_up/main.dust index af4f875..94a2d2f 100644 --- a/js/test_cases/walk_up/main.dust +++ b/js/test_cases/walk_up/main.dust @@ -1,4 +1,5 @@ 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} diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index d542a61..d260408 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -181,6 +181,7 @@ impl<'a> DustRenderer<'a> { fn walk_path<'a>( context: &'a dyn ContextElement, path: &Vec<&str>, + breadcrumbs: Vec<&'a dyn ContextElement>, ) -> Result<&'a dyn ContextElement, RenderError<'a>> { let mut output = context; From 2b6c3990a965eefd120dcf9f778a95e430b2e3ac Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sun, 3 May 2020 16:32:29 -0400 Subject: [PATCH 03/13] Add test for a multi-step walk into a parent context. This test proves that the dynamic scoping does not add the intermediate steps when doing a multi-step walk. --- js/test_cases/walk_up/README.md | 33 +++++++++++++++++++++++++++++++ js/test_cases/walk_up/input1.json | 10 +++++++++- js/test_cases/walk_up/main.dust | 7 +++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/js/test_cases/walk_up/README.md b/js/test_cases/walk_up/README.md index f30e7cc..7099d95 100644 --- a/js/test_cases/walk_up/README.md +++ b/js/test_cases/walk_up/README.md @@ -31,3 +31,36 @@ After walk "foo": ``` 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`. diff --git a/js/test_cases/walk_up/input1.json b/js/test_cases/walk_up/input1.json index 1d96d10..e1d0c8f 100644 --- a/js/test_cases/walk_up/input1.json +++ b/js/test_cases/walk_up/input1.json @@ -7,5 +7,13 @@ {"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"} + }, + "deep_people": [ + {"name": "Dave"}, + {"name": "Emily", "item": "pen"} + ] } diff --git a/js/test_cases/walk_up/main.dust b/js/test_cases/walk_up/main.dust index 94a2d2f..b143107 100644 --- a/js/test_cases/walk_up/main.dust +++ b/js/test_cases/walk_up/main.dust @@ -3,3 +3,10 @@ Directory for {company}:{~n} {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}{~n} +{/deep_globals.things} +{/deep_people} From c3fe7b47afe995258d13002803e0439d03d6f110 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sun, 3 May 2020 16:49:34 -0400 Subject: [PATCH 04/13] Added a test for backtracking. DustJS appears to not do any backtracking. --- js/test_cases/walk_up/README.md | 24 ++++++++++++++++++++++++ js/test_cases/walk_up/input1.json | 4 ++-- js/test_cases/walk_up/main.dust | 3 ++- src/renderer/renderer.rs | 17 ++++++++++++++++- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/js/test_cases/walk_up/README.md b/js/test_cases/walk_up/README.md index 7099d95..3f9a4b5 100644 --- a/js/test_cases/walk_up/README.md +++ b/js/test_cases/walk_up/README.md @@ -30,6 +30,9 @@ After walk "foo": {"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: @@ -64,3 +67,24 @@ After walk globals.things ``` 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. diff --git a/js/test_cases/walk_up/input1.json b/js/test_cases/walk_up/input1.json index e1d0c8f..889e443 100644 --- a/js/test_cases/walk_up/input1.json +++ b/js/test_cases/walk_up/input1.json @@ -10,10 +10,10 @@ ], "deep_globals": { "item": "pencil", - "things": {"color": "purple"} + "things": {"color": "purple", "deeper_item": {"style": "number 2"}} }, "deep_people": [ {"name": "Dave"}, - {"name": "Emily", "item": "pen"} + {"name": "Emily", "item": "pen", "deeper_item": {"style": "ballpoint", "material": "plastic"}} ] } diff --git a/js/test_cases/walk_up/main.dust b/js/test_cases/walk_up/main.dust index b143107..efdcfad 100644 --- a/js/test_cases/walk_up/main.dust +++ b/js/test_cases/walk_up/main.dust @@ -7,6 +7,7 @@ Testing walking after entering a parent context {#globals}job: {job}, email: {em 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}{~n} +{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} diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index d260408..cd48c5d 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -178,7 +178,7 @@ impl<'a> DustRenderer<'a> { } } -fn walk_path<'a>( +fn new_walk_path<'a>( context: &'a dyn ContextElement, path: &Vec<&str>, breadcrumbs: Vec<&'a dyn ContextElement>, @@ -192,6 +192,21 @@ fn walk_path<'a>( Ok(output) } +// TODO: rename walk_path_from_single_level +/// Attempts to walk a path from only 1 level in the context breadcrumbs +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)?; + } + + Ok(output) +} + #[cfg(test)] mod tests { use super::*; From 033fc9de6bd713c105dd8e13ea3ab890b894a5b1 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 May 2020 23:10:35 -0400 Subject: [PATCH 05/13] Implement walking for a single segment, --- src/renderer/renderer.rs | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index cd48c5d..3f368d3 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -178,18 +178,41 @@ impl<'a> DustRenderer<'a> { } } -fn new_walk_path<'a>( +enum WalkResult<'a> { + NoWalk, + PartialWalk, + FullyWalked(&'a dyn ContextElement), +} + +fn walk_path_from_single_level<'a>( context: &'a dyn ContextElement, path: &Vec<&str>, - breadcrumbs: Vec<&'a dyn ContextElement>, -) -> Result<&'a dyn ContextElement, RenderError<'a>> { - let mut output = context; - - for elem in path.iter() { - output = output.walk(elem)?; +) -> Result, RenderError<'a>> { + if path.is_empty() { + return Ok(WalkResult::FullyWalked(context)); } - Ok(output) + 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(context)) +} + +fn new_walk_path<'a>( + breadcrumbs: Vec<&'a dyn ContextElement>, + path: &Vec<&str>, +) -> Result<&'a dyn ContextElement, RenderError<'a>> { + Err(RenderError::Generic("temp".to_owned())) } // TODO: rename walk_path_from_single_level From 6bcc66dff58204cd72bca474d442ab6af4f224be Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 May 2020 23:36:13 -0400 Subject: [PATCH 06/13] Adding NotFound error type. --- src/renderer/errors.rs | 10 ++++++++++ src/renderer/renderer.rs | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/src/renderer/errors.rs b/src/renderer/errors.rs index 663065c..8af8bb1 100644 --- a/src/renderer/errors.rs +++ b/src/renderer/errors.rs @@ -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) + } } } } diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index 3f368d3..c0a2a64 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -212,6 +212,13 @@ fn new_walk_path<'a>( breadcrumbs: Vec<&'a dyn ContextElement>, path: &Vec<&str>, ) -> Result<&'a dyn ContextElement, RenderError<'a>> { + for context in breadcrumbs.iter().rev() { + match walk_path_from_single_level(*context, path)? { + WalkResult::NoWalk => {} + WalkResult::PartialWalk => {} + WalkResult::FullyWalked(_) => {} + } + } Err(RenderError::Generic("temp".to_owned())) } From a3bb8e47c15ae08eb1a083d0184c2b79d3e3c8f4 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 May 2020 23:45:21 -0400 Subject: [PATCH 07/13] Implemented the backtracing logic for walking. --- src/renderer/renderer.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index c0a2a64..f430d28 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -210,16 +210,27 @@ fn walk_path_from_single_level<'a>( fn new_walk_path<'a>( breadcrumbs: Vec<&'a dyn ContextElement>, - path: &Vec<&str>, + 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 => {} - WalkResult::PartialWalk => {} - WalkResult::FullyWalked(_) => {} + // If we partially walked then stop trying to find + // anything + WalkResult::PartialWalk => { + return Err(RenderError::NotFound { + path: path, + breadcrumbs: breadcrumbs, + }) + } + WalkResult::FullyWalked(new_context) => return Ok(new_context), } } - Err(RenderError::Generic("temp".to_owned())) + Err(RenderError::NotFound { + path: path, + breadcrumbs: breadcrumbs, + }) } // TODO: rename walk_path_from_single_level From 26fe996b0dbdbfc39a9095f7424591d3578b638c Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Tue, 5 May 2020 19:51:07 -0400 Subject: [PATCH 08/13] Implement the new render functions. --- src/bin.rs | 3 +- src/renderer/renderer.rs | 105 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/bin.rs b/src/bin.rs index 7535818..6d709fc 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -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) + .new_render(main_template_name, &breadcrumbs) .expect("Failed to render") ); } diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index f430d28..bb820d0 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -63,6 +63,23 @@ impl<'a> DustRenderer<'a> { .insert(template.name.clone(), &template.template); } + pub fn new_render( + &'a self, + name: &str, + breadcrumbs: &Vec<&'a dyn ContextElement>, + ) -> Result> { + let main_template = match self.templates.get(name) { + Some(tmpl) => tmpl, + None => { + return Err(RenderError::Generic(format!( + "No template named {} in context", + name + ))); + } + }; + self.new_render_body(&main_template.contents, breadcrumbs) + } + pub fn render(&'a self, name: &str, context: &'a C) -> Result> where C: ContextElement, @@ -79,6 +96,24 @@ impl<'a> DustRenderer<'a> { self.render_body(&main_template.contents, context) } + fn new_render_body( + &'a self, + body: &'a Body, + breadcrumbs: &Vec<&'a dyn ContextElement>, + ) -> Result> { + 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.new_render_tag(dt, breadcrumbs)?); + } + } + } + Ok(output) + } + fn render_body(&'a self, body: &Body, context: &'a C) -> Result> where C: ContextElement, @@ -96,6 +131,70 @@ impl<'a> DustRenderer<'a> { Ok(output) } + fn new_render_tag( + &'a self, + tag: &'a DustTag, + breadcrumbs: &Vec<&'a dyn ContextElement>, + ) -> Result> { + 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 = new_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 { + return val?.render(&reference.filters); + } + } + DustTag::DTSection(container) => { + let val = new_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 + // an empty array or null), Dust uses the + // original context before walking the path as + // the context for rendering the else block + // + // TODO: do filters apply? I don't think so + // but I should test + return match &container.else_contents { + Some(body) => self.new_render_body(&body, breadcrumbs), + None => Ok("".to_owned()), + }; + } else { + match &container.contents { + None => return Ok("".to_owned()), + Some(body) => { + let rendered_results: Result, RenderError> = loop_elements + .into_iter() + .map(|array_elem| { + let mut new_breadcumbs = breadcrumbs.clone(); + new_breadcumbs.push(array_elem); + self.new_render_body(&body, &new_breadcumbs) + }) + .collect(); + let rendered_slice: &[String] = &rendered_results?; + return Ok(rendered_slice.join("")); + } + } + } + } + _ => (), // TODO: Implement the rest + } + Ok("".to_owned()) + } + fn render_tag(&'a self, tag: &DustTag, context: &'a C) -> Result> where C: ContextElement, @@ -209,7 +308,7 @@ fn walk_path_from_single_level<'a>( } fn new_walk_path<'a>( - breadcrumbs: Vec<&'a dyn ContextElement>, + breadcrumbs: &Vec<&'a dyn ContextElement>, path: &'a Vec<&str>, ) -> Result<&'a dyn ContextElement, RenderError<'a>> { for context in breadcrumbs.iter().rev() { @@ -221,7 +320,7 @@ fn new_walk_path<'a>( WalkResult::PartialWalk => { return Err(RenderError::NotFound { path: path, - breadcrumbs: breadcrumbs, + breadcrumbs: breadcrumbs.clone(), }) } WalkResult::FullyWalked(new_context) => return Ok(new_context), @@ -229,7 +328,7 @@ fn new_walk_path<'a>( } Err(RenderError::NotFound { path: path, - breadcrumbs: breadcrumbs, + breadcrumbs: breadcrumbs.clone(), }) } From 5760566be0fe3524cb45cdc7ad11b2cbf03f845b Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Tue, 5 May 2020 20:22:25 -0400 Subject: [PATCH 09/13] Start of porting over the walk tests. --- src/renderer/renderer.rs | 185 ++++++++++++++++++++++----------------- 1 file changed, 106 insertions(+), 79 deletions(-) diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index bb820d0..c5761f8 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -304,7 +304,7 @@ fn walk_path_from_single_level<'a>( output = new_val?; } - Ok(WalkResult::FullyWalked(context)) + Ok(WalkResult::FullyWalked(output)) } fn new_walk_path<'a>( @@ -321,7 +321,7 @@ fn new_walk_path<'a>( return Err(RenderError::NotFound { path: path, breadcrumbs: breadcrumbs.clone(), - }) + }); } WalkResult::FullyWalked(new_context) => return Ok(new_context), } @@ -332,8 +332,6 @@ fn new_walk_path<'a>( }) } -// TODO: rename walk_path_from_single_level -/// Attempts to walk a path from only 1 level in the context breadcrumbs fn walk_path<'a>( context: &'a dyn ContextElement, path: &Vec<&str>, @@ -355,83 +353,112 @@ mod tests { use crate::renderer::context_element::Renderable; use crate::renderer::context_element::Walkable; + impl ContextElement for u32 {} + impl ContextElement for &str {} + impl ContextElement for HashMap<&str, I> {} + + impl Renderable for u32 { + fn render(&self, _filters: &Vec) -> Result { + // TODO: handle the filters + Ok(self.to_string()) + } + } + + impl Renderable for &str { + fn render(&self, _filters: &Vec) -> Result { + // TODO: handle the filters + Ok(self.to_string()) + } + } + + impl Renderable for HashMap<&str, I> { + fn render(&self, _filters: &Vec) -> Result { + // TODO: handle the filters + Err(RenderError::CantRender { elem: self }) + } + } + + impl Walkable for HashMap<&str, I> { + fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> { + let child = self.get(segment).ok_or(RenderError::WontWalk { + segment: segment.to_string(), + elem: self, + })?; + Ok(child) + } + } + + impl Walkable for &str { + fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> { + Err(RenderError::CantWalk { + segment: segment.to_string(), + elem: self, + }) + } + } + + impl Walkable for u32 { + fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> { + Err(RenderError::CantWalk { + segment: segment.to_string(), + elem: self, + }) + } + } + + impl Loopable for &str { + fn get_loop_elements(&self) -> Result, RenderError> { + if self.is_empty() { + Ok(Vec::new()) + } else { + Ok(vec![self]) + } + } + } + + impl Loopable for u32 { + fn get_loop_elements(&self) -> Result, RenderError> { + Ok(vec![self]) + } + } + + impl Loopable for HashMap<&str, I> { + fn get_loop_elements(&self) -> Result, RenderError> { + Ok(vec![self]) + } + } + + #[test] + fn test_new_walk_path() { + let context: HashMap<&str, &str> = + [("cat", "kitty"), ("dog", "doggy"), ("tiger", "murderkitty")] + .iter() + .cloned() + .collect(); + let number_context: HashMap<&str, u32> = [("cat", 1), ("dog", 2), ("tiger", 3)] + .iter() + .cloned() + .collect(); + let deep_context: HashMap<&str, HashMap<&str, &str>> = [ + ("cat", [("food", "meat")].iter().cloned().collect()), + ("dog", [("food", "meat")].iter().cloned().collect()), + ("tiger", [("food", "people")].iter().cloned().collect()), + ] + .iter() + .cloned() + .collect(); + + assert_eq!( + new_walk_path(&vec![&context as &dyn ContextElement], &vec!["cat"]) + .unwrap() + .render(&Vec::new()) + .unwrap(), + "kitty".to_owned() + ); + } + #[test] fn test_walk_path() { - impl ContextElement for u32 {} - impl ContextElement for &str {} - impl ContextElement for HashMap<&str, I> {} - - impl Renderable for u32 { - fn render(&self, _filters: &Vec) -> Result { - // TODO: handle the filters - Ok(self.to_string()) - } - } - - impl Renderable for &str { - fn render(&self, _filters: &Vec) -> Result { - // TODO: handle the filters - Ok(self.to_string()) - } - } - - impl Renderable for HashMap<&str, I> { - fn render(&self, _filters: &Vec) -> Result { - // TODO: handle the filters - Err(RenderError::CantRender { elem: self }) - } - } - - impl Walkable for HashMap<&str, I> { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> { - let child = self.get(segment).ok_or(RenderError::WontWalk { - segment: segment.to_string(), - elem: self, - })?; - Ok(child) - } - } - - impl Walkable for &str { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> { - Err(RenderError::CantWalk { - segment: segment.to_string(), - elem: self, - }) - } - } - - impl Walkable for u32 { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> { - Err(RenderError::CantWalk { - segment: segment.to_string(), - elem: self, - }) - } - } - - impl Loopable for &str { - fn get_loop_elements(&self) -> Result, RenderError> { - if self.is_empty() { - Ok(Vec::new()) - } else { - Ok(vec![self]) - } - } - } - - impl Loopable for u32 { - fn get_loop_elements(&self) -> Result, RenderError> { - Ok(vec![self]) - } - } - - impl Loopable for HashMap<&str, I> { - fn get_loop_elements(&self) -> Result, RenderError> { - Ok(vec![self]) - } - } - let context: HashMap<&str, &str> = [("cat", "kitty"), ("dog", "doggy"), ("tiger", "murderkitty")] .iter() From 9c414d4d066562dcea477414daa343988e480a5e Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Tue, 5 May 2020 20:32:30 -0400 Subject: [PATCH 10/13] Fixed rendering else blocks. --- src/renderer/renderer.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index c5761f8..b0d11c6 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -265,7 +265,10 @@ impl<'a> DustRenderer<'a> { &'a self, walk_result: Result<&'b dyn ContextElement, RenderError<'b>>, ) -> Result, 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::WontWalk { .. }) = walk_result { // If reference does not exist in the context, render the else block Ok(vec![]) } else if let Err(RenderError::CantWalk { .. }) = walk_result { From 05527377c4bd94a81107992e86536cd771ec25c0 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Tue, 5 May 2020 20:38:42 -0400 Subject: [PATCH 11/13] Finish porting over the walk test. --- src/renderer/renderer.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index b0d11c6..6ad133b 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -458,6 +458,26 @@ mod tests { .unwrap(), "kitty".to_owned() ); + assert_eq!( + new_walk_path( + &vec![&number_context as &dyn ContextElement], + &vec!["tiger"] + ) + .unwrap() + .render(&Vec::new()) + .unwrap(), + "3".to_owned() + ); + assert_eq!( + new_walk_path( + &vec![&deep_context as &dyn ContextElement], + &vec!["tiger", "food"] + ) + .unwrap() + .render(&Vec::new()) + .unwrap(), + "people".to_owned() + ); } #[test] From 18f9fb7f572006b0831e604aae593b4df45e5809 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Tue, 5 May 2020 20:43:53 -0400 Subject: [PATCH 12/13] Delete old render functions. --- src/renderer/context_element.rs | 3 +- src/renderer/renderer.rs | 176 +------------------------------- 2 files changed, 2 insertions(+), 177 deletions(-) diff --git a/src/renderer/context_element.rs b/src/renderer/context_element.rs index 0dccc2c..14f1aae 100644 --- a/src/renderer/context_element.rs +++ b/src/renderer/context_element.rs @@ -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>; diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index 6ad133b..bbd1727 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -20,24 +20,6 @@ pub struct DustRenderer<'a> { templates: HashMap>, } -pub trait RenderWrapper { - fn render_body<'a>( - &'a self, - renderer: &'a DustRenderer, - body: &Body, - ) -> Result; -} - -impl RenderWrapper for C { - fn render_body<'a>( - &'a self, - renderer: &'a DustRenderer, - body: &Body, - ) -> Result> { - renderer.render_body(body, self) - } -} - pub fn compile_template<'a>( source: &'a str, name: String, @@ -80,22 +62,6 @@ impl<'a> DustRenderer<'a> { self.new_render_body(&main_template.contents, breadcrumbs) } - pub fn render(&'a self, name: &str, context: &'a C) -> Result> - where - C: ContextElement, - { - let main_template = match self.templates.get(name) { - Some(tmpl) => tmpl, - None => { - return Err(RenderError::Generic(format!( - "No template named {} in context", - name - ))); - } - }; - self.render_body(&main_template.contents, context) - } - fn new_render_body( &'a self, body: &'a Body, @@ -114,23 +80,6 @@ impl<'a> DustRenderer<'a> { Ok(output) } - fn render_body(&'a self, body: &Body, context: &'a C) -> Result> - where - C: ContextElement, - { - 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)?); - } - } - } - Ok(output) - } - fn new_render_tag( &'a self, tag: &'a DustTag, @@ -195,68 +144,6 @@ impl<'a> DustRenderer<'a> { Ok("".to_owned()) } - fn render_tag(&'a self, tag: &DustTag, context: &'a C) -> Result> - where - C: ContextElement, - { - match tag { - DustTag::DTComment(_comment) => (), - DustTag::DTReference(reference) => { - let val = walk_path(context, &reference.path.keys); - if let Err(RenderError::WontWalk { .. }) = 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 loop_elements: Vec<&dyn ContextElement> = self.get_loop_elements(val)?; - if loop_elements.is_empty() { - // Oddly enough if the value is falsey (like - // an empty array or null), Dust uses the - // original context before walking the path as - // the context for rendering the else block - // - // 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), - None => Ok("".to_owned()), - }; - } else { - match &container.contents { - None => return Ok("".to_owned()), - Some(body) => { - let rendered_results: Result, RenderError> = loop_elements - .into_iter() - .map(|array_elem| array_elem.render_body(self, &body)) - .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 - } - Ok("".to_owned()) - } - /// Gets the elements to loop over for a section. /// /// If the value is falsey, and therefore should render the else @@ -268,12 +155,6 @@ impl<'a> DustRenderer<'a> { 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::WontWalk { .. }) = 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()?) } @@ -335,19 +216,6 @@ fn new_walk_path<'a>( }) } -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)?; - } - - Ok(output) -} - #[cfg(test)] mod tests { use super::*; @@ -432,7 +300,7 @@ mod tests { } #[test] - fn test_new_walk_path() { + fn test_walk_path() { let context: HashMap<&str, &str> = [("cat", "kitty"), ("dog", "doggy"), ("tiger", "murderkitty")] .iter() @@ -479,46 +347,4 @@ mod tests { "people".to_owned() ); } - - #[test] - fn test_walk_path() { - let context: HashMap<&str, &str> = - [("cat", "kitty"), ("dog", "doggy"), ("tiger", "murderkitty")] - .iter() - .cloned() - .collect(); - let number_context: HashMap<&str, u32> = [("cat", 1), ("dog", 2), ("tiger", 3)] - .iter() - .cloned() - .collect(); - let deep_context: HashMap<&str, HashMap<&str, &str>> = [ - ("cat", [("food", "meat")].iter().cloned().collect()), - ("dog", [("food", "meat")].iter().cloned().collect()), - ("tiger", [("food", "people")].iter().cloned().collect()), - ] - .iter() - .cloned() - .collect(); - assert_eq!( - walk_path(&context, &vec!["cat"]) - .unwrap() - .render(&Vec::new()) - .unwrap(), - "kitty".to_owned() - ); - assert_eq!( - walk_path(&number_context, &vec!["tiger"]) - .unwrap() - .render(&Vec::new()) - .unwrap(), - "3".to_owned() - ); - assert_eq!( - walk_path(&deep_context, &vec!["tiger", "food"]) - .unwrap() - .render(&Vec::new()) - .unwrap(), - "people".to_owned() - ); - } } From 3cf47fa1a8278d463660508c8ff92403e3df1a62 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Tue, 5 May 2020 20:46:31 -0400 Subject: [PATCH 13/13] Rename the new functions to replace the old functions. --- src/bin.rs | 2 +- src/renderer/renderer.rs | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/bin.rs b/src/bin.rs index 6d709fc..bb1c1f1 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -48,7 +48,7 @@ fn main() { println!( "{}", dust_renderer - .new_render(main_template_name, &breadcrumbs) + .render(main_template_name, &breadcrumbs) .expect("Failed to render") ); } diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index bbd1727..4a0b1eb 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -45,7 +45,7 @@ impl<'a> DustRenderer<'a> { .insert(template.name.clone(), &template.template); } - pub fn new_render( + pub fn render( &'a self, name: &str, breadcrumbs: &Vec<&'a dyn ContextElement>, @@ -59,10 +59,10 @@ impl<'a> DustRenderer<'a> { ))); } }; - self.new_render_body(&main_template.contents, breadcrumbs) + self.render_body(&main_template.contents, breadcrumbs) } - fn new_render_body( + fn render_body( &'a self, body: &'a Body, breadcrumbs: &Vec<&'a dyn ContextElement>, @@ -73,14 +73,14 @@ impl<'a> DustRenderer<'a> { TemplateElement::TEIgnoredWhitespace(_) => {} TemplateElement::TESpan(span) => output.push_str(span.contents), TemplateElement::TETag(dt) => { - output.push_str(&self.new_render_tag(dt, breadcrumbs)?); + output.push_str(&self.render_tag(dt, breadcrumbs)?); } } } Ok(output) } - fn new_render_tag( + fn render_tag( &'a self, tag: &'a DustTag, breadcrumbs: &Vec<&'a dyn ContextElement>, @@ -98,7 +98,7 @@ impl<'a> DustRenderer<'a> { .to_owned()) } DustTag::DTReference(reference) => { - let val = new_walk_path(breadcrumbs, &reference.path.keys); + 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()); @@ -107,7 +107,7 @@ impl<'a> DustRenderer<'a> { } } DustTag::DTSection(container) => { - let val = new_walk_path(breadcrumbs, &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 @@ -118,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.new_render_body(&body, breadcrumbs), + Some(body) => self.render_body(&body, breadcrumbs), None => Ok("".to_owned()), }; } else { @@ -130,7 +130,7 @@ impl<'a> DustRenderer<'a> { .map(|array_elem| { let mut new_breadcumbs = breadcrumbs.clone(); new_breadcumbs.push(array_elem); - self.new_render_body(&body, &new_breadcumbs) + self.render_body(&body, &new_breadcumbs) }) .collect(); let rendered_slice: &[String] = &rendered_results?; @@ -191,7 +191,7 @@ fn walk_path_from_single_level<'a>( Ok(WalkResult::FullyWalked(output)) } -fn new_walk_path<'a>( +fn walk_path<'a>( breadcrumbs: &Vec<&'a dyn ContextElement>, path: &'a Vec<&str>, ) -> Result<&'a dyn ContextElement, RenderError<'a>> { @@ -320,14 +320,14 @@ mod tests { .collect(); assert_eq!( - new_walk_path(&vec![&context as &dyn ContextElement], &vec!["cat"]) + walk_path(&vec![&context as &dyn ContextElement], &vec!["cat"]) .unwrap() .render(&Vec::new()) .unwrap(), "kitty".to_owned() ); assert_eq!( - new_walk_path( + walk_path( &vec![&number_context as &dyn ContextElement], &vec!["tiger"] ) @@ -337,7 +337,7 @@ mod tests { "3".to_owned() ); assert_eq!( - new_walk_path( + walk_path( &vec![&deep_context as &dyn ContextElement], &vec!["tiger", "food"] )