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.
This commit is contained in:
Kelvin Clement Mwinuka
2023-07-03 12:35:37 +08:00
parent 9b830f9750
commit f3d36b3c3d
8 changed files with 189 additions and 204 deletions

View File

@@ -17,7 +17,7 @@ func Decode(raw string) ([]string, error) {
return nil, err 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 return []string{v.String()}, nil
} }

View File

@@ -1,6 +1,5 @@
build-plugins: 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_setget.so plugins/commands/setget/setget.go
go build -buildmode=plugin -o bin/plugins/commands/command_set.so plugins/commands/set/set.go
build-server: build-server:
go build -o bin/server ./*.go go build -o bin/server ./*.go

View File

@@ -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)
}
}

View File

@@ -5,10 +5,10 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
"os"
"path" "path"
"plugin" "plugin"
"strings" "strings"
@@ -20,9 +20,9 @@ import (
type Plugin interface { type Plugin interface {
Name() string Name() string
Command() string Commands() []string
Description() string Description() string
HandleCommand(tokens []string, server interface{}) error HandleCommand(cmd []string, server interface{}, conn *bufio.Writer)
} }
type Data struct { type Data struct {
@@ -36,6 +36,18 @@ type Server struct {
plugins []Plugin 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) { func (server *Server) handleConnection(conn net.Conn) {
connRW := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(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())) serialization.Encode(connRW, fmt.Sprintf("Error %s", err.Error()))
continue continue
} else { } 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 conf := server.config
// Load plugins // Load plugins
pluginDirs, err := ioutil.ReadDir(conf.Plugins) pluginDirs, err := os.ReadDir(conf.Plugins)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@@ -143,7 +162,7 @@ func (server *Server) LoadPlugins() {
if file.IsDir() { if file.IsDir() {
switch file.Name() { switch file.Name() {
case "commands": case "commands":
files, err := ioutil.ReadDir(path.Join(conf.Plugins, "commands")) files, err := os.ReadDir(path.Join(conf.Plugins, "commands"))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@@ -173,8 +192,9 @@ func (server *Server) LoadPlugins() {
// Check if a plugin that handles the same command already exists // Check if a plugin that handles the same command already exists
for _, loadedPlugin := range server.plugins { for _, loadedPlugin := range server.plugins {
if loadedPlugin.Command() == plugin.Command() { containsMutual, elem := utils.ContainsMutual[string](loadedPlugin.Commands(), plugin.Commands())
fmt.Printf("plugin that handles %s command already exists. Please handle a different command.", plugin.Command()) 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() { func (server *Server) Start() {
server.data.data = make(map[string]interface{}) server.data.data = make(map[string]interface{})
server.config = utils.GetConfig()
conf := server.config conf := server.config
server.LoadPlugins() server.LoadPlugins()
@@ -205,10 +226,6 @@ func (server *Server) Start() {
} }
func main() { func main() {
server := Server{ server := Server{}
config: utils.GetConfig(),
plugins: []Plugin{},
}
server.Start() server.Start()
} }

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -9,6 +9,7 @@ import (
"math" "math"
"os" "os"
"path" "path"
"sync"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -80,6 +81,17 @@ func Contains[T comparable](arr []T, elem T) bool {
return false 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 { func IsInteger(n float64) bool {
return math.Mod(n, 1.0) == 0 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 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{}
}