use crate::parser::template; use crate::parser::Body; use crate::parser::DustTag; use crate::parser::KVPair; use crate::parser::PartialNameElement; use crate::parser::Path; use crate::parser::RValue; use crate::parser::Special; use crate::parser::Template; use crate::parser::{Filter, TemplateElement}; use crate::renderer::context_element::ContextElement; use crate::renderer::errors::CompileError; use crate::renderer::errors::RenderError; use crate::renderer::errors::WalkError; use crate::renderer::inline_partial_tree::extract_inline_partials; use crate::renderer::inline_partial_tree::InlinePartialTreeElement; use crate::renderer::iteration_context::IterationContext; use crate::renderer::parameters_context::ParametersContext; use crate::renderer::walking::walk_path; use std::collections::HashMap; #[derive(Clone, Debug)] pub struct CompiledTemplate<'a> { template: Template<'a>, pub name: String, } #[derive(Clone, Debug)] pub struct DustRenderer<'a> { templates: HashMap>, } pub fn compile_template<'a>( source: &'a str, name: String, ) -> Result, CompileError> { // TODO: This could use better error management let (_remaining, parsed_template) = template(source).expect("Failed to compile template"); Ok(CompiledTemplate { template: parsed_template, name: name, }) } impl<'a> DustRenderer<'a> { pub fn new() -> DustRenderer<'a> { DustRenderer { templates: HashMap::new(), } } pub fn load_source(&mut self, template: &'a CompiledTemplate) { self.templates .insert(template.name.clone(), &template.template); } pub fn render( &'a self, name: &str, breadcrumbs: &Vec<&'a dyn ContextElement>, ) -> Result { self.render_template(name, breadcrumbs, None) } fn render_template( &'a self, name: &str, breadcrumbs: &Vec<&'a dyn ContextElement>, blocks: Option<&'a InlinePartialTreeElement<'a>>, ) -> Result { let main_template = match self.templates.get(name) { Some(tmpl) => tmpl, None => { return Err(RenderError::TemplateNotFound(name.to_owned())); } }; let extracted_inline_partials = extract_inline_partials(main_template); let new_blocks = InlinePartialTreeElement::new(blocks, extracted_inline_partials); let new_block_context = BlockContext { breadcrumbs: breadcrumbs, blocks: &new_blocks, }; self.render_body(&main_template.contents, breadcrumbs, &new_block_context) } fn render_maybe_body( &'a self, body: &'a Option, breadcrumbs: &Vec<&'a dyn ContextElement>, blocks: &'a BlockContext<'a>, ) -> Result { match body { None => Ok("".to_owned()), Some(body) => Ok(self.render_body(body, breadcrumbs, blocks)?), } } fn render_body( &'a self, body: &'a Body, breadcrumbs: &Vec<&'a dyn ContextElement>, blocks: &'a BlockContext<'a>, ) -> 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.render_tag(dt, breadcrumbs, blocks)?); } } } Ok(output) } /// For rendering a dynamic partial's name fn render_partial_name( &'a self, body: &'a Vec, breadcrumbs: &Vec<&'a dyn ContextElement>, blocks: &'a BlockContext<'a>, ) -> Result { let converted_to_template_elements: Vec> = body.into_iter().map(|e| e.into()).collect(); self.render_body( &Body { elements: converted_to_template_elements, }, breadcrumbs, blocks, ) } fn render_tag( &'a self, tag: &'a DustTag, breadcrumbs: &Vec<&'a dyn ContextElement>, blocks: &'a BlockContext<'a>, ) -> 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::DTLiteralStringBlock(literal) => return Ok((*literal).to_owned()), DustTag::DTReference(reference) => { let val = walk_path(breadcrumbs, &reference.path.keys); match val { Err(WalkError::CantWalk) => return Ok("".to_owned()), Ok(final_val) => { return if final_val.is_truthy() { final_val.render(&Self::preprocess_filters(&reference.filters)) } else { Ok("".to_owned()) }; } } } DustTag::DTSection(container) => { let val = walk_path(breadcrumbs, &container.path.keys); match val { Err(WalkError::CantWalk) => { let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, None, &container.explicit_context, None, ); return self.render_maybe_body( &container.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } Ok(final_val) => { return if final_val.is_truthy() { match &container.contents { // If the body is empty, just shortcut // to an empty string now rather than // generating intermediate contexts // and iterating for nothing. None => Ok("".to_owned()), Some(body) => { let loop_elements: Vec<&dyn ContextElement> = final_val.get_loop_elements(); if loop_elements.is_empty() { // Scalar value let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, None, &container.explicit_context, Some(final_val), ); self.render_body( body, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ) } else { // Array-like value let total_length = loop_elements.len(); let rendered_results: Result, RenderError> = loop_elements .into_iter() .enumerate() .map(|(i, array_elem)| { let injected_context = IterationContext::new(i, total_length); let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, Some(&injected_context), &container.explicit_context, Some(array_elem), ); self.render_body( &body, new_breadcrumbs .as_ref() .unwrap_or(breadcrumbs), blocks, ) }) .collect(); let rendered_slice: &[String] = &rendered_results?; return Ok(rendered_slice.join("")); } } } } else { // 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 let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, None, &container.explicit_context, None, ); return self.render_maybe_body( &container.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); }; } } } DustTag::DTExists(container) => { let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, None, &container.explicit_context, None, ); let val = walk_path(breadcrumbs, &container.path.keys); return if val.map(|v| v.is_truthy()).unwrap_or(false) { self.render_maybe_body( &container.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ) } else { self.render_maybe_body( &container.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ) }; } DustTag::DTNotExists(container) => { let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, None, &container.explicit_context, None, ); let val = walk_path(breadcrumbs, &container.path.keys); return if !val.map(|v| v.is_truthy()).unwrap_or(false) { self.render_maybe_body( &container.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ) } else { self.render_maybe_body( &container.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ) }; } DustTag::DTPartial(partial) => { let partial_name = self.render_partial_name(&partial.name, breadcrumbs, blocks)?; if partial.params.is_empty() { let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, None, &partial.explicit_context, None, ); let rendered_content = self.render_template( &partial_name, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), Some(blocks.blocks), )?; return Ok(rendered_content); } else { let injected_context = ParametersContext::new(breadcrumbs, &partial.params); let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, Some(&injected_context), &partial.explicit_context, None, ); let rendered_content = self.render_template( &partial_name, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), Some(blocks.blocks), )?; return Ok(rendered_content); } } DustTag::DTInlinePartial(_named_block) => { // Inline partials are blank during rendering (they get injected into blocks) return Ok("".to_owned()); } DustTag::DTBlock(named_block) => { let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, blocks.breadcrumbs, None, &named_block.explicit_context, None, ); return match blocks.blocks.get_block(named_block.name) { None => self.render_maybe_body( &named_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ), Some(inline_partial) => self.render_maybe_body( inline_partial, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ), }; } DustTag::DTHelperEquals(parameterized_block) => { let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, None, ¶meterized_block.explicit_context, None, ); let param_map: HashMap<&str, &RValue<'a>> = Self::get_rval_map(¶meterized_block.params); // Special case: when comparing two RVPaths, if the // path is equal then dust assumes the values are // equal (otherwise, non-scalar values are // automatically not equal) if Self::are_paths_identical(¶m_map) { return match ¶meterized_block.contents { None => Ok("".to_owned()), Some(body) => { let rendered_content = self.render_body( body, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, )?; Ok(rendered_content) } }; } let left_side: Result<&dyn ContextElement, WalkError> = match Self::get_rval(breadcrumbs, ¶m_map, "key") { None => return Ok("".to_owned()), Some(res) => res, }; let right_side: Result<&dyn ContextElement, WalkError> = Self::get_rval(breadcrumbs, ¶m_map, "value") .unwrap_or(Err(WalkError::CantWalk)); if left_side == right_side { return self.render_maybe_body( ¶meterized_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } else { return self.render_maybe_body( ¶meterized_block.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } } DustTag::DTHelperNotEquals(parameterized_block) => { let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, None, ¶meterized_block.explicit_context, None, ); let param_map: HashMap<&str, &RValue<'a>> = Self::get_rval_map(¶meterized_block.params); // Special case: when comparing two RVPaths, if the // path is equal then dust assumes the values are // equal (otherwise, non-scalar values are // automatically not equal) if Self::are_paths_identical(¶m_map) { return match ¶meterized_block.else_contents { None => Ok("".to_owned()), Some(body) => { let rendered_content = self.render_body( body, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, )?; Ok(rendered_content) } }; } let left_side: Result<&dyn ContextElement, WalkError> = match Self::get_rval(breadcrumbs, ¶m_map, "key") { None => return Ok("".to_owned()), Some(res) => res, }; let right_side: Result<&dyn ContextElement, WalkError> = Self::get_rval(breadcrumbs, ¶m_map, "value") .unwrap_or(Err(WalkError::CantWalk)); if left_side != right_side { return self.render_maybe_body( ¶meterized_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } else { return self.render_maybe_body( ¶meterized_block.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } } DustTag::DTHelperGreaterThan(parameterized_block) => { let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, None, ¶meterized_block.explicit_context, None, ); let param_map: HashMap<&str, &RValue<'a>> = Self::get_rval_map(¶meterized_block.params); let left_side: Result<&dyn ContextElement, WalkError> = match Self::get_rval(breadcrumbs, ¶m_map, "key") { None => return Ok("".to_owned()), Some(res) => res, }; let right_side: Result<&dyn ContextElement, WalkError> = Self::get_rval(breadcrumbs, ¶m_map, "value") .unwrap_or(Err(WalkError::CantWalk)); match (left_side, right_side) { (Err(_), _) | (_, Err(_)) => { return self.render_maybe_body( ¶meterized_block.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ) } (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { if left_side_unwrapped > right_side_unwrapped { return self.render_maybe_body( ¶meterized_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } else { return self.render_maybe_body( ¶meterized_block.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } } } } DustTag::DTHelperGreaterThanOrEquals(parameterized_block) => { let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, None, ¶meterized_block.explicit_context, None, ); let param_map: HashMap<&str, &RValue<'a>> = Self::get_rval_map(¶meterized_block.params); let left_side: Result<&dyn ContextElement, WalkError> = match Self::get_rval(breadcrumbs, ¶m_map, "key") { None => return Ok("".to_owned()), Some(res) => res, }; let right_side: Result<&dyn ContextElement, WalkError> = Self::get_rval(breadcrumbs, ¶m_map, "value") .unwrap_or(Err(WalkError::CantWalk)); match (left_side, right_side) { (Err(_), _) | (_, Err(_)) => { return self.render_maybe_body( ¶meterized_block.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ) } (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { if left_side_unwrapped >= right_side_unwrapped { return self.render_maybe_body( ¶meterized_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } else { return self.render_maybe_body( ¶meterized_block.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } } } } DustTag::DTHelperLessThan(parameterized_block) => { let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, None, ¶meterized_block.explicit_context, None, ); let param_map: HashMap<&str, &RValue<'a>> = Self::get_rval_map(¶meterized_block.params); let left_side: Result<&dyn ContextElement, WalkError> = match Self::get_rval(breadcrumbs, ¶m_map, "key") { None => return Ok("".to_owned()), Some(res) => res, }; let right_side: Result<&dyn ContextElement, WalkError> = Self::get_rval(breadcrumbs, ¶m_map, "value") .unwrap_or(Err(WalkError::CantWalk)); match (left_side, right_side) { (Err(_), _) | (_, Err(_)) => { return self.render_maybe_body( ¶meterized_block.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ) } (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { if left_side_unwrapped < right_side_unwrapped { return self.render_maybe_body( ¶meterized_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } else { return self.render_maybe_body( ¶meterized_block.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } } } } DustTag::DTHelperLessThanOrEquals(parameterized_block) => { let new_breadcrumbs = Self::new_breadcrumbs( breadcrumbs, breadcrumbs, None, ¶meterized_block.explicit_context, None, ); let param_map: HashMap<&str, &RValue<'a>> = Self::get_rval_map(¶meterized_block.params); let left_side: Result<&dyn ContextElement, WalkError> = match Self::get_rval(breadcrumbs, ¶m_map, "key") { None => return Ok("".to_owned()), Some(res) => res, }; let right_side: Result<&dyn ContextElement, WalkError> = Self::get_rval(breadcrumbs, ¶m_map, "value") .unwrap_or(Err(WalkError::CantWalk)); match (left_side, right_side) { (Err(_), _) | (_, Err(_)) => { return self.render_maybe_body( ¶meterized_block.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ) } (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { if left_side_unwrapped <= right_side_unwrapped { return self.render_maybe_body( ¶meterized_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } else { return self.render_maybe_body( ¶meterized_block.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } } } } } Ok("".to_owned()) } /// Gets the elements to loop over for a section. /// /// If the value is falsey, and therefore should render the else /// block, this will return an empty vector. fn get_loop_elements<'b>( walk_result: Result<&'b dyn ContextElement, WalkError>, ) -> Vec<&'b dyn ContextElement> { match walk_result { Err(WalkError::CantWalk) => Vec::new(), Ok(walk_target) => walk_target.get_loop_elements(), } } fn are_paths_identical<'b>(param_map: &HashMap<&str, &RValue<'b>>) -> bool { match (param_map.get("key"), param_map.get("value")) { (None, _) => false, (_, None) => false, (Some(key_rval), Some(value_rval)) => match (key_rval, value_rval) { (RValue::RVPath(key_path), RValue::RVPath(value_path)) => { key_path.keys == value_path.keys } _ => false, }, } } fn get_rval_map<'b>(params: &'b Vec>) -> HashMap<&'b str, &'b RValue<'b>> { params .iter() .map(|pair: &KVPair<'b>| (pair.key, &pair.value)) .collect() } fn get_rval<'b>( breadcrumbs: &'b Vec<&'b dyn ContextElement>, param_map: &HashMap<&str, &'b RValue<'b>>, key: &str, ) -> Option> { match param_map.get(key) { None => None, Some(rval) => match rval { RValue::RVLiteral(literal) => Some(Ok(literal)), RValue::RVPath(path) => Some(walk_path(breadcrumbs, &path.keys)), }, } } fn preprocess_filters(filters: &Vec) -> Vec { let mut final_filters: Vec = filters .into_iter() .filter(|f| f != &&Filter::DisableHtmlEncode) .map(|f| f.clone()) .collect(); // If the user has not specified any escaping filter (|s or // |h), automatically add an html escape filter if !filters.iter().any(|f| f == &Filter::DisableHtmlEncode) { final_filters.push(Filter::HtmlEncode); } final_filters } /// Generate a new breadcrumbs object /// /// This function generates a new breadcrumbs object based on the /// new context information provided. /// /// breadcrumbs are the breadcrumbs that will be used in the final /// breadcrumbs (unless omitted due to an explicit context) /// /// explicit_context_breadcrumbs are the breadcrumbs used to /// evaluate an explicit context path. Most of the time the two /// breadcrumbs parameters will be identical, but for /// blocks/inline partials the explicit_context_breadcrumbs will /// be the breadcrumbs from the start of the partial containing /// the block. /// /// explicit_context is for contexts specified with a `:path` /// inside a dust tag. /// /// injected_context is for any generated context. This includes /// both parameters on a tag and also the handling of $idx and /// $len. /// /// New context element is the element is an element to append to /// the end, generally for use in section tags which walk to a new /// context. /// /// If explicit_context is not None, then the final breadcrumb stack will be: /// /// ```text /// breadcrumbs /// injected_context /// new_context_element /// ``` /// /// However, if explicit_context is not None, then the old /// breadcrumbs are omitted, leading to the new breadcrumb stack /// as: /// /// ```text /// injected_context /// explicit_context /// new_context_element /// ``` fn new_breadcrumbs<'b>( breadcrumbs: &'b Vec<&'b dyn ContextElement>, explicit_context_breadcrumbs: &'b Vec<&'b dyn ContextElement>, injected_context: Option<&'b dyn ContextElement>, explicit_context: &Option>, new_context_element: Option<&'b dyn ContextElement>, ) -> Option> { // If none of the additional contexts are present, return None // to signal that the original breadcrumbs should be used // rather than incurring a copy here. match (injected_context, explicit_context, new_context_element) { (None, None, None) => return None, _ => (), }; let mut new_stack = match explicit_context { Some(_) => Vec::with_capacity(3), None => breadcrumbs.clone(), }; // TODO: Can sections have parameters, and if so, what happens then? Currently when there is an injected context or an explicit context it gets inserted behind the current context, so 1->2->3 becomes 1->2->injected->3 or explicit->3. When there is a new context(4) with injected we're doing 1->2->3->injected->4. When there is an explicit context and a new context we're doing explicit->4. But what happens if there is a section with parameters and an explicit context, hitting all the categories? Would it be parameters->explicit->4? I would definitely have to change the parameters to this function since right now iteration variables and parameters are both sharing injected_context. injected_context.map(|ctx| { // Special case: when there is no explicit context or new // context element, the injected context gets inserted 1 // spot behind the current context. Otherwise, the // injected context gets added after the current context // but before the explicit context and new context // element. match (explicit_context, new_context_element) { (None, None) => new_stack.insert(std::cmp::max(new_stack.len() - 1, 0), ctx), _ => new_stack.push(ctx), } }); explicit_context.as_ref().map(|path| { walk_path(explicit_context_breadcrumbs, &path.keys).map(|val| { if val.is_truthy() { new_stack.push(val) } }); }); new_context_element.map(|ctx| new_stack.push(ctx)); Some(new_stack) } } struct BlockContext<'a> { /// The breadcrumbs at the time of entering the current partial breadcrumbs: &'a Vec<&'a dyn ContextElement>, blocks: &'a InlinePartialTreeElement<'a>, } #[cfg(test)] mod tests { use super::*; use crate::parser::Filter; use crate::renderer::context_element::Loopable; use crate::renderer::context_element::Renderable; use crate::renderer::context_element::Truthiness; use crate::renderer::context_element::Walkable; use crate::renderer::CompareContextElement; use std::cmp::Ordering; impl ContextElement for String {} impl Truthiness for String { fn is_truthy(&self) -> bool { !self.is_empty() } } impl Renderable for String { fn render(&self, _filters: &Vec) -> Result { Ok(self.clone()) } } impl Loopable for String { fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { Vec::new() } } impl Walkable for String { fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { Err(WalkError::CantWalk) } } impl CompareContextElement for String { fn equals(&self, other: &dyn ContextElement) -> bool { match other.to_any().downcast_ref::() { None => false, Some(other_string) => self == other_string, } } fn partial_compare(&self, other: &dyn ContextElement) -> Option { match other.to_any().downcast_ref::() { None => None, Some(other_string) => self.partial_cmp(other_string), } } } impl ContextElement for u64 {} impl Truthiness for u64 { fn is_truthy(&self) -> bool { true } } impl Renderable for u64 { fn render(&self, _filters: &Vec) -> Result { Ok(self.to_string()) } } impl Loopable for u64 { fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { Vec::new() } } impl Walkable for u64 { fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { Err(WalkError::CantWalk) } } impl CompareContextElement for u64 { fn equals(&self, other: &dyn ContextElement) -> bool { match other.to_any().downcast_ref::() { None => false, Some(other_num) => self == other_num, } } fn partial_compare(&self, other: &dyn ContextElement) -> Option { match other.to_any().downcast_ref::() { None => None, Some(other_num) => self.partial_cmp(other_num), } } } impl ContextElement for HashMap {} impl Truthiness for HashMap { fn is_truthy(&self) -> bool { true } } impl Renderable for HashMap { fn render(&self, _filters: &Vec) -> Result { // TODO: handle the filters Ok("[object Object]".to_owned()) } } impl Walkable for HashMap { fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { let child = self.get(segment).ok_or(WalkError::CantWalk)?; Ok(child) } } impl Loopable for HashMap { fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { Vec::new() } } impl CompareContextElement for HashMap { fn equals(&self, other: &dyn ContextElement) -> bool { false } fn partial_compare(&self, other: &dyn ContextElement) -> Option { // TODO: Implement None } } #[test] fn test_walk_path() { let context: HashMap = [ ("cat".to_string(), "kitty".to_string()), ("dog".to_string(), "doggy".to_string()), ("tiger".to_string(), "murderkitty".to_string()), ] .iter() .cloned() .collect(); let number_context: HashMap = [ ("cat".to_string(), 1), ("dog".to_string(), 2), ("tiger".to_string(), 3), ] .iter() .cloned() .collect(); let deep_context: HashMap> = [ ( "cat".to_string(), [("food".to_string(), "meat".to_string())] .iter() .cloned() .collect(), ), ( "dog".to_string(), [("food".to_string(), "meat".to_string())] .iter() .cloned() .collect(), ), ( "tiger".to_string(), [("food".to_string(), "people".to_string())] .iter() .cloned() .collect(), ), ] .iter() .cloned() .collect(); assert_eq!( walk_path(&vec![&context as &dyn ContextElement], &vec!["cat"]) .unwrap() .render(&Vec::new()) .unwrap(), "kitty".to_owned() ); assert_eq!( walk_path( &vec![&number_context as &dyn ContextElement], &vec!["tiger"] ) .unwrap() .render(&Vec::new()) .unwrap(), "3".to_owned() ); assert_eq!( walk_path( &vec![&deep_context as &dyn ContextElement], &vec!["tiger", "food"] ) .unwrap() .render(&Vec::new()) .unwrap(), "people".to_owned() ); } }