Skip to content

Commit

Permalink
#495: Implementation JSON.FORGET command (#524)
Browse files Browse the repository at this point in the history
* Added the command JSON.FORGET implementation
  • Loading branch information
Nijin-P-S authored Sep 12, 2024
1 parent a4342af commit b46c0f9
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 0 deletions.
12 changes: 12 additions & 0 deletions internal/eval/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@ var (
Arity: -2,
KeySpecs: KeySpecs{BeginIndex: 1},
}

jsonforgetCmdMeta = DiceCmdMeta{
Name: "JSON.FORGET",
Info: `JSON.FORGET key [path]
Returns an integer reply specified as the number of paths deleted (0 or more).
Returns RespZero if the key doesn't exist or key is expired.
Error reply: If the number of arguments is incorrect.`,
Eval: evalJSONFORGET,
Arity: -2,
KeySpecs: KeySpecs{BeginIndex: 1},
}
jsonarrlenCmdMeta = DiceCmdMeta{
Name: "JSON.ARRLEN",
Info: `JSON.ARRLEN key [path]
Expand Down Expand Up @@ -642,6 +653,7 @@ func init() {
DiceCmds["JSON.TYPE"] = jsontypeCmdMeta
DiceCmds["JSON.CLEAR"] = jsonclearCmdMeta
DiceCmds["JSON.DEL"] = jsondelCmdMeta
DiceCmds["JSON.FORGET"] = jsonforgetCmdMeta
DiceCmds["JSON.ARRLEN"] = jsonarrlenCmdMeta
DiceCmds["TTL"] = ttlCmdMeta
DiceCmds["DEL"] = delCmdMeta
Expand Down
15 changes: 15 additions & 0 deletions internal/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,20 @@ func evalGETDEL(args []string, store *dstore.Store) []byte {
return clientio.Encode(obj.Value, false)
}

// evaLJSONFORGET removes the field specified by the given JSONPath from the JSON document stored under the provided key.
// calls the evalJSONDEL() with the arguments passed
// Returns response.RespZero if key is expired or it does not exist
// Returns encoded error response if incorrect number of arguments
// If the JSONPath points to the root of the JSON document, the entire key is deleted from the store.
// Returns an integer reply specified as the number of paths deleted (0 or more)
func evalJSONFORGET(args []string, store *dstore.Store) []byte {
if len(args) < 1 {
return diceerrors.NewErrArity("JSON.FORGET")
}

return evalJSONDEL(args, store)
}

// evalJSONARRLEN return the length of the JSON array at path in key
// Returns an array of integer replies, an integer for each matching value,
// each is the array's length, or nil, if the matching value is not an array.
Expand Down Expand Up @@ -384,6 +398,7 @@ func evalJSONARRLEN(args []string, store *dstore.Store) []byte {
return clientio.Encode(arrlenList, false)
}


// evalJSONDEL delete a value that the given json path include in.
// Returns response.RespZero if key is expired or it does not exist
// Returns encoded error response if incorrect number of arguments
Expand Down
62 changes: 62 additions & 0 deletions internal/eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func TestEval(t *testing.T) {
testEvalGET(t, store)
testEvalJSONARRLEN(t, store)
testEvalJSONDEL(t, store)
testEvalJSONFORGET(t, store)
testEvalJSONCLEAR(t, store)
testEvalJSONTYPE(t, store)
testEvalJSONGET(t, store)
Expand Down Expand Up @@ -432,6 +433,67 @@ func testEvalJSONDEL(t *testing.T, store *dstore.Store) {
runEvalTests(t, tests, evalJSONDEL, store)
}

func testEvalJSONFORGET(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"nil value": {
setup: func() {},
input: nil,
output: []byte("-ERR wrong number of arguments for 'json.forget' command\r\n"),
},
"key does not exist": {
setup: func() {},
input: []string{"NONEXISTENT_KEY"},
output: clientio.RespZero,
},
"root path forget": {
setup: func() {
key := "EXISTING_KEY"
value := "{\"age\":13,\"high\":1.60,\"pet\":null,\"language\":[\"python\",\"golang\"], " +
"\"flag\":false, \"partner\":{\"name\":\"tom\",\"language\":[\"rust\"]}}"
var rootData interface{}
_ = sonic.Unmarshal([]byte(value), &rootData)
obj := store.NewObj(rootData, -1, object.ObjTypeJSON, object.ObjEncodingJSON)
store.Put(key, obj)

},
input: []string{"EXISTING_KEY"},
output: clientio.RespOne,
},
"part path forget": {
setup: func() {
key := "EXISTING_KEY"
value := "{\"age\":13,\"high\":1.60,\"pet\":null,\"language\":[\"python\",\"golang\"], " +
"\"flag\":false, \"partner\":{\"name\":\"tom\",\"language\":[\"rust\"]}}"
var rootData interface{}
_ = sonic.Unmarshal([]byte(value), &rootData)
obj := store.NewObj(rootData, -1, object.ObjTypeJSON, object.ObjEncodingJSON)
store.Put(key, obj)

},

input: []string{"EXISTING_KEY", "$..language"},
output: []byte(":2\r\n"),
},
"wildcard path forget": {
setup: func() {
key := "EXISTING_KEY"
value := "{\"age\":13,\"high\":1.60,\"pet\":null,\"language\":[\"python\",\"golang\"], " +
"\"flag\":false, \"partner\":{\"name\":\"tom\",\"language\":[\"rust\"]}}"
var rootData interface{}
_ = sonic.Unmarshal([]byte(value), &rootData)
obj := store.NewObj(rootData, -1, object.ObjTypeJSON, object.ObjEncodingJSON)
store.Put(key, obj)

},

input: []string{"EXISTING_KEY", "$.*"},
output: []byte(":6\r\n"),
},
}
runEvalTests(t, tests, evalJSONFORGET, store)
}


func testEvalJSONCLEAR(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"nil value": {
Expand Down
74 changes: 74 additions & 0 deletions tests/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,3 +474,77 @@ func TestJSONDelOperations(t *testing.T) {
})
}
}

func TestJSONForgetOperations(t *testing.T) {
conn := getLocalConnection()
defer conn.Close()

fireCommand(conn, "FORGET user")

stringForgetTestJson := `{"flag":true,"name":"Tom"}`
booleanForgetTestJson := `{"flag":true,"name":"Tom"}`
arrayForgetTestJson := `{"names":["Rahul","Tom"],"bosses":{"names":["Jerry","Rocky"],"hobby":"swim"}}`
integerForgetTestJson := `{"age":28,"name":"Tom"}`
floatForgetTestJson := `{"price":3.14,"name":"sugar"}`
nullForgetTestJson := `{"name":null,"age":28}`
multiForgetTestJson := `{"age":13,"high":1.60,"flag":true,"name":"jerry","pet":null,"language":["python","golang"],"partner":{"name":"tom","language":["rust"]}}`

testCases := []struct {
name string
commands []string
expected []interface{}
}{
{
name: "forget root path",
commands: []string{"JSON.SET user $ " + multiForgetTestJson, "JSON.FORGET user $", "JSON.GET user $"},
expected: []interface{}{"OK", int64(1), "(nil)"},
},
{
name: "forget string type",
commands: []string{"JSON.SET user $ " + stringForgetTestJson, "JSON.FORGET user $.name", "JSON.GET user $"},
expected: []interface{}{"OK", int64(1), `{"flag":true}`},
},
{
name: "forget bool type",
commands: []string{"JSON.SET user $ " + booleanForgetTestJson, "JSON.FORGET user $.flag", "JSON.GET user $"},
expected: []interface{}{"OK", int64(1), `{"name":"Tom"}`},
},
{
name: "forget null type",
commands: []string{"JSON.SET user $ " + nullForgetTestJson, "JSON.FORGET user $.name", "JSON.GET user $"},
expected: []interface{}{"OK", int64(1), `{"age":28}`},
},
{
name: "forget array type",
commands: []string{"JSON.SET user $ " + arrayForgetTestJson, "JSON.FORGET user $..names", "JSON.GET user $"},
expected: []interface{}{"OK", int64(2), `{"bosses":{"hobby":"swim"}}`},
},
{
name: "forget integer type",
commands: []string{"JSON.SET user $ " + integerForgetTestJson, "JSON.FORGET user $.age", "JSON.GET user $"},
expected: []interface{}{"OK", int64(1), `{"name":"Tom"}`},
},
{
name: "forget float type",
commands: []string{"JSON.SET user $ " + floatForgetTestJson, "JSON.FORGET user $.price", "JSON.GET user $"},
expected: []interface{}{"OK", int64(1), `{"name":"sugar"}`},
},
}

for _, tcase := range testCases {
t.Run(tcase.name, func(t *testing.T) {
for i := 0; i < len(tcase.commands); i++ {
cmd := tcase.commands[i]
out := tcase.expected[i]
result := fireCommand(conn, cmd)
jsonResult, isString := result.(string)
if isString && testutils.IsJSONResponse(jsonResult) {
testutils.AssertJSONEqual(t, out.(string), jsonResult)
} else {
assert.Equal(t, out, result)
}

}
})
}
}

0 comments on commit b46c0f9

Please sign in to comment.