forked from mirror/redcon
Added RESP type
This commit is contained in:
parent
fc7a9e8758
commit
f50c3d4246
|
@ -0,0 +1,129 @@
|
||||||
|
package redcon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Type of RESP
|
||||||
|
type Type byte
|
||||||
|
|
||||||
|
// Various RESP kinds
|
||||||
|
const (
|
||||||
|
Integer = ':'
|
||||||
|
String = '+'
|
||||||
|
Bulk = '$'
|
||||||
|
Array = '*'
|
||||||
|
Error = '-'
|
||||||
|
)
|
||||||
|
|
||||||
|
// RESP ...
|
||||||
|
type RESP struct {
|
||||||
|
Type Type
|
||||||
|
Raw []byte
|
||||||
|
Data []byte
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForEach iterates over each Array element
|
||||||
|
func (r *RESP) ForEach(iter func(resp RESP) bool) {
|
||||||
|
data := r.Data
|
||||||
|
for i := 0; i < r.Count; i++ {
|
||||||
|
n, resp := ReadNextRESP(data)
|
||||||
|
if !iter(resp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data = data[n:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadNextRESP returns the next resp in b and returns the number of bytes the
|
||||||
|
// took up the result.
|
||||||
|
func ReadNextRESP(b []byte) (n int, resp RESP) {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return 0, RESP{} // no data to read
|
||||||
|
}
|
||||||
|
resp.Type = Type(b[0])
|
||||||
|
switch resp.Type {
|
||||||
|
case Integer, String, Bulk, Array, Error:
|
||||||
|
default:
|
||||||
|
return 0, RESP{} // invalid kind
|
||||||
|
}
|
||||||
|
// read to end of line
|
||||||
|
i := 1
|
||||||
|
for ; ; i++ {
|
||||||
|
if i == len(b) {
|
||||||
|
return 0, RESP{} // not enough data
|
||||||
|
}
|
||||||
|
if b[i] == '\n' {
|
||||||
|
if b[i-1] != '\r' {
|
||||||
|
return 0, RESP{} //, missing CR character
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.Raw = b[0:i]
|
||||||
|
resp.Data = b[1 : i-2]
|
||||||
|
if resp.Type == Integer {
|
||||||
|
// Integer
|
||||||
|
if len(resp.Data) == 0 {
|
||||||
|
return 0, RESP{} //, invalid integer
|
||||||
|
}
|
||||||
|
var j int
|
||||||
|
if resp.Data[0] == '-' {
|
||||||
|
if len(resp.Data) == 1 {
|
||||||
|
return 0, RESP{} //, invalid integer
|
||||||
|
}
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
for ; j < len(resp.Data); j++ {
|
||||||
|
if resp.Data[j] < '0' || resp.Data[j] > '9' {
|
||||||
|
return 0, RESP{} // invalid integer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(resp.Raw), resp
|
||||||
|
}
|
||||||
|
if resp.Type == String || resp.Type == Error {
|
||||||
|
// String, Error
|
||||||
|
return len(resp.Raw), resp
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
resp.Count, err = strconv.Atoi(string(resp.Data))
|
||||||
|
if resp.Type == Bulk {
|
||||||
|
// Bulk
|
||||||
|
if err != nil {
|
||||||
|
return 0, RESP{} // invalid number of bytes
|
||||||
|
}
|
||||||
|
if resp.Count < 0 {
|
||||||
|
resp.Data = nil
|
||||||
|
resp.Count = 0
|
||||||
|
return len(resp.Raw), resp
|
||||||
|
}
|
||||||
|
if len(b) < i+resp.Count+2 {
|
||||||
|
return 0, RESP{} // not enough data
|
||||||
|
}
|
||||||
|
if b[i+resp.Count] != '\r' || b[i+resp.Count+1] != '\n' {
|
||||||
|
return 0, RESP{} // invalid end of line
|
||||||
|
}
|
||||||
|
resp.Data = b[i : i+resp.Count]
|
||||||
|
resp.Raw = b[0 : i+resp.Count+2]
|
||||||
|
resp.Count = 0
|
||||||
|
return len(resp.Raw), resp
|
||||||
|
}
|
||||||
|
// Array
|
||||||
|
if err != nil {
|
||||||
|
return 0, RESP{} // invalid number of elements
|
||||||
|
}
|
||||||
|
var tn int
|
||||||
|
sdata := b[i:]
|
||||||
|
for j := 0; j < resp.Count; j++ {
|
||||||
|
rn, rresp := ReadNextRESP(sdata)
|
||||||
|
if rresp.Type == 0 {
|
||||||
|
return 0, RESP{}
|
||||||
|
}
|
||||||
|
tn += rn
|
||||||
|
}
|
||||||
|
resp.Data = b[i : i+tn]
|
||||||
|
resp.Raw = b[0 : i+tn]
|
||||||
|
return len(resp.Raw), resp
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
package redcon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isEmptyRESP(resp RESP) bool {
|
||||||
|
return resp.Type == 0 && resp.Count == 0 &&
|
||||||
|
resp.Data == nil && resp.Raw == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectBad(t *testing.T, payload string) {
|
||||||
|
t.Helper()
|
||||||
|
n, resp := ReadNextRESP([]byte(payload))
|
||||||
|
if n > 0 || !isEmptyRESP(resp) {
|
||||||
|
t.Fatalf("expected empty resp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func respVOut(a RESP) string {
|
||||||
|
var data string
|
||||||
|
var raw string
|
||||||
|
if a.Data == nil {
|
||||||
|
data = "nil"
|
||||||
|
} else {
|
||||||
|
data = strconv.Quote(string(a.Data))
|
||||||
|
}
|
||||||
|
if a.Raw == nil {
|
||||||
|
raw = "nil"
|
||||||
|
} else {
|
||||||
|
raw = strconv.Quote(string(a.Raw))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("{Type: %d, Count: %d, Data: %s, Raw: %s}",
|
||||||
|
a.Type, a.Count, data, raw,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func respEquals(a, b RESP) bool {
|
||||||
|
if a.Count != b.Count {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a.Type != b.Type {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (a.Data == nil && b.Data != nil) || (a.Data != nil && b.Data == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if string(a.Data) != string(b.Data) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (a.Raw == nil && b.Raw != nil) || (a.Raw != nil && b.Raw == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if string(a.Raw) != string(b.Raw) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectGood(t *testing.T, payload string, exp RESP) {
|
||||||
|
t.Helper()
|
||||||
|
n, resp := ReadNextRESP([]byte(payload))
|
||||||
|
if n != len(payload) || isEmptyRESP(resp) {
|
||||||
|
t.Fatalf("expected good resp")
|
||||||
|
}
|
||||||
|
if string(resp.Raw) != payload {
|
||||||
|
t.Fatalf("expected '%s', got '%s'", payload, resp.Raw)
|
||||||
|
}
|
||||||
|
exp.Raw = []byte(payload)
|
||||||
|
switch exp.Type {
|
||||||
|
case Integer, String, Error:
|
||||||
|
exp.Data = []byte(payload[1 : len(payload)-2])
|
||||||
|
}
|
||||||
|
if !respEquals(resp, exp) {
|
||||||
|
t.Fatalf("expected %v, got %v", respVOut(exp), respVOut(resp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRESP(t *testing.T) {
|
||||||
|
expectBad(t, "")
|
||||||
|
expectBad(t, "^hello\r\n")
|
||||||
|
expectBad(t, "+hello\r")
|
||||||
|
expectBad(t, "+hello\n")
|
||||||
|
expectBad(t, ":\r\n")
|
||||||
|
expectBad(t, ":-\r\n")
|
||||||
|
expectBad(t, ":-abc\r\n")
|
||||||
|
expectBad(t, ":abc\r\n")
|
||||||
|
expectGood(t, ":-123\r\n", RESP{Type: Integer})
|
||||||
|
expectGood(t, ":123\r\n", RESP{Type: Integer})
|
||||||
|
expectBad(t, "+\r")
|
||||||
|
expectBad(t, "+\n")
|
||||||
|
expectGood(t, "+\r\n", RESP{Type: String})
|
||||||
|
expectGood(t, "+hello world\r\n", RESP{Type: String})
|
||||||
|
expectBad(t, "-\r")
|
||||||
|
expectBad(t, "-\n")
|
||||||
|
expectGood(t, "-\r\n", RESP{Type: Error})
|
||||||
|
expectGood(t, "-hello world\r\n", RESP{Type: Error})
|
||||||
|
expectBad(t, "$")
|
||||||
|
expectBad(t, "$\r")
|
||||||
|
expectBad(t, "$\r\n")
|
||||||
|
expectGood(t, "$-1\r\n", RESP{Type: Bulk})
|
||||||
|
expectGood(t, "$0\r\n\r\n", RESP{Type: Bulk, Data: []byte("")})
|
||||||
|
expectBad(t, "$5\r\nhello\r")
|
||||||
|
expectBad(t, "$5\r\nhello\n\n")
|
||||||
|
expectGood(t, "$5\r\nhello\r\n", RESP{Type: Bulk, Data: []byte("hello")})
|
||||||
|
expectBad(t, "*a\r\n")
|
||||||
|
expectBad(t, "*3\r\n")
|
||||||
|
expectBad(t, "*3\r\n:hello\r")
|
||||||
|
expectGood(t, "*3\r\n:1\r\n:2\r\n:3\r\n",
|
||||||
|
RESP{Type: Array, Count: 3, Data: []byte(":1\r\n:2\r\n:3\r\n")})
|
||||||
|
|
||||||
|
var xx int
|
||||||
|
_, r := ReadNextRESP([]byte("*4\r\n:1\r\n:2\r\n:3\r\n:4\r\n"))
|
||||||
|
r.ForEach(func(resp RESP) bool {
|
||||||
|
xx++
|
||||||
|
x, _ := strconv.Atoi(string(resp.Data))
|
||||||
|
if x != xx {
|
||||||
|
t.Fatalf("expected %v, got %v", x, xx)
|
||||||
|
}
|
||||||
|
if xx == 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if xx != 3 {
|
||||||
|
t.Fatalf("expected %v, got %v", 3, xx)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue