Skip to content

Commit 4542766

Browse files
authored
dicedb#500: Added JSON.RESP Command (dicedb#751)
1 parent 87f5d83 commit 4542766

File tree

7 files changed

+349
-5
lines changed

7 files changed

+349
-5
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package async
2+
3+
import (
4+
"testing"
5+
6+
testifyAssert "github.com/stretchr/testify/assert"
7+
"gotest.tools/v3/assert"
8+
)
9+
10+
func TestJSONRESP(t *testing.T) {
11+
conn := getLocalConnection()
12+
defer conn.Close()
13+
FireCommand(conn, "DEL key")
14+
15+
arrayAtRoot := `["dice",10,10.5,true,null]`
16+
object := `{"b":["dice",10,10.5,true,null]}`
17+
18+
testCases := []struct {
19+
name string
20+
commands []string
21+
expected []interface{}
22+
assert_type []string
23+
jsonResp []bool
24+
nestedArray bool
25+
path string
26+
}{
27+
{
28+
name: "print array with mixed types",
29+
commands: []string{"json.set key $ " + arrayAtRoot, "json.resp key $"},
30+
expected: []interface{}{"OK", []interface{}{"[", "dice", int64(10), "10.5", "true", "(nil)"}},
31+
assert_type: []string{"equal", "equal"},
32+
},
33+
{
34+
name: "print nested array with mixed types",
35+
commands: []string{"json.set key $ " + object, "json.resp key $.b"},
36+
expected: []interface{}{"OK", []interface{}{[]interface{}{"[", "dice", int64(10), "10.5", "true", "(nil)"}}},
37+
assert_type: []string{"equal", "equal"},
38+
},
39+
{
40+
name: "print object at root path",
41+
commands: []string{"json.set key $ " + object, "json.resp key"},
42+
expected: []interface{}{"OK", []interface{}{"{", "b", []interface{}{"[", "dice", int64(10), "10.5", "true", "(nil)"}}},
43+
assert_type: []string{"equal", "equal"},
44+
},
45+
}
46+
47+
for _, tcase := range testCases {
48+
t.Run(tcase.name, func(t *testing.T) {
49+
for i := 0; i < len(tcase.commands); i++ {
50+
cmd := tcase.commands[i]
51+
out := tcase.expected[i]
52+
result := FireCommand(conn, cmd)
53+
54+
if tcase.assert_type[i] == "equal" {
55+
testifyAssert.Equal(t, out, result)
56+
} else if tcase.assert_type[i] == "deep_equal" {
57+
assert.Assert(t, arraysArePermutations(out.([]interface{}), result.([]interface{})))
58+
}
59+
}
60+
})
61+
}
62+
}

internal/clientio/resp.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,17 +152,37 @@ func encodeString(v string) []byte {
152152
return []byte(fmt.Sprintf("$%d\r\n%s\r\n", len(v), v))
153153
}
154154

155+
// encodeBool encodes bool as simple strings
156+
func encodeBool(v bool) []byte {
157+
return []byte(fmt.Sprintf("+%t\r\n", v))
158+
}
159+
155160
func Encode(value interface{}, isSimple bool) []byte {
156161
switch v := value.(type) {
157162
case string:
158-
if isSimple {
163+
// encode as simple strings
164+
if isSimple || v == "[" || v == "{" {
159165
return []byte(fmt.Sprintf("+%s\r\n", v))
160166
}
167+
// encode as bulk strings
161168
return encodeString(v)
162169
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
163170
return []byte(fmt.Sprintf(":%d\r\n", v))
164171
case float32, float64:
165-
return []byte(fmt.Sprintf(":%v\r\n", v))
172+
// In case the element being encoded was obtained after parsing a JSON value,
173+
// it is possible for integers to have been encoded as floats
174+
// (since encoding/json unmarshals numeric values as floats).
175+
// Therefore, we need to check if value is an integer
176+
intValue, isInteger := utils.IsFloatToIntPossible(v.(float64))
177+
if isInteger {
178+
return []byte(fmt.Sprintf(":%d\r\n", intValue))
179+
}
180+
181+
// if it is a float, encode like a string
182+
str := strconv.FormatFloat(v.(float64), 'f', -1, 64)
183+
return encodeString(str)
184+
case bool:
185+
return encodeBool(v)
166186
case []string:
167187
var b []byte
168188
buf := bytes.NewBuffer(b)

internal/clientio/resp_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/dicedb/dice/internal/clientio"
1010
"github.com/dicedb/dice/internal/server/utils"
11+
testifyAssert "github.com/stretchr/testify/assert"
1112
)
1213

1314
func TestSimpleStringDecode(t *testing.T) {
@@ -175,3 +176,45 @@ func TestArrayInt(t *testing.T) {
175176
}
176177
}
177178
}
179+
180+
func TestBoolean(t *testing.T) {
181+
tests := []struct {
182+
input bool
183+
output []byte
184+
}{
185+
{
186+
input: true,
187+
output: []byte("+true\r\n"),
188+
},
189+
{
190+
input: false,
191+
output: []byte("+false\r\n"),
192+
},
193+
}
194+
195+
for _, v := range tests {
196+
ev := clientio.Encode(v.input, false)
197+
testifyAssert.Equal(t, ev, v.output)
198+
}
199+
}
200+
201+
func TestInteger(t *testing.T) {
202+
tests := []struct {
203+
input int
204+
output []byte
205+
}{
206+
{
207+
input: 10,
208+
output: []byte(":10\r\n"),
209+
},
210+
{
211+
input: -19,
212+
output: []byte(":-19\r\n"),
213+
},
214+
}
215+
216+
for _, v := range tests {
217+
ev := clientio.Encode(v.input, false)
218+
testifyAssert.Equal(t, ev, v.output)
219+
}
220+
}

internal/eval/commands.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,16 +288,23 @@ var (
288288
Arity: -5,
289289
KeySpecs: KeySpecs{BeginIndex: 1},
290290
}
291+
jsonrespCmdMeta = DiceCmdMeta{
292+
Name: "JSON.RESP",
293+
Info: `JSON.RESP key [path]
294+
Return the JSON in key in Redis serialization protocol specification form`,
295+
Eval: evalJSONRESP,
296+
Arity: -2,
297+
KeySpecs: KeySpecs{BeginIndex: 1},
298+
}
291299
jsonarrtrimCmdMeta = DiceCmdMeta{
292300
Name: "JSON.ARRTRIM",
293301
Info: `JSON.ARRTRIM key path start stop
294302
Trim an array so that it contains only the specified inclusive range of elements
295303
Returns an array of integer replies for each path.
296304
Returns error response if the key doesn't exist or key is expired.
297305
Error reply: If the number of arguments is incorrect.`,
298-
Eval: evalJSONARRTRIM,
299-
Arity: -5,
300-
KeySpecs: KeySpecs{BeginIndex: 1},
306+
Eval: evalJSONARRTRIM,
307+
Arity: -5,
301308
}
302309
ttlCmdMeta = DiceCmdMeta{
303310
Name: "TTL",
@@ -996,6 +1003,7 @@ func init() {
9961003
DiceCmds["JSON.ARRPOP"] = jsonarrpopCmdMeta
9971004
DiceCmds["JSON.INGEST"] = jsoningestCmdMeta
9981005
DiceCmds["JSON.ARRINSERT"] = jsonarrinsertCmdMeta
1006+
DiceCmds["JSON.RESP"] = jsonrespCmdMeta
9991007
DiceCmds["JSON.ARRTRIM"] = jsonarrtrimCmdMeta
10001008
DiceCmds["TTL"] = ttlCmdMeta
10011009
DiceCmds["DEL"] = delCmdMeta

internal/eval/eval.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4336,6 +4336,83 @@ func selectRandomFields(hashMap HashMap, count int, withValues bool) []byte {
43364336
return clientio.Encode(results, false)
43374337
}
43384338

4339+
func evalJSONRESP(args []string, store *dstore.Store) []byte {
4340+
if len(args) < 1 {
4341+
return diceerrors.NewErrArity("json.resp")
4342+
}
4343+
key := args[0]
4344+
4345+
path := defaultRootPath
4346+
if len(args) > 1 {
4347+
path = args[1]
4348+
}
4349+
4350+
obj := store.Get(key)
4351+
if obj == nil {
4352+
return clientio.RespNIL
4353+
}
4354+
4355+
// Check if the object is of JSON type
4356+
errWithMessage := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeJSON, object.ObjEncodingJSON)
4357+
if errWithMessage != nil {
4358+
return errWithMessage
4359+
}
4360+
4361+
jsonData := obj.Value
4362+
if path == defaultRootPath {
4363+
resp := parseJSONStructure(jsonData, false)
4364+
4365+
return clientio.Encode(resp, false)
4366+
}
4367+
4368+
// if path is not root then extract value at path
4369+
expr, err := jp.ParseString(path)
4370+
if err != nil {
4371+
return diceerrors.NewErrWithMessage("invalid JSONPath")
4372+
}
4373+
results := expr.Get(jsonData)
4374+
4375+
// process value at each path
4376+
ret := []any{}
4377+
for _, result := range results {
4378+
resp := parseJSONStructure(result, false)
4379+
ret = append(ret, resp)
4380+
}
4381+
4382+
return clientio.Encode(ret, false)
4383+
}
4384+
4385+
func parseJSONStructure(jsonData interface{}, nested bool) (resp []any) {
4386+
switch json := jsonData.(type) {
4387+
case string, bool:
4388+
resp = append(resp, json)
4389+
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, nil:
4390+
resp = append(resp, json)
4391+
case map[string]interface{}:
4392+
resp = append(resp, "{")
4393+
for key, value := range json {
4394+
resp = append(resp, key)
4395+
resp = append(resp, parseJSONStructure(value, true)...)
4396+
}
4397+
// wrap in another array to offset print
4398+
if nested {
4399+
resp = []interface{}{resp}
4400+
}
4401+
case []interface{}:
4402+
resp = append(resp, "[")
4403+
for _, value := range json {
4404+
resp = append(resp, parseJSONStructure(value, true)...)
4405+
}
4406+
// wrap in another array to offset print
4407+
if nested {
4408+
resp = []interface{}{resp}
4409+
}
4410+
default:
4411+
resp = append(resp, []byte("(unsupported type)"))
4412+
}
4413+
return resp
4414+
}
4415+
43394416
// evalZADD adds all the specified members with the specified scores to the sorted set stored at key.
43404417
// If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering.
43414418
// If key does not exist, a new sorted set with the specified members as sole members is created.

0 commit comments

Comments
 (0)