1
0
Fork 0
mirror of https://github.com/dragonflydb/dragonfly.git synced 2024-12-14 11:58:02 +00:00

chore: JSON.GET now works with our own jsonpath (#2653)

This commit is contained in:
Roman Gershman 2024-02-24 13:23:41 +02:00 committed by GitHub
parent 5ee61db0f3
commit 2b30f69fe3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 61 additions and 34 deletions

View file

@ -148,7 +148,7 @@ jobs:
./dragonfly_test
./multi_test --multi_exec_mode=1
./multi_test --multi_exec_mode=3
# GLOG_logtostderr=1 GLOG_vmodule=transaction=1,engine_shard_set=1 CTEST_OUTPUT_ON_FAILURE=1 ninja server/test
./json_family_test --jsonpathv2
- name: Upload unit logs on failure
if: failure()

View file

@ -30,6 +30,12 @@ class TestDriver : public Driver {
}
};
inline JsonType ValidJson(string_view str) {
auto res = ::dfly::JsonFromString(str, pmr::get_default_resource());
CHECK(res) << "Failed to parse json: " << str;
return *res;
}
class JsonPathTest : public ::testing::Test {
protected:
JsonPathTest() {
@ -123,6 +129,19 @@ TEST_F(JsonPathTest, Parser) {
EXPECT_EQ(0, Parse("$.plays[*].game"));
}
TEST_F(JsonPathTest, Root) {
JsonType json = ValidJson(R"({"foo" : 1, "bar": "str" })");
ASSERT_EQ(0, Parse("$"));
Path path = driver_.TakePath();
int called = 0;
EvaluatePath(path, json, [&](optional<string_view>, const JsonType& val) {
++called;
ASSERT_TRUE(val.is_object());
ASSERT_EQ(json, val);
});
ASSERT_EQ(1, called);
}
TEST_F(JsonPathTest, Functions) {
ASSERT_EQ(0, Parse("max($.plays[*].score)"));
Path path = driver_.TakePath();
@ -131,9 +150,7 @@ TEST_F(JsonPathTest, Functions) {
EXPECT_THAT(path[1], SegType(SegmentType::IDENTIFIER));
EXPECT_THAT(path[2], SegType(SegmentType::WILDCARD));
EXPECT_THAT(path[3], SegType(SegmentType::IDENTIFIER));
JsonType json =
JsonFromString(R"({"plays": [{"score": 1}, {"score": 2}]})", pmr::get_default_resource())
.value();
JsonType json = ValidJson(R"({"plays": [{"score": 1}, {"score": 2}]})");
int called = 0;
EvaluatePath(path, json, [&](auto, const JsonType& val) {
ASSERT_TRUE(val.is<int>());
@ -163,16 +180,15 @@ TEST_F(JsonPathTest, Descent) {
TEST_F(JsonPathTest, Path) {
Path path;
JsonType json = JsonFromString(R"({"v11":{ "f" : 1, "a2": [0]}, "v12": {"f": 2, "a2": [1]},
JsonType json = ValidJson(R"({"v11":{ "f" : 1, "a2": [0]}, "v12": {"f": 2, "a2": [1]},
"v13": 3
})",
pmr::get_default_resource())
.value();
})");
int called = 0;
// Empty path
EvaluatePath(path, json, [&](optional<string_view>, const JsonType& val) { ++called; });
ASSERT_EQ(0, called);
ASSERT_EQ(1, called);
called = 0;
path.emplace_back(SegmentType::IDENTIFIER, "v13");
EvaluatePath(path, json, [&](optional<string_view> key, const JsonType& val) {
@ -206,11 +222,10 @@ TEST_F(JsonPathTest, Path) {
}
TEST_F(JsonPathTest, EvalDescent) {
JsonType json = JsonFromString(R"(
JsonType json = ValidJson(R"(
{"v11":{ "f" : 1, "a2": [0]}, "v12": {"f": 2, "v21": {"f": 3, "a2": [1]}},
"v13": { "a2" : { "b" : {"f" : 4}}}
})",
pmr::get_default_resource());
})");
Path path;
@ -241,10 +256,9 @@ TEST_F(JsonPathTest, EvalDescent) {
});
ASSERT_EQ(4, called);
json = JsonFromString(R"(
json = ValidJson(R"(
{"a":[7], "inner": {"a": {"b": 2, "c": 1337}}}
)",
pmr::get_default_resource());
)");
path.pop_back();
path.emplace_back(SegmentType::IDENTIFIER, "a");
@ -263,7 +277,7 @@ TEST_F(JsonPathTest, Wildcard) {
ASSERT_EQ(1, path.size());
EXPECT_THAT(path[0], SegType(SegmentType::WILDCARD));
JsonType json = JsonFromString(R"([1, 2, 3])", pmr::get_default_resource()).value();
JsonType json = ValidJson(R"([1, 2, 3])");
vector<int> arr;
EvaluatePath(path, json, [&](optional<string_view> key, const JsonType& val) {
ASSERT_FALSE(key);
@ -273,7 +287,7 @@ TEST_F(JsonPathTest, Wildcard) {
}
TEST_F(JsonPathTest, Mutate) {
JsonType json = JsonFromString(R"([1, 2, 3, 5, 6])", pmr::get_default_resource()).value();
JsonType json = ValidJson(R"([1, 2, 3, 5, 6])");
ASSERT_EQ(0, Parse("$[*]"));
Path path = driver_.TakePath();
MutateCallback cb = [&](optional<string_view>, JsonType* val) {
@ -289,10 +303,9 @@ TEST_F(JsonPathTest, Mutate) {
}
ASSERT_THAT(arr, ElementsAre(2, 3, 4, 6, 7));
json = JsonFromString(R"(
json = ValidJson(R"(
{"a":[7], "inner": {"a": {"bool": true, "c": 42}}}
)",
pmr::get_default_resource());
)");
ASSERT_EQ(0, Parse("$..a.*"));
path = driver_.TakePath();
MutatePath(

View file

@ -432,8 +432,10 @@ JsonType PathSegment::GetResult() const {
}
void EvaluatePath(const Path& path, const JsonType& json, PathCallback callback) {
if (path.empty())
if (path.empty()) { // root node
callback(nullopt, json);
return;
}
if (path.front().type() != SegmentType::FUNCTION) {
Dfs().Traverse(path, json, std::move(callback));

View file

@ -65,6 +65,17 @@ inline void Evaluate(const json::Path& expr, const JsonType& obj, ExprCallback c
});
}
inline JsonType Evaluate(const JsonExpression& expr, const JsonType& obj) {
return expr.evaluate(obj);
}
inline JsonType Evaluate(const json::Path& expr, const JsonType& obj) {
JsonType res(json_array_arg);
json::EvaluatePath(expr, obj,
[&res](optional<string_view>, const JsonType& val) { res.push_back(val); });
return res;
}
inline OpStatus JsonReplaceVerifyNoOp(JsonType&) {
return OpStatus::OK;
}
@ -418,7 +429,7 @@ void SendJsonValue(RedisReplyBuilder* rb, const JsonType& j) {
}
OpResult<string> OpJsonGet(const OpArgs& op_args, string_view key,
const vector<pair<string_view, optional<JsonExpression>>>& expressions,
const vector<pair<string_view, optional<JsonPathV2>>>& expressions,
bool should_format, const OptString& indent, const OptString& new_line,
const OptString& space) {
OpResult<JsonType*> result = GetJson(op_args, key);
@ -456,16 +467,17 @@ OpResult<string> OpJsonGet(const OpArgs& op_args, string_view key,
}
}
auto eval_wrapped = [&json_entry](const optional<JsonExpression>& expr) {
return expr ? expr->evaluate(json_entry) : json_entry;
auto eval_wrapped = [&json_entry](const optional<JsonPathV2>& expr) -> JsonType {
return expr ? visit([&](auto& arg) { return Evaluate(arg, json_entry); }, *expr) : json_entry;
};
JsonType out{json_object_arg}; // see https://github.com/danielaparker/jsoncons/issues/482
if (expressions.size() == 1) {
out = eval_wrapped(expressions[0].second);
} else {
for (auto& [expr_str, expr] : expressions)
for (const auto& [expr_str, expr] : expressions) {
out[expr_str] = eval_wrapped(expr);
}
}
if (should_format) {
@ -1826,7 +1838,9 @@ void JsonFamily::Get(CmdArgList args, ConnectionContext* cntx) {
OptString indent;
OptString new_line;
OptString space;
vector<pair<string_view, optional<JsonExpression>>> expressions;
// '.' corresponds to the legacy, non-array format and is passed as nullopt.
vector<pair<string_view, optional<JsonPathV2>>> expressions;
while (parser.HasNext()) {
if (parser.Check("SPACE").IgnoreCase().ExpectTail(1)) {
@ -1842,17 +1856,12 @@ void JsonFamily::Get(CmdArgList args, ConnectionContext* cntx) {
continue;
}
optional<JsonExpression> expr;
optional<JsonPathV2> expr;
string_view expr_str = parser.Next();
if (expr_str != ".") {
io::Result<JsonExpression> res = ParseJsonPath(expr_str);
if (!res) {
LOG(WARNING) << "path '" << expr_str
<< "': Invalid JSONPath syntax: " << res.error().message();
return cntx->SendError(kSyntaxErr);
}
expr.emplace(std::move(*res));
JsonPathV2 expression = PARSE_PATHV2(expr_str);
expr.emplace(std::move(expression));
}
expressions.emplace_back(expr_str, std::move(expr));

View file

@ -276,6 +276,9 @@ TEST_F(JsonFamilyTest, Toggle) {
auto resp = Run({"JSON.SET", "json", ".", json});
ASSERT_THAT(resp, "OK");
resp = Run({"JSON.GET", "json", "$.*"});
EXPECT_EQ(R"([true,false,1,null,"foo",[],{}])", resp);
resp = Run({"JSON.TOGGLE", "json", "$.*"});
ASSERT_THAT(resp, ArgType(RespExpr::ARRAY));
EXPECT_THAT(resp.GetVec(),