Skip to content

Commit

Permalink
Merge pull request #84 from DMcP89/enhancement/62-append-command
Browse files Browse the repository at this point in the history
Enhancement/62 append command
  • Loading branch information
kelvinmwinuka authored Jul 2, 2024
2 parents b794a33 + d896972 commit 47bafa9
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 4 deletions.
19 changes: 18 additions & 1 deletion echovault/api_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
package echovault

import (
"github.com/echovault/echovault/internal"
"strconv"

"github.com/echovault/echovault/internal"
)

// SetRange replaces a portion of the string at the provided key starting at the offset with a new string.
Expand Down Expand Up @@ -76,3 +77,19 @@ func (server *EchoVault) GetRange(key string, start, end int) (string, error) {
}
return internal.ParseStringResponse(b)
}

// Append concatenates the string at the key with the value provided
// If the key does not exists it functions like a SET command
//
// Returns: The lenght of the new concatenated value at key
//
// Errors:
//
// - "value at key <key> is not a string" - when the value at the keys is not a string.
func (server *EchoVault) Append(key string, value string) (int, error) {
b, err := server.handleCommand(server.context, internal.EncodeCommand([]string{"APPEND", key, value}), nil, false, true)
if err != nil {
return 0, err
}
return internal.ParseIntegerResponse(b)
}
54 changes: 54 additions & 0 deletions echovault/api_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,57 @@ func TestEchoVault_STRLEN(t *testing.T) {
})
}
}

func TestEchoVault_APPEND(t *testing.T) {
server := createEchoVault()
tests := []struct {
name string
presetValue interface{}
key string
value string
want int
wantErr bool
}{
{
name: "Test APPEND with no preset value",
key: "key1",
value: "Hello ",
want: 6,
wantErr: false,
},
{
name: "Test APPEND with preset value",
presetValue: "Hello ",
key: "key2",
value: "World",
want: 11,
wantErr: false,
},
{
name: "Test APPEND with integer preset value",
key: "key3",
presetValue: 10,
value: "Hello ",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.presetValue != nil {
err := presetValue(server, context.Background(), tt.key, tt.presetValue)
if err != nil {
t.Error(err)
return
}
}
got, err := server.Append(tt.key, tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("APPEND() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("APPEND() got = %v, want %v", got, tt.want)
}
})
}
}
40 changes: 40 additions & 0 deletions internal/modules/string/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package str
import (
"errors"
"fmt"

"github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/constants"
)
Expand Down Expand Up @@ -166,6 +167,36 @@ func handleSubStr(params internal.HandlerFuncParams) ([]byte, error) {
return []byte(fmt.Sprintf("$%d\r\n%s\r\n", len(str), str)), nil
}

func handleAppend(params internal.HandlerFuncParams) ([]byte, error) {
keys, err := appendKeyFunc(params.Command)
if err != nil {
return nil, err
}

key := keys.WriteKeys[0]
keyExists := params.KeysExist(params.Context, keys.WriteKeys)[key]
value := params.Command[2]
if !keyExists {
if err = params.SetValues(params.Context, map[string]interface{}{
key: internal.AdaptType(value),
}); err != nil {
return nil, err
}
return []byte(fmt.Sprintf(":%d\r\n", len(value))), nil
}
currentValue, ok := params.GetValues(params.Context, []string{key})[key].(string)
if !ok {
return nil, fmt.Errorf("Value at key %s is not a string", key)
}
newValue := fmt.Sprintf("%v%s", currentValue, value)
if err = params.SetValues(params.Context, map[string]interface{}{
key: internal.AdaptType(newValue),
}); err != nil {
return nil, err
}
return []byte(fmt.Sprintf(":%d\r\n", len(newValue))), nil
}

func Commands() []internal.Command {
return []internal.Command{
{
Expand Down Expand Up @@ -205,5 +236,14 @@ Overwrites part of a string value with another by offset. Creates the key if it
KeyExtractionFunc: subStrKeyFunc,
HandlerFunc: handleSubStr,
},
{
Command: "append",
Module: constants.StringModule,
Categories: []string{constants.StringCategory, constants.WriteCategory, constants.SlowCategory},
Description: `(APPEND key value) If key already exists and is a string, this command appends the value at the end of the string. If key does not exist it is created and set as an empty string, so APPEND will be similar to [SET] in this special case.`,
Sync: true,
KeyExtractionFunc: appendKeyFunc,
HandlerFunc: handleAppend,
},
}
}
109 changes: 106 additions & 3 deletions internal/modules/string/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ package str_test

import (
"errors"
"strconv"
"strings"
"testing"

"github.com/echovault/echovault/echovault"
"github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/config"
"github.com/echovault/echovault/internal/constants"
"github.com/tidwall/resp"
"strconv"
"strings"
"testing"
)

func Test_String(t *testing.T) {
Expand Down Expand Up @@ -450,4 +451,106 @@ func Test_String(t *testing.T) {
})
}
})

t.Run("Test_HandleAppend", func(t *testing.T) {
t.Parallel()
conn, err := internal.GetConnection("localhost", port)
if err != nil {
t.Error(err)
return
}
defer func() {
_ = conn.Close()
}()
client := resp.NewConn(conn)

tests := []struct {
name string
key string
presetValue interface{}
command []string
expectedResponse int
expectedError error
}{
{
name: "Test APPEND with no preset value",
key: "AppendKey1",
command: []string{"APPEND", "AppendKey1", "Hello"},
expectedResponse: 5,
expectedError: nil,
},
{
name: "Test APPEND with preset value",
key: "AppendKey2",
presetValue: "Hello ",
command: []string{"APPEND", "AppendKey2", "World"},
expectedResponse: 11,
expectedError: nil,
},
{
name: "Test APPEND with integer preset value",
key: "AppendKey4",
presetValue: 10,
command: []string{"APPEND", "AppendKey4", "World"},
expectedResponse: 0,
expectedError: errors.New("Value at key AppendKey4 is not a string"),
},
{
name: "Command too short",
command: []string{"APPEND", "AppendKey5"},
expectedError: errors.New(constants.WrongArgsResponse),
},
{
name: "Command too long",
command: []string{"APPEND", "AppendKey5", "new value", "extra value"},
expectedError: errors.New(constants.WrongArgsResponse),
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.presetValue != "" {
if err = client.WriteArray([]resp.Value{
resp.StringValue("SET"),
resp.StringValue(test.key),
resp.AnyValue(test.presetValue),
}); err != nil {
t.Error(err)
}
res, _, err := client.ReadValue()
if err != nil {
t.Error(err)
}

if !strings.EqualFold(res.String(), "ok") {
t.Errorf("expected preset response to be OK, got %s", res.String())
}
}

command := make([]resp.Value, len(test.command))
for i, c := range test.command {
command[i] = resp.StringValue(c)
}

if err = client.WriteArray(command); err != nil {
t.Error(err)
}
res, _, err := client.ReadValue()
if err != nil {
t.Error(err)
}

if test.expectedError != nil {
if !strings.Contains(res.Error().Error(), test.expectedError.Error()) {
t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error())
}
return
}

if res.Integer() != test.expectedResponse {
t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer())
}
})
}
})
}
12 changes: 12 additions & 0 deletions internal/modules/string/key_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package str

import (
"errors"

"github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/constants"
)
Expand Down Expand Up @@ -52,3 +53,14 @@ func subStrKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
WriteKeys: make([]string, 0),
}, nil
}

func appendKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
if len(cmd) != 3 {
return internal.KeyExtractionFuncResult{}, errors.New(constants.WrongArgsResponse)
}
return internal.KeyExtractionFuncResult{
Channels: make([]string, 0),
ReadKeys: make([]string, 0),
WriteKeys: cmd[1:2],
}, nil
}

0 comments on commit 47bafa9

Please sign in to comment.