diff --git a/crates/core/src/model/find.rs b/crates/core/src/model/find.rs index fb8051c29..d6c687359 100644 --- a/crates/core/src/model/find.rs +++ b/crates/core/src/model/find.rs @@ -18,6 +18,10 @@ use super::ValueView; pub struct Path<'s>(Vec>); impl<'s> Path<'s> { + pub fn empty() -> Self { + Path(vec![]) + } + /// Create a `Value` reference. pub fn with_index>>(value: I) -> Self { let indexes = vec![value.into()]; diff --git a/crates/core/src/parser/grammar.pest b/crates/core/src/parser/grammar.pest index ecc678495..2ad79b864 100644 --- a/crates/core/src/parser/grammar.pest +++ b/crates/core/src/parser/grammar.pest @@ -29,7 +29,8 @@ Raw = @{ (!(TagStart | ExpressionStart) ~ ANY)+ } // Inner parsing Identifier = @{ (ASCII_ALPHA | "_" | NON_WHITESPACE_CONTROL_HYPHEN) ~ (ASCII_ALPHANUMERIC | "_" | NON_WHITESPACE_CONTROL_HYPHEN)* } -Variable = ${ Identifier +// For root level hash access we'd need to accept that there might not be an Identifier preceding a hash access (the brackets with a value inside) +Variable = ${ ( Identifier | ("[" ~ WHITESPACE* ~ Value ~ WHITESPACE* ~ "]") ) ~ ( ("." ~ Identifier) | ("[" ~ WHITESPACE* ~ Value ~ WHITESPACE* ~ "]") )* diff --git a/crates/core/src/parser/parser.rs b/crates/core/src/parser/parser.rs index a1dbed61e..724027061 100644 --- a/crates/core/src/parser/parser.rs +++ b/crates/core/src/parser/parser.rs @@ -142,11 +142,17 @@ fn parse_variable_pair(variable: Pair) -> Variable { let mut indexes = variable.into_inner(); let first_identifier = indexes - .next() - .expect("A variable starts with an identifier.") - .as_str() - .to_owned(); - let mut variable = Variable::with_literal(first_identifier); + .peek() + .expect("A variable starts with an identifier or an index"); + + let mut variable = match first_identifier.as_rule() { + Rule::Identifier => { + indexes.next(); + Variable::with_literal(first_identifier.as_str().to_owned()) + } + Rule::Value => Variable::empty(), + _ => unreachable!(), + }; let indexes = indexes.map(|index| match index.as_rule() { Rule::Identifier => Expression::with_literal(index.as_str().to_owned()), @@ -1153,6 +1159,38 @@ mod test { assert_eq!(parse_variable_pair(variable), expected); } + #[test] + fn test_parse_variable_pair_with_root_index_literals() { + let variable = LiquidParser::parse(Rule::Variable, "['bar']") + .unwrap() + .next() + .unwrap(); + + let indexes: Vec = vec![Expression::Literal(Value::scalar("bar"))]; + + let mut expected = Variable::empty(); + expected.extend(indexes); + + assert_eq!(parse_variable_pair(variable), expected); + } + + #[test] + fn test_parse_variable_pair_with_root_index_variable() { + let variable = LiquidParser::parse(Rule::Variable, "[foo.bar]") + .unwrap() + .next() + .unwrap(); + + let indexes = vec![Expression::Variable( + Variable::with_literal("foo").push_literal("bar"), + )]; + + let mut expected = Variable::empty(); + expected.extend(indexes); + + assert_eq!(parse_variable_pair(variable), expected); + } + #[test] fn test_whitespace_control() { let options = Language::default(); diff --git a/crates/core/src/runtime/variable.rs b/crates/core/src/runtime/variable.rs index 60a8f663b..d0c678e15 100644 --- a/crates/core/src/runtime/variable.rs +++ b/crates/core/src/runtime/variable.rs @@ -11,15 +11,22 @@ use super::Runtime; /// A `Value` reference. #[derive(Clone, Debug, PartialEq)] pub struct Variable { - variable: Scalar, + variable: Option, indexes: Vec, } impl Variable { + pub fn empty() -> Self { + Self { + variable: None, + indexes: Default::default(), + } + } + /// Create a `Value` reference. pub fn with_literal>(value: S) -> Self { Self { - variable: value.into(), + variable: Some(value.into()), indexes: Default::default(), } } @@ -32,7 +39,10 @@ impl Variable { /// Convert to a `Path`. pub fn try_evaluate<'c>(&'c self, runtime: &'c dyn Runtime) -> Option> { - let mut path = Path::with_index(self.variable.as_ref()); + let mut path = match self.variable.as_ref() { + Some(v) => Path::with_index(v.clone()), + None => Path::empty(), + }; path.reserve(self.indexes.len()); for expr in &self.indexes { let v = expr.try_evaluate(runtime)?; @@ -47,7 +57,10 @@ impl Variable { /// Convert to a `Path`. pub fn evaluate<'c>(&'c self, runtime: &'c dyn Runtime) -> Result> { - let mut path = Path::with_index(self.variable.as_ref()); + let mut path = match self.variable.as_ref() { + Some(v) => Path::with_index(v.clone()), + None => Path::empty(), + }; path.reserve(self.indexes.len()); for expr in &self.indexes { let v = expr.evaluate(runtime)?; diff --git a/tests/conformance_ruby/variable_test.rs b/tests/conformance_ruby/variable_test.rs index 30f284b88..3e14f5dda 100644 --- a/tests/conformance_ruby/variable_test.rs +++ b/tests/conformance_ruby/variable_test.rs @@ -11,6 +11,47 @@ fn test_simple_variable() { ); } +#[test] +fn test_simple_root_index_with_literal() { + assert_template_result!(r#"worked"#, r#"{{['test']}}"#, o!({"test": "worked"})); +} + +#[test] +fn test_variable_index_access_with_literal() { + assert_template_result!( + r#"worked"#, + r#"{{nested['test']}}"#, + o!({"nested": {"test": "worked"}}) + ); +} + +#[test] +fn test_nested_hash_access() { + assert_template_result!( + r#"worked"#, + r#"{{['nested']['test']}}"#, + o!({"nested": {"test": "worked"}}) + ); +} + +#[test] +fn test_nested_literal_and_variable_access() { + assert_template_result!( + r#"worked"#, + r#"{{['nested'].test}}"#, + o!({"nested": {"test": "worked"}}) + ); +} + +#[test] +fn test_root_index_with_variable() { + assert_template_result!( + r#"it did"#, + r#"{{[nested.test]}}"#, + o!({"nested": {"test": "worked"}, "worked": "it did" }) + ); +} + #[test] #[should_panic] fn test_variable_render_calls_to_liquid() {