diff --git a/internal/protcl/resp3.go b/internal/protcl/resp3.go index 8d1d94f..8c2fed4 100644 --- a/internal/protcl/resp3.go +++ b/internal/protcl/resp3.go @@ -39,11 +39,55 @@ type Resp3 struct { Elems []*Resp3 } -func (r *Resp3) String() string { - return r.string("") +// RenderString convert resp3 to show-message on client +func (r *Resp3) RenderString() string { + return r.renderString("") } -func (r *Resp3) string(pre string) string { +// ProtocolString convert resp3 to protocol raw string +func (r *Resp3) ProtocolString() string { + buf := new(strings.Builder) + buf.WriteByte(r.Type) + r.protocolString(buf) + return buf.String() +} + +func (r *Resp3) protocolString(buf *strings.Builder) { + switch r.Type { + case RepSimpleString, Resp3SimpleError: + buf.WriteString(r.Str) + case Resp3BlobString, Resp3BolbError: + buf.WriteString(strconv.Itoa(len(r.Str))) + buf.WriteByte('\n') + buf.WriteString(r.Str) + case Resp3Number: + buf.WriteString(strconv.Itoa(r.Integer)) + case Resp3Double: + buf.WriteString(strconv.FormatFloat(r.Double, 'f', -1, 64)) + case Resp3BigNumber: + buf.WriteString(r.BigInt.String()) + case Resp3Null: + case Resp3Boolean: + if r.Boolean { + buf.WriteByte('t') + } else { + buf.WriteByte('f') + } + case Resp3Array, Resp3Set: + buf.WriteString(strconv.Itoa(len(r.Elems))) + buf.WriteByte('\n') + + for _, v := range r.Elems { + buf.WriteByte(v.Type) + v.protocolString(buf) + } + return + } + + buf.WriteByte('\n') +} + +func (r *Resp3) renderString(pre string) string { switch r.Type { case RepSimpleString, Resp3BlobString: return fmt.Sprintf("%s%q", pre, r.Str) @@ -72,7 +116,7 @@ func (r *Resp3) string(pre string) string { } for _, elem := range r.Elems { str.WriteString("\n") - str.WriteString(elem.string(pre + "\t")) + str.WriteString(elem.renderString(pre + "\t")) } return str.String() } diff --git a/internal/protcl/resp3_test.go b/internal/protcl/resp3_test.go index 9a76ec3..ccb5f8e 100644 --- a/internal/protcl/resp3_test.go +++ b/internal/protcl/resp3_test.go @@ -2,6 +2,7 @@ package protcl import ( "bufio" + "math/big" "strings" "testing" @@ -14,7 +15,7 @@ func testResp3Parser(t *testing.T, input, expect string) { parser := NewResp3Parser(bufio.NewReader(strings.NewReader(input))) result, err := parser.Parse() assert.Nil(err) - assert.Equal(expect, result.String()) + assert.Equal(expect, result.RenderString()) } func testResp3Error(t *testing.T, input, e string) { @@ -27,59 +28,138 @@ func testResp3Error(t *testing.T, input, e string) { assert.Equal(e, err.Error()) } +type TestResp3 struct { + resp3 *Resp3 + protocol string + render string + err string +} + func TestResp3Parser(t *testing.T) { - // simple string - testResp3Error(t, "+string", `EOF`) - testResp3Parser(t, "+string\n", `"string"`) - - // blob string - testResp3Parser(t, "$0\n\n", `""`) - testResp3Error(t, "$1\n\n", `unexpected line end`) - testResp3Error(t, "$1\naa\n", `unexpected line end`) - testResp3Parser(t, "$10\n1234567890\n", `"1234567890"`) - - // simple error - testResp3Parser(t, "-error\n", `(error) error`) - testResp3Parser(t, "-Err error\n", `(error) Err error`) - - // blob error - testResp3Parser(t, "!3\nerr\n", `(error) err`) - testResp3Parser(t, "!17\nErr this is error\n", `(error) Err this is error`) - - // number - testResp3Parser(t, ":-1\n", `(integer) -1`) - testResp3Parser(t, ":0\n", `(integer) 0`) - testResp3Parser(t, ":100\n", `(integer) 100`) - - // double - testResp3Parser(t, ",-1\n", `(double) -1`) - testResp3Parser(t, ",0\n", `(double) 0`) - testResp3Parser(t, ",10\n", `(double) 10`) - testResp3Parser(t, ",.1\n", `(double) 0.1`) - testResp3Parser(t, ",1.23\n", `(double) 1.23`) - testResp3Parser(t, ",1.\n", `(double) 1`) - testResp3Error(t, ",invalid\n", `convert invalid to double fail, because of strconv.ParseFloat: parsing "invalid": invalid syntax`) - - // big number - testResp3Parser(t, "(3492890328409238509324850943850943825024385\n", `(big number) 3492890328409238509324850943850943825024385`) - testResp3Error(t, "(invalid string\n", `convert invalid string to Big Number fail`) - - // null - testResp3Parser(t, "_\n", "(null)") - - // boolean - testResp3Parser(t, "#t\n", `(boolean) true`) - testResp3Parser(t, "#f\n", `(boolean) false`) - testResp3Error(t, "#x\n", `unexpect string: t/f`) - testResp3Error(t, "#\n", `unexpected line end`) - - // array - testResp3Error(t, "*3\n:1\n:2\n", "EOF") - testResp3Parser(t, "*3\n:1\n:2\n:3\n", "(array)\n\t(integer) 1\n\t(integer) 2\n\t(integer) 3") - testResp3Parser(t, "*2\n*3\n:1\n$5\nhello\n:2\n#f\n", "(array)\n\t(array)\n\t\t(integer) 1\n\t\t\"hello\"\n\t\t(integer) 2\n\t(boolean) false") - - // set - testResp3Error(t, "~3\n:1\n:2\n", "EOF") - testResp3Parser(t, "~3\n:1\n:2\n:3\n", "(set)\n\t(integer) 1\n\t(integer) 2\n\t(integer) 3") - testResp3Parser(t, "~2\n*3\n:1\n$5\nhello\n:2\n#f\n", "(set)\n\t(array)\n\t\t(integer) 1\n\t\t\"hello\"\n\t\t(integer) 2\n\t(boolean) false") + assert := testifyAssert.New(t) + + bigNumber, _ := big.NewInt(0).SetString("3492890328409238509324850943850943825024385", 10) + testCases := []TestResp3{ + // simple renderString + {protocol: "+renderString", err: "EOF"}, + {resp3: &Resp3{Type: Resp3SimpleString, Str: ""}, protocol: "+\n", render: `""`}, + {resp3: &Resp3{Type: Resp3SimpleString, Str: "hello"}, protocol: "+hello\n", render: `"hello"`}, + {resp3: &Resp3{Type: Resp3SimpleString, Str: "hello world"}, protocol: "+hello world\n", render: `"hello world"`}, + + // blob renderString + {protocol: "$1\n\n", err: "unexpected line end"}, + {protocol: "$1\naa\n", err: "unexpected line end"}, + {resp3: &Resp3{Type: Resp3BlobString, Str: ""}, protocol: "$0\n\n", render: `""`}, + {resp3: &Resp3{Type: Resp3BlobString, Str: "hello"}, protocol: "$5\nhello\n", render: `"hello"`}, + {resp3: &Resp3{Type: Resp3BlobString, Str: "hello\nworld"}, protocol: "$11\nhello\nworld\n", render: "\"hello\\nworld\""}, + + // simple error + {protocol: "-renderString", err: "EOF"}, + {resp3: &Resp3{Type: Resp3SimpleError, Str: ""}, protocol: "-\n", render: `(error) `}, + {resp3: &Resp3{Type: Resp3SimpleError, Str: "hello"}, protocol: "-hello\n", render: `(error) hello`}, + {resp3: &Resp3{Type: Resp3SimpleError, Str: "hello world"}, protocol: "-hello world\n", render: `(error) hello world`}, + + // blob error + {protocol: "!1\n\n", err: "unexpected line end"}, + {protocol: "!1\naa\n", err: "unexpected line end"}, + {resp3: &Resp3{Type: Resp3BolbError, Str: ""}, protocol: "!0\n\n", render: `(error) `}, + {resp3: &Resp3{Type: Resp3BolbError, Str: "hello"}, protocol: "!5\nhello\n", render: `(error) hello`}, + {resp3: &Resp3{Type: Resp3BolbError, Str: "hello\nworld"}, protocol: "!11\nhello\nworld\n", render: "(error) hello\nworld"}, + + // number + {protocol: ":invalid", err: "EOF"}, + {protocol: ":invalid\n", err: "ERR: error casting invalid to int"}, + {resp3: &Resp3{Type: Resp3Number, Integer: -1}, protocol: ":-1\n", render: `(integer) -1`}, + {resp3: &Resp3{Type: Resp3Number, Integer: 0}, protocol: ":0\n", render: `(integer) 0`}, + {resp3: &Resp3{Type: Resp3Number, Integer: 100}, protocol: ":100\n", render: `(integer) 100`}, + + // double + {protocol: ",invalid", err: "EOF"}, + {protocol: ",invalid\n", err: "convert invalid to double fail, because of strconv.ParseFloat: parsing \"invalid\": invalid syntax"}, + {resp3: &Resp3{Type: Resp3Double, Double: -1}, protocol: ",-1\n", render: "(double) -1"}, + {resp3: &Resp3{Type: Resp3Double, Double: 0}, protocol: ",0\n", render: "(double) 0"}, + {resp3: &Resp3{Type: Resp3Double, Double: 10}, protocol: ",10\n", render: "(double) 10"}, + {resp3: &Resp3{Type: Resp3Double, Double: 1.23}, protocol: ",1.23\n", render: "(double) 1.23"}, + {protocol: ",.1\n", render: "(double) 0.1"}, + {protocol: ",1.\n", render: "(double) 1"}, + + // big number + {protocol: "(invalid", err: "EOF"}, + {protocol: "(invalid\n", err: "convert invalid to Big Number fail"}, + {resp3: &Resp3{Type: Resp3BigNumber, BigInt: bigNumber}, protocol: "(3492890328409238509324850943850943825024385\n", render: "(big number) 3492890328409238509324850943850943825024385"}, + + // null + {protocol: "_invalid", err: "unexpect string: "}, + {protocol: "_invalid\n", err: "unexpect string: "}, + {resp3: &Resp3{Type: Resp3Null}, protocol: "_\n", render: "(null)"}, + + // boolean + {protocol: "#", err: "EOF"}, + {protocol: "#\n", err: "unexpected line end"}, + {protocol: "#x\n", err: "unexpect string: t/f"}, + {protocol: "#invalid", err: "unexpected line end"}, + {protocol: "#invalid\n", err: "unexpected line end"}, + {resp3: &Resp3{Type: Resp3Boolean, Boolean: true}, protocol: "#t\n", render: "(boolean) true"}, + {resp3: &Resp3{Type: Resp3Boolean, Boolean: false}, protocol: "#f\n", render: "(boolean) false"}, + + // array + {protocol: "*", err: "EOF"}, + {protocol: "*\n", err: "ERR: error casting to int"}, + {protocol: "*invalid", err: "EOF"}, + {protocol: "*invalid\n", err: "ERR: error casting invalid to int"}, + {protocol: "*1\n\n", err: "unknown protocol type: \n"}, + {protocol: "*1\ninvalid\n", err: "unknown protocol type: i"}, + {protocol: "*3\n:1\n:2\n", err: "EOF"}, + {resp3: &Resp3{Type: Resp3Array, Elems: []*Resp3{ + {Type: Resp3Number, Integer: 1}, + {Type: Resp3Number, Integer: 2}, + {Type: Resp3Number, Integer: 3}, + }}, protocol: "*3\n:1\n:2\n:3\n", render: "(array)\n\t(integer) 1\n\t(integer) 2\n\t(integer) 3"}, + {resp3: &Resp3{Type: Resp3Array, Elems: []*Resp3{ + {Type: Resp3Array, Elems: []*Resp3{ + {Type: Resp3Number, Integer: 1}, + {Type: Resp3BlobString, Str: "hello"}, + {Type: Resp3Number, Integer: 2}, + }}, + {Type: Resp3Boolean, Boolean: false}, + }}, protocol: "*2\n*3\n:1\n$5\nhello\n:2\n#f\n", render: "(array)\n\t(array)\n\t\t(integer) 1\n\t\t\"hello\"\n\t\t(integer) 2\n\t(boolean) false"}, + + // set + {protocol: "~", err: "EOF"}, + {protocol: "~\n", err: "ERR: error casting to int"}, + {protocol: "~invalid", err: "EOF"}, + {protocol: "~invalid\n", err: "ERR: error casting invalid to int"}, + {protocol: "~1\n\n", err: "unknown protocol type: \n"}, + {protocol: "~1\ninvalid\n", err: "unknown protocol type: i"}, + {protocol: "~3\n:1\n:2\n", err: "EOF"}, + {resp3: &Resp3{Type: Resp3Set, Elems: []*Resp3{ + {Type: Resp3Number, Integer: 1}, + {Type: Resp3Number, Integer: 2}, + {Type: Resp3Number, Integer: 3}, + }}, protocol: "~3\n:1\n:2\n:3\n", render: "(set)\n\t(integer) 1\n\t(integer) 2\n\t(integer) 3"}, + {resp3: &Resp3{Type: Resp3Set, Elems: []*Resp3{ + {Type: Resp3Array, Elems: []*Resp3{ + {Type: Resp3Number, Integer: 1}, + {Type: Resp3BlobString, Str: "hello"}, + {Type: Resp3Number, Integer: 2}, + }}, + {Type: Resp3Boolean, Boolean: false}, + }}, protocol: "~2\n*3\n:1\n$5\nhello\n:2\n#f\n", render: "(set)\n\t(array)\n\t\t(integer) 1\n\t\t\"hello\"\n\t\t(integer) 2\n\t(boolean) false"}, + } + + for _, testCase := range testCases { + if testCase.resp3 == nil { + if testCase.render != "" { + // protocol renderString -> render renderString + testResp3Parser(t, testCase.protocol, testCase.render) + } else { + // err test + testResp3Error(t, testCase.protocol, testCase.err) + } + } else { + // resp3 -> protocol renderString -> render renderString + assert.Equal(testCase.protocol, testCase.resp3.ProtocolString()) + testResp3Parser(t, testCase.protocol, testCase.render) + } + } }