diff --git a/src/commands/cmd_json.cc b/src/commands/cmd_json.cc index 86017fb514e..08bcf7abe85 100644 --- a/src/commands/cmd_json.cc +++ b/src/commands/cmd_json.cc @@ -219,6 +219,35 @@ class CommandJsonClear : public Commander { } }; +class CommandJsonToggle : public Commander { + public: + Status Execute(Server *svr, Connection *conn, std::string *output) override { + redis::Json json(svr->storage, conn->GetNamespace()); + + std::string path = (args_.size() > 2) ? args_[2] : "$"; + std::vector> results; + auto s = json.Toggle(args_[1], path, results); + + if (s.IsNotFound()) { + *output = redis::NilString(); + return Status::OK(); + } + + *output = redis::MultiLen(results.size()); + for (auto it = results.rbegin(); it != results.rend(); ++it) { + if (it->has_value()) { + *output += redis::Integer(it->value()); + } else { + *output += redis::NilString(); + } + } + + if (!s.ok()) return {Status::RedisExecErr, s.ToString()}; + + return Status::OK(); + } +}; + class CommandJsonArrLen : public Commander { public: Status Execute(Server *svr, Connection *conn, std::string *output) override { @@ -297,8 +326,8 @@ REDIS_REGISTER_COMMANDS(MakeCmdAttr("json.set", 4, "write", 1, 1 MakeCmdAttr("json.type", -2, "read-only", 1, 1, 1), MakeCmdAttr("json.arrappend", -4, "write", 1, 1, 1), MakeCmdAttr("json.clear", -2, "write", 1, 1, 1), + MakeCmdAttr("json.toggle", -2, "write", 1, 1, 1), MakeCmdAttr("json.arrlen", -2, "read-only", 1, 1, 1), MakeCmdAttr("json.objkeys", -2, "read-only", 1, 1, 1), MakeCmdAttr("json.arrpop", -2, "write", 1, 1, 1), ); - } // namespace redis diff --git a/src/types/json.h b/src/types/json.h index 442bef76166..1b78ec063d1 100644 --- a/src/types/json.h +++ b/src/types/json.h @@ -212,6 +212,23 @@ struct JsonValue { return types; } + StatusOr>> Toggle(std::string_view path) { + std::vector> result; + try { + jsoncons::jsonpath::json_replace(value, path, [&result](const std::string & /*path*/, jsoncons::json &val) { + if (val.is_bool()) { + val = !val.as_bool(); + result.emplace_back(val.as_bool()); + } else { + result.emplace_back(std::nullopt); + } + }); + } catch (const jsoncons::jsonpath::jsonpath_error &e) { + return {Status::NotOK, e.what()}; + } + return result; + } + StatusOr Clear(std::string_view path) { size_t count = 0; try { diff --git a/src/types/redis_json.cc b/src/types/redis_json.cc index 44da953d3b3..94536605257 100644 --- a/src/types/redis_json.cc +++ b/src/types/redis_json.cc @@ -231,6 +231,24 @@ rocksdb::Status Json::ArrLen(const std::string &user_key, const std::string &pat return rocksdb::Status::OK(); } +rocksdb::Status Json::Toggle(const std::string &user_key, const std::string &path, + std::vector> &result) { + auto ns_key = AppendNamespacePrefix(user_key); + + LockGuard guard(storage_->GetLockManager(), ns_key); + + JsonMetadata metadata; + JsonValue origin; + auto s = read(ns_key, &metadata, &origin); + if (!s.ok()) return s; + + auto toggle_res = origin.Toggle(path); + if (!toggle_res) return rocksdb::Status::InvalidArgument(toggle_res.Msg()); + result = *toggle_res; + + return write(ns_key, &metadata, origin); +} + rocksdb::Status Json::ArrPop(const std::string &user_key, const std::string &path, int64_t index, std::vector> *results) { auto ns_key = AppendNamespacePrefix(user_key); diff --git a/src/types/redis_json.h b/src/types/redis_json.h index 708199af09d..e1f5cde961f 100644 --- a/src/types/redis_json.h +++ b/src/types/redis_json.h @@ -42,6 +42,8 @@ class Json : public Database { rocksdb::Status Clear(const std::string &user_key, const std::string &path, size_t *result); rocksdb::Status ArrLen(const std::string &user_key, const std::string &path, std::vector> &arr_lens); + rocksdb::Status Toggle(const std::string &user_key, const std::string &path, + std::vector> &result); rocksdb::Status ObjKeys(const std::string &user_key, const std::string &path, std::vector>> &keys); rocksdb::Status ArrPop(const std::string &user_key, const std::string &path, int64_t index, diff --git a/tests/cppunit/types/json_test.cc b/tests/cppunit/types/json_test.cc index 9e023c3e1da..c1c10e8dc2b 100644 --- a/tests/cppunit/types/json_test.cc +++ b/tests/cppunit/types/json_test.cc @@ -273,6 +273,64 @@ TEST_F(RedisJsonTest, ArrLen) { ASSERT_TRUE(res.empty()); } +TEST_F(RedisJsonTest, Toggle) { + std::vector> res; + ASSERT_TRUE(json_->Set(key_, "$", "true").ok()); + ASSERT_TRUE(json_->Toggle(key_, "$", res).ok()); + ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok()); + ASSERT_EQ(json_val_.Dump().GetValue(), "false"); + ASSERT_EQ(res.size(), 1); + ASSERT_THAT(res, testing::ElementsAre(false)); + res.clear(); + + ASSERT_TRUE(json_->Set(key_, "$", R"({"bool":true})").ok()); + ASSERT_TRUE(json_->Toggle(key_, "$.bool", res).ok()); + ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok()); + ASSERT_EQ(json_val_.Dump().GetValue(), R"({"bool":false})"); + ASSERT_EQ(res.size(), 1); + ASSERT_THAT(res, testing::ElementsAre(false)); + res.clear(); + + ASSERT_TRUE(json_->Set(key_, "$", R"({"bool":true,"bools":{"bool":true}})").ok()); + ASSERT_TRUE(json_->Toggle(key_, "$.bool", res).ok()); + ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok()); + ASSERT_EQ(json_val_.Dump().GetValue(), R"({"bool":false,"bools":{"bool":true}})"); + ASSERT_EQ(res.size(), 1); + ASSERT_THAT(res, testing::ElementsAre(false)); + res.clear(); + + ASSERT_TRUE(json_->Set(key_, "$", R"({"bool":true,"bools":{"bool":true}})").ok()); + ASSERT_TRUE(json_->Toggle(key_, "$..bool", res).ok()); + ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok()); + ASSERT_EQ(json_val_.Dump().GetValue(), R"({"bool":false,"bools":{"bool":false}})"); + ASSERT_EQ(res.size(), 2); + ASSERT_THAT(res, testing::ElementsAre(false, false)); + res.clear(); + + ASSERT_TRUE(json_->Set(key_, "$", R"({"bool":false,"bools":{"bool":true}})").ok()); + ASSERT_TRUE(json_->Toggle(key_, "$..bool", res).ok()); + ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok()); + ASSERT_EQ(json_val_.Dump().GetValue(), R"({"bool":true,"bools":{"bool":false}})"); + ASSERT_EQ(res.size(), 2); + ASSERT_THAT(res, testing::ElementsAre(false, true)); + res.clear(); + + ASSERT_TRUE(json_->Set(key_, "$", R"({"bool":false,"bools":{"bool":true},"incorrectbool":{"bool":88}})").ok()); + ASSERT_TRUE(json_->Toggle(key_, "$..bool", res).ok()); + ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok()); + ASSERT_EQ(json_val_.Dump().GetValue(), R"({"bool":true,"bools":{"bool":false},"incorrectbool":{"bool":88}})"); + ASSERT_EQ(res.size(), 3); + ASSERT_THAT(res, testing::ElementsAre(std::nullopt, false, true)); + res.clear(); + + ASSERT_TRUE(json_->Set(key_, "$", "[true,true,99]").ok()); + ASSERT_TRUE(json_->Toggle(key_, "$..*", res).ok()); + ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok()); + ASSERT_EQ(json_val_.Dump().GetValue(), "[false,false,99]"); + ASSERT_EQ(res.size(), 3); + ASSERT_THAT(res, testing::ElementsAre(std::nullopt, false, false)); +} + TEST_F(RedisJsonTest, ArrPop) { std::vector> res; diff --git a/tests/gocase/unit/type/json/json_test.go b/tests/gocase/unit/type/json/json_test.go index b5d741cd510..0d5f3fd9517 100644 --- a/tests/gocase/unit/type/json/json_test.go +++ b/tests/gocase/unit/type/json/json_test.go @@ -245,4 +245,34 @@ func TestJson(t *testing.T) { require.ErrorContains(t, rdb.Do(ctx, "JSON.ARRPOP", "a", "$", "0", "1").Err(), "wrong number of arguments") }) + t.Run("JSON.TOGGLE basics", func(t *testing.T) { + require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `true`).Err()) + require.EqualValues(t, []interface{}{int64(0)}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$").Val()) + require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `false`) + + require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"bool":true}`).Err()) + require.EqualValues(t, []interface{}{int64(0)}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$.bool").Val()) + require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `{"bool":false}`) + + require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"bool":true,"bools":{"bool":true}}`).Err()) + require.EqualValues(t, []interface{}{int64(0)}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$.bool").Val()) + require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `{"bool":false,"bools":{"bool":true}}`) + + require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"bool":true,"bools":{"bool":true}}`).Err()) + require.EqualValues(t, []interface{}{int64(0), int64(0)}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$..bool").Val()) + require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `{"bool":false,"bools":{"bool":false}}`) + + require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"bool":false,"bools":{"bool":true}}`).Err()) + require.EqualValues(t, []interface{}{int64(1), int64(0)}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$..bool").Val()) + require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `{"bool":true,"bools":{"bool":false}}`) + + require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"incorrectbool":99,"bools":{"bool":true},"bool":{"bool":false}}`).Err()) + require.EqualValues(t, []interface{}{nil, int64(1), int64(0)}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$..bool").Val()) + require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `{"bool":{"bool":true},"bools":{"bool":false},"incorrectbool":99}`) + + require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `[99,true,99]`).Err()) + require.EqualValues(t, []interface{}{nil, int64(0), nil}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$..*").Val()) + require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `[99,false,99]`) + }) + }