-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ae8e454
commit 3238315
Showing
6 changed files
with
360 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
t.Fatalf("Parser returned wrong length, expected=%d, got=%d", | ||
test.expectedLength, len(p.commands)) | ||
} | ||
|
||
if !reflect.DeepEqual(p.commands, test.expectedCommands) { | ||
t.Fatalf("Parser returned wrong commands, expected=%v, got=%v", | ||
test.expectedCommands, p.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" { | ||
t.Fatalf("ParseBulkString did not parse correctly, expected=%s, got=%s", | ||
"SET", p.commands[0]) | ||
} | ||
} | ||
|
||
|
||
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 { | ||
t.Fatalf("ParseArray returned wrong length, expected=%d, got=%d", | ||
3, len(p.commands)) | ||
} | ||
|
||
expected := []string{"SET", "mykey", "myvalue"} | ||
if !reflect.DeepEqual(p.commands, expected) { | ||
t.Fatalf("ParseArray returned wrong length, expected=%d, got=%d", | ||
3, len(p.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) | ||
} | ||
} |
Oops, something went wrong.