From 2ea87c64f3d7d4044b7d4178fe7f3fd5a986da89 Mon Sep 17 00:00:00 2001 From: Kelvin Clement Mwinuka Date: Wed, 5 Jul 2023 07:10:59 +0800 Subject: [PATCH] Implemented LPUSH, RPUSH, and LRANGE commands --- server/main.go | 12 +- server/plugins/commands/list/list.go | 187 +++++++++++++++++++++++ server/plugins/commands/ping/ping.go | 2 + server/plugins/commands/setget/setget.go | 16 +- utils/utils.go | 34 ++--- 5 files changed, 228 insertions(+), 23 deletions(-) diff --git a/server/main.go b/server/main.go index 454e52a..32d15ce 100644 --- a/server/main.go +++ b/server/main.go @@ -36,15 +36,19 @@ type Server struct { plugins []Plugin } -func (server *Server) GetData(key string) interface{} { +func (server *Server) Lock() { server.data.mu.Lock() - defer server.data.mu.Unlock() +} + +func (Server *Server) Unlock() { + Server.data.mu.Unlock() +} + +func (server *Server) GetData(key string) interface{} { return server.data.data[key] } func (server *Server) SetData(key string, value interface{}) { - server.data.mu.Lock() - defer server.data.mu.Unlock() server.data.data[key] = value } diff --git a/server/plugins/commands/list/list.go b/server/plugins/commands/list/list.go index bc1f9bf..9aa6144 100644 --- a/server/plugins/commands/list/list.go +++ b/server/plugins/commands/list/list.go @@ -2,9 +2,20 @@ package main import ( "bufio" + "fmt" + "math" + "strings" + + "github.com/kelvinmwinuka/memstore/utils" +) + +const ( + OK = "+OK\r\n\n" ) type Server interface { + Lock() + Unlock() GetData(key string) interface{} SetData(key string, value interface{}) } @@ -30,6 +41,182 @@ func (p *plugin) Description() string { } func (p *plugin) HandleCommand(cmd []string, server interface{}, conn *bufio.Writer) { + switch strings.ToLower(cmd[0]) { + case "lrange": + handleLRange(cmd, server.(Server), conn) + case "lpush": + handleLPush(cmd, server.(Server), conn) + case "rpush": + handleRPush(cmd, server.(Server), conn) + } +} + +func handleLRange(cmd []string, server Server, conn *bufio.Writer) { + if len(cmd) != 4 { + conn.Write([]byte("-Error wrong number of arguments for LRANGE command\r\n\n")) + conn.Flush() + return + } + + start, startOk := utils.AdaptType(cmd[2]).(int) + end, endOk := utils.AdaptType(cmd[3]).(int) + + if !startOk || !endOk { + conn.Write([]byte("-Error both start and end indices must be integers\r\n\n")) + conn.Flush() + return + } + + server.Lock() + + list, ok := server.GetData(cmd[1]).([]interface{}) + + server.Unlock() + + if !ok { + conn.Write([]byte("-Error type cannot be returned with LRANGE command\r\n\n")) + conn.Flush() + return + } + + // Make sure start is within range + if !(start >= 0 && start < len(list)) { + conn.Write([]byte("-Error start index not within list range\r\n\n")) + conn.Flush() + return + } + + // Make sure end is within range, or is -1 otherwise + if !((end >= 0 && end < len(list)) || end == -1) { + conn.Write([]byte("-Error end index must be within list range or -1\r\n\n")) + conn.Flush() + return + } + + // If end is -1, read list from start to the end of the list + if end == -1 { + conn.Write([]byte("*" + fmt.Sprint(len(list)-start) + "\r\n")) + for i := start; i < len(list); i++ { + str := fmt.Sprintf("%v", list[i]) + conn.Write([]byte("$" + fmt.Sprint(len(str)) + "\r\n" + str + "\r\n")) + } + conn.Write([]byte("\n")) + conn.Flush() + return + } + + // Make sure start and end are not equal to each other + if start == end { + conn.Write([]byte("-Error start and end indices cannot be equal equal\r\n\n")) + conn.Flush() + return + } + + // If end is not -1: + // 1) If end is larger than start, return slice from start -> end + // 2) If end is smaller than start, return slice from end -> start + conn.Write([]byte("*" + fmt.Sprint(int(math.Abs(float64(start-end)))+1) + "\r\n")) + + i := start + j := end + 1 + if start > end { + j = end - 1 + } + + for i != j { + str := fmt.Sprintf("%v", list[i]) + conn.Write([]byte("$" + fmt.Sprint(len(str)) + "\r\n" + str + "\r\n")) + if start < end { + i++ + } else { + i-- + } + + } + conn.Write([]byte("\n")) + conn.Flush() +} + +func handleLPush(cmd []string, server Server, conn *bufio.Writer) { + if len(cmd) < 3 { + conn.Write([]byte("-Error wrong number of arguments for LPUSH command\r\n\n")) + conn.Flush() + return + } + + server.Lock() + + newElems := []interface{}{} + + for _, elem := range cmd[2:] { + newElems = append(newElems, utils.AdaptType(elem)) + } + + currentList := server.GetData(cmd[1]) + + if currentList == nil { + server.SetData(cmd[1], newElems) + server.Unlock() + conn.Write([]byte(OK)) + conn.Flush() + return + } + + l, ok := currentList.([]interface{}) + + if !ok { + server.Unlock() + conn.Write([]byte("-Error LPUSH command on non-list item\r\n\n")) + conn.Flush() + return + } + + server.SetData(cmd[1], append(newElems, l...)) + server.Unlock() + + conn.Write([]byte(OK)) + conn.Flush() +} + +func handleRPush(cmd []string, server Server, conn *bufio.Writer) { + if len(cmd) < 3 { + conn.Write([]byte("-Error wrong number of arguments for LPUSH command\r\n\n")) + conn.Flush() + return + } + + server.Lock() + + newElems := []interface{}{} + + for _, elem := range cmd[2:] { + newElems = append(newElems, utils.AdaptType(elem)) + } + + currentList := server.GetData(cmd[1]) + + if currentList == nil { + server.SetData(cmd[1], newElems) + server.Unlock() + conn.Write([]byte(OK)) + conn.Flush() + return + } + + l, ok := currentList.([]interface{}) + + if !ok { + server.Unlock() + conn.Write([]byte("-Error RPUSH command on non-list item\r\n\n")) + conn.Flush() + return + } + + server.SetData(cmd[1], append(l, newElems...)) + server.Unlock() + + conn.Write([]byte(OK)) + conn.Flush() } func init() { diff --git a/server/plugins/commands/ping/ping.go b/server/plugins/commands/ping/ping.go index 9173b51..3888be0 100644 --- a/server/plugins/commands/ping/ping.go +++ b/server/plugins/commands/ping/ping.go @@ -3,6 +3,8 @@ package main import "bufio" type Server interface { + Lock() + Unlock() GetData(key string) interface{} SetData(key string, value interface{}) } diff --git a/server/plugins/commands/setget/setget.go b/server/plugins/commands/setget/setget.go index 39ea8c5..3f66c9f 100644 --- a/server/plugins/commands/setget/setget.go +++ b/server/plugins/commands/setget/setget.go @@ -10,6 +10,8 @@ import ( ) type Server interface { + Lock() + Unlock() GetData(key string) interface{} SetData(key string, value interface{}) } @@ -53,11 +55,13 @@ func handleGet(cmd []string, s Server, conn *bufio.Writer) { return } + s.Lock() value := s.GetData(cmd[1]) + s.Unlock() switch value.(type) { default: - fmt.Println("Error. The requested object's type cannot be returned with the GET command") + conn.Write([]byte("-Error type cannot be returned with the GET command\r\n\n")) case nil: conn.Write([]byte("+nil\r\n\n")) case string: @@ -74,6 +78,8 @@ func handleGet(cmd []string, s Server, conn *bufio.Writer) { func handleMGet(cmd []string, s Server, conn *bufio.Writer) { vals := []string{} + s.Lock() + for _, key := range cmd[1:] { switch s.GetData(key).(type) { case nil: @@ -87,6 +93,8 @@ func handleMGet(cmd []string, s Server, conn *bufio.Writer) { } } + s.Unlock() + conn.Write([]byte(fmt.Sprintf("*%d\r\n", len(vals)))) for _, val := range vals { @@ -103,11 +111,15 @@ func handleSet(cmd []string, s Server, conn *bufio.Writer) { conn.Write([]byte("-Error wrong number of args for SET command\r\n\n")) conn.Flush() case x > 3: + s.Lock() s.SetData(cmd[1], strings.Join(cmd[2:], " ")) + s.Unlock() conn.Write([]byte("+OK\r\n")) case x == 3: val, err := strconv.ParseFloat(cmd[2], 32) + s.Lock() + if err != nil { s.SetData(cmd[1], cmd[2]) } else if !utils.IsInteger(val) { @@ -116,6 +128,8 @@ func handleSet(cmd []string, s Server, conn *bufio.Writer) { s.SetData(cmd[1], int(val)) } + s.Unlock() + conn.Write([]byte("+OK\r\n\n")) conn.Flush() } diff --git a/utils/utils.go b/utils/utils.go index 8257062..24e545e 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -9,7 +9,7 @@ import ( "math" "os" "path" - "sync" + "strconv" "gopkg.in/yaml.v3" ) @@ -96,6 +96,21 @@ func IsInteger(n float64) bool { return math.Mod(n, 1.0) == 0 } +func AdaptType(s string) interface{} { + // Adapt the type of the parameter to string, float64 or int + n, err := strconv.ParseFloat(s, 32) + + if err != nil { + return s + } + + if IsInteger(n) { + return int(n) + } + + return n +} + func ReadMessage(r *bufio.ReadWriter) (message string, err error) { var line [][]byte @@ -116,20 +131,3 @@ func ReadMessage(r *bufio.ReadWriter) (message string, err error) { return fmt.Sprintf("%s\r\n", string(bytes.Join(line, []byte("\r\n")))), nil } - -type Plugin interface { - Name() string - Commands() []string - Description() string - HandleCommand( - cmd []string, - GetData *func(key string) interface{}, - SetData *func(key string, value interface{}), - conn *bufio.Writer, - ) -} - -type Data struct { - mu sync.Mutex - data map[string]interface{} -}