From 4fafd7909a897c18bf6e358ebbd3714e390de606 Mon Sep 17 00:00:00 2001 From: Dmitry Dodzin Date: Sat, 7 Jun 2025 12:42:03 +0300 Subject: [PATCH 1/7] Allow every other Expr type to OperatorExpr --- src/parser/jslt.pest | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/parser/jslt.pest b/src/parser/jslt.pest index efddd4a..fbc51d5 100644 --- a/src/parser/jslt.pest +++ b/src/parser/jslt.pest @@ -2,8 +2,9 @@ Jslt = _{ SOI ~ Expr ~ EOI } Value = _{ Array | Object | Number | String | Boolean | Null | FunctionCall | Accessor | Scope | Variable } -Expr = _{ OperatorExpr | VariableDef | FunctionDef | IfStatement | Value } -OperatorExpr = { Value ~ (Operator ~ Value)+ } +Expr = _{ OperatorExpr | ValueExpr } +ValueExpr = _{ VariableDef | FunctionDef | IfStatement | Value } +OperatorExpr = { ValueExpr ~ (Operator ~ ValueExpr)+ } Operator = _{ Add | Sub | Div | Mul | Gte | Gt | Lte | Lt | And | Or | Equal | NotEqual } From 203729635d687db13def5fddea16d669ca3248c5 Mon Sep 17 00:00:00 2001 From: Dmitry Dodzin Date: Sat, 7 Jun 2025 13:33:23 +0300 Subject: [PATCH 2/7] Ident successor with double quotes --- src/parser/jslt.pest | 2 +- src/transform/value/accessor.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parser/jslt.pest b/src/parser/jslt.pest index fbc51d5..6dc5afc 100644 --- a/src/parser/jslt.pest +++ b/src/parser/jslt.pest @@ -28,7 +28,7 @@ Let = _{ "let" } Scope = { "(" ~ Expr ~ ")" } Accessor = ${ - (("." ~ Ident?) | KeyAccessor) ~ Accessor? + (("." ~ (Ident | String)?) | KeyAccessor) ~ Accessor? } KeyAccessor = !{ "[" ~ (RangeAccessor | Expr) ~ "]" } FromRangeAccessor = { Expr ~ ":" ~ Expr? } diff --git a/src/transform/value/accessor.rs b/src/transform/value/accessor.rs index cd25b1a..d585b60 100644 --- a/src/transform/value/accessor.rs +++ b/src/transform/value/accessor.rs @@ -99,6 +99,7 @@ impl FromPairs for AccessorTransformer { for pair in pairs { match pair.as_rule() { Rule::Ident => ident = Some(pair.as_str()), + Rule::String => ident = Some(pair.into_inner().as_str()), Rule::KeyAccessor => keys.push(KeyAccessorTransformer::from_pairs(&mut pair.into_inner())?), Rule::Accessor => { nested = Some(Box::new(AccessorTransformer::from_pairs( From b4fe93998bc8e19223e3f5d25bae9bfd908076a3 Mon Sep 17 00:00:00 2001 From: Dmitry Dodzin Date: Sat, 7 Jun 2025 13:41:04 +0300 Subject: [PATCH 3/7] Add tests --- src/lib.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 0ddff82..977eaf3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -288,6 +288,25 @@ mod tests { Ok(()) } + #[test] + fn quoted_string_accessor() -> Result<()> { + let jslt: Jslt = r#" + { + "result" : { + "Open" : ."menu"."popup"."menuitem"[0]."onclick", + "Close" : ."menu"."popup"."menuitem"[1]."onclick" + } + } + "# + .parse()?; + + let output = jslt.transform_value(&BASIC_INPUT)?; + + assert_eq!(&output, BASIC_OUTPUT.deref()); + + Ok(()) + } + #[rstest] #[case("[0]", "[1, 2, 3, 4, 5]")] #[case("[0][0:3]", "[1, 2, 3]")] @@ -297,6 +316,9 @@ mod tests { #[case("[0][0 : 3]", "[1, 2, 3]")] #[case("[0][2:]", "[3, 4, 5]")] #[case("[0][:3]", "[1, 2, 3]")] + #[case("[0][-3:]", "[3, 4, 5]")] + #[case("[0][:-2]", "[1, 2, 3]")] + #[case("[0][-4:-2]", "[2, 3]")] fn array_range(#[case] accessor: &str, #[case] expected: Value) -> Result<()> { let jslt: Jslt = format!("{{ \"result\" : .data{accessor} }}").parse()?; From cf3439fd3455cd60d22eb537d41dcdb5575131f6 Mon Sep 17 00:00:00 2001 From: Dmitry Dodzin Date: Sat, 7 Jun 2025 13:45:30 +0300 Subject: [PATCH 4/7] Ops --- src/transform/value/accessor.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/transform/value/accessor.rs b/src/transform/value/accessor.rs index d585b60..0a044a0 100644 --- a/src/transform/value/accessor.rs +++ b/src/transform/value/accessor.rs @@ -230,9 +230,6 @@ impl FromPairs for KeyAccessorTransformer { let inner = pairs.peek().ok_or(JsltError::UnexpectedEnd)?; match inner.as_rule() { - Rule::Number | Rule::String | Rule::Variable => Ok(KeyAccessorTransformer::Index( - ExprTransformer::from_pairs(pairs)?, - )), Rule::RangeAccessor => { let mut inner = pairs.next().expect("Sould be fine").into_inner(); @@ -270,7 +267,9 @@ impl FromPairs for KeyAccessorTransformer { _ => Err(JsltError::UnexpectedContent(Rule::RangeAccessor)), } } - _ => Err(JsltError::UnexpectedContent(Rule::KeyAccessor)), + _ => Ok(KeyAccessorTransformer::Index(ExprTransformer::from_pairs( + pairs, + )?)), } } } From 2a00c7441f207d7c4b9690c54fe0666a1f1671ac Mon Sep 17 00:00:00 2001 From: Dmitry Dodzin Date: Sat, 7 Jun 2025 14:22:05 +0300 Subject: [PATCH 5/7] More operations with null --- src/lib.rs | 61 +++++++++++++++++++++++++++++++++++++++++++ src/transform/expr.rs | 10 +++++++ 2 files changed, 71 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 977eaf3..502f439 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -307,6 +307,20 @@ mod tests { Ok(()) } + #[test] + fn inline_if() -> Result<()> { + let jslt: Jslt = r#" + "foobar" + if (false) 3000 else 2000 + "# + .parse()?; + + let output = jslt.transform_value(&BASIC_INPUT)?; + + assert_eq!(&output, "foobar2000"); + + Ok(()) + } + #[rstest] #[case("[0]", "[1, 2, 3, 4, 5]")] #[case("[0][0:3]", "[1, 2, 3]")] @@ -1207,6 +1221,12 @@ mod tests { #[case("1 < 3 and 4 * 2 > 5", "true")] #[case("1 <= 3 and 4 * 2 <= 8", "true")] #[case("[for ([1, 2, 3]) . if ( . > 2 )]", "[3]")] + #[case::null_add_string("null + \"-foobar\"", "\"null-foobar\"")] + #[case::string_add_null("\"foobar-\" + null", "\"foobar-null\"")] + #[case::null_add_array("null + [1, 2, 3]", "[1, 2, 3]")] + #[case::array_add_null("[1, 2, 3] + null", "[1, 2, 3]")] + #[case::null_add_object("null + { \"foo\": \"bar\" }", "{ \"foo\": \"bar\" }")] + #[case::object_add_null("{ \"foo\": \"bar\" } + null", "{ \"foo\": \"bar\" }")] fn operators(#[case] jslt: &str, #[case] expected: Value) -> Result<()> { let jslt: Jslt = jslt.parse()?; @@ -1217,6 +1237,47 @@ mod tests { Ok(()) } + #[rstest] + #[case::null_add_null("null + null")] + #[case::null_add_number("null + 1")] + #[case::number_add_null("1 + null")] + #[case::null_sub_null("null - null")] + #[case::null_sub_number(r#"null - 1"#)] + #[case::number_sub_null(r#"1 - null"#)] + #[case::null_sub_string(r#"null - "-foobar""#)] + #[case::string_sub_null(r#""foobar-" - null"#)] + #[case::null_sub_array(r#"null - [1, 2, 3]"#)] + #[case::array_sub_null(r#"[1, 2, 3] - null"#)] + #[case::null_sub_object(r#"null - { "foo": "bar" }"#)] + #[case::object_sub_null(r#"{ "foo": "bar" } - null"#)] + #[case::null_mul_null("null * null")] + #[case::null_mul_number(r#"null * 1"#)] + #[case::number_mul_null(r#"1 * null"#)] + #[case::null_mul_string(r#"null * "-foobar""#)] + #[case::string_mul_null(r#""foobar-" * null"#)] + #[case::null_mul_array(r#"null * [1, 2, 3]"#)] + #[case::array_mul_null(r#"[1, 2, 3] * null"#)] + #[case::null_mul_object(r#"null * { "foo": "bar" }"#)] + #[case::object_mul_null(r#"{ "foo": "bar" } * null"#)] + #[case::null_div_null("null / null")] + #[case::null_div_number(r#"null / 1"#)] + #[case::number_div_null(r#"1 / null"#)] + #[case::null_div_string(r#"null / "-foobar""#)] + #[case::string_div_null(r#""foobar-" / null"#)] + #[case::null_div_array(r#"null / [1, 2, 3]"#)] + #[case::array_div_null(r#"[1, 2, 3] / null"#)] + #[case::null_div_object(r#"null / { "foo": "bar" }"#)] + #[case::object_div_null(r#"{ "foo": "bar" } / null"#)] + fn operators_with_null_resulting_in_null(#[case] jslt: &str) -> Result<()> { + let jslt: Jslt = jslt.parse()?; + + let output = jslt.transform_value(&Value::Null)?; + + assert_eq!(output, Value::Null); + + Ok(()) + } + #[rstest] #[case("def my_add(a, b)\n $a + $b \n my_add(1, 2)", "3")] #[case("def my_add(a, b)\n let foo = $a \n $foo + $b \n my_add(1, 2)", "3")] diff --git a/src/transform/expr.rs b/src/transform/expr.rs index 5f5c043..259ba87 100644 --- a/src/transform/expr.rs +++ b/src/transform/expr.rs @@ -196,6 +196,11 @@ impl Transform for OperatorExprTransformer { match self.operator { OperatorTransformer::Add => match (&left, &right) { + (Value::Null, Value::Number(_)) + | (Value::Number(_), Value::Null) + | (Value::Null, Value::Null) => Ok(Value::Null), + (Value::Array(_) | Value::Object(_), Value::Null) => Ok(left), + (Value::Null, Value::Array(_) | Value::Object(_)) => Ok(right), (Value::Array(left), Value::Array(right)) => Ok(Value::Array( left.clone().into_iter().chain(right.clone()).collect(), )), @@ -213,6 +218,8 @@ impl Transform for OperatorExprTransformer { (left.as_f64().expect("Should be f64") + right.as_f64().expect("Should be f64")).into(), ), (Value::String(left), Value::String(right)) => Ok(Value::String(format!("{left}{right}"))), + (left, Value::String(right)) => Ok(Value::String(format!("{left}{right}"))), + (Value::String(left), right) => Ok(Value::String(format!("{left}{right}"))), (Value::Object(left), Value::Object(right)) => Ok(Value::Object( left .into_iter() @@ -225,6 +232,7 @@ impl Transform for OperatorExprTransformer { ))), }, OperatorTransformer::Sub => match (&left, &right) { + (Value::Null, _) | (_, Value::Null) => Ok(Value::Null), (Value::Number(left), Value::Number(right)) if left.is_u64() && right.is_u64() => { Ok(Value::Number( (left.as_u64().expect("Should be u64") - right.as_u64().expect("Should be u64")).into(), @@ -243,6 +251,7 @@ impl Transform for OperatorExprTransformer { ))), }, OperatorTransformer::Mul => match (&left, &right) { + (Value::Null, _) | (_, Value::Null) => Ok(Value::Null), (Value::String(left), Value::Number(right)) if right.is_u64() => Ok(Value::String( std::iter::from_fn(|| Some(left.clone())) .take(right.as_u64().expect("Should be u64") as usize) @@ -266,6 +275,7 @@ impl Transform for OperatorExprTransformer { ))), }, OperatorTransformer::Div => match (&left, &right) { + (Value::Null, _) | (_, Value::Null) => Ok(Value::Null), (Value::Number(left), Value::Number(right)) if left.is_u64() && right.is_u64() => { Ok(Value::Number( (left.as_u64().expect("Should be u64") / right.as_u64().expect("Should be u64")).into(), From e72b3767d13ff0c8bde419055235fcba18b7303a Mon Sep 17 00:00:00 2001 From: Dmitry Dodzin Date: Sat, 7 Jun 2025 14:22:59 +0300 Subject: [PATCH 6/7] get-key function with null --- src/context/builtins.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/context/builtins.rs b/src/context/builtins.rs index edfe9a9..3825de4 100644 --- a/src/context/builtins.rs +++ b/src/context/builtins.rs @@ -670,12 +670,12 @@ pub fn is_object(maybe_object: &Value) -> Result { #[static_function] pub fn get_key(object: &Value, key: &Value, fallback: Option<&Value>) -> Result { match (object, key) { + (Value::Null, _) | (_, Value::Null) => Ok(fallback.map(Value::clone).unwrap_or_default()), (Value::Object(map), Value::String(key)) => match (map.get(key), fallback) { (Some(Value::Null) | None, Some(fallback)) => Ok(fallback.clone()), (Some(value), _) => Ok(value.clone()), (None, None) => Ok(Value::Null), }, - (Value::Null, _) => Ok(fallback.map(Value::clone).unwrap_or_default()), _ => Err(JsltError::InvalidInput( "Input of get-key must be object with string key".to_string(), )), From 5d21dac40f8b3b9d053af47290d1ad0d9d30f69d Mon Sep 17 00:00:00 2001 From: Dmitry Dodzin Date: Sat, 7 Jun 2025 14:29:08 +0300 Subject: [PATCH 7/7] Simplify CI --- .github/workflows/rust.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 384e5a0..abf7b30 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -10,16 +10,22 @@ env: CARGO_TERM_COLOR: always jobs: - build: + test: + name: cargo test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Setup Rust - run: rustup toolchain install nightly --profile minimal - - name: Setup ZigBuild - run: pip install cargo-zigbuild - - uses: Swatinem/rust-cache@v2 - - name: Build - run: cargo zigbuild --workspace --all-features --verbose + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Run tests run: cargo test --workspace --all-features --verbose + + formatting: + name: cargo fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: rustfmt + - name: Rustfmt Check + uses: actions-rust-lang/rustfmt@v1