Skip to content

Commit

Permalink
Add RPOP command
Browse files Browse the repository at this point in the history
  • Loading branch information
namreg committed Nov 8, 2018
1 parent 164eaf6 commit 1004357
Show file tree
Hide file tree
Showing 5 changed files with 343 additions and 0 deletions.
19 changes: 19 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,25 @@ func (c *Client) rpush(ctx context.Context, key, value string, values ...string)
return newStatusResult(resp)
}

// RPop removes and returns the last element of the list stored at the given key.
func (c *Client) RPop(key string) ScalarResult {
return c.rpop(context.Background(), key)
}

// RPopWithContext similar to RPop but with context.
func (c *Client) RPopWithContext(ctx context.Context, key string) ScalarResult {
return c.rpop(ctx, key)
}

func (c *Client) rpop(ctx context.Context, key string) ScalarResult {
req := c.newExecuteRequest("RPOP", key)
resp, err := c.executor.ExecuteCommand(ctx, req)
if err != nil {
return ScalarResult{err: fmt.Errorf("could not execute command: %v", err)}
}
return newScalarResult(resp)
}

//LRange returns elements from the list stored at the given key.
//Start and stop are zero-based indexes.
func (c *Client) LRange(key string, start, stop int) ListResult {
Expand Down
174 changes: 174 additions & 0 deletions client/client_rpop_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package client

import (
context "context"
"errors"
"testing"

"github.com/gojuno/minimock"
"github.com/namreg/godown/internal/api"
"github.com/stretchr/testify/assert"
)

func TestClient_Rpop(t *testing.T) {
mc := minimock.NewController(t)
defer mc.Finish()

tests := []struct {
name string
arg string
expectCommand string
mockResponse *api.ExecuteCommandResponse
mockErr error
wantResult ScalarResult
}{
{
name: "could_not_execute_command",
arg: "key",
expectCommand: "RPOP key",
mockErr: errors.New("something went wrong"),
wantResult: ScalarResult{
err: errors.New("could not execute command: something went wrong"),
},
},
{
name: "server_responds_with_error",
arg: "key",
expectCommand: "RPOP key",
mockResponse: &api.ExecuteCommandResponse{
Reply: api.ErrCommandReply,
Item: "internal server error",
},
wantResult: ScalarResult{err: errors.New("internal server error")},
},
{
name: "server_responds_with_nil",
arg: "key",
expectCommand: "RPOP key",
mockResponse: &api.ExecuteCommandResponse{
Reply: api.NilCommandReply,
},
wantResult: ScalarResult{},
},
{
name: "server_responds_with_string",
arg: "key",
expectCommand: "RPOP key",
mockResponse: &api.ExecuteCommandResponse{
Reply: api.StringCommandReply,
Item: "value",
},
wantResult: ScalarResult{val: stringToPtr("value")},
},
{
name: "server_responds_with_unexpected_reply",
arg: "key",
expectCommand: "RPOP key",
mockResponse: &api.ExecuteCommandResponse{
Reply: api.OkCommandReply,
},
wantResult: ScalarResult{err: errors.New("unexpected reply: OK")},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock := NewexecutorMock(mc)
mock.ExecuteCommandMock.
Expect(context.Background(), &api.ExecuteCommandRequest{Command: tt.expectCommand}).
Return(tt.mockResponse, tt.mockErr)

cl := Client{executor: mock}

res := cl.RPop(tt.arg)
assert.Equal(t, tt.wantResult, res)
})
}
}

func TestClient_RpopWithContext(t *testing.T) {
mc := minimock.NewController(t)
defer mc.Finish()

type args struct {
ctx context.Context
key string
}

tests := []struct {
name string
args args
expectCtx context.Context
expectCommand string
mockResponse *api.ExecuteCommandResponse
mockErr error
wantResult ScalarResult
}{
{
name: "could_not_execute_command",
args: args{ctx: context.Background(), key: "key"},
expectCtx: context.Background(),
expectCommand: "RPOP key",
mockErr: errors.New("something went wrong"),
wantResult: ScalarResult{
err: errors.New("could not execute command: something went wrong"),
},
},
{
name: "server_responds_with_error",
args: args{ctx: context.Background(), key: "key"},
expectCtx: context.Background(),
expectCommand: "RPOP key",
mockResponse: &api.ExecuteCommandResponse{
Reply: api.ErrCommandReply,
Item: "internal server error",
},
wantResult: ScalarResult{err: errors.New("internal server error")},
},
{
name: "server_responds_with_nil",
args: args{ctx: context.Background(), key: "key"},
expectCtx: context.Background(),
expectCommand: "RPOP key",
mockResponse: &api.ExecuteCommandResponse{
Reply: api.NilCommandReply,
},
wantResult: ScalarResult{},
},
{
name: "server_responds_with_string",
args: args{ctx: context.Background(), key: "key"},
expectCtx: context.Background(),
expectCommand: "RPOP key",
mockResponse: &api.ExecuteCommandResponse{
Reply: api.StringCommandReply,
Item: "value",
},
wantResult: ScalarResult{val: stringToPtr("value")},
},
{
name: "server_responds_with_unexpected_reply",
args: args{ctx: context.Background(), key: "key"},
expectCtx: context.Background(),
expectCommand: "RPOP key",
mockResponse: &api.ExecuteCommandResponse{
Reply: api.OkCommandReply,
},
wantResult: ScalarResult{err: errors.New("unexpected reply: OK")},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock := NewexecutorMock(mc)
mock.ExecuteCommandMock.
Expect(tt.expectCtx, &api.ExecuteCommandRequest{Command: tt.expectCommand}).
Return(tt.mockResponse, tt.mockErr)

cl := Client{executor: mock}

res := cl.RPopWithContext(tt.args.ctx, tt.args.key)
assert.Equal(t, tt.wantResult, res)
})
}
}
4 changes: 4 additions & 0 deletions internal/command/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ func (p *Parser) Parse(str string) (Command, []string, error) {
cmd = &Lpop{strg: p.strg}
case "LPUSH":
cmd = &Lpush{strg: p.strg}
case "RPUSH":
cmd = &Rpush{strg: p.strg}
case "RPOP":
cmd = &Rpop{strg: p.strg}
case "LRANGE":
cmd = &Lrange{strg: p.strg}
case "LREM":
Expand Down
59 changes: 59 additions & 0 deletions internal/command/rpop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package command

import "github.com/namreg/godown/internal/storage"

// Rpop is a RPOP command.
type Rpop struct {
strg dataStore
}

// Name returns a command name.
// Implements Name method of Command interface.
func (c *Rpop) Name() string {
return "RPOP"
}

// Help returns a help message for the command.
// Implements Help method of Command interface.
func (c *Rpop) Help() string {
return `Usage: RPOP key
Removes and returns the last element of the list stored at key.`
}

// Execute excutes a command with the given arguments.
// Implements Execute method of Command interface.
func (c *Rpop) Execute(args ...string) Reply {
if len(args) != 1 {
return ErrReply{Value: ErrWrongArgsNumber}
}

var popped string

setter := func(old *storage.Value) (*storage.Value, error) {
if old == nil {
return nil, nil
}

if old.Type() != storage.ListDataType {
return nil, ErrWrongTypeOp
}

list := old.Data().([]string)
popped, list = list[len(list)-1], list[:len(list)-1]

if len(list) == 0 {
return nil, nil
}

return storage.NewList(list), nil
}

if err := c.strg.Put(storage.Key(args[0]), setter); err != nil {
return ErrReply{Value: err}
}

if popped == "" {
return NilReply{}
}
return StringReply{Value: popped}
}
87 changes: 87 additions & 0 deletions internal/command/rpop_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package command

import (
"errors"
"testing"
"time"

"github.com/gojuno/minimock"

"github.com/namreg/godown/internal/storage"
"github.com/namreg/godown/internal/storage/memory"
"github.com/stretchr/testify/assert"
)

func TestRpop_Name(t *testing.T) {
cmd := new(Rpop)
assert.Equal(t, "RPOP", cmd.Name())
}

func TestRpop_Help(t *testing.T) {
cmd := new(Rpop)
expected := `Usage: RPOP key
Removes and returns the last element of the list stored at key.`
assert.Equal(t, expected, cmd.Help())
}

func TestRpop_Execute(t *testing.T) {
expired := storage.NewList([]string{"val1", "val2"})
expired.SetTTL(time.Now().Add(-1 * time.Second))

strg := memory.New(map[storage.Key]*storage.Value{
"string": storage.NewString("string"),
"list": storage.NewList([]string{"val1", "val2"}),
"expired": expired,
})

tests := []struct {
name string
args []string
want Reply
}{
{"ok", []string{"list"}, StringReply{Value: "val2"}},
{"expired_key", []string{"expired"}, NilReply{}},
{"not_existing_key", []string{"not_existing_key"}, NilReply{}},
{"wrong_type_op", []string{"string"}, ErrReply{Value: ErrWrongTypeOp}},
{"wrong_args_number/1", []string{}, ErrReply{Value: ErrWrongArgsNumber}},
{"wrong_args_number/2", []string{"list", "0"}, ErrReply{Value: ErrWrongArgsNumber}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := Rpop{strg: strg}
res := cmd.Execute(tt.args...)
assert.Equal(t, tt.want, res)
})
}
}

func TestRpop_Execute_StorageErr(t *testing.T) {
mc := minimock.NewController(t)
defer mc.Finish()

err := errors.New("error")

strg := NewdataStoreMock(mc)
strg.PutMock.Return(err)

cmd := Rpop{strg: strg}

res := cmd.Execute("list")
assert.Equal(t, ErrReply{Value: err}, res)
}

func TestRpop_Execute_DelEmptyList(t *testing.T) {
strg := memory.New(map[storage.Key]*storage.Value{
"list": storage.NewList([]string{"val1"}),
})

cmd := Rpop{strg: strg}
_ = cmd.Execute("list")

items, err := strg.All()
assert.NoError(t, err)

value, ok := items["list"]
assert.Nil(t, value)
assert.False(t, ok)
}

0 comments on commit 1004357

Please sign in to comment.