From a8ed70e01b075928e7f84611d0df9cad8d4a15e6 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 13:44:50 -0700 Subject: [PATCH 1/8] fix wildcard handling --- src/jsonata/Jsonata.cpp | 12 ------------ src/jsonata/Utils.cpp | 16 +--------------- test/ArrayTest.cpp | 27 +++++++++++++++++++++++++-- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/jsonata/Jsonata.cpp b/src/jsonata/Jsonata.cpp index bf0370d..2072a6b 100644 --- a/src/jsonata/Jsonata.cpp +++ b/src/jsonata/Jsonata.cpp @@ -775,18 +775,6 @@ std::any Jsonata::evaluateWildcard(std::shared_ptr expr, auto flattenedVec = Utils::arrayify(flattened); Utils::JList appendArgs = {results, flattenedVec}; results = Utils::arrayify(Functions::append(appendArgs)); - } else if (value.has_value() && - value.type() == - typeid(nlohmann::ordered_map)) { - // Recursively call evaluateWildcard on map objects - auto recursiveResult = evaluateWildcard(expr, value); - if (recursiveResult.has_value() && - Utils::isArray(recursiveResult)) { - auto recursiveVec = Utils::arrayify(recursiveResult); - results.insert(results.end(), recursiveVec.begin(), - recursiveVec.end()); - } } else { results.push_back(value); } diff --git a/src/jsonata/Utils.cpp b/src/jsonata/Utils.cpp index a216c81..af969c3 100644 --- a/src/jsonata/Utils.cpp +++ b/src/jsonata/Utils.cpp @@ -289,21 +289,7 @@ Utils::JList Utils::createSequence(const std::any& el) { } if (!isNoneSentinel) { - if (el.type() == typeid(Utils::JList)) { - auto vec = arrayify(el); - // If single element list, add that element; otherwise add the list - // itself - if (vec.size() == 1) { - sequence.push_back(vec[0]); - } else { - sequence.push_back(el); - } - } else { - // Add the element directly, even if it is an explicit JSON null or - // an undefined/empty value. This matches Java's - // Utils.createSequence which adds null as an element (el != NONE). - sequence.push_back(el); - } + sequence.push_back(el); } return sequence; diff --git a/test/ArrayTest.cpp b/test/ArrayTest.cpp index 3fa5c73..4a93287 100644 --- a/test/ArrayTest.cpp +++ b/test/ArrayTest.cpp @@ -36,11 +36,34 @@ TEST_F(ArrayTest, DISABLED_filterTest) { // Frame value not evaluated if used in array filter #45 // This test is disabled as in the Java version Jsonata expr("($arr := [{'x':1}, {'x':2}];$arr[x=$number(variable.field)])"); - + auto inputData = nlohmann::ordered_json::parse(R"({"variable": {"field": "1"}})"); - + auto result = expr.evaluate(inputData); EXPECT_TRUE(result != nullptr); } +TEST_F(ArrayTest, testWildcard) { + Jsonata expr("*"); + auto input = nlohmann::ordered_json::parse(R"([{"x": 1}])"); + auto result = expr.evaluate(input); + auto expected = nlohmann::ordered_json::parse(R"({"x": 1})"); + EXPECT_EQ(result.dump(), expected.dump()); +} + +TEST_F(ArrayTest, testWildcardFilter) { + auto data = nlohmann::ordered_json::parse( + R"([{"value": {"Name": "Cell1", "Product": "Product1"}}, {"value": {"Name": "Cell2", "Product": "Product2"}}])"); + + Jsonata expression("*[value.Product = 'Product1']"); + auto result1 = expression.evaluate(data); + auto expected = nlohmann::ordered_json::parse( + R"({"value": {"Name": "Cell1", "Product": "Product1"}})"); + EXPECT_EQ(result1.dump(), expected.dump()); + + Jsonata expression2("**[value.Product = 'Product1']"); + auto result2 = expression2.evaluate(data); + EXPECT_EQ(result2.dump(), expected.dump()); +} + } // namespace jsonata From 4224774cc620062ceacc07e0614db2e85fc67379 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 13:57:32 -0700 Subject: [PATCH 2/8] avoid tuplebindings to be empty --- src/jsonata/Jsonata.cpp | 3 --- test/ArrayTest.cpp | 10 ++++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/jsonata/Jsonata.cpp b/src/jsonata/Jsonata.cpp index 2072a6b..9a0c288 100644 --- a/src/jsonata/Jsonata.cpp +++ b/src/jsonata/Jsonata.cpp @@ -3385,9 +3385,6 @@ std::any Jsonata::evaluateTupleStep( // Java line 435: create initial tuple bindings (only when tupleBindings // is null, not just empty) for (const auto& item : input) { - if (!item.has_value()) { - continue; - } nlohmann::ordered_map tuple; tuple["@"] = item; bindings.push_back(std::any(tuple)); diff --git a/test/ArrayTest.cpp b/test/ArrayTest.cpp index 4a93287..a4ecb0d 100644 --- a/test/ArrayTest.cpp +++ b/test/ArrayTest.cpp @@ -43,6 +43,16 @@ TEST_F(ArrayTest, DISABLED_filterTest) { EXPECT_TRUE(result != nullptr); } +TEST_F(ArrayTest, testIndex) { + Jsonata expr("($x:=['a','b']; $x#$i.$i)"); + auto result1 = expr.evaluate(nlohmann::ordered_json(1)); + auto expected = nlohmann::ordered_json::parse("[0, 1]"); + EXPECT_EQ(result1.dump(), expected.dump()); + + auto result2 = expr.evaluate(nlohmann::ordered_json(nullptr)); + EXPECT_EQ(result2.dump(), expected.dump()); +} + TEST_F(ArrayTest, testWildcard) { Jsonata expr("*"); auto input = nlohmann::ordered_json::parse(R"([{"x": 1}])"); From 5ae3f2ce784b3ba6517ec8d777f3a6f464049699 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 14:07:47 -0700 Subject: [PATCH 3/8] Fix index increment in Signature validation --- test/SignatureTest.cpp | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test/SignatureTest.cpp b/test/SignatureTest.cpp index c9a4e0e..a49c372 100644 --- a/test/SignatureTest.cpp +++ b/test/SignatureTest.cpp @@ -93,4 +93,55 @@ TEST_F(SignatureTest, testVarArg) { EXPECT_EQ(result.get(), 6); } +TEST_F(SignatureTest, testVarArgMany) { + Jsonata expr("$customArgs('test',[1,2,3,4],3)"); + + JFunction customArgsFn; + customArgsFn.signature = std::make_shared("n:s>", "customArgs"); + customArgsFn.implementation = [](const Utils::JList& args, const std::any&, std::shared_ptr) -> std::any { + std::string out = "["; + for (size_t i = 0; i < args.size(); ++i) { + if (i > 0) out += ", "; + const auto& a = args[i]; + if (!a.has_value()) { + out += "null"; + } else if (a.type() == typeid(std::string)) { + out += std::any_cast(a); + } else if (a.type() == typeid(double)) { + auto v = std::any_cast(a); + if (v == static_cast(v)) + out += std::to_string(static_cast(v)); + else + out += std::to_string(v); + } else if (a.type() == typeid(long long)) { + out += std::to_string(std::any_cast(a)); + } else if (a.type() == typeid(Utils::JList)) { + const auto& arr = std::any_cast(a); + out += "["; + for (size_t j = 0; j < arr.size(); ++j) { + if (j > 0) out += ", "; + const auto& el = arr[j]; + if (el.type() == typeid(double)) { + auto v = std::any_cast(el); + if (v == static_cast(v)) + out += std::to_string(static_cast(v)); + else + out += std::to_string(v); + } else if (el.type() == typeid(long long)) { + out += std::to_string(std::any_cast(el)); + } + } + out += "]"; + } + } + out += "]"; + return out; + }; + expr.registerFunction("customArgs", customArgsFn); + + auto result = expr.evaluate(nullptr); + ASSERT_TRUE(result.is_string()); + EXPECT_EQ(result.get(), "[test, [1, 2, 3, 4], 3]"); +} + } // namespace jsonata From c8022137dc26926eeef110dbfe2ae1530ce8e3c2 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 14:48:34 -0700 Subject: [PATCH 4/8] escape special chars in field names --- src/jsonata/Functions.cpp | 3 ++- test/StringTest.cpp | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/jsonata/Functions.cpp b/src/jsonata/Functions.cpp index 0f1032c..8139c8a 100644 --- a/src/jsonata/Functions.cpp +++ b/src/jsonata/Functions.cpp @@ -2432,7 +2432,8 @@ void Functions::stringifyInternal(std::ostringstream& os, const std::any& arg, size_t count = 0; for (const auto& [key, value] : map) { if (prettify) os << indent << " "; - os << '"' << key << '"' << ':'; + quoteString(os, key); + os << ':'; if (prettify) os << " "; // Handle special function values with quotes per Java logic diff --git a/test/StringTest.cpp b/test/StringTest.cpp index 97aa72d..4eeb82c 100644 --- a/test/StringTest.cpp +++ b/test/StringTest.cpp @@ -183,6 +183,14 @@ TEST_F(StringTest, regexTest) { EXPECT_EQ(result, expected); } +TEST_F(StringTest, fieldnameWithSpecialCharTest) { + Jsonata expr("$ ~> |$|{}|"); + nlohmann::ordered_json input; + input["a\nb"] = "c\nd"; + auto result = expr.evaluate(input); + EXPECT_EQ(result, input); +} + TEST_F(StringTest, DISABLED_replaceTest) { auto input = nlohmann::ordered_json("http://example.org/test{par}"); auto result = Jsonata("$replace($, /{par}/, '')").evaluate(input); From 3fa9fc6b54b382f647f97bafddc9c3fa89b44eed Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 14:56:01 -0700 Subject: [PATCH 5/8] negative index --- .gitignore | 1 + test/ArrayTest.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/.gitignore b/.gitignore index 4f8903b..792950b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,6 @@ _deps srclient-test # IDE specific files (e.g., CLion, VS Code) +.claude .idea .vscode diff --git a/test/ArrayTest.cpp b/test/ArrayTest.cpp index a4ecb0d..b6b86b5 100644 --- a/test/ArrayTest.cpp +++ b/test/ArrayTest.cpp @@ -16,6 +16,17 @@ class ArrayTest : public ::testing::Test { } }; +TEST_F(ArrayTest, testNegativeIndex) { + Jsonata expr1("item[-1]"); + auto input1 = nlohmann::ordered_json::parse(R"({"item": []})"); + auto result1 = expr1.evaluate(input1); + EXPECT_TRUE(result1.is_null()); + + Jsonata expr2("$[-1]"); + auto result2 = expr2.evaluate(nlohmann::ordered_json::array()); + EXPECT_TRUE(result2.is_null()); +} + TEST_F(ArrayTest, testArray) { // Create test data equivalent to Java: Map.of("key", Arrays.asList(Map.of("x", "y"), Map.of("a", "b"))) auto data = nlohmann::ordered_json::parse(R"({"key": [{"x": "y"}, {"a": "b"}]})"); From 4e8584e69e245f32414a253a6cef348890d8bc2e Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 15:08:24 -0700 Subject: [PATCH 6/8] Update assertFn to use custom error message --- src/jsonata/Functions.cpp | 2 +- src/jsonata/JException.cpp | 8 ++++++++ test/ArrayTest.cpp | 11 +++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/jsonata/Functions.cpp b/src/jsonata/Functions.cpp index 8139c8a..96e9342 100644 --- a/src/jsonata/Functions.cpp +++ b/src/jsonata/Functions.cpp @@ -4134,7 +4134,7 @@ void Functions::error(const std::string& message) { void Functions::assertFn(bool condition, const std::string& message) { if (!condition) { - error(message); + throw JException("D3141", -1, !message.empty() ? message : std::string("$assert() statement failed")); } } diff --git a/src/jsonata/JException.cpp b/src/jsonata/JException.cpp index 1fc5ee8..d44192d 100644 --- a/src/jsonata/JException.cpp +++ b/src/jsonata/JException.cpp @@ -355,6 +355,14 @@ std::string JException::generateMessage(const std::string& error, int64_t locati } std::string formatted = message; + + if (formatted == "{{{message}}}") { + if (arg1.has_value() && arg1.type() == typeid(std::string)) { + return std::any_cast(arg1); + } + return formatted; + } + try { // Replace any {{var}} with the actual argument values std::regex pattern1(R"(\{\{\w+\}\})"); diff --git a/test/ArrayTest.cpp b/test/ArrayTest.cpp index b6b86b5..0ec2713 100644 --- a/test/ArrayTest.cpp +++ b/test/ArrayTest.cpp @@ -87,4 +87,15 @@ TEST_F(ArrayTest, testWildcardFilter) { EXPECT_EQ(result2.dump(), expected.dump()); } +TEST_F(ArrayTest, testAssertCustomMessage) { + Jsonata expr("$assert(false, 'custom error')"); + try { + expr.evaluate(nullptr); + FAIL() << "Expected JException"; + } catch (const JException& e) { + std::string msg = e.what(); + EXPECT_TRUE(msg.find("custom error") != std::string::npos) << "Expected 'custom error' in: " << msg; + } +} + } // namespace jsonata From a6af2fff5143f2a426397fd43adf201b7786d41f Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 15:18:19 -0700 Subject: [PATCH 7/8] add check value of position in while cycle after increment --- test/StringTest.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/StringTest.cpp b/test/StringTest.cpp index 4eeb82c..ebb22bd 100644 --- a/test/StringTest.cpp +++ b/test/StringTest.cpp @@ -191,6 +191,22 @@ TEST_F(StringTest, fieldnameWithSpecialCharTest) { EXPECT_EQ(result, input); } +TEST_F(StringTest, regexLiteralTest) { + // Verify regex at end of expression doesn't crash (issue #88) + EXPECT_NO_THROW({ + Jsonata expr("/^test.*$/"); + expr.evaluate(nullptr); + }); +} + +TEST_F(StringTest, evalRegexTest) { + // Verify $eval of regex at end of expression doesn't crash (issue #88) + EXPECT_NO_THROW({ + Jsonata expr("$eval('/^test.*$/')"); + expr.evaluate(nullptr); + }); +} + TEST_F(StringTest, DISABLED_replaceTest) { auto input = nlohmann::ordered_json("http://example.org/test{par}"); auto result = Jsonata("$replace($, /{par}/, '')").evaluate(input); From 2b4a2fe2dd7d7d52a01ca9315c95a4faba5a15d4 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 15:30:14 -0700 Subject: [PATCH 8/8] fix regex matcher body and next func --- src/jsonata/Jsonata.cpp | 43 +++++++++++++++++++++++++++++++++-------- src/jsonata/Parser.cpp | 3 ++- test/StringTest.cpp | 22 +++++++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/jsonata/Jsonata.cpp b/src/jsonata/Jsonata.cpp index 9a0c288..8dc42c1 100644 --- a/src/jsonata/Jsonata.cpp +++ b/src/jsonata/Jsonata.cpp @@ -2646,6 +2646,32 @@ std::any Jsonata::apply(const std::any& proc, const Utils::JList& args, return result; } +struct RegexState { + std::shared_ptr str; + std::shared_ptr regex; + std::sregex_iterator it; + std::sregex_iterator end; +}; + +static std::any regexClosure(std::shared_ptr state) { + if (state->it == state->end) return std::any{}; + auto match = *state->it; + ++(state->it); + nlohmann::ordered_map result; + result["match"] = std::string(match.str()); + result["start"] = static_cast(match.position()); + result["end"] = static_cast(match.position() + match.length()); + Utils::JList groups; + groups.push_back(std::string(match.str())); + result["groups"] = std::any(groups); + JFunction nextFn; + nextFn.implementation = [state](const Utils::JList&, const std::any&, std::shared_ptr) -> std::any { + return regexClosure(state); + }; + result["next"] = std::any(nextFn); + return std::any(result); +} + std::any Jsonata::applyInner(const std::any& proc, const Utils::JList& args, const std::any& input, std::shared_ptr environment) { @@ -2674,18 +2700,19 @@ std::any Jsonata::applyInner(const std::any& proc, const Utils::JList& args, auto regex = std::any_cast(proc); Utils::JList results; - // Java: for (String s : (List)validatedArgs) for (const auto& arg : validatedArgs) { if (arg.has_value() && arg.type() == typeid(std::string)) { - auto str = std::any_cast(arg); - // Java: if (((Pattern)proc).matcher(s).find()) - if (std::regex_search(str, regex)) { - // Java: _res.add(s); - results.push_back(str); - } + auto state = std::make_shared(); + state->str = std::make_shared(std::any_cast(arg)); + state->regex = std::make_shared(regex); + state->it = std::sregex_iterator(state->str->begin(), state->str->end(), *state->regex); + state->end = std::sregex_iterator(); + results.push_back(regexClosure(state)); } } - // Java: result = _res; + if (results.size() == 1) { + return results[0]; + } return results; } diff --git a/src/jsonata/Parser.cpp b/src/jsonata/Parser.cpp index e294630..c85c977 100644 --- a/src/jsonata/Parser.cpp +++ b/src/jsonata/Parser.cpp @@ -1706,7 +1706,8 @@ std::shared_ptr Parser::processAST( !rest->procedure->steps.empty() && rest->procedure->steps[0]->type == "name" && !result->steps.empty() && - result->steps.back()->type == "function") { + result->steps.back()->type == "function" && + rest->procedure->steps[0]->value.type() == typeid(std::shared_ptr)) { // Next function in chain of functions - will override a // thenable result->steps.back()->nextFunction = rest->procedure->steps[0]; diff --git a/test/StringTest.cpp b/test/StringTest.cpp index ebb22bd..04958da 100644 --- a/test/StringTest.cpp +++ b/test/StringTest.cpp @@ -207,6 +207,28 @@ TEST_F(StringTest, evalRegexTest) { }); } +TEST_F(StringTest, evalRegexCheckAnswerDataTest) { + Jsonata expr("(\n $matcher := $eval('/l/');\n ('Hello World' ~> $matcher);\n)"); + auto result = expr.evaluate(nullptr); + ASSERT_TRUE(result.is_object()); + EXPECT_EQ(result["match"].get(), "l"); + EXPECT_EQ(result["start"].get(), 2); + EXPECT_EQ(result["end"].get(), 3); + ASSERT_TRUE(result["groups"].is_array()); + EXPECT_EQ(result["groups"][0].get(), "l"); +} + +TEST_F(StringTest, evalRegexCallNextTest) { + Jsonata expr("(\n $matcher := $eval('/l/');\n ('Hello World' ~> $matcher).next();\n)"); + auto result = expr.evaluate(nullptr); + ASSERT_TRUE(result.is_object()); + EXPECT_EQ(result["match"].get(), "l"); + EXPECT_EQ(result["start"].get(), 3); + EXPECT_EQ(result["end"].get(), 4); + ASSERT_TRUE(result["groups"].is_array()); + EXPECT_EQ(result["groups"][0].get(), "l"); +} + TEST_F(StringTest, DISABLED_replaceTest) { auto input = nlohmann::ordered_json("http://example.org/test{par}"); auto result = Jsonata("$replace($, /{par}/, '')").evaluate(input);