From f3d36b3c3d9ac74b6f0ee2bdac05ec6974713c32 Mon Sep 17 00:00:00 2001 From: Kelvin Clement Mwinuka Date: Mon, 3 Jul 2023 12:35:37 +0800 Subject: [PATCH] Added Error to decoded types. Removed cmd_functions.go as it is replaced by plugin system. Consolidated get and set plugins into one plugin setget that handles GET, SET and MGET commands. Plugins can declare multiple commands that they handle. --- serialization/decode.go | 2 +- server/Makefile | 3 +- server/cmd_functions.go | 116 -------------------- server/main.go | 43 +++++--- server/plugins/commands/get/get.go | 36 ------- server/plugins/commands/set/set.go | 36 ------- server/plugins/commands/setget/setget.go | 128 +++++++++++++++++++++++ utils/utils.go | 29 +++++ 8 files changed, 189 insertions(+), 204 deletions(-) delete mode 100644 server/cmd_functions.go delete mode 100644 server/plugins/commands/get/get.go delete mode 100644 server/plugins/commands/set/set.go create mode 100644 server/plugins/commands/setget/setget.go diff --git a/serialization/decode.go b/serialization/decode.go index 34c607c..74a0c5c 100644 --- a/serialization/decode.go +++ b/serialization/decode.go @@ -17,7 +17,7 @@ func Decode(raw string) ([]string, error) { return nil, err } - if utils.Contains[string]([]string{"SimpleString", "Integer"}, v.Type().String()) { + if utils.Contains[string]([]string{"SimpleString", "Integer", "Error"}, v.Type().String()) { return []string{v.String()}, nil } diff --git a/server/Makefile b/server/Makefile index 80c68a3..151e202 100644 --- a/server/Makefile +++ b/server/Makefile @@ -1,6 +1,5 @@ build-plugins: - go build -buildmode=plugin -o bin/plugins/commands/command_get.so plugins/commands/get/get.go - go build -buildmode=plugin -o bin/plugins/commands/command_set.so plugins/commands/set/set.go + go build -buildmode=plugin -o bin/plugins/commands/command_setget.so plugins/commands/setget/setget.go build-server: go build -o bin/server ./*.go diff --git a/server/cmd_functions.go b/server/cmd_functions.go deleted file mode 100644 index fff5fba..0000000 --- a/server/cmd_functions.go +++ /dev/null @@ -1,116 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "strconv" - "strings" - - "github.com/kelvinmwinuka/memstore/serialization" - "github.com/kelvinmwinuka/memstore/utils" -) - -func processPing(cmd []string, connRW *bufio.ReadWriter) { - if len(cmd) == 1 { - serialization.Encode(connRW, "SimpleString PONG") - connRW.Write([]byte("\n")) - connRW.Flush() - } - if len(cmd) == 2 { - serialization.Encode(connRW, fmt.Sprintf("SimpleString \"%s\"", cmd[1])) - connRW.Write([]byte("\n")) - connRW.Flush() - } -} - -func processSet(cmd []string, connRW *bufio.ReadWriter, server *Server) { - fmt.Println("Process set command") - server.data.mu.Lock() - defer server.data.mu.Unlock() - - switch x := len(cmd); { - default: - fmt.Println("Wrong number of args for SET commands") - case x > 3: - server.data.data[cmd[1]] = strings.Join(cmd[2:], " ") - serialization.Encode(connRW, "SimpleString OK") - case x == 3: - val, err := strconv.ParseFloat(cmd[2], 32) - - if err != nil { - server.data.data[cmd[1]] = cmd[2] - } else if !utils.IsInteger(val) { - server.data.data[cmd[1]] = val - } else { - server.data.data[cmd[1]] = int(val) - } - - serialization.Encode(connRW, "SimpleString OK") - } - - connRW.Write([]byte("\n")) - connRW.Flush() -} - -func processGet(cmd []string, connRW *bufio.ReadWriter, server *Server) { - server.data.mu.Lock() - defer server.data.mu.Unlock() - - // Use reflection to determine the type of the value and how to encode it - switch server.data.data[cmd[1]].(type) { - default: - fmt.Println("Error. The requested object's type cannot be returned with the GET command") - case nil: - serialization.Encode(connRW, "SimpleString nil") - case string: - serialization.Encode(connRW, fmt.Sprintf("SimpleString \"%s\"", server.data.data[cmd[1]])) - case float64: - serialization.Encode(connRW, fmt.Sprintf("SimpleString %f", server.data.data[cmd[1]])) - case int: - serialization.Encode(connRW, fmt.Sprintf("Integer %d", server.data.data[cmd[1]])) - } - - connRW.Write([]byte("\n")) - connRW.Flush() -} - -func processMGet(cmd []string, connRW *bufio.ReadWriter, server *Server) { - server.data.mu.Lock() - defer server.data.mu.Unlock() - - vals := []string{} - - for _, key := range cmd[1:] { - switch server.data.data[key].(type) { - case nil: - vals = append(vals, "nil") - case string: - vals = append(vals, fmt.Sprintf("%s", server.data.data[key])) - case float64: - vals = append(vals, fmt.Sprintf("%f", server.data.data[key])) - case int: - vals = append(vals, fmt.Sprintf("%d", server.data.data[key])) - } - } - - serialization.Encode(connRW, fmt.Sprintf("Array %s", strings.Join(vals, " "))) - - connRW.Write([]byte("\n")) - connRW.Flush() -} - -func processCommand(cmd []string, connRW *bufio.ReadWriter, server *Server) { - // Return encoded message to client - switch strings.ToLower(cmd[0]) { - default: - fmt.Println("The command is unknown") - case "ping": - processPing(cmd, connRW) - case "set": - processSet(cmd, connRW, server) - case "get": - processGet(cmd, connRW, server) - case "mget": - processMGet(cmd, connRW, server) - } -} diff --git a/server/main.go b/server/main.go index 865e8f1..e3b0997 100644 --- a/server/main.go +++ b/server/main.go @@ -5,10 +5,10 @@ import ( "crypto/tls" "fmt" "io" - "io/ioutil" "log" "net" "net/http" + "os" "path" "plugin" "strings" @@ -20,9 +20,9 @@ import ( type Plugin interface { Name() string - Command() string + Commands() []string Description() string - HandleCommand(tokens []string, server interface{}) error + HandleCommand(cmd []string, server interface{}, conn *bufio.Writer) } type Data struct { @@ -36,6 +36,18 @@ type Server struct { plugins []Plugin } +func (server *Server) GetData(key string) interface{} { + server.data.mu.Lock() + defer server.data.mu.Unlock() + 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 +} + func (server *Server) handleConnection(conn net.Conn) { connRW := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) @@ -57,7 +69,14 @@ func (server *Server) handleConnection(conn net.Conn) { serialization.Encode(connRW, fmt.Sprintf("Error %s", err.Error())) continue } else { - processCommand(cmd, connRW, server) + // Look for plugin that handles this command and trigger it + for _, plugin := range server.plugins { + fmt.Println(server) + if utils.Contains[string](plugin.Commands(), strings.ToLower(cmd[0])) { + plugin.HandleCommand(cmd, server, connRW.Writer) + } + } + } } @@ -133,7 +152,7 @@ func (server *Server) LoadPlugins() { conf := server.config // Load plugins - pluginDirs, err := ioutil.ReadDir(conf.Plugins) + pluginDirs, err := os.ReadDir(conf.Plugins) if err != nil { log.Fatal(err) @@ -143,7 +162,7 @@ func (server *Server) LoadPlugins() { if file.IsDir() { switch file.Name() { case "commands": - files, err := ioutil.ReadDir(path.Join(conf.Plugins, "commands")) + files, err := os.ReadDir(path.Join(conf.Plugins, "commands")) if err != nil { log.Fatal(err) @@ -173,8 +192,9 @@ func (server *Server) LoadPlugins() { // Check if a plugin that handles the same command already exists for _, loadedPlugin := range server.plugins { - if loadedPlugin.Command() == plugin.Command() { - fmt.Printf("plugin that handles %s command already exists. Please handle a different command.", plugin.Command()) + containsMutual, elem := utils.ContainsMutual[string](loadedPlugin.Commands(), plugin.Commands()) + if containsMutual { + fmt.Printf("plugin that handles %s command already exists. Please handle a different command.\n", elem) } } @@ -188,6 +208,7 @@ func (server *Server) LoadPlugins() { func (server *Server) Start() { server.data.data = make(map[string]interface{}) + server.config = utils.GetConfig() conf := server.config server.LoadPlugins() @@ -205,10 +226,6 @@ func (server *Server) Start() { } func main() { - server := Server{ - config: utils.GetConfig(), - plugins: []Plugin{}, - } - + server := Server{} server.Start() } diff --git a/server/plugins/commands/get/get.go b/server/plugins/commands/get/get.go deleted file mode 100644 index fda450b..0000000 --- a/server/plugins/commands/get/get.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -type Server interface { - GetData(key string) interface{} - SetData(key string, value interface{}) -} - -type plugin struct { - name string - command string - description string -} - -var Plugin plugin - -func (p *plugin) Name() string { - return p.name -} - -func (p *plugin) Command() string { - return p.command -} - -func (p *plugin) Description() string { - return p.description -} - -func (p *plugin) HandleCommand(tokens []string, server interface{}) error { - return nil -} - -func init() { - Plugin.name = "GetCommand" - Plugin.command = "get" - Plugin.description = "Get the value from the specified key" -} diff --git a/server/plugins/commands/set/set.go b/server/plugins/commands/set/set.go deleted file mode 100644 index c197753..0000000 --- a/server/plugins/commands/set/set.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -type Server interface { - GetData(key string) interface{} - SetData(key string, value interface{}) -} - -type plugin struct { - name string - command string - description string -} - -var Plugin plugin - -func (p *plugin) Name() string { - return p.name -} - -func (p *plugin) Command() string { - return p.command -} - -func (p *plugin) Description() string { - return p.description -} - -func (p *plugin) HandleCommand(tokens []string, server interface{}) error { - return nil -} - -func init() { - Plugin.name = "SetCommand" - Plugin.command = "set" - Plugin.description = "Set the value of the specified key" -} diff --git a/server/plugins/commands/setget/setget.go b/server/plugins/commands/setget/setget.go new file mode 100644 index 0000000..39ea8c5 --- /dev/null +++ b/server/plugins/commands/setget/setget.go @@ -0,0 +1,128 @@ +package main + +import ( + "bufio" + "fmt" + "strconv" + "strings" + + "github.com/kelvinmwinuka/memstore/utils" +) + +type Server interface { + GetData(key string) interface{} + SetData(key string, value interface{}) +} + +type plugin struct { + name string + commands []string + description string +} + +var Plugin plugin + +func (p *plugin) Name() string { + return p.name +} + +func (p *plugin) Commands() []string { + return p.commands +} + +func (p *plugin) Description() string { + return p.description +} + +func (p *plugin) HandleCommand(cmd []string, server interface{}, conn *bufio.Writer) { + switch strings.ToLower(cmd[0]) { + case "get": + handleGet(cmd, server.(Server), conn) + case "set": + handleSet(cmd, server.(Server), conn) + case "mget": + handleMGet(cmd, server.(Server), conn) + } +} + +func handleGet(cmd []string, s Server, conn *bufio.Writer) { + + if len(cmd) != 2 { + conn.Write([]byte("-Error wrong number of args for GET command\r\n\n")) + conn.Flush() + return + } + + value := s.GetData(cmd[1]) + + switch value.(type) { + default: + fmt.Println("Error. The requested object's type cannot be returned with the GET command") + case nil: + conn.Write([]byte("+nil\r\n\n")) + case string: + conn.Write([]byte(fmt.Sprintf("+%s\r\n\n", value))) + case float64: + conn.Write([]byte(fmt.Sprintf("+%f\r\n\n", value))) + case int: + conn.Write([]byte(fmt.Sprintf("+%d\r\n\n", value))) + } + + conn.Flush() +} + +func handleMGet(cmd []string, s Server, conn *bufio.Writer) { + vals := []string{} + + for _, key := range cmd[1:] { + switch s.GetData(key).(type) { + case nil: + vals = append(vals, "nil") + case string: + vals = append(vals, fmt.Sprintf("%s", s.GetData(key))) + case float64: + vals = append(vals, fmt.Sprintf("%f", s.GetData(key))) + case int: + vals = append(vals, fmt.Sprintf("%d", s.GetData(key))) + } + } + + conn.Write([]byte(fmt.Sprintf("*%d\r\n", len(vals)))) + + for _, val := range vals { + conn.Write([]byte(fmt.Sprintf("$%d\r\n%s\r\n", len(val), val))) + } + + conn.Write([]byte("\n")) + conn.Flush() +} + +func handleSet(cmd []string, s Server, conn *bufio.Writer) { + switch x := len(cmd); { + default: + conn.Write([]byte("-Error wrong number of args for SET command\r\n\n")) + conn.Flush() + case x > 3: + s.SetData(cmd[1], strings.Join(cmd[2:], " ")) + conn.Write([]byte("+OK\r\n")) + case x == 3: + val, err := strconv.ParseFloat(cmd[2], 32) + + if err != nil { + s.SetData(cmd[1], cmd[2]) + } else if !utils.IsInteger(val) { + s.SetData(cmd[1], val) + } else { + s.SetData(cmd[1], int(val)) + } + + conn.Write([]byte("+OK\r\n\n")) + conn.Flush() + } +} + +func init() { + Plugin.name = "GetCommand" + Plugin.commands = []string{"set", "get", "mget"} + Plugin.description = "Handle basic SET, GET and MGET commands" +} diff --git a/utils/utils.go b/utils/utils.go index af0f5c3..8257062 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -9,6 +9,7 @@ import ( "math" "os" "path" + "sync" "gopkg.in/yaml.v3" ) @@ -80,6 +81,17 @@ func Contains[T comparable](arr []T, elem T) bool { return false } +func ContainsMutual[T comparable](arr1 []T, arr2 []T) (bool, T) { + for _, a := range arr1 { + for _, b := range arr2 { + if a == b { + return true, a + } + } + } + return false, arr1[0] +} + func IsInteger(n float64) bool { return math.Mod(n, 1.0) == 0 } @@ -104,3 +116,20 @@ 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{} +}