diff --git a/Makefile b/Makefile index 4080347..376b862 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,5 @@ build-plugins: - CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -buildmode=plugin -o $(DEST)/command_ping.so ./src/plugins/ping/*.go - CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -buildmode=plugin -o $(DEST)/command_set.so ./src/plugins/set/*.go - CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -buildmode=plugin -o $(DEST)/command_get.so ./src/plugins/get/*.go - CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -buildmode=plugin -o $(DEST)/command_list.so ./src/plugins/list/*.go CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -buildmode=plugin -o $(DEST)/command_pubsub.so ./src/plugins/pubsub/*.go - CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -buildmode=plugin -o $(DEST)/command_string.so ./src/plugins/string/*.go - build-server: CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(DEST)/server ./src/*.go diff --git a/src/main.go b/src/main.go index 45f50b3..e724603 100644 --- a/src/main.go +++ b/src/main.go @@ -6,7 +6,12 @@ import ( "crypto/tls" "encoding/json" "fmt" - "github.com/kelvinmwinuka/memstore/src/acl" + "github.com/kelvinmwinuka/memstore/src/modules/acl" + "github.com/kelvinmwinuka/memstore/src/modules/get" + "github.com/kelvinmwinuka/memstore/src/modules/list" + "github.com/kelvinmwinuka/memstore/src/modules/ping" + "github.com/kelvinmwinuka/memstore/src/modules/set" + str "github.com/kelvinmwinuka/memstore/src/modules/string" "io" "log" "net" @@ -131,6 +136,7 @@ func (server *Server) handlePluginCommand(ctx context.Context, cmd []string, con if command.HandleWithConnection { return command.Plugin.HandleCommandWithConnection(ctx, cmd, server, conn) } + fmt.Println("SERVER: ", server) return command.Plugin.HandleCommand(ctx, cmd, server) } @@ -303,8 +309,18 @@ func (server *Server) StartHTTP(ctx context.Context) { func (server *Server) LoadPlugins(ctx context.Context) { conf := server.config - // Load ACL Internal Commands - server.commands = append(server.commands, server.ACL.GetPluginCommands()...) + // Load Ping module + server.commands = append(server.commands, ping.NewModule().GetCommands()...) + // Load ACL Commands module + server.commands = append(server.commands, acl.NewModule(server.ACL).GetCommands()...) + // Load Set module + server.commands = append(server.commands, set.NewModule().GetCommands()...) + // Load String module + server.commands = append(server.commands, str.NewModule().GetCommands()...) + // Load Get module + server.commands = append(server.commands, get.NewModule().GetCommands()...) + // Load List module + server.commands = append(server.commands, list.NewModule().GetCommands()...) // Load plugins /usr/local/lib/memstore files, err := os.ReadDir(conf.PluginDir) diff --git a/src/acl/acl.go b/src/modules/acl/acl.go similarity index 97% rename from src/acl/acl.go rename to src/modules/acl/acl.go index ace9257..84c41d7 100644 --- a/src/acl/acl.go +++ b/src/modules/acl/acl.go @@ -42,7 +42,6 @@ type ACL struct { Users []User Connections map[*net.Conn]*User Config utils.Config - Plugin Plugin } func NewACL(config utils.Config) *ACL { @@ -113,17 +112,12 @@ func NewACL(config utils.Config) *ACL { Connections: make(map[*net.Conn]*User), Config: config, } - acl.Plugin = NewACLPlugin(&acl) fmt.Println(acl.Users) return &acl } -func (acl *ACL) GetPluginCommands() []utils.Command { - return acl.Plugin.GetCommands() -} - func (acl *ACL) RegisterConnection(conn *net.Conn) { fmt.Println("Register connection...") } diff --git a/src/acl/command.go b/src/modules/acl/commands.go similarity index 99% rename from src/acl/command.go rename to src/modules/acl/commands.go index eb17681..ed921ec 100644 --- a/src/acl/command.go +++ b/src/modules/acl/commands.go @@ -17,6 +17,8 @@ type Plugin struct { acl *ACL } +var ACLPlugin Plugin + func (p Plugin) Name() string { return p.name } @@ -25,6 +27,10 @@ func (p Plugin) Commands() ([]byte, error) { return json.Marshal(p.commands) } +func (p Plugin) GetCommands() []utils.Command { + return p.commands +} + func (p Plugin) Description() string { return p.description } @@ -72,10 +78,6 @@ func (p Plugin) HandleCommand(ctx context.Context, cmd []string, server utils.Se return nil, errors.New("not implemented") } -func (p Plugin) GetCommands() []utils.Command { - return p.commands -} - func (p Plugin) handleAuth(ctx context.Context, cmd []string, server utils.Server, conn *net.Conn) ([]byte, error) { return nil, errors.New("AUTH not implemented") } @@ -120,9 +122,7 @@ func (p Plugin) handleSave(ctx context.Context, cmd []string, server utils.Serve return nil, errors.New("ACL SAVE not implemented") } -var ACLPlugin Plugin - -func NewACLPlugin(acl *ACL) Plugin { +func NewModule(acl *ACL) Plugin { ACLPlugin = Plugin{ acl: acl, name: "ACLCommands", diff --git a/src/modules/get/commands.go b/src/modules/get/commands.go new file mode 100644 index 0000000..93009b5 --- /dev/null +++ b/src/modules/get/commands.go @@ -0,0 +1,128 @@ +package get + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/kelvinmwinuka/memstore/src/utils" + "net" + "strings" +) + +type Plugin struct { + name string + commands []utils.Command + categories []string + description string +} + +var GetModule Plugin + +func (p Plugin) Name() string { + return p.name +} + +func (p Plugin) Commands() ([]byte, error) { + return json.Marshal(p.commands) +} + +func (p Plugin) GetCommands() []utils.Command { + return p.commands +} + +func (p Plugin) Description() string { + return p.description +} + +func (p Plugin) HandleCommandWithConnection(ctx context.Context, cmd []string, server utils.Server, conn *net.Conn) ([]byte, error) { + return nil, errors.New("not implemented") +} + +func (p Plugin) HandleCommand(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { + switch strings.ToLower(cmd[0]) { + default: + return nil, errors.New("command unknown") + case "get": + return handleGet(ctx, cmd, server) + case "mget": + return handleMGet(ctx, cmd, server) + } +} + +func handleGet(ctx context.Context, cmd []string, s utils.Server) ([]byte, error) { + if len(cmd) != 2 { + return nil, errors.New("wrong number of args for GET command") + } + + key := cmd[1] + + s.KeyRLock(ctx, key) + value := s.GetValue(key) + s.KeyRUnlock(key) + + switch value.(type) { + default: + return []byte(fmt.Sprintf("+%v\r\n\n", value)), nil + case nil: + return []byte("+nil\r\n\n"), nil + } +} + +func handleMGet(ctx context.Context, cmd []string, s utils.Server) ([]byte, error) { + if len(cmd) < 2 { + return nil, errors.New("wrong number of args for MGET command") + } + + vals := []string{} + + for _, key := range cmd[1:] { + func(key string) { + s.KeyRLock(ctx, key) + switch s.GetValue(key).(type) { + default: + vals = append(vals, fmt.Sprintf("%v", s.GetValue(key))) + case nil: + vals = append(vals, "nil") + } + s.KeyRUnlock(key) + + }(key) + } + + bytes := []byte(fmt.Sprintf("*%d\r\n", len(vals))) + + for _, val := range vals { + bytes = append(bytes, []byte(fmt.Sprintf("$%d\r\n%s\r\n", len(val), val))...) + } + + bytes = append(bytes, []byte("\n")...) + + return bytes, nil +} + +func NewModule() Plugin { + GetModule := Plugin{ + name: "GetCommands", + commands: []utils.Command{ + { + Command: "get", + Categories: []string{}, + Description: "", + HandleWithConnection: false, + Sync: false, + Plugin: GetModule, + }, + { + Command: "mget", + Categories: []string{}, + Description: "", + HandleWithConnection: false, + Sync: true, + Plugin: GetModule, + }, + }, + description: "Handle basic GET and MGET commands", + } + return GetModule +} diff --git a/src/plugins/list/command.go b/src/modules/list/commands.go similarity index 60% rename from src/plugins/list/command.go rename to src/modules/list/commands.go index 5fcdbe5..412ab5b 100644 --- a/src/plugins/list/command.go +++ b/src/modules/list/commands.go @@ -1,10 +1,11 @@ -package main +package list import ( "context" "encoding/json" "errors" "fmt" + "github.com/kelvinmwinuka/memstore/src/utils" "math" "net" "reflect" @@ -15,89 +16,74 @@ const ( OK = "+OK\r\n\n" ) -type Server interface { - KeyLock(ctx context.Context, key string) (bool, error) - KeyUnlock(key string) - KeyRLock(ctx context.Context, key string) (bool, error) - KeyRUnlock(key string) - KeyExists(key string) bool - CreateKeyAndLock(ctx context.Context, key string) (bool, error) - GetValue(key string) interface{} - SetValue(ctx context.Context, key string, value interface{}) -} - -type Command struct { - Command string `json:"Command"` - Categories []string `json:"Categories"` - Description string `json:"Description"` - HandleWithConnection bool `json:"HandleWithConnection"` - Sync bool `json:"Sync"` -} - -type plugin struct { +type Plugin struct { name string - commands []Command + commands []utils.Command categories []string description string } -var Plugin plugin +var ListModule Plugin -func (p *plugin) Name() string { +func (p Plugin) Name() string { return p.name } -func (p *plugin) Commands() ([]byte, error) { +func (p Plugin) Commands() ([]byte, error) { return json.Marshal(p.commands) } -func (p *plugin) Description() string { +func (p Plugin) GetCommands() []utils.Command { + return p.commands +} + +func (p Plugin) Description() string { return p.description } -func (p *plugin) HandleCommandWithConnection(ctx context.Context, cmd []string, server interface{}, conn *net.Conn) ([]byte, error) { +func (p Plugin) HandleCommandWithConnection(ctx context.Context, cmd []string, server utils.Server, conn *net.Conn) ([]byte, error) { return nil, errors.New("not implemented") } -func (p *plugin) HandleCommand(ctx context.Context, cmd []string, server interface{}) ([]byte, error) { +func (p Plugin) HandleCommand(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { c := strings.ToLower(cmd[0]) switch { default: return nil, errors.New("command unknown") case c == "llen": - return handleLLen(ctx, cmd, server.(Server)) + return handleLLen(ctx, cmd, server) case c == "lindex": - return handleLIndex(ctx, cmd, server.(Server)) + return handleLIndex(ctx, cmd, server) case c == "lrange": - return handleLRange(ctx, cmd, server.(Server)) + return handleLRange(ctx, cmd, server) case c == "lset": - return handleLSet(ctx, cmd, server.(Server)) + return handleLSet(ctx, cmd, server) case c == "ltrim": - return handleLTrim(ctx, cmd, server.(Server)) + return handleLTrim(ctx, cmd, server) case c == "lrem": - return handleLRem(ctx, cmd, server.(Server)) + return handleLRem(ctx, cmd, server) case c == "lmove": - return handleLMove(ctx, cmd, server.(Server)) + return handleLMove(ctx, cmd, server) - case Contains[string]([]string{"lpush", "lpushx"}, c): - return handleLPush(ctx, cmd, server.(Server)) + case utils.Contains[string]([]string{"lpush", "lpushx"}, c): + return handleLPush(ctx, cmd, server) - case Contains[string]([]string{"rpush", "rpushx"}, c): - return handleRPush(ctx, cmd, server.(Server)) + case utils.Contains[string]([]string{"rpush", "rpushx"}, c): + return handleRPush(ctx, cmd, server) - case Contains[string]([]string{"lpop", "rpop"}, c): - return handlePop(ctx, cmd, server.(Server)) + case utils.Contains[string]([]string{"lpop", "rpop"}, c): + return handlePop(ctx, cmd, server) } } -func handleLLen(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handleLLen(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd) != 2 { return nil, errors.New("wrong number of args for LLEN command") } @@ -118,12 +104,12 @@ func handleLLen(ctx context.Context, cmd []string, server Server) ([]byte, error return []byte(fmt.Sprintf(":%d\r\n\n", len(list))), nil } -func handleLIndex(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handleLIndex(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd) != 3 { return nil, errors.New("wrong number of args for LINDEX command") } - index, ok := AdaptType(cmd[2]).(int64) + index, ok := utils.AdaptType(cmd[2]).(int64) if !ok { return nil, errors.New("index must be an integer") @@ -148,13 +134,13 @@ func handleLIndex(ctx context.Context, cmd []string, server Server) ([]byte, err return []byte(fmt.Sprintf("+%s\r\n\n", list[index])), nil } -func handleLRange(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handleLRange(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd) != 4 { return nil, errors.New("wrong number of arguments for LRANGE command") } - start, startOk := AdaptType(cmd[2]).(int64) - end, endOk := AdaptType(cmd[3]).(int64) + start, startOk := utils.AdaptType(cmd[2]).(int64) + end, endOk := utils.AdaptType(cmd[3]).(int64) if !startOk || !endOk { return nil, errors.New("both start and end indices must be integers") @@ -227,7 +213,7 @@ func handleLRange(ctx context.Context, cmd []string, server Server) ([]byte, err return bytes, nil } -func handleLSet(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handleLSet(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd) != 4 { return nil, errors.New("wrong number of arguments for LSET command") } @@ -244,7 +230,7 @@ func handleLSet(ctx context.Context, cmd []string, server Server) ([]byte, error return nil, errors.New("LSET command on non-list item") } - index, ok := AdaptType(cmd[2]).(int64) + index, ok := utils.AdaptType(cmd[2]).(int64) fmt.Printf("LSET INDEX: `%v`, OK: %v\n", reflect.TypeOf(index), ok) @@ -258,20 +244,20 @@ func handleLSet(ctx context.Context, cmd []string, server Server) ([]byte, error return nil, errors.New("index must be within range") } - list[index] = AdaptType(cmd[3]) + list[index] = utils.AdaptType(cmd[3]) server.SetValue(ctx, cmd[1], list) server.KeyUnlock(cmd[1]) return []byte(OK), nil } -func handleLTrim(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handleLTrim(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd) != 4 { return nil, errors.New("wrong number of args for command LTRIM") } - start, startOk := AdaptType(cmd[2]).(int64) - end, endOk := AdaptType(cmd[3]).(int64) + start, startOk := utils.AdaptType(cmd[2]).(int64) + end, endOk := utils.AdaptType(cmd[3]).(int64) if !startOk || !endOk { return nil, errors.New("start and end indices must be integers") @@ -307,13 +293,13 @@ func handleLTrim(ctx context.Context, cmd []string, server Server) ([]byte, erro return []byte(OK), nil } -func handleLRem(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handleLRem(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd) != 4 { return nil, errors.New("wrong number of arguments for LREM command") } value := cmd[3] - count, ok := AdaptType(cmd[2]).(int64) + count, ok := utils.AdaptType(cmd[2]).(int64) if !ok { return nil, errors.New("count must be an integer") @@ -359,7 +345,7 @@ func handleLRem(ctx context.Context, cmd []string, server Server) ([]byte, error } } - list = Filter[interface{}](list, func(elem interface{}) bool { + list = utils.Filter[interface{}](list, func(elem interface{}) bool { return elem != nil }) @@ -369,7 +355,7 @@ func handleLRem(ctx context.Context, cmd []string, server Server) ([]byte, error return []byte(OK), nil } -func handleLMove(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handleLMove(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd) != 5 { return nil, errors.New("wrong number of arguments for LMOVE command") } @@ -377,7 +363,7 @@ func handleLMove(ctx context.Context, cmd []string, server Server) ([]byte, erro whereFrom := strings.ToLower(cmd[3]) whereTo := strings.ToLower(cmd[4]) - if !Contains[string]([]string{"left", "right"}, whereFrom) || !Contains[string]([]string{"left", "right"}, whereTo) { + if !utils.Contains[string]([]string{"left", "right"}, whereFrom) || !utils.Contains[string]([]string{"left", "right"}, whereTo) { return nil, errors.New("wherefrom and whereto arguments must be either LEFT or RIGHT") } @@ -418,7 +404,7 @@ func handleLMove(ctx context.Context, cmd []string, server Server) ([]byte, erro return []byte(OK), nil } -func handleLPush(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handleLPush(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd) < 3 { return nil, fmt.Errorf("wrong number of arguments for %s command", strings.ToUpper(cmd[0])) } @@ -426,7 +412,7 @@ func handleLPush(ctx context.Context, cmd []string, server Server) ([]byte, erro newElems := []interface{}{} for _, elem := range cmd[2:] { - newElems = append(newElems, AdaptType(elem)) + newElems = append(newElems, utils.AdaptType(elem)) } key := cmd[1] @@ -456,7 +442,7 @@ func handleLPush(ctx context.Context, cmd []string, server Server) ([]byte, erro return []byte(OK), nil } -func handleRPush(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handleRPush(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd) < 3 { return nil, fmt.Errorf("wrong number of arguments for %s command", strings.ToUpper(cmd[0])) } @@ -464,7 +450,7 @@ func handleRPush(ctx context.Context, cmd []string, server Server) ([]byte, erro newElems := []interface{}{} for _, elem := range cmd[2:] { - newElems = append(newElems, AdaptType(elem)) + newElems = append(newElems, utils.AdaptType(elem)) } if !server.KeyExists(cmd[1]) { @@ -492,7 +478,7 @@ func handleRPush(ctx context.Context, cmd []string, server Server) ([]byte, erro return []byte(OK), nil } -func handlePop(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handlePop(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd) != 2 { return nil, fmt.Errorf("wrong number of args for %s command", strings.ToUpper(cmd[0])) } @@ -525,100 +511,116 @@ func handlePop(ctx context.Context, cmd []string, server Server) ([]byte, error) } -func init() { - Plugin.name = "ListCommands" - Plugin.commands = []Command{ - { - Command: "lpush", - Categories: []string{}, - Description: "(LPUSH key value1 [value2]) Prepends one or more values to the beginning of a list, creates the list if it does not exist.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "lpushx", - Categories: []string{}, - Description: "(LPUSHX key value) Prepends a value to the beginning of a list only if the list exists.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "lpop", - Categories: []string{}, - Description: "(LPOP key) Removes and returns the first element of a list.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "llen", - Categories: []string{}, - Description: "(LLEN key) Return the length of a list.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "lrange", - Categories: []string{}, - Description: "(LRANGE key start end) Return a range of elements between the given indices.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "lindex", - Categories: []string{}, - Description: "(LINDEX key index) Gets list element by index.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "lset", - Categories: []string{}, - Description: "(LSET key index value) Sets the value of an element in a list by its index.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "ltrim", - Categories: []string{}, - Description: "(LTRIM key start end) Trims a list to the specified range.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "lrem", - Categories: []string{}, - Description: "(LREM key count value) Remove elements from list.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "lmove", - Categories: []string{}, - Description: "(LMOVE source destination Move element from one list to the other specifying left/right for both lists.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "rpop", - Categories: []string{}, - Description: "(RPOP key) Removes and gets the last element in a list.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "rpush", - Categories: []string{}, - Description: "(RPUSH key value [value2]) Appends one or multiple elements to the end of a list.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "rpushx", - Categories: []string{}, - Description: "(RPUSHX key value) Appends an element to the end of a list, only if the list exists.", - HandleWithConnection: false, - Sync: true, +func NewModule() Plugin { + ListModule := Plugin{ + name: "ListCommands", + commands: []utils.Command{ + { + Command: "lpush", + Categories: []string{}, + Description: "(LPUSH key value1 [value2]) Prepends one or more values to the beginning of a list, creates the list if it does not exist.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, + { + Command: "lpushx", + Categories: []string{}, + Description: "(LPUSHX key value) Prepends a value to the beginning of a list only if the list exists.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, + { + Command: "lpop", + Categories: []string{}, + Description: "(LPOP key) Removes and returns the first element of a list.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, + { + Command: "llen", + Categories: []string{}, + Description: "(LLEN key) Return the length of a list.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, + { + Command: "lrange", + Categories: []string{}, + Description: "(LRANGE key start end) Return a range of elements between the given indices.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, + { + Command: "lindex", + Categories: []string{}, + Description: "(LINDEX key index) Gets list element by index.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, + { + Command: "lset", + Categories: []string{}, + Description: "(LSET key index value) Sets the value of an element in a list by its index.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, + { + Command: "ltrim", + Categories: []string{}, + Description: "(LTRIM key start end) Trims a list to the specified range.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, + { + Command: "lrem", + Categories: []string{}, + Description: "(LREM key count value) Remove elements from list.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, + { + Command: "lmove", + Categories: []string{}, + Description: "(LMOVE source destination Move element from one list to the other specifying left/right for both lists.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, + { + Command: "rpop", + Categories: []string{}, + Description: "(RPOP key) Removes and gets the last element in a list.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, + { + Command: "rpush", + Categories: []string{}, + Description: "(RPUSH key value [value2]) Appends one or multiple elements to the end of a list.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, + { + Command: "rpushx", + Categories: []string{}, + Description: "(RPUSHX key value) Appends an element to the end of a list, only if the list exists.", + HandleWithConnection: false, + Sync: true, + Plugin: ListModule, + }, }, + description: "Handle List commands", } - Plugin.description = "Handle List commands" + return ListModule } diff --git a/src/modules/ping/commands.go b/src/modules/ping/commands.go new file mode 100644 index 0000000..cf734be --- /dev/null +++ b/src/modules/ping/commands.go @@ -0,0 +1,90 @@ +package ping + +import ( + "context" + "encoding/json" + "errors" + "github.com/kelvinmwinuka/memstore/src/utils" + "net" + "strings" +) + +const ( + OK = "+OK\r\n\n" +) + +type Plugin struct { + name string + commands []utils.Command + description string +} + +var PingModule Plugin + +func (p Plugin) Name() string { + return p.name +} + +func (p Plugin) Commands() ([]byte, error) { + return json.Marshal(p.commands) +} + +func (p Plugin) GetCommands() []utils.Command { + return p.commands +} + +func (p Plugin) Description() string { + return p.description +} + +func (p Plugin) HandleCommandWithConnection(ctx context.Context, cmd []string, server utils.Server, conn *net.Conn) ([]byte, error) { + return nil, errors.New("not implemented") +} + +func (p Plugin) HandleCommand(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { + switch strings.ToLower(cmd[0]) { + default: + return nil, errors.New("not implemented") + case "ping": + return handlePing(ctx, cmd, server) + case "ack": + return []byte("$-1\r\n\n"), nil + } +} + +func handlePing(ctx context.Context, cmd []string, s utils.Server) ([]byte, error) { + switch len(cmd) { + default: + return nil, errors.New("wrong number of arguments for PING command") + case 1: + return []byte("+PONG\r\n\n"), nil + case 2: + return []byte("+" + cmd[1] + "\r\n\n"), nil + } +} + +func NewModule() Plugin { + PingModule := Plugin{ + name: "PingCommands", + commands: []utils.Command{ + { + Command: "ping", + Categories: []string{}, + Description: "", + HandleWithConnection: false, + Sync: false, + Plugin: PingModule, + }, + { + Command: "ack", + Categories: []string{}, + Description: "", + HandleWithConnection: false, + Sync: false, + Plugin: PingModule, + }, + }, + description: "Handle PING command", + } + return PingModule +} diff --git a/src/plugins/set/command.go b/src/modules/set/commands.go similarity index 50% rename from src/plugins/set/command.go rename to src/modules/set/commands.go index 99f58fe..9e8e70c 100644 --- a/src/plugins/set/command.go +++ b/src/modules/set/commands.go @@ -1,77 +1,63 @@ -package main +package set import ( "context" "encoding/json" "errors" "fmt" + "github.com/kelvinmwinuka/memstore/src/utils" "net" "strings" "time" ) -type Server interface { - KeyLock(ctx context.Context, key string) (bool, error) - KeyUnlock(key string) - KeyRLock(ctx context.Context, key string) (bool, error) - KeyRUnlock(key string) - KeyExists(key string) bool - CreateKeyAndLock(ctx context.Context, key string) (bool, error) - GetValue(key string) interface{} - SetValue(ctx context.Context, key string, value interface{}) -} - type KeyObject struct { value interface{} locked bool } -type Command struct { - Command string `json:"Command"` - Categories []string `json:"Categories"` - Description string `json:"Description"` - HandleWithConnection bool `json:"HandleWithConnection"` - Sync bool `json:"Sync"` -} - -type plugin struct { +type Plugin struct { name string - commands []Command + commands []utils.Command description string } -var Plugin plugin +var SetModule Plugin -func (p *plugin) Name() string { +func (p Plugin) Name() string { return p.name } -func (p *plugin) Commands() ([]byte, error) { +func (p Plugin) Commands() ([]byte, error) { return json.Marshal(p.commands) } -func (p *plugin) Description() string { +func (p Plugin) GetCommands() []utils.Command { + return p.commands +} + +func (p Plugin) Description() string { return p.description } -func (p *plugin) HandleCommandWithConnection(ctx context.Context, cmd []string, server interface{}, conn *net.Conn) ([]byte, error) { +func (p Plugin) HandleCommandWithConnection(ctx context.Context, cmd []string, server utils.Server, conn *net.Conn) ([]byte, error) { return nil, errors.New("not implemented") } -func (p *plugin) HandleCommand(ctx context.Context, cmd []string, server interface{}) ([]byte, error) { +func (p Plugin) HandleCommand(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { switch strings.ToLower(cmd[0]) { default: return nil, errors.New("command unknown") case "set": - return handleSet(ctx, cmd, server.(Server)) + return handleSet(ctx, cmd, server) case "setnx": - return handleSetNX(ctx, cmd, server.(Server)) + return handleSetNX(ctx, cmd, server) case "mset": - return handleMSet(ctx, cmd, server.(Server)) + return handleMSet(ctx, cmd, server) } } -func handleSet(ctx context.Context, cmd []string, s Server) ([]byte, error) { +func handleSet(ctx context.Context, cmd []string, s utils.Server) ([]byte, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -84,7 +70,7 @@ func handleSet(ctx context.Context, cmd []string, s Server) ([]byte, error) { if !s.KeyExists(key) { // TODO: Retry CreateKeyAndLock until we manage to obtain the key s.CreateKeyAndLock(ctx, key) - s.SetValue(ctx, key, AdaptType(cmd[2])) + s.SetValue(ctx, key, utils.AdaptType(cmd[2])) s.KeyUnlock(key) return []byte("+OK\r\n\n"), nil } @@ -93,13 +79,13 @@ func handleSet(ctx context.Context, cmd []string, s Server) ([]byte, error) { return nil, err } - s.SetValue(ctx, key, AdaptType(cmd[2])) + s.SetValue(ctx, key, utils.AdaptType(cmd[2])) s.KeyUnlock(key) return []byte("+OK\r\n\n"), nil } } -func handleSetNX(ctx context.Context, cmd []string, s Server) ([]byte, error) { +func handleSetNX(ctx context.Context, cmd []string, s utils.Server) ([]byte, error) { switch x := len(cmd); { default: return nil, errors.New("wrong number of args for SETNX command") @@ -110,13 +96,13 @@ func handleSetNX(ctx context.Context, cmd []string, s Server) ([]byte, error) { } // TODO: Retry CreateKeyAndLock until we manage to obtain the key s.CreateKeyAndLock(ctx, key) - s.SetValue(ctx, key, AdaptType(cmd[2])) + s.SetValue(ctx, key, utils.AdaptType(cmd[2])) s.KeyUnlock(key) } return []byte("+OK\r\n\n"), nil } -func handleMSet(ctx context.Context, cmd []string, s Server) ([]byte, error) { +func handleMSet(ctx context.Context, cmd []string, s utils.Server) ([]byte, error) { ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond) defer cancel() @@ -144,7 +130,7 @@ func handleMSet(ctx context.Context, cmd []string, s Server) ([]byte, error) { for i, key := range cmd[1:] { if i%2 == 0 { entries[key] = KeyObject{ - value: AdaptType(cmd[1:][i+1]), + value: utils.AdaptType(cmd[1:][i+1]), locked: false, } } @@ -174,30 +160,37 @@ func handleMSet(ctx context.Context, cmd []string, s Server) ([]byte, error) { return []byte("+OK\r\n\n"), nil } -func init() { - Plugin.name = "SetCommands" - Plugin.commands = []Command{ - { - Command: "set", - Categories: []string{}, - Description: "(SET key value) Set the value of a key, considering the value's type.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "setnx", - Categories: []string{}, - Description: "(SETNX key value) Set the key/value only if the key doesn't exist.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "mset", - Categories: []string{}, - Description: "(MSET key value [key value ...]) Automatically set or modify multiple key/value pairs.", - HandleWithConnection: false, - Sync: true, +func NewModule() Plugin { + SetModule := Plugin{ + name: "SetCommands", + commands: []utils.Command{ + { + Command: "set", + Categories: []string{}, + Description: "(SET key value) Set the value of a key, considering the value's type.", + HandleWithConnection: false, + Sync: true, + Plugin: SetModule, + }, + { + Command: "setnx", + Categories: []string{}, + Description: "(SETNX key value) Set the key/value only if the key doesn't exist.", + HandleWithConnection: false, + Sync: true, + Plugin: SetModule, + }, + { + Command: "mset", + Categories: []string{}, + Description: "(MSET key value [key value ...]) Automatically set or modify multiple key/value pairs.", + HandleWithConnection: false, + Sync: true, + Plugin: SetModule, + }, }, + description: "Handle basic SET commands", } - Plugin.description = "Handle basic SET commands" + + return SetModule } diff --git a/src/plugins/string/command.go b/src/modules/string/commands.go similarity index 55% rename from src/plugins/string/command.go rename to src/modules/string/commands.go index 5f5c16d..9336b6f 100644 --- a/src/plugins/string/command.go +++ b/src/modules/string/commands.go @@ -1,79 +1,66 @@ -package main +package str import ( "context" "encoding/json" "errors" "fmt" + "github.com/kelvinmwinuka/memstore/src/utils" "net" "strings" ) -type Server interface { - KeyLock(ctx context.Context, key string) (bool, error) - KeyUnlock(key string) - KeyRLock(ctx context.Context, key string) (bool, error) - KeyRUnlock(key string) - KeyExists(key string) bool - CreateKeyAndLock(ctx context.Context, key string) (bool, error) - GetValue(key string) interface{} - SetValue(ctx context.Context, key string, value interface{}) -} - -type Command struct { - Command string `json:"Command"` - Categories []string `json:"Categories"` - Description string `json:"Description"` - HandleWithConnection bool `json:"HandleWithConnection"` - Sync bool `json:"Sync"` -} - -type plugin struct { +type Plugin struct { name string - commands []Command + commands []utils.Command description string } -var Plugin plugin +var StringModule Plugin -func (p *plugin) Name() string { +func (p Plugin) Name() string { return p.name } -func (p *plugin) Commands() ([]byte, error) { + +func (p Plugin) Commands() ([]byte, error) { return json.Marshal(p.commands) } -func (p *plugin) Description() string { +func (p Plugin) GetCommands() []utils.Command { + return p.commands +} + +func (p Plugin) Description() string { return p.description } -func (p *plugin) HandleCommandWithConnection(ctx context.Context, cmd []string, server interface{}, conn *net.Conn) ([]byte, error) { +func (p Plugin) HandleCommandWithConnection(ctx context.Context, cmd []string, server utils.Server, conn *net.Conn) ([]byte, error) { return nil, errors.New("not implemented") } -func (p *plugin) HandleCommand(ctx context.Context, cmd []string, server interface{}) ([]byte, error) { +func (p Plugin) HandleCommand(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { switch strings.ToLower(cmd[0]) { default: return nil, errors.New("command unknown") case "setrange": - return handleSetRange(ctx, cmd, server.(Server)) + return handleSetRange(ctx, cmd, server) case "strlen": - return handleStrLen(ctx, cmd, server.(Server)) + return handleStrLen(ctx, cmd, server) case "substr": - return handleSubStr(ctx, cmd, server.(Server)) + return handleSubStr(ctx, cmd, server) case "getrange": - return handleSubStr(ctx, cmd, server.(Server)) + return handleSubStr(ctx, cmd, server) } } -func handleSetRange(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handleSetRange(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd[1:]) != 3 { return nil, errors.New("wrong number of args for SETRANGE command") } key := cmd[1] - offset, ok := AdaptType(cmd[2]).(int64) + offset, ok := utils.AdaptType(cmd[2]).(int64) if !ok { return nil, errors.New("offset must be integer") } @@ -132,7 +119,7 @@ func handleSetRange(ctx context.Context, cmd []string, server Server) ([]byte, e return []byte(fmt.Sprintf(":%d\r\n\n", len(newStr))), nil } -func handleStrLen(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handleStrLen(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd[1:]) != 1 { return nil, errors.New("wrong number of args for STRLEN command") } @@ -157,15 +144,15 @@ func handleStrLen(ctx context.Context, cmd []string, server Server) ([]byte, err return []byte(fmt.Sprintf(":%d\r\n\n", len(value))), nil } -func handleSubStr(ctx context.Context, cmd []string, server Server) ([]byte, error) { +func handleSubStr(ctx context.Context, cmd []string, server utils.Server) ([]byte, error) { if len(cmd[1:]) != 3 { return nil, errors.New("wrong number of args for SUBSTR command") } key := cmd[1] - start, startOk := AdaptType(cmd[2]).(int64) - end, endOk := AdaptType(cmd[3]).(int64) + start, startOk := utils.AdaptType(cmd[2]).(int64) + end, endOk := utils.AdaptType(cmd[3]).(int64) reversed := false if !startOk || !endOk { @@ -220,37 +207,44 @@ func handleSubStr(ctx context.Context, cmd []string, server Server) ([]byte, err return []byte(fmt.Sprintf("$%d\r\n%s\r\n\n", len(str), str)), nil } -func init() { - Plugin.name = "StringCommands" - Plugin.commands = []Command{ - { - Command: "setrange", - Categories: []string{}, - Description: "(SETRANGE key offset value) Overwrites part of a string value with another by offset. Creates the key if it doesn't exist.", - HandleWithConnection: false, - Sync: true, - }, - { - Command: "strlen", - Categories: []string{}, - Description: "(STRLEN key) Returns length of the key's value if it's a string.", - HandleWithConnection: false, - Sync: false, - }, - { - Command: "substr", - Categories: []string{}, - Description: "(SUBSTR key start end) Returns a substring from the string value.", - HandleWithConnection: false, - Sync: false, - }, - { - Command: "getrange", - Categories: []string{}, - Description: "(GETRANGE key start end) Returns a substring from the string value.", - HandleWithConnection: false, - Sync: false, +func NewModule() Plugin { + StringModule := Plugin{ + name: "StringCommands", + commands: []utils.Command{ + { + Command: "setrange", + Categories: []string{}, + Description: "(SETRANGE key offset value) Overwrites part of a string value with another by offset. Creates the key if it doesn't exist.", + HandleWithConnection: false, + Sync: true, + Plugin: StringModule, + }, + { + Command: "strlen", + Categories: []string{}, + Description: "(STRLEN key) Returns length of the key's value if it's a string.", + HandleWithConnection: false, + Sync: false, + Plugin: StringModule, + }, + { + Command: "substr", + Categories: []string{}, + Description: "(SUBSTR key start end) Returns a substring from the string value.", + HandleWithConnection: false, + Sync: false, + Plugin: StringModule, + }, + { + Command: "getrange", + Categories: []string{}, + Description: "(GETRANGE key start end) Returns a substring from the string value.", + HandleWithConnection: false, + Sync: false, + Plugin: StringModule, + }, }, + description: "Handle basic STRING commands", } - Plugin.description = "Handle basic STRING commands" + return StringModule } diff --git a/src/plugins/get/command.go b/src/plugins/get/command.go deleted file mode 100644 index 8fb376e..0000000 --- a/src/plugins/get/command.go +++ /dev/null @@ -1,137 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net" - "strings" -) - -type Server interface { - KeyLock(ctx context.Context, key string) (bool, error) - KeyUnlock(key string) - KeyRLock(ctx context.Context, key string) (bool, error) - KeyRUnlock(key string) - KeyExists(key string) bool - CreateKeyAndLock(ctx context.Context, key string) (bool, error) - GetValue(key string) interface{} - SetValue(ctx context.Context, key string, value interface{}) -} - -type Command struct { - Command string `json:"Command"` - Categories []string `json:"Categories"` - Description string `json:"Description"` - HandleWithConnection bool `json:"HandleWithConnection"` - Sync bool `json:"Sync"` -} - -type plugin struct { - name string - commands []Command - categories []string - description string -} - -var Plugin plugin - -func (p *plugin) Name() string { - return p.name -} - -func (p *plugin) Commands() ([]byte, error) { - return json.Marshal(p.commands) -} - -func (p *plugin) Description() string { - return p.description -} - -func (p *plugin) HandleCommandWithConnection(ctx context.Context, cmd []string, server interface{}, conn *net.Conn) ([]byte, error) { - return nil, errors.New("not implemented") -} - -func (p *plugin) HandleCommand(ctx context.Context, cmd []string, server interface{}) ([]byte, error) { - switch strings.ToLower(cmd[0]) { - default: - return nil, errors.New("command unknown") - case "get": - return handleGet(ctx, cmd, server.(Server)) - case "mget": - return handleMGet(ctx, cmd, server.(Server)) - } -} - -func handleGet(ctx context.Context, cmd []string, s Server) ([]byte, error) { - if len(cmd) != 2 { - return nil, errors.New("wrong number of args for GET command") - } - - key := cmd[1] - - s.KeyRLock(ctx, key) - value := s.GetValue(key) - s.KeyRUnlock(key) - - switch value.(type) { - default: - return []byte(fmt.Sprintf("+%v\r\n\n", value)), nil - case nil: - return []byte("+nil\r\n\n"), nil - } -} - -func handleMGet(ctx context.Context, cmd []string, s Server) ([]byte, error) { - if len(cmd) < 2 { - return nil, errors.New("wrong number of args for MGET command") - } - - vals := []string{} - - for _, key := range cmd[1:] { - func(key string) { - s.KeyRLock(ctx, key) - switch s.GetValue(key).(type) { - default: - vals = append(vals, fmt.Sprintf("%v", s.GetValue(key))) - case nil: - vals = append(vals, "nil") - } - s.KeyRUnlock(key) - - }(key) - } - - var bytes []byte = []byte(fmt.Sprintf("*%d\r\n", len(vals))) - - for _, val := range vals { - bytes = append(bytes, []byte(fmt.Sprintf("$%d\r\n%s\r\n", len(val), val))...) - } - - bytes = append(bytes, []byte("\n")...) - - return bytes, nil -} - -func init() { - Plugin.name = "GetCommands" - Plugin.commands = []Command{ - { - Command: "get", - Categories: []string{}, - Description: "", - HandleWithConnection: false, - Sync: false, - }, - { - Command: "mget", - Categories: []string{}, - Description: "", - HandleWithConnection: false, - Sync: true, - }, - } - Plugin.description = "Handle basic GET and MGET commands" -} diff --git a/src/plugins/list/utils.go b/src/plugins/list/utils.go deleted file mode 100644 index 1166abd..0000000 --- a/src/plugins/list/utils.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - "math/big" -) - -func Contains[T comparable](arr []T, elem T) bool { - for _, v := range arr { - if v == elem { - return true - } - } - return false -} - -func Filter[T comparable](arr []T, test func(elem T) bool) (res []T) { - for _, e := range arr { - if test(e) { - res = append(res, e) - } - } - return -} - -func AdaptType(s string) interface{} { - // Adapt the type of the parameter to string, float64 or int - n, _, err := big.ParseFloat(s, 10, 256, big.RoundingMode(big.Exact)) - - if err != nil { - return s - } - - if n.IsInt() { - i, _ := n.Int64() - return i - } - - return n -} diff --git a/src/plugins/ping/command.go b/src/plugins/ping/command.go deleted file mode 100644 index 9922c6a..0000000 --- a/src/plugins/ping/command.go +++ /dev/null @@ -1,99 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "errors" - "net" - "strings" -) - -const ( - OK = "+OK\r\n\n" -) - -type Server interface { - KeyLock(ctx context.Context, key string) (bool, error) - KeyUnlock(key string) - KeyRLock(ctx context.Context, key string) (bool, error) - KeyRUnlock(key string) - KeyExists(key string) bool - CreateKeyAndLock(ctx context.Context, key string) (bool, error) - GetValue(key string) interface{} - SetValue(ctx context.Context, key string, value interface{}) -} - -type Command struct { - Command string `json:"Command"` - Categories []string `json:"Categories"` - Description string `json:"Description"` - HandleWithConnection bool `json:"HandleWithConnection"` - Sync bool `json:"Sync"` -} - -type plugin struct { - name string - commands []Command - description string -} - -var Plugin plugin - -func (p *plugin) Name() string { - return p.name -} - -func (p *plugin) Commands() ([]byte, error) { - return json.Marshal(p.commands) -} - -func (p *plugin) Description() string { - return p.description -} - -func (p *plugin) HandleCommandWithConnection(ctx context.Context, cmd []string, server interface{}, conn *net.Conn) ([]byte, error) { - return nil, errors.New("not implemented") -} - -func (p *plugin) HandleCommand(ctx context.Context, cmd []string, server interface{}) ([]byte, error) { - switch strings.ToLower(cmd[0]) { - default: - return nil, errors.New("not implemented") - case "ping": - return handlePing(ctx, cmd, server.(Server)) - case "ack": - return []byte("$-1\r\n\n"), nil - } -} - -func handlePing(ctx context.Context, cmd []string, s Server) ([]byte, error) { - switch len(cmd) { - default: - return nil, errors.New("wrong number of arguments for PING command") - case 1: - return []byte("+PONG\r\n\n"), nil - case 2: - return []byte("+" + cmd[1] + "\r\n\n"), nil - } -} - -func init() { - Plugin.name = "PingCommands" - Plugin.commands = []Command{ - { - Command: "ping", - Categories: []string{}, - Description: "", - HandleWithConnection: false, - Sync: false, - }, - { - Command: "ack", - Categories: []string{}, - Description: "", - HandleWithConnection: false, - Sync: false, - }, - } - Plugin.description = "Handle PING command" -} diff --git a/src/plugins/set/utils.go b/src/plugins/set/utils.go deleted file mode 100644 index 158bd86..0000000 --- a/src/plugins/set/utils.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "math/big" -) - -func AdaptType(s string) interface{} { - // Adapt the type of the parameter to string, float64 or int - n, _, err := big.ParseFloat(s, 10, 256, big.RoundingMode(big.Exact)) - - if err != nil { - return s - } - - if n.IsInt() { - i, _ := n.Int64() - return i - } - - return n -} diff --git a/src/plugins/string/utils.go b/src/plugins/string/utils.go deleted file mode 100644 index 158bd86..0000000 --- a/src/plugins/string/utils.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "math/big" -) - -func AdaptType(s string) interface{} { - // Adapt the type of the parameter to string, float64 or int - n, _, err := big.ParseFloat(s, 10, 256, big.RoundingMode(big.Exact)) - - if err != nil { - return s - } - - if n.IsInt() { - i, _ := n.Int64() - return i - } - - return n -} diff --git a/src/utils/utils.go b/src/utils/utils.go index cc9c3d7..6505362 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -5,6 +5,7 @@ import ( "bytes" "encoding/json" "fmt" + "math/big" "net" "strings" "time" @@ -13,6 +14,22 @@ import ( "github.com/tidwall/resp" ) +func AdaptType(s string) interface{} { + // Adapt the type of the parameter to string, float64 or int + n, _, err := big.ParseFloat(s, 10, 256, big.RoundingMode(big.Exact)) + + if err != nil { + return s + } + + if n.IsInt() { + i, _ := n.Int64() + return i + } + + return n +} + func Contains[T comparable](arr []T, elem T) bool { for _, v := range arr { if v == elem {