Files
SugarDB/server/command_list.go
Kelvin Clement Mwinuka d4a5997b30 Scrapped plugin design in favour of simple command interfaces.
Setup docker build process for running server.
Deleted test files.
2023-07-22 05:34:07 +08:00

571 lines
13 KiB
Go

package main
import (
"bufio"
"fmt"
"math"
"strings"
)
type ListCommand struct {
name string
commands []string
description string
}
func (p *ListCommand) Name() string {
return p.name
}
func (p *ListCommand) Commands() []string {
return p.commands
}
func (p *ListCommand) Description() string {
return p.description
}
func (p *ListCommand) HandleCommand(cmd []string, server *Server, conn *bufio.Writer) {
c := strings.ToLower(cmd[0])
switch {
case c == "llen":
handleLLen(cmd, server, conn)
case c == "lindex":
handleLIndex(cmd, server, conn)
case c == "lrange":
handleLRange(cmd, server, conn)
case c == "lset":
handleLSet(cmd, server, conn)
case c == "ltrim":
handleLTrim(cmd, server, conn)
case c == "lrem":
handleLRem(cmd, server, conn)
case c == "lmove":
handleLMove(cmd, server, conn)
case Contains[string]([]string{"lpush", "lpushx"}, c):
handleLPush(cmd, server, conn)
case Contains[string]([]string{"rpush", "rpushx"}, c):
handleRPush(cmd, server, conn)
case Contains[string]([]string{"lpop", "rpop"}, c):
handlePop(cmd, server, conn)
}
}
func handleLLen(cmd []string, server *Server, conn *bufio.Writer) {
if len(cmd) != 2 {
conn.Write([]byte("-Error wrong number of args for LLEN command\r\n\n"))
conn.Flush()
return
}
server.Lock()
list, ok := server.GetData(cmd[1]).([]interface{})
if !ok {
server.Unlock()
conn.Write([]byte("-Error LLEN command on non-list item\r\n\n"))
conn.Flush()
return
}
server.Unlock()
conn.Write([]byte(fmt.Sprintf(":%d\r\n\n", len(list))))
conn.Flush()
}
func handleLIndex(cmd []string, server *Server, conn *bufio.Writer) {
if len(cmd) != 3 {
conn.Write([]byte("-Error wrong number of args for LINDEX command\r\n\n"))
conn.Flush()
return
}
index, ok := AdaptType(cmd[2]).(int)
if !ok {
conn.Write([]byte("-Error index must be an integer\r\n\n"))
conn.Flush()
return
}
server.Lock()
list, ok := server.GetData(cmd[1]).([]interface{})
if !ok {
server.Unlock()
conn.Write([]byte("-Error LINDEX command on non-list item\r\n\n"))
conn.Flush()
return
}
if !(index >= 0 && index < len(list)) {
server.Unlock()
conn.Write([]byte("-Error index must be within list range\r\n\n"))
conn.Flush()
return
}
server.Unlock()
conn.Write([]byte(fmt.Sprintf("+%s\r\n\n", list[index])))
conn.Flush()
}
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 := AdaptType(cmd[2]).(int)
end, endOk := 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 handleLSet(cmd []string, server *Server, conn *bufio.Writer) {
if len(cmd) != 4 {
conn.Write([]byte("-Error wrong number of arguments for LSET command\r\n\n"))
conn.Flush()
return
}
server.Lock()
list, ok := server.GetData(cmd[1]).([]interface{})
if !ok {
server.Unlock()
conn.Write([]byte("-Error LSET command on non-list item\r\n\n"))
conn.Flush()
return
}
index, ok := AdaptType(cmd[2]).(int)
if !ok {
server.Unlock()
conn.Write([]byte("-Error index must be an integer\r\n\n"))
conn.Flush()
return
}
if !(index >= 0 && index < len(list)) {
server.Unlock()
conn.Write([]byte("-Error index must be within range\r\n\n"))
conn.Flush()
return
}
list[index] = AdaptType(cmd[3])
server.SetData(cmd[1], list)
server.Unlock()
conn.Write([]byte(OK))
conn.Flush()
}
func handleLTrim(cmd []string, server *Server, conn *bufio.Writer) {
if len(cmd) != 4 {
conn.Write([]byte("-Error wrong number of args for command LTRIM \r\n\n"))
conn.Flush()
return
}
start, startOk := AdaptType(cmd[2]).(int)
end, endOk := AdaptType(cmd[3]).(int)
if !startOk || !endOk {
conn.Write([]byte("-Error start and end indices must be integers\r\n\n"))
conn.Flush()
return
}
if end < start && end != -1 {
conn.Write([]byte("-Error end index must be greater than start index or -1\r\n\n"))
conn.Flush()
return
}
server.Lock()
list, ok := server.GetData(cmd[1]).([]interface{})
if !ok {
server.Unlock()
conn.Write([]byte("-Error LTRIM command on non-list item\r\n\n"))
conn.Flush()
return
}
if !(start >= 0 && start < len(list)) {
server.Unlock()
conn.Write([]byte("-Error start index must be within list boundary\r\n\n"))
conn.Flush()
return
}
if end == -1 || end > len(list) {
server.SetData(cmd[1], list[start:])
server.Unlock()
conn.Write([]byte(OK))
conn.Flush()
return
}
server.SetData(cmd[1], list[start:end])
server.Unlock()
conn.Write([]byte(OK))
conn.Flush()
}
func handleLRem(cmd []string, server *Server, conn *bufio.Writer) {
if len(cmd) != 4 {
conn.Write([]byte("-Error wrong number of arguments for LREM command\r\n\n"))
conn.Flush()
return
}
value := cmd[3]
count, ok := AdaptType(cmd[2]).(int)
if !ok {
conn.Write([]byte("-Error count must be an integer\r\n\n"))
conn.Flush()
return
}
absoluteCount := math.Abs(float64(count))
server.Lock()
list, ok := server.GetData(cmd[1]).([]interface{})
if !ok {
server.Unlock()
conn.Write([]byte("-Error LREM command on non-list item\r\n\n"))
conn.Flush()
return
}
switch {
default:
// Count is zero, keep list the same
case count > 0:
// Start from the head
for i := 0; i < len(list); i++ {
if absoluteCount == 0 {
break
}
if fmt.Sprintf("%v", list[i]) == value {
list[i] = nil
absoluteCount -= 1
}
}
case count < 0:
// Start from the tail
for i := len(list) - 1; i >= 0; i-- {
if absoluteCount == 0 {
break
}
if fmt.Sprintf("%v", list[i]) == value {
list[i] = nil
absoluteCount -= 1
}
}
}
list = Filter[interface{}](list, func(elem interface{}) bool {
return elem != nil
})
server.SetData(cmd[1], list)
server.Unlock()
conn.Write([]byte(OK))
conn.Flush()
}
func handleLMove(cmd []string, server *Server, conn *bufio.Writer) {
if len(cmd) != 5 {
conn.Write([]byte("-Error wrong number of arguments for LMOVE command\r\n\n"))
conn.Flush()
return
}
whereFrom := strings.ToLower(cmd[3])
whereTo := strings.ToLower(cmd[4])
if !Contains[string]([]string{"left", "right"}, whereFrom) || !Contains[string]([]string{"left", "right"}, whereTo) {
conn.Write([]byte("-Error wherefrom and whereto arguments must be either LEFT or RIGHT\r\n\n"))
conn.Flush()
return
}
server.Lock()
source, sourceOk := server.GetData(cmd[1]).([]interface{})
destination, destinationOk := server.GetData(cmd[2]).([]interface{})
if !sourceOk || !destinationOk {
server.Unlock()
conn.Write([]byte("-Error source and destination must both be lists\r\n\n"))
conn.Flush()
return
}
switch whereFrom {
case "left":
server.SetData(cmd[1], append([]interface{}{}, source[1:]...))
if whereTo == "left" {
server.SetData(cmd[2], append(source[0:1], destination...))
} else if whereTo == "right" {
server.SetData(cmd[2], append(destination, source[0]))
}
case "right":
server.SetData(cmd[1], append([]interface{}{}, source[:len(source)-1]...))
if whereTo == "left" {
server.SetData(cmd[2], append(source[len(source)-1:], destination...))
} else if whereTo == "right" {
server.SetData(cmd[2], append(destination, source[len(source)-1]))
}
}
server.Unlock()
conn.Write([]byte(OK))
conn.Flush()
}
func handleLPush(cmd []string, server *Server, conn *bufio.Writer) {
if len(cmd) < 3 {
conn.Write([]byte(fmt.Sprintf("-Error wrong number of arguments for %s command\r\n\n", strings.ToUpper(cmd[0]))))
conn.Flush()
return
}
server.Lock()
newElems := []interface{}{}
for _, elem := range cmd[2:] {
newElems = append(newElems, AdaptType(elem))
}
currentList := server.GetData(cmd[1])
if currentList == nil {
switch strings.ToLower(cmd[0]) {
default:
server.SetData(cmd[1], newElems)
conn.Write([]byte(OK))
case "lpushx":
conn.Write([]byte("-Error no list at key\r\n\n"))
}
server.Unlock()
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(fmt.Sprintf("-Error wrong number of arguments for %s command\r\n\n", strings.ToUpper(cmd[0]))))
conn.Flush()
return
}
server.Lock()
newElems := []interface{}{}
for _, elem := range cmd[2:] {
newElems = append(newElems, AdaptType(elem))
}
currentList := server.GetData(cmd[1])
if currentList == nil {
switch strings.ToLower(cmd[0]) {
default:
server.SetData(cmd[1], newElems)
conn.Write([]byte(OK))
case "rpushx":
conn.Write([]byte("-Error no list at key\r\n\n"))
}
server.Unlock()
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 handlePop(cmd []string, server *Server, conn *bufio.Writer) {
if len(cmd) != 2 {
conn.Write([]byte(fmt.Sprintf("-Error wrong number of args for %s command\r\n\n", strings.ToUpper(cmd[0]))))
conn.Flush()
return
}
server.Lock()
list, ok := server.GetData(cmd[1]).([]interface{})
if !ok {
server.Unlock()
conn.Write([]byte(fmt.Sprintf("-Error %s command on non-list item\r\n\n", strings.ToUpper(cmd[0]))))
conn.Flush()
return
}
switch strings.ToLower(cmd[0]) {
default:
server.SetData(cmd[1], list[1:])
conn.Write([]byte(fmt.Sprintf("+%v\r\n\n", list[0])))
case "rpop":
server.SetData(cmd[1], list[:len(list)-1])
conn.Write([]byte(fmt.Sprintf("+%v\r\n\n", list[len(list)-1])))
}
server.Unlock()
conn.Flush()
}
func NewListCommand() *ListCommand {
return &ListCommand{
name: "ListCommand",
commands: []string{
"lpush", // (LPUSH key value1 [value2]) Prepends one or more values to the beginning of a list, creates the list if it does not exist.
"lpushx", // (LPUSHX key value) Prepends a value to the beginning of a list only if the list exists.
"lpop", // (LPOP key) Removes and returns the first element of a list.
"llen", // (LLEN key) Return the length of a list.
"lrange", // (LRANGE key start end) Return a range of elements between the given indices.
"lindex", // (LINDEX key index) Gets list element by index.
"lset", // (LSET key index value) Sets the value of an element in a list by its index.
"ltrim", // (LTRIM key start end) Trims a list to the specified range.
"lrem", // (LREM key count value) Remove elements from list.
"lmove", // (LMOVE source destination <LEFT | RIGHT> <LEFT | RIGHT> Move element from one list to the other specifying left/right for both lists.
"rpop", // (RPOP key) Removes and gets the last element in a list.
"rpush", // (RPUSH key value [value2]) Appends one or multiple elements to the end of a list.
"rpushx", // (RPUSHX key value) Appends an element to the end of a list, only if the list exists.
},
description: "Handle List commands",
}
}