Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ _deps
srclient-test

# IDE specific files (e.g., CLion, VS Code)
.claude
.idea
.vscode
5 changes: 3 additions & 2 deletions src/jsonata/Functions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -4133,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"));
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/jsonata/JException.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string>(arg1);
}
return formatted;
}

try {
// Replace any {{var}} with the actual argument values
std::regex pattern1(R"(\{\{\w+\}\})");
Expand Down
58 changes: 35 additions & 23 deletions src/jsonata/Jsonata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -775,18 +775,6 @@ std::any Jsonata::evaluateWildcard(std::shared_ptr<Parser::Symbol> 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<std::string,
std::any>)) {
// 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);
}
Expand Down Expand Up @@ -2658,6 +2646,32 @@ std::any Jsonata::apply(const std::any& proc, const Utils::JList& args,
return result;
}

struct RegexState {
std::shared_ptr<std::string> str;
std::shared_ptr<std::regex> regex;
std::sregex_iterator it;
std::sregex_iterator end;
};

static std::any regexClosure(std::shared_ptr<RegexState> state) {
if (state->it == state->end) return std::any{};
auto match = *state->it;
++(state->it);
nlohmann::ordered_map<std::string, std::any> result;
result["match"] = std::string(match.str());
result["start"] = static_cast<long long>(match.position());
result["end"] = static_cast<long long>(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<Frame>) -> 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<Frame> environment) {
Expand Down Expand Up @@ -2686,18 +2700,19 @@ std::any Jsonata::applyInner(const std::any& proc, const Utils::JList& args,
auto regex = std::any_cast<std::regex>(proc);
Utils::JList results;

// Java: for (String s : (List<String>)validatedArgs)
for (const auto& arg : validatedArgs) {
if (arg.has_value() && arg.type() == typeid(std::string)) {
auto str = std::any_cast<std::string>(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<RegexState>();
state->str = std::make_shared<std::string>(std::any_cast<std::string>(arg));
state->regex = std::make_shared<std::regex>(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;
}

Expand Down Expand Up @@ -3397,9 +3412,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<std::string, std::any> tuple;
tuple["@"] = item;
bindings.push_back(std::any(tuple));
Expand Down
3 changes: 2 additions & 1 deletion src/jsonata/Parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1706,7 +1706,8 @@ std::shared_ptr<Parser::Symbol> 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<Symbol>)) {
// Next function in chain of functions - will override a
// thenable
result->steps.back()->nextFunction = rest->procedure->steps[0];
Expand Down
16 changes: 1 addition & 15 deletions src/jsonata/Utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
59 changes: 57 additions & 2 deletions test/ArrayTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]})");
Expand All @@ -36,11 +47,55 @@ 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, 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}])");
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());
}

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
51 changes: 51 additions & 0 deletions test/SignatureTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,55 @@ TEST_F(SignatureTest, testVarArg) {
EXPECT_EQ(result.get<long long>(), 6);
}

TEST_F(SignatureTest, testVarArgMany) {
Jsonata expr("$customArgs('test',[1,2,3,4],3)");

JFunction customArgsFn;
customArgsFn.signature = std::make_shared<utils::Signature>("<sa<n>n:s>", "customArgs");
customArgsFn.implementation = [](const Utils::JList& args, const std::any&, std::shared_ptr<Frame>) -> 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<std::string>(a);
} else if (a.type() == typeid(double)) {
auto v = std::any_cast<double>(a);
if (v == static_cast<long long>(v))
out += std::to_string(static_cast<long long>(v));
else
out += std::to_string(v);
} else if (a.type() == typeid(long long)) {
out += std::to_string(std::any_cast<long long>(a));
} else if (a.type() == typeid(Utils::JList)) {
const auto& arr = std::any_cast<Utils::JList>(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<double>(el);
if (v == static_cast<long long>(v))
out += std::to_string(static_cast<long long>(v));
else
out += std::to_string(v);
} else if (el.type() == typeid(long long)) {
out += std::to_string(std::any_cast<long long>(el));
}
}
out += "]";
}
}
out += "]";
return out;
};
expr.registerFunction("customArgs", customArgsFn);

auto result = expr.evaluate(nullptr);
ASSERT_TRUE(result.is_string());
EXPECT_EQ(result.get<std::string>(), "[test, [1, 2, 3, 4], 3]");
}

} // namespace jsonata
46 changes: 46 additions & 0 deletions test/StringTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,52 @@ 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, 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, 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<std::string>(), "l");
EXPECT_EQ(result["start"].get<long long>(), 2);
EXPECT_EQ(result["end"].get<long long>(), 3);
ASSERT_TRUE(result["groups"].is_array());
EXPECT_EQ(result["groups"][0].get<std::string>(), "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<std::string>(), "l");
EXPECT_EQ(result["start"].get<long long>(), 3);
EXPECT_EQ(result["end"].get<long long>(), 4);
ASSERT_TRUE(result["groups"].is_array());
EXPECT_EQ(result["groups"][0].get<std::string>(), "l");
}

TEST_F(StringTest, DISABLED_replaceTest) {
auto input = nlohmann::ordered_json("http://example.org/test{par}");
auto result = Jsonata("$replace($, /{par}/, '')").evaluate(input);
Expand Down
Loading