Skip to content

Commit

Permalink
add resp protocol parser
Browse files Browse the repository at this point in the history
  • Loading branch information
UsamaHameed committed Jan 14, 2024
1 parent ae8e454 commit 3238315
Show file tree
Hide file tree
Showing 6 changed files with 360 additions and 27 deletions.
39 changes: 16 additions & 23 deletions commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package commands

import (
"fmt"
"strings"

"github.com/UsamaHameed/tiny-redis/parser"
"github.com/UsamaHameed/tiny-redis/storage"
)

Expand All @@ -17,32 +17,25 @@ type ParseCommandResponse struct {
Errors []string
}

func ParseCommand(input string) *ParseCommandResponse {
chunks := strings.Split(input, " ")
comm := chunks[0]
func ParseCommand(input string) []string {
//chunks := strings.Split(input, " ")
//comm := chunks[0]

if comm == "PING" {
return &ParseCommandResponse{ Response: Ping(), Success: true}
} else if comm == "ECHO" {
return &ParseCommandResponse{ Response: Echo(chunks[1]), Success: true}
} else if comm == "SET" {
return &ParseCommandResponse{
Response: "", Success: Set(chunks[1], chunks[2]),
}
} else {
return &ParseCommandResponse{
Success: false, Response: "", Errors: []string{
fmt.Sprintf("unknown command %s", comm),
},
}
}
p := parser.New(input)
p.ParseRespString()
fmt.Println("commands", p.Commands)

return p.Commands
}

type CommandType string
const (
PING = iota
ECHO
SET
GET
PING CommandType = "PING"
ECHO CommandType = "ECHO"
SET CommandType = "SET"
GET CommandType = "GET"
EXISTS CommandType = "EXISTS"
OK CommandType = "OK"
)

func Ping() string {
Expand Down
167 changes: 167 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
@@ -1 +1,168 @@
package parser

import (
"errors"
"fmt"
"strconv"
"strings"

"github.com/UsamaHameed/tiny-redis/utils"
)

const DELIMITER = "\\r\\n"

type Parser struct {
currPos int
input string
Commands []string
}

func New(str string) *Parser {
p := Parser{ currPos: 0, input: str }
return &p
}

func (p *Parser) advancePointer() {
str := p.input[p.currPos:]
index := strings.Index(str, DELIMITER)

if index != -1 {
p.currPos = index + p.currPos + len(DELIMITER)
} else {
p.currPos = len(p.input)
}
}

func (p *Parser) incrementPointer() {
p.currPos += 1
}

func (p *Parser) appendParsedString(str string) {
p.Commands = append(p.Commands, str)
}

func (p *Parser) trimCommas() {
p.input = p.input[1:len(p.input) - 1]
}

func (p *Parser) ParseRespString() error {
fmt.Println("input to ParseRespString", p.input)
if len(p.input) > 0 {
//p.trimCommas()
respType := p.input[p.currPos]
str := p.input[1:]

switch respType {
case '+':
return p.parseSimpleString()
case '$':
return p.ParseBulkString()
case ':':
return p.ParseInt()
case '*':
return p.ParseArray()
case '-':
return p.parseError(str)
}

return errors.New(
fmt.Sprintf("unsupported redis command type: %s", string(respType)),
)
}
return errors.New(fmt.Sprintf("zero length"))
}

// simple strings can only be ok, ping and pong
func (p *Parser) parseSimpleString() error {
p.incrementPointer() // skip the type
start := p.currPos
end := strings.Index(p.input[start:], DELIMITER)
str := strings.ToLower(p.input[p.currPos:end + 1])

if str == "ping" {
p.appendParsedString("PING")
return nil
} else if str == "echo" {
p.appendParsedString("ECHO")
return nil
} else if str == "ok" {
p.appendParsedString("OK")
return nil
}

return errors.New(fmt.Sprintf("unsupported simple string: %s", p.input[start:]))
}

func (p *Parser) ParseSize() int {
current := p.input[p.currPos:]
start := 0
end := strings.Index(current, DELIMITER)
fmt.Println("current", current, "start", start, "end", end)
input := current[start:end]
size, err := utils.ParseByteToInt([]rune(input))

if err != nil {
e := errors.New(fmt.Sprint("unable to find size", input))
joinedErr := errors.Join(e, err)
panic(joinedErr)
}
return size
}

func (p *Parser) ParseBulkString() error {
p.incrementPointer() // skip the type
input := p.input[p.currPos:]
size := p.ParseSize()
index := strings.Index(input, DELIMITER)

start := index + len(DELIMITER)
end := start + size

comm := input[start:end]
if size != len(input[start:end]) {
panic(fmt.Sprintf("invalid bulk string=%s", input))
}

fmt.Println("parsing bulk string", comm)

p.advancePointer()
p.appendParsedString(comm)

return nil // no error
}

func (p *Parser) ParseInt() error {
start := 1 // skip the type
end := strings.Index(p.input[1:], DELIMITER)

input := p.input[start:end + 1]
i, e := strconv.ParseInt(input, 10, 64)

if e != nil {
err := errors.New(fmt.Sprintf("unable to parse %s to int", input))
joinedErr := errors.Join(e, err)
return joinedErr
}

p.appendParsedString(fmt.Sprint(i))
return nil
}

func (p *Parser) ParseArray() error {
//fmt.Println("parsing array", p.input)
p.incrementPointer() // skip the type
size := p.ParseSize()

// offset size
p.advancePointer()
for i := 0; i < int(size); i++ {
p.ParseRespString()
p.advancePointer()
}

return nil
}

func (p *Parser) parseError(input string) error {
return errors.New(fmt.Sprint("method not implemented", input))
}
158 changes: 158 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
@@ -1 +1,159 @@
package parser

import (
"reflect"
"testing"
)

func TestParser(t *testing.T) {
tests := []struct{
input string
error bool
expectedLength int
expectedCommands []string
}{
{
input: "*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n",
expectedLength: 3, expectedCommands: []string{"SET", "mykey", "myvalue"},
},
{
input: "*2\r\n$3\r\nGET\r\n$5\r\nmykey\r\n",
expectedLength: 2, expectedCommands: []string{"GET", "mykey"},
},
{
input: "*1\r\n$4\r\nping\r\n",
expectedLength: 1, expectedCommands: []string{"ping"},
},
{
input: "*2\r\n$4\r\necho\r\n$11\r\nhello world\r\n",
expectedLength: 2, expectedCommands: []string{"echo", "hello world"},
},
{
input: "+OK\r\n",
expectedLength: 1, expectedCommands: []string{"OK"},
},
{
input: "$0\r\n\r\n",
expectedLength: 1, expectedCommands: []string{""},
},
//{
// input: "+hello world\r\n",
// error: true,
// expectedLength: 0, expectedCommands: []string{""},
//},
}

for _, test := range tests {
p := New(test.input)
err := p.ParseRespString()

if err != nil {
t.Fatalf("ParseRespString returned an error, %s", err)
}

if len(p.commands) != test.expectedLength {

Check failure on line 54 in parser/parser_test.go

View workflow job for this annotation

GitHub Actions / build

p.commands undefined (type *Parser has no field or method commands, but does have Commands)
t.Fatalf("Parser returned wrong length, expected=%d, got=%d",
test.expectedLength, len(p.commands))

Check failure on line 56 in parser/parser_test.go

View workflow job for this annotation

GitHub Actions / build

p.commands undefined (type *Parser has no field or method commands, but does have Commands)
}

if !reflect.DeepEqual(p.commands, test.expectedCommands) {

Check failure on line 59 in parser/parser_test.go

View workflow job for this annotation

GitHub Actions / build

p.commands undefined (type *Parser has no field or method commands, but does have Commands)
t.Fatalf("Parser returned wrong commands, expected=%v, got=%v",
test.expectedCommands, p.commands)

Check failure on line 61 in parser/parser_test.go

View workflow job for this annotation

GitHub Actions / build

p.commands undefined (type *Parser has no field or method commands, but does have Commands)
}
}
}

func TestAdvancePointer(t *testing.T) {
input := "*3\r\n$3\r\nSET\r\n"
p := New(input)

if p.currPos != 0 {
t.Fatalf("incorrect initial currPos, expected=%d, got=%d", 0, p.currPos)
}

p.advancePointer()
if p.currPos != 4 {
t.Fatalf("incorrect currPos, expected=%d, got=%d", 4, p.currPos)
}

p.advancePointer()
if p.currPos != 8 {
t.Fatalf("incorrect currPos, expected=%d, got=%d", 8, p.currPos)
}
}

func TestParseSimpleString(t *testing.T) {
input := "+PING\r\n"

p := New(input)
err := p.parseSimpleString()

if err != nil { // command.PING is 0
t.Fatalf("ParseSimpleString returned an error: %v", err)
}
}

func TestParseBulkString(t *testing.T) {
input := "$3\r\nSETxx"

p := New(input)
err := p.ParseBulkString()

if err != nil {
t.Fatalf("ParseBulkString returned an error, %s", err)
}
if p.commands[0] != "SET" {

Check failure on line 105 in parser/parser_test.go

View workflow job for this annotation

GitHub Actions / build

p.commands undefined (type *Parser has no field or method commands, but does have Commands)
t.Fatalf("ParseBulkString did not parse correctly, expected=%s, got=%s",
"SET", p.commands[0])

Check failure on line 107 in parser/parser_test.go

View workflow job for this annotation

GitHub Actions / build

p.commands undefined (type *Parser has no field or method commands, but does have Commands)
}
}


func TestParseArray(t *testing.T) {
input := "*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"

p := New(input)
err := p.ParseArray()

if err != nil {
t.Fatalf("ParseArray returned an error, %s", err)
}

if len(p.commands) != 3 {

Check failure on line 122 in parser/parser_test.go

View workflow job for this annotation

GitHub Actions / build

p.commands undefined (type *Parser has no field or method commands, but does have Commands)
t.Fatalf("ParseArray returned wrong length, expected=%d, got=%d",
3, len(p.commands))

Check failure on line 124 in parser/parser_test.go

View workflow job for this annotation

GitHub Actions / build

p.commands undefined (type *Parser has no field or method commands, but does have Commands)
}

expected := []string{"SET", "mykey", "myvalue"}
if !reflect.DeepEqual(p.commands, expected) {

Check failure on line 128 in parser/parser_test.go

View workflow job for this annotation

GitHub Actions / build

p.commands undefined (type *Parser has no field or method commands, but does have Commands)
t.Fatalf("ParseArray returned wrong length, expected=%d, got=%d",
3, len(p.commands))

Check failure on line 130 in parser/parser_test.go

View workflow job for this annotation

GitHub Actions / build

p.commands undefined (type *Parser has no field or method commands, but does have Commands)
}
}

func TestParseInt(t *testing.T) {
input := ":10\r\n"
p := New(input)
err := p.ParseInt()

if err != nil {
t.Fatalf("ParseInt returned an error, %s", err)
}

if p.commands[0] != "10" {
t.Fatalf("ParseInt returned wrong int, expected=%s, got=%s",
"10", p.commands[0])
}
}

func TestParseSize(t *testing.T) {
input := "*10\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
p := New(input)
p.currPos = 1
res := p.ParseSize()

if res != 10 {
t.Fatalf("ParseSize returned wrong int, expected=%d, got=%d",
10, res)
}
}
Loading

0 comments on commit 3238315

Please sign in to comment.