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

View File

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

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"
"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()
}

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