Skip to content
This repository was archived by the owner on Apr 2, 2021. It is now read-only.

add resp3 protocol part 5(replace to resp3) #83

Merged
merged 1 commit into from
Aug 27, 2018
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions internal/arch/command-table.go
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ import (
)

// CommandFunc holds a function signature which can be used as a command.
type CommandFunc func(*db.DB, []string) *protcl.Message
type CommandFunc func(*db.DB, []string) *protcl.Resp3

// Command holds a command structure which is used to execute a kache command
type Command struct {
@@ -72,14 +72,14 @@ func getCommand(cmd string) (*Command, error) {
}

// Execute a single command on the given database with args
func (DBCommand) Execute(db *db.DB, cmd string, args []string) *protcl.Message {
func (DBCommand) Execute(db *db.DB, cmd string, args []string) *protcl.Resp3 {
command, err := getCommand(cmd)
if err != nil {
return protcl.NewMessage(nil, err)
return &protcl.Resp3{Type: protcl.Resp3SimpleError, Str: err.Error()}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not we add Err error to Resp3 to directly accept errors

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for now i will merge it we can replace it later

}

if argsLen := len(args); (command.MinArgs > 0 && argsLen < command.MinArgs) || (command.MaxArgs != -1 && argsLen > command.MaxArgs) {
return protcl.NewMessage(nil, &protcl.ErrWrongNumberOfArgs{Cmd: cmd})
return &protcl.Resp3{Type: protcl.Resp3SimpleError, Str: (&protcl.ErrWrongNumberOfArgs{Cmd: cmd}).Error()}
}

return command.Fn(db, args)
38 changes: 24 additions & 14 deletions internal/arch/command-table_test.go
Original file line number Diff line number Diff line change
@@ -9,38 +9,48 @@ import (
"github.com/kasvith/kache/internal/protcl"
)

func testRespError(t *testing.T, err error, resp3 *protcl.Resp3) {
assert := testifyAssert.New(t)
if err == nil {
assert.NotEqual(protcl.Resp3SimpleError, resp3.Type)
assert.NotEqual(protcl.Resp3BolbError, resp3.Type)
return
}

assert.Equal(err.Error(), resp3.Str)
}

// TestCommandArgsCountValidator will validate the command args count field
func TestCommandArgsCountValidator(t *testing.T) {
assert := testifyAssert.New(t)
cmd := &DBCommand{}
db := db.NewDB()

// ping at most 1
{
assert.Nil(cmd.Execute(db, "ping", nil).Err)
assert.Nil(cmd.Execute(db, "ping", []string{"1"}).Err)
assert.Equal(&protcl.ErrWrongNumberOfArgs{Cmd: "ping"}, cmd.Execute(db, "ping", []string{"1", "2"}).Err)
testRespError(t, nil, cmd.Execute(db, "ping", nil))
testRespError(t, nil, cmd.Execute(db, "ping", []string{"1"}))
testRespError(t, &protcl.ErrWrongNumberOfArgs{Cmd: "ping"}, cmd.Execute(db, "ping", []string{"1", "2"}))
}

// del at least 1
{
assert.Equal(&protcl.ErrWrongNumberOfArgs{Cmd: "del"}, cmd.Execute(db, "del", nil).Err)
assert.Nil(cmd.Execute(db, "del", []string{"1"}).Err)
assert.Nil(cmd.Execute(db, "del", []string{"1", "2"}).Err)
testRespError(t, &protcl.ErrWrongNumberOfArgs{Cmd: "del"}, cmd.Execute(db, "del", nil))
testRespError(t, nil, cmd.Execute(db, "del", []string{"1"}))
testRespError(t, nil, cmd.Execute(db, "del", []string{"1", "2"}))
}

// set equal 2
{
assert.Equal(&protcl.ErrWrongNumberOfArgs{Cmd: "set"}, cmd.Execute(db, "set", nil).Err)
assert.Equal(&protcl.ErrWrongNumberOfArgs{Cmd: "set"}, cmd.Execute(db, "set", []string{"1"}).Err)
assert.Nil(cmd.Execute(db, "set", []string{"1", "2"}).Err)
assert.Equal(&protcl.ErrWrongNumberOfArgs{Cmd: "set"}, cmd.Execute(db, "set", []string{"1", "2", "3"}).Err)
testRespError(t, &protcl.ErrWrongNumberOfArgs{Cmd: "set"}, cmd.Execute(db, "set", nil))
testRespError(t, &protcl.ErrWrongNumberOfArgs{Cmd: "set"}, cmd.Execute(db, "set", []string{"1"}))
testRespError(t, nil, cmd.Execute(db, "set", []string{"1", "2"}))
testRespError(t, &protcl.ErrWrongNumberOfArgs{Cmd: "set"}, cmd.Execute(db, "set", []string{"1", "2", "3"}))
}

// equal 1: get exists incr decr
for _, command := range []string{"get", "exists", "incr", "decr"} {
assert.Equal(&protcl.ErrWrongNumberOfArgs{Cmd: command}, cmd.Execute(db, command, nil).Err)
assert.Nil(cmd.Execute(db, command, []string{"1"}).Err)
assert.Equal(&protcl.ErrWrongNumberOfArgs{Cmd: command}, cmd.Execute(db, command, []string{"1", "2"}).Err)
testRespError(t, &protcl.ErrWrongNumberOfArgs{Cmd: command}, cmd.Execute(db, command, nil))
testRespError(t, nil, cmd.Execute(db, command, []string{"1"}))
testRespError(t, &protcl.ErrWrongNumberOfArgs{Cmd: command}, cmd.Execute(db, command, []string{"1", "2"}))
}
}
12 changes: 8 additions & 4 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
@@ -30,6 +30,8 @@ import (
"strings"

"github.com/c-bata/go-prompt"

"github.com/kasvith/kache/internal/protcl"
)

// RunCli start kache-cli command
@@ -59,18 +61,20 @@ func Executor(s string) {
return
}

if err := c.Write(s); err != nil {
if err := c.Write(protcl.NewSliceResp3(strings.Split(s, " "))); err != nil {
fmt.Println(err)
return
}

resp, err := c.parseResp()
resp, err := c.resp3Parser.Parse()
if err != nil {
fmt.Println(err)
return
} else if resp != nil {
fmt.Println(resp.RenderString())
return
}

fmt.Println(resp)
fmt.Println("(empty)")
}

// Completer used in CLI
20 changes: 12 additions & 8 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
@@ -2,13 +2,15 @@ package cli

import (
"strconv"
"strings"
"testing"
"time"

testifyAssert "github.com/stretchr/testify/assert"

"github.com/kasvith/kache/internal/config"
"github.com/kasvith/kache/internal/klogs"
"github.com/kasvith/kache/internal/protcl"
"github.com/kasvith/kache/internal/srv"
)

@@ -37,10 +39,11 @@ func initTestServerClient(t *testing.T) {
func runTestSendRecv(t *testing.T, send, recv string) {
assert := testifyAssert.New(t)

assert.Nil(c.Write(send))
resp, err := c.parseResp()
assert.Nil(c.Write(protcl.NewSliceResp3(strings.Split(send, " "))))
resp, err := c.resp3Parser.Parse()
assert.Nil(err)
assert.Equal(recv, resp)
assert.NotNil(resp)
assert.Equal(recv, resp.RenderString())
}

func TestCli(t *testing.T) {
@@ -53,14 +56,14 @@ func TestCli(t *testing.T) {
// strings
{
// get not found
runTestSendRecv(t, "get a", "(error) ERR: a not found")
runTestSendRecv(t, "get a", "(error) a not found")

// set
runTestSendRecv(t, "set a 1", `"OK"`)
runTestSendRecv(t, "set b 2", `"OK"`)

// get exist
runTestSendRecv(t, "get a", "1")
runTestSendRecv(t, "get a", `"1"`)

// incr decr
runTestSendRecv(t, "incr b", "(integer) 3")
@@ -72,10 +75,11 @@ func TestCli(t *testing.T) {
// key space
{
// keys
assert.Nil(c.Write("keys"))
resp, err := c.parseResp()
assert.Nil(c.Write("+keys\n"))
resp, err := c.resp3Parser.Parse()
assert.Nil(err)
assert.Contains([]string{"1) b\n2) a\n", "1) a\n2) b\n"}, resp)
assert.NotNil(resp)
assert.Contains([]string{"(array)\n\t\"a\"\n\t\"b\"", "(array)\n\t\"b\"\n\t\"a\""}, resp.RenderString())

// exists
runTestSendRecv(t, "exists a", "(integer) 1")
53 changes: 5 additions & 48 deletions internal/cli/conn.go
Original file line number Diff line number Diff line change
@@ -27,9 +27,7 @@ package cli
import (
"bufio"
"fmt"
"io"
"net"
"strings"
"time"

"github.com/kasvith/kache/internal/protcl"
@@ -38,9 +36,9 @@ import (
var c *cli

type cli struct {
conn net.Conn
reader *bufio.Reader
addr string
conn net.Conn
resp3Parser *protcl.Resp3Parser
addr string
}

// Write send string to server
@@ -49,11 +47,7 @@ func (r *cli) Write(s string) error {
}

func (r *cli) write(s string, reconnect bool) error {
send := make([]byte, len(s)+2)
copy(send[:len(s)], s)
copy(send[len(s):], []byte{'\r', '\n'})

n, err := c.conn.Write(send)
n, err := c.conn.Write([]byte(s))
if n == 0 && err != nil && reconnect {
fmt.Println("reconnecting...")

@@ -65,43 +59,6 @@ func (r *cli) write(s string, reconnect bool) error {
return err
}

func (r *cli) parseResp() (string, error) {
buf, err := r.reader.ReadBytes('\n')
if err != nil {
if err == io.EOF {
return "", nil
}
return "", err
}

if err := protcl.EndWithCRLF(buf); err != nil {
return "", err
}

switch buf[0] {
case '*':
strs, err := protcl.ParseMultiBulkReply(r.reader, buf)
if err != nil {
return "", err
}
builder := strings.Builder{}
for key, value := range strs {
builder.WriteString(fmt.Sprintf("%d) %s\n", key+1, value))
}
return builder.String(), nil
case '$':
return protcl.ParseBulkString(r.reader, buf)
case '+':
return fmt.Sprintf("%q", buf[1:len(buf)-2]), nil
case ':':
return fmt.Sprintf("(integer) %s", buf[1:len(buf)-2]), nil
case '-':
return fmt.Sprintf("(error) %s", buf[1:len(buf)-2]), nil
}

return "", fmt.Errorf("unsupport resp type: %s", []byte{buf[0]})
}

// Dial conn kache server
func Dial(addr string) error {
conn, err := net.DialTimeout("tcp", addr, time.Second)
@@ -111,7 +68,7 @@ func Dial(addr string) error {

c = new(cli)
c.conn = conn
c.reader = bufio.NewReader(conn)
c.resp3Parser = protcl.NewResp3Parser(bufio.NewReader(conn))
c.addr = addr

return nil
12 changes: 6 additions & 6 deletions internal/cmds/keys.go
Original file line number Diff line number Diff line change
@@ -30,19 +30,19 @@ import (
)

// Exists will check for key existency in given db
func Exists(d *db.DB, args []string) *protcl.Message {
func Exists(d *db.DB, args []string) *protcl.Resp3 {
found := d.Exists(args[0])
return protcl.NewMessage(protcl.NewIntegerReply(found), nil)
return &protcl.Resp3{Type: protcl.Resp3Number, Integer: found}
}

// Del will delete set of keys and return number of deleted keys
func Del(d *db.DB, args []string) *protcl.Message {
func Del(d *db.DB, args []string) *protcl.Resp3 {
deleted := d.Del(args)
return protcl.NewMessage(protcl.NewIntegerReply(deleted), nil)
return &protcl.Resp3{Type: protcl.Resp3Number, Integer: deleted}
}

// Keys will return all keys of the db as a list
func Keys(d *db.DB, args []string) *protcl.Message {
func Keys(d *db.DB, args []string) *protcl.Resp3 {
keys := d.Keys()
return protcl.NewMessage(protcl.NewArrayReply(false, keys), nil)
return &protcl.Resp3{Type: protcl.Resp3Array, Elems: keys}
}
6 changes: 3 additions & 3 deletions internal/cmds/server.go
Original file line number Diff line number Diff line change
@@ -30,10 +30,10 @@ import (
)

// Ping will return PONG when no argument found or will echo the given argument
func Ping(d *db.DB, args []string) *protcl.Message {
func Ping(d *db.DB, args []string) *protcl.Resp3 {
if len(args) == 0 {
return protcl.NewMessage(protcl.NewSimpleStringReply("PONG"), nil)
return &protcl.Resp3{Type: protcl.RepSimpleString, Str: "PONG"}
}

return protcl.NewMessage(protcl.NewBulkStringReply(false, args[0]), nil)
return &protcl.Resp3{Type: protcl.Resp3BlobString, Str: args[0]}
}
26 changes: 13 additions & 13 deletions internal/cmds/strings.go
Original file line number Diff line number Diff line change
@@ -33,59 +33,59 @@ import (
)

// Get will find the value of a given string key and return it
func Get(d *db.DB, args []string) *protcl.Message {
func Get(d *db.DB, args []string) *protcl.Resp3 {
val, err := d.Get(args[0])
if err != nil {
return protcl.NewMessage(nil, &protcl.ErrGeneric{Err: err})
return &protcl.Resp3{Type: protcl.Resp3BolbError, Str: err.Error()}
}

if val.Type != db.TypeString {
return protcl.NewMessage(nil, &protcl.ErrWrongType{})
return &protcl.Resp3{Type: protcl.Resp3BolbError, Str: (&protcl.ErrWrongType{}).Error()}
}

return protcl.NewMessage(protcl.NewBulkStringReply(false, util.ToString(val.Value)), nil)
return &protcl.Resp3{Type: protcl.RepBulkString, Str: util.ToString(val.Value)}
}

// Set will create a new string key value pair
func Set(d *db.DB, args []string) *protcl.Message {
func Set(d *db.DB, args []string) *protcl.Resp3 {
key := args[0]
val := args[1]

d.Set(key, db.NewDataNode(db.TypeString, -1, val))

return protcl.NewMessage(protcl.NewSimpleStringReply("OK"), nil)
return &protcl.Resp3{Type: protcl.RepSimpleString, Str: "OK"}
}

// Incr will increment a given string key by 1
// If key not found it will be set to 0 and will do operation
// If key type is invalid it will return an error
func Incr(d *db.DB, args []string) *protcl.Message {
func Incr(d *db.DB, args []string) *protcl.Resp3 {
return accumulateBy(d, args[0], 1, true)
}

// Decr will decrement a given string key by 1
// If key not found it will be set to 0 and will do operation
// If key type is invalid it will return an error
func Decr(d *db.DB, args []string) *protcl.Message {
func Decr(d *db.DB, args []string) *protcl.Resp3 {
return accumulateBy(d, args[0], -1, true)
}

// accumulateBy will accumulate the value of key by given amount
func accumulateBy(d *db.DB, key string, v int, incr bool) *protcl.Message {
func accumulateBy(d *db.DB, key string, v int, incr bool) *protcl.Resp3 {
val, found := d.GetIfNotSet(key, db.NewDataNode(db.TypeString, -1, strconv.Itoa(v)))

if !found {
return protcl.NewMessage(protcl.NewIntegerReply(v), nil)
return &protcl.Resp3{Type: protcl.Resp3Number, Integer: v}
}

if val.Type != db.TypeString {
return protcl.NewMessage(nil, protcl.ErrWrongType{})
return &protcl.Resp3{Type: protcl.Resp3BolbError, Str: (&protcl.ErrWrongType{}).Error()}
}

i, err := strconv.Atoi(util.ToString(val.Value))

if err != nil {
return protcl.NewMessage(nil, &protcl.ErrCastFailedToInt{Val: val.Value})
return &protcl.Resp3{Type: protcl.Resp3BolbError, Str: (&protcl.ErrCastFailedToInt{Val: val.Value}).Error()}
}

var n int
@@ -97,5 +97,5 @@ func accumulateBy(d *db.DB, key string, v int, incr bool) *protcl.Message {

d.Set(key, db.NewDataNode(db.TypeString, -1, strconv.Itoa(n)))

return protcl.NewMessage(protcl.NewIntegerReply(n), nil)
return &protcl.Resp3{Type: protcl.Resp3Number, Integer: n}
}
6 changes: 3 additions & 3 deletions internal/db/db.go
Original file line number Diff line number Diff line change
@@ -113,13 +113,13 @@ func (db *DB) Exists(key string) int {
}

// Keys returns all keys of the db
func (db *DB) Keys() []protcl.Reply {
func (db *DB) Keys() []*protcl.Resp3 {
db.mux.RLock()

keys := make([]protcl.Reply, len(db.file))
keys := make([]*protcl.Resp3, len(db.file))
i := 0
for key := range db.file {
keys[i] = protcl.NewBulkStringReply(false, key)
keys[i] = &protcl.Resp3{Type: protcl.Resp3BlobString, Str: key}
i++
}

221 changes: 62 additions & 159 deletions internal/protcl/resp3.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package protcl

import (
"bufio"
"fmt"
"math/big"
"strconv"
@@ -39,6 +38,22 @@ type Resp3 struct {
Elems []*Resp3
}

// NewSliceResp3 convert string slice to resp3 protocol raw string
func NewSliceResp3(slices []string) string {
buf := new(strings.Builder)
buf.WriteByte('*')
buf.WriteString(strconv.Itoa(len(slices)))
buf.WriteByte('\n')
for _, v := range slices {
buf.WriteByte('$')
buf.WriteString(strconv.Itoa(len(v)))
buf.WriteByte('\n')
buf.WriteString(v)
buf.WriteByte('\n')
}
return buf.String()
}

// RenderString convert resp3 to show-message on client
func (r *Resp3) RenderString() string {
return r.renderString("")
@@ -52,6 +67,52 @@ func (r *Resp3) ProtocolString() string {
return buf.String()
}

func (r *Resp3) commands() ([]string, error) {
switch r.Type {
case RepSimpleString, Resp3BlobString, Resp3Number, Resp3Double, Resp3BigNumber, Resp3Boolean:
c, err := r.command()
if err != nil {
return nil, err
}
return []string{c}, nil
case Resp3Array:
var slices = make([]string, len(r.Elems))
i := 0
for _, v := range r.Elems {
if v.Type != RepSimpleString && v.Type != Resp3BlobString && v.Type != Resp3Number && v.Type != Resp3Double && v.Type != Resp3BigNumber && v.Type != Resp3Boolean {
return nil, &ErrInvalidCommand{}
}
c, err := v.command()
if err != nil {
return nil, err
}
slices[i] = c
i++
}
return slices, nil
}
return nil, &ErrInvalidCommand{}
}

func (r *Resp3) command() (string, error) {
switch r.Type {
case Resp3SimpleString, Resp3BlobString:
return r.Str, nil
case Resp3Number:
return strconv.Itoa(r.Integer), nil
case Resp3Double:
return strconv.FormatFloat(r.Double, 'f', -1, 64), nil
case Resp3BigNumber:
return r.BigInt.String(), nil
case Resp3Boolean:
if r.Boolean {
return "true", nil
}
return "false", nil
}
return "", &ErrInvalidCommand{}
}

func (r *Resp3) protocolString(buf *strings.Builder) {
switch r.Type {
case RepSimpleString, Resp3SimpleError:
@@ -123,161 +184,3 @@ func (r *Resp3) renderString(pre string) string {

return pre + "(error) unknown protocol type: " + string(r.Type)
}

// Resp3Parser is for parser resp3 protocol
type Resp3Parser struct {
reader *bufio.Reader
}

// NewResp3Parser return a Resp3Parser
func NewResp3Parser(r *bufio.Reader) *Resp3Parser {
return &Resp3Parser{reader: r}
}

// Parse return Resp3
func (r *Resp3Parser) Parse() (*Resp3, error) {
b, err := r.reader.ReadByte()
if err != nil {
return nil, err
}

switch b {
case Resp3SimpleString, Resp3SimpleError:
str, err := r.stringBeforeLF()
if err != nil {
return nil, err
}
return &Resp3{Type: b, Str: str}, nil
case Resp3BlobString, Resp3BolbError:
length, err := r.intBeforeLF()
if err != nil {
return nil, err
}

bs, err := r.readLengthBytesWithLF(length)
if err != nil {
return nil, err
}

return &Resp3{Type: b, Str: string(bs)}, nil
case Resp3Number:
integer, err := r.intBeforeLF()
if err != nil {
return nil, err
}
return &Resp3{Type: b, Integer: integer}, nil
case Resp3Double:
str, err := r.stringBeforeLF()
if err != nil {
return nil, err
}
f, err := strconv.ParseFloat(str, 64)
if err != nil {
return nil, &ErrConvertType{Type: "double", Value: str, Err: err}
}
return &Resp3{Type: b, Double: f}, nil
case Resp3BigNumber:
str, err := r.stringBeforeLF()
if err != nil {
return nil, err
}
bigInt, ok := big.NewInt(0).SetString(str, 10)
if !ok {
return nil, &ErrConvertType{Type: "Big Number", Value: str}
}
return &Resp3{Type: b, BigInt: bigInt}, nil
case Resp3Null:
if _, err := r.readLengthBytesWithLF(0); err != nil {
return nil, err
}
return &Resp3{Type: b}, nil
case Resp3Boolean:
buf, err := r.readLengthBytesWithLF(1)
if err != nil {
return nil, err
}

switch buf[0] {
case 't':
return &Resp3{Type: b, Boolean: true}, nil
case 'f':
return &Resp3{Type: b, Boolean: false}, nil
}
return nil, &ErrUnexpectString{Str: "t/f"}
case Resp3Array, Resp3Set:
length, err := r.intBeforeLF()
if err != nil {
return nil, err
}
resp := &Resp3{Type: b}
for i := 0; i < length; i++ {
elem, err := r.Parse()
if err != nil {
return nil, err
}
resp.Elems = append(resp.Elems, elem)
}
return resp, nil
}

return nil, &ErrProtocolType{Type: b}
}

func (r *Resp3Parser) stringBeforeLF() (string, error) {
buf, err := r.reader.ReadBytes(LF)
if err != nil {
return "", err
}
bs, err := trimLastLF(buf)
if err != nil {
return "", err
}
return string(bs), nil
}

func (r *Resp3Parser) intBeforeLF() (int, error) {
buf, err := r.reader.ReadBytes(LF)
if err != nil {
return 0, err
}
bs, err := trimLastLF(buf)
if err != nil {
return 0, err
}
s := string(bs)
i, err := strconv.Atoi(s)
if err != nil {
return 0, &ErrCastFailedToInt{Val: s}
}
return i, nil
}

func (r *Resp3Parser) readLengthBytesWithLF(length int) ([]byte, error) {
if length == 0 {
if b, err := r.reader.ReadByte(); err != nil {
return nil, err
} else if b != LF {
return nil, &ErrUnexpectString{Str: "<LF>"}
}
return nil, nil
}

buf := make([]byte, length+1)
n, err := r.reader.Read(buf)
if err != nil {
return nil, err
} else if n < length+1 {
return nil, &ErrUnexpectedLineEnd{}
}

return trimLastLF(buf)
}

func trimLastLF(buf []byte) ([]byte, error) {
bufLen := len(buf)
if len(buf) == 0 || buf[bufLen-1] != LF {
return nil, &ErrUnexpectedLineEnd{}
}

return buf[:bufLen-1], nil
}
191 changes: 191 additions & 0 deletions internal/protcl/resp3_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package protcl

import (
"bufio"
"math/big"
"strconv"
"strings"
)

// Resp3Parser is for parser resp3 protocol
type Resp3Parser struct {
reader *bufio.Reader
}

// NewResp3Parser return a Resp3Parser
func NewResp3Parser(r *bufio.Reader) *Resp3Parser {
return &Resp3Parser{reader: r}
}

// Commands parse resp3 message to kache command
func (r *Resp3Parser) Commands() (*RespCommand, error) {
resp3, err := r.Parse()
if err != nil {
return nil, err
}
args, err := resp3.commands()
if err != nil {
return nil, err
}
if len(args) == 0 {
return nil, &ErrInvalidCommand{}
}

return &RespCommand{Name: strings.ToLower(args[0]), Args: args[1:]}, nil
}

// Parse return Resp3
func (r *Resp3Parser) Parse() (*Resp3, error) {
resp, err := r.parse()
if err != nil {
return nil, err
}
return resp, nil
}

func (r *Resp3Parser) parse() (*Resp3, error) {
b, err := r.reader.ReadByte()
if err != nil {
return nil, err
}

switch b {
case Resp3SimpleString, Resp3SimpleError:
str, err := r.stringBeforeLF()
if err != nil {
return nil, err
}
return &Resp3{Type: b, Str: str}, nil
case Resp3BlobString, Resp3BolbError:
length, err := r.intBeforeLF()
if err != nil {
return nil, err
}

bs, err := r.readLengthBytesWithLF(length)
if err != nil {
return nil, err
}

return &Resp3{Type: b, Str: string(bs)}, nil
case Resp3Number:
integer, err := r.intBeforeLF()
if err != nil {
return nil, err
}
return &Resp3{Type: b, Integer: integer}, nil
case Resp3Double:
str, err := r.stringBeforeLF()
if err != nil {
return nil, err
}
f, err := strconv.ParseFloat(str, 64)
if err != nil {
return nil, &ErrConvertType{Type: "double", Value: str, Err: err}
}
return &Resp3{Type: b, Double: f}, nil
case Resp3BigNumber:
str, err := r.stringBeforeLF()
if err != nil {
return nil, err
}
bigInt, ok := big.NewInt(0).SetString(str, 10)
if !ok {
return nil, &ErrConvertType{Type: "Big Number", Value: str}
}
return &Resp3{Type: b, BigInt: bigInt}, nil
case Resp3Null:
if _, err := r.readLengthBytesWithLF(0); err != nil {
return nil, err
}
return &Resp3{Type: b}, nil
case Resp3Boolean:
buf, err := r.readLengthBytesWithLF(1)
if err != nil {
return nil, err
}

switch buf[0] {
case 't':
return &Resp3{Type: b, Boolean: true}, nil
case 'f':
return &Resp3{Type: b, Boolean: false}, nil
}
return nil, &ErrUnexpectString{Str: "t/f"}
case Resp3Array, Resp3Set:
length, err := r.intBeforeLF()
if err != nil {
return nil, err
}
resp := &Resp3{Type: b}
for i := 0; i < length; i++ {
elem, err := r.Parse()
if err != nil {
return nil, err
}
resp.Elems = append(resp.Elems, elem)
}
return resp, nil
}

return nil, &ErrProtocolType{Type: b}
}

func (r *Resp3Parser) stringBeforeLF() (string, error) {
buf, err := r.reader.ReadBytes(LF)
if err != nil {
return "", err
}
bs, err := trimLastLF(buf)
if err != nil {
return "", err
}
return string(bs), nil
}

func (r *Resp3Parser) intBeforeLF() (int, error) {
buf, err := r.reader.ReadBytes(LF)
if err != nil {
return 0, err
}
bs, err := trimLastLF(buf)
if err != nil {
return 0, err
}
s := string(bs)
i, err := strconv.Atoi(s)
if err != nil {
return 0, &ErrCastFailedToInt{Val: s}
}
return i, nil
}

func (r *Resp3Parser) readLengthBytesWithLF(length int) ([]byte, error) {
if length == 0 {
if b, err := r.reader.ReadByte(); err != nil {
return nil, err
} else if b != LF {
return nil, &ErrUnexpectString{Str: "<LF>"}
}
return nil, nil
}

buf := make([]byte, length+1)
n, err := r.reader.Read(buf)
if err != nil {
return nil, err
} else if n < length+1 {
return nil, &ErrUnexpectedLineEnd{}
}

return trimLastLF(buf)
}

func trimLastLF(buf []byte) ([]byte, error) {
bufLen := len(buf)
if len(buf) == 0 || buf[bufLen-1] != LF {
return nil, &ErrUnexpectedLineEnd{}
}

return buf[:bufLen-1], nil
}
11 changes: 3 additions & 8 deletions internal/srv/client.go
Original file line number Diff line number Diff line change
@@ -61,11 +61,11 @@ func (client *Client) RemoteAddr() net.Addr {
func (client *Client) Handle() {
// TODO determine client type by first issued command to kache, this can improve performance

reader := protcl.NewReader(client.Connection)
resp3Parser := protcl.NewResp3Parser(bufio.NewReader(client.Connection))
writer := bufio.NewWriter(client.Connection)

for {
command, err := reader.ParseMessage()
command, err := resp3Parser.Commands()

// handle any parse errors
if err != nil {
@@ -95,12 +95,7 @@ func (client *Client) Handle() {
// executes the command
message := commander.Execute(DB, command.Name, command.Args)

// return the results
if message.Err == nil {
writer.WriteString(message.RespReply())
} else {
writer.WriteString(protcl.RespError(message.Err))
}
writer.WriteString(message.ProtocolString())

writer.Flush()
}