├── .gitignore ├── bench ├── nim.cfg ├── bench.nim ├── bench_new.nim ├── bench_old.nim └── oldredisparser.nim ├── tests ├── nim.cfg └── testrespparser.nim ├── redisparser.nimble ├── README.md ├── LICENSE └── src └── redisparser.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | -------------------------------------------------------------------------------- /bench/nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../src/" 2 | -------------------------------------------------------------------------------- /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../src/" 2 | -------------------------------------------------------------------------------- /bench/bench.nim: -------------------------------------------------------------------------------- 1 | import benchy, bench_old, bench_new 2 | 3 | timeIt "old parser": 4 | for i in 0 .. 1_000_000: 5 | testOld() 6 | 7 | timeIt "new parser": 8 | for i in 0 .. 1_000_000: 9 | testNew() -------------------------------------------------------------------------------- /redisparser.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.2.0" 4 | author = "xmonader" 5 | description = "RESP(REdis Serialization Protocol) Serialization for Nim" 6 | license = "Apache2" 7 | srcDir = "src" 8 | skipDirs = @["tests", "bench"] 9 | # Dependencies 10 | 11 | requires "nim >= 0.18.0" 12 | -------------------------------------------------------------------------------- /bench/bench_new.nim: -------------------------------------------------------------------------------- 1 | import redisparser, tables 2 | 3 | var testpairs = initOrderedTable[RedisValue, string]() 4 | testpairs[RedisValue(kind:vkStr, s:"Hello, World")] = "+Hello, World\r\n" 5 | testpairs[RedisValue(kind:vkInt, i:341)] = ":341\r\n" 6 | testpairs[RedisValue(kind:vkError, err:"Not found")] = "-Not found\r\n" 7 | testpairs[RedisValue(kind:vkArray, l: @[RedisValue(kind:vkStr, s:"Hello World"), RedisValue(kind:vkInt, i:23)])] = "*2\r\n+Hello World\r\n:23\r\n\r\n" 8 | testpairs[RedisValue(kind:vkBulkStr, bs:"Hello, World THIS IS REALLY NICE")] = "$32\r\nHello, World THIS IS REALLY NICE\r\n" 9 | 10 | proc testNew*() = 11 | for k, v in testpairs.pairs(): 12 | if encodeValue(k) != v: 13 | raise newException(ValueError, "parser error") 14 | if decodeString(v) != k: 15 | raise newException(ValueError, "parser error") -------------------------------------------------------------------------------- /bench/bench_old.nim: -------------------------------------------------------------------------------- 1 | import oldredisparser, tables 2 | 3 | var testpairs = initOrderedTable[RedisValue, string]() 4 | testpairs[RedisValue(kind:vkStr, s:"Hello, World")] = "+Hello, World\r\n" 5 | testpairs[RedisValue(kind:vkInt, i:341)] = ":341\r\n" 6 | testpairs[RedisValue(kind:vkError, err:"Not found")] = "-Not found\r\n" 7 | testpairs[RedisValue(kind:vkArray, l: @[RedisValue(kind:vkStr, s:"Hello World"), RedisValue(kind:vkInt, i:23)])] = "*2\r\n+Hello World\r\n:23\r\n\r\n" 8 | testpairs[RedisValue(kind:vkBulkStr, bs:"Hello, World THIS IS REALLY NICE")] = "$32\r\nHello, World THIS IS REALLY NICE\r\n" 9 | 10 | 11 | proc testOld*() = 12 | for k, v in testpairs.pairs(): 13 | if encodeValue(k) != v: 14 | raise newException(ValueError, "parser error") 15 | if decodeString(v) != k: 16 | raise newException(ValueError, "parser error") -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nim-resp 2 | 3 | RESP(REdis Serialization Protocol) Serialization for Nim 4 | 5 | ## How to use 6 | 7 | ```nim 8 | echo $encodeValue(RedisValue(kind:vkStr, s:"Hello, World")) 9 | # # +Hello, World 10 | echo $encodeValue(RedisValue(kind:vkInt, i:341)) 11 | # # :341 12 | echo $encodeValue(RedisValue(kind:vkError, err:"Not found")) 13 | # # -Not found 14 | echo $encodeValue(RedisValue(kind:vkArray, l: @[RedisValue(kind:vkStr, s:"Hello World"), RedisValue(kind:vkInt, i:23)] )) 15 | # #*2 16 | # #+Hello World 17 | # #:23 18 | 19 | echo $encodeValue(RedisValue(kind:vkBulkStr, bs:"Hello, World THIS IS REALLY NICE")) 20 | # #$32 21 | # # Hello, World THIS IS REALLY NICE 22 | echo decodeString("*3\r\n:1\r\n:2\r\n:3\r\n\r\n") 23 | # # @[1, 2, 3] 24 | echo decodeString("+Hello, World\r\n") 25 | # # Hello, World 26 | echo decodeString("-Not found\r\n") 27 | # # Not found 28 | echo decodeString(":1512\r\n") 29 | # # 1512 30 | echo $decodeString("$32\r\nHello, World THIS IS REALLY NICE\r\n") 31 | # Hello, World THIS IS REALLY NICE 32 | echo decodeString("*2\r\n+Hello World\r\n:23\r\n") 33 | # @[Hello World, 23] 34 | echo decodeString("*2\r\n*3\r\n:1\r\n:2\r\n:3\r\n\r\n*5\r\n:5\r\n:7\r\n+Hello Word\r\n-Err\r\n$6\r\nfoobar\r\n") 35 | # @[@[1, 2, 3], @[5, 7, Hello Word, Err, foobar]] 36 | echo $decodeString("*4\r\n:51231\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n") 37 | # @[51231, foo, , bar] 38 | ``` 39 | 40 | ## Roadmap 41 | 42 | - [X] Protocol serializer/deserializer 43 | - [X] Tests 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Ahmed T. Youssef 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /tests/testrespparser.nim: -------------------------------------------------------------------------------- 1 | import redisparser, tables 2 | 3 | 4 | var testpairs = initOrderedTable[RedisValue, string]() 5 | testpairs[newRedisString("Hello, World")] = "+Hello, World\r\n" 6 | testpairs[newRedisInt(341)] = ":341\r\n" 7 | testpairs[newRedisError("Not found")] = "-Not found\r\n" 8 | testpairs[newRedisArray(@[newRedisString("Hello World"), newRedisInt(23)])] = "*2\r\n+Hello World\r\n:23\r\n\r\n" 9 | testpairs[newRedisBulkString("Hello, World THIS IS REALLY NICE")] = "$32\r\nHello, World THIS IS REALLY NICE\r\n" 10 | 11 | 12 | # echo $encodeValue(RedisValue(kind:vkStr, s:"Hello, World")) 13 | # # # +Hello, World 14 | # echo $encodeValue(RedisValue(kind:vkInt, i:341)) 15 | # # # :341 16 | # echo $encodeValue(RedisValue(kind:vkError, err:"Not found")) 17 | # # # -Not found 18 | # echo $encodeValue(RedisValue(kind:vkArray, l: @[RedisValue(kind:vkStr, s:"Hello World"), RedisValue(kind:vkInt, i:23)] )) 19 | # # #*2 20 | # # #+Hello World 21 | # # #:23 22 | 23 | # echo $encodeValue(RedisValue(kind:vkBulkStr, bs:"Hello, World THIS IS REALLY NICE")) 24 | # # #$32 25 | # # # Hello, World THIS IS REALLY NICE 26 | 27 | # MORE TESTS 28 | # echo decodeString("*3\r\n:1\r\n:2\r\n:3\r\n\r\n") 29 | # # # @[1, 2, 3] 30 | # echo decodeString("+Hello, World\r\n") 31 | # # # Hello, World 32 | # echo decodeString("-Not found\r\n") 33 | # # # Not found 34 | # echo decodeString(":1512\r\n") 35 | # # # 1512 36 | # echo $decodeString("$32\r\nHello, World THIS IS REALLY NICE\r\n") 37 | # # Hello, World THIS IS REALLY NICE 38 | # echo decodeString("*2\r\n+Hello World\r\n:23\r\n") 39 | # # @[Hello World, 23] 40 | # echo decodeString("*2\r\n*3\r\n:1\r\n:2\r\n:3\r\n\r\n*5\r\n:5\r\n:7\r\n+Hello Word\r\n-Err\r\n$6\r\nfoobar\r\n") 41 | # # @[@[1, 2, 3], @[5, 7, Hello Word, Err, foobar]] 42 | # echo $decodeString("*4\r\n:51231\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n") 43 | # # @[51231, foo, , bar] 44 | for k, v in testpairs.pairs(): 45 | doAssert encodeValue(k) == v 46 | doAssert decodeString(v) == k 47 | 48 | echo decodeString("*14\r\n$6\r\nlength\r\n:1\r\n$15\r\nradix-tree-keys\r\n:1\r\n$16\r\nradix-tree-nodes\r\n:2\r\n$17\r\nlast-generated-id\r\n$15\r\n1631073404886-0\r\n$6\r\ngroups\r\n:0\r\n$11\r\nfirst-entry\r\n*2\r\n$15\r\n1631073404886-0\r\n*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$10\r\nlast-entry\r\n*2\r\n$15\r\n1631073404886-0\r\n*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n") 49 | echo decodeString("*2\r\n*2\r\n$15\r\n1631095633882-0\r\n*2\r\n$4\r\nitem\r\n$1\r\n0\r\n*2\r\n$15\r\n1631095635235-0\r\n*2\r\n$4\r\nitem\r\n$1\r\n0\r\n") 50 | echo decodeString("*2\r\n$-1\r\n\r\n*2\r\n$15\r\n1631096564062-1\r\n*2\r\n$4\r\nitem\r\n$1\r\n0") 51 | echo decodeString("*4\r\n:0\r\n$-1\r\n\r\n$-1\r\n\r\n*-1\r\n") -------------------------------------------------------------------------------- /bench/oldredisparser.nim: -------------------------------------------------------------------------------- 1 | import strformat, tables, json, strutils, sequtils, hashes, net, asyncdispatch, asyncnet, os, strutils, parseutils, deques, options 2 | 3 | const CRLF* = "\r\n" 4 | const REDISNIL = "\0\0" 5 | 6 | type 7 | ValueKind* = enum 8 | vkStr, vkError, vkInt, vkBulkStr, vkArray 9 | 10 | RedisValue* = ref object 11 | case kind*: ValueKind 12 | of vkStr: s*: string 13 | of vkError : err*: string 14 | of vkInt: i*: int 15 | of vkBulkStr: bs*: string 16 | of vkArray: l*: seq[RedisValue] 17 | 18 | proc `$`*(obj: RedisValue): string = 19 | case obj.kind 20 | of vkStr : return fmt"{$(obj.s)}" 21 | of vkBulkStr: return fmt"{$(obj.bs)}" 22 | of vkInt : return fmt"{$(obj.i)}" 23 | of vkArray: return fmt"{$(obj.l)}" 24 | of vkError: return fmt"{$(obj.err)}" 25 | 26 | proc hash*(obj: RedisValue): Hash = 27 | case obj.kind 28 | of vkStr : !$(hash(obj.s)) 29 | of vkBulkStr: !$(hash(obj.bs)) 30 | of vkInt : !$(hash(obj.i)) 31 | of vkArray: !$(hash(obj.l)) 32 | of vkError: !$(hash(obj.err)) 33 | 34 | proc `==`* (a, b: RedisValue): bool = 35 | ## Check two nodes for equality 36 | if a.isNil: 37 | if b.isNil: return true 38 | return false 39 | elif b.isNil or a.kind != b.kind: 40 | return false 41 | else: 42 | case a.kind 43 | of vkStr: 44 | result = a.s == b.s 45 | of vkBulkStr: 46 | result = a.bs == b.bs 47 | of vkInt: 48 | result = a.i == b.i 49 | of vkArray: 50 | result = a.l == b.l 51 | result = true 52 | of vkError: 53 | result = a.err == b.err 54 | 55 | proc encode*(v: RedisValue) : string 56 | proc encodeStr(v: RedisValue) : string = 57 | return fmt"+{v.s}{CRLF}" 58 | 59 | proc encodeBulkStr(v: RedisValue) : string = 60 | return fmt"${v.bs.len}{CRLF}{v.bs}{CRLF}" 61 | 62 | proc encodeErr(v: RedisValue) : string = 63 | return fmt"-{v.err}{CRLF}" 64 | 65 | proc encodeInt(v: RedisValue) : string = 66 | return fmt":{v.i}{CRLF}" 67 | 68 | proc encodeArray(v: RedisValue): string = 69 | var res = "*" & $len(v.l) & CRLF 70 | for el in v.l: 71 | res &= encode(el) 72 | res &= CRLF 73 | return res 74 | 75 | 76 | proc encode*(v: RedisValue) : string = 77 | case v.kind 78 | of vkStr: return encodeStr(v) 79 | of vkInt: return encodeInt(v) 80 | of vkError: return encodeErr(v) 81 | of vkBulkStr: return encodeBulkStr(v) 82 | of vkArray: return encodeArray(v) 83 | 84 | 85 | proc decodeStr(s: string): (RedisValue, int) = 86 | let crlfpos = s.find(CRLF) 87 | return (RedisValue(kind:vkStr, s:s[1..crlfpos-1]), crlfpos+len(CRLF)) 88 | 89 | proc decodeBulkStr(s:string): (RedisValue, int) = 90 | let crlfpos = s.find(CRLF) 91 | var bulklen = 0 92 | let slen = s[1..crlfpos-1] 93 | bulklen = parseInt(slen) 94 | var bulk: string 95 | if bulklen == -1: 96 | bulk = REDISNIL 97 | return (RedisValue(kind:vkBulkStr, bs:bulk), crlfpos+len(CRLF)) 98 | # elif bulklen == 0: 99 | # bulk = "" 100 | # return (RedisValue(kind:vkBulkStr, bs:bulk), crlfpos+len(CRLF)+len(CRLF)) 101 | else: 102 | let nextcrlf = s.find(CRLF, crlfpos+len(CRLF)) 103 | bulk = s[crlfpos+len(CRLF)..nextcrlf-1] 104 | return (RedisValue(kind:vkBulkStr, bs:bulk), nextcrlf+len(CRLF)) 105 | 106 | proc decodeError(s: string): (RedisValue, int) = 107 | let crlfpos = s.find(CRLF) 108 | return (RedisValue(kind:vkError, err:s[1..crlfpos-1]), crlfpos+len(CRLF)) 109 | 110 | proc decodeInt(s: string): (RedisValue, int) = 111 | var i: int 112 | let crlfpos = s.find(CRLF) 113 | let sInt = s[1..crlfpos-1] 114 | discard parseInt(sInt,i) 115 | return (RedisValue(kind:vkInt, i:i), crlfpos+len(CRLF)) 116 | 117 | 118 | proc decode(s: string): (RedisValue, int) 119 | 120 | proc decodeArray(s: string): (RedisValue, int) = 121 | var arr = newSeq[RedisValue]() 122 | var arrlen = 0 123 | var crlfpos = s.find(CRLF) 124 | var arrlenStr = s[1..crlfpos-1] 125 | discard parseInt(arrlenStr,arrlen) 126 | 127 | var nextobjpos = s.find(CRLF)+len(CRLF) 128 | var i = nextobjpos 129 | 130 | if arrlen == -1: 131 | 132 | return (RedisValue(kind:vkArray, l:arr), i) 133 | 134 | while i < len(s) and len(arr) < arrlen: 135 | var pair = decode(s[i..^1]) 136 | var obj = pair[0] 137 | arr.add(obj) 138 | i += pair[1] 139 | return (RedisValue(kind:vkArray, l:arr), i+len(CRLF)) 140 | 141 | 142 | proc decode(s: string): (RedisValue, int) = 143 | var i = 0 144 | while i < len(s): 145 | var curchar = $s[i] 146 | var endindex = s.find(CRLF, i)+len(CRLF) 147 | if endindex >= s.len: 148 | endindex = s.len-1 149 | if curchar == "+": 150 | var pair = decodeStr(s[i..endindex]) 151 | var obj = pair[0] 152 | var count = pair[1] 153 | i += count 154 | return (obj, i) 155 | elif curchar == "-": 156 | var pair = decodeError(s[i..endindex]) 157 | var obj = pair[0] 158 | var count = pair[1] 159 | i += count 160 | return (obj, i) 161 | elif curchar == "$": 162 | var pair = decodeBulkStr(s[i..^1]) 163 | var obj = pair[0] 164 | var count = pair[1] 165 | i += count 166 | return (obj, i) 167 | elif curchar == ":": 168 | var pair = decodeInt(s[i..endindex]) 169 | var obj = pair[0] 170 | var count = pair[1] 171 | i += count 172 | return (obj, i) 173 | elif curchar == "*": 174 | var pair = decodeArray(s[i..^1]) 175 | let obj = pair[0] 176 | let count = pair[1] 177 | i += count 178 | return (obj, i) 179 | elif curchar == "\r": 180 | i += 1 181 | continue 182 | elif curchar == "\n": 183 | i += 1 184 | continue 185 | else: 186 | echo fmt"Unreognized char {repr curchar}" 187 | break 188 | 189 | 190 | proc decodeResponse(resp: string): RedisValue = 191 | let pair = decode(resp) 192 | return pair[0] 193 | 194 | let decodeForm = decodeResponse 195 | let encodeValue* = encode 196 | let decodeString* = decodeResponse -------------------------------------------------------------------------------- /src/redisparser.nim: -------------------------------------------------------------------------------- 1 | import strformat, strutils, hashes, net, strutils 2 | 3 | const 4 | CRLF* = "\r\n" 5 | CRLF_LEN* = len(CRLF) 6 | 7 | type 8 | RespError* = object of IOError 9 | TypeError* = object of IOError 10 | 11 | ValueKind* = enum 12 | vkStr, vkError, vkInt, vkBulkStr, vkArray 13 | 14 | RedisValue* = ref object 15 | case kind: ValueKind 16 | of vkStr: s: string 17 | of vkError : err: string 18 | of vkInt: i: int 19 | of vkBulkStr: bs: string 20 | of vkArray: l: seq[RedisValue] 21 | 22 | proc `$`*(v: RedisValue): string = 23 | if v.isNil: 24 | raise newException(ValueError, "Redis value is nil") 25 | case v.kind 26 | of vkStr : return v.s 27 | of vkBulkStr: return v.bs 28 | of vkInt : return $v.i 29 | of vkArray: return $v.l 30 | of vkError: return v.err 31 | 32 | proc hash*(v: RedisValue): Hash = 33 | if v.isNil: 34 | raise newException(ValueError, "Redis value is nil") 35 | var h: Hash = 0 36 | h = h !& hash(v.kind) 37 | case v.kind 38 | of vkStr : h = h !& hash(v.s) 39 | of vkBulkStr: h = h !& hash(v.bs) 40 | of vkInt : h = h !& hash(v.i) 41 | of vkArray: h = h !& hash(v.l) 42 | of vkError: h = h !& hash(v.err) 43 | result = !$h 44 | 45 | proc `==`*(a, b: RedisValue): bool = 46 | ## Check two nodes for equality 47 | if a.isNil: 48 | if b.isNil: return true 49 | return false 50 | elif b.isNil or a.kind != b.kind: 51 | return false 52 | else: 53 | case a.kind 54 | of vkStr: 55 | return a.s == b.s 56 | of vkBulkStr: 57 | return a.bs == b.bs 58 | of vkInt: 59 | return a.i == b.i 60 | of vkArray: 61 | return a.l == b.l 62 | of vkError: 63 | return a.err == b.err 64 | 65 | proc len*(v: RedisValue): int = 66 | if v.isNil: 67 | raise newException(ValueError, "Redis value is nil") 68 | case v.kind 69 | of vkStr: 70 | result = v.s.len 71 | of vkError: 72 | result = v.err.len 73 | of vkBulkStr: 74 | result = v.bs.len 75 | of vkArray: 76 | result = v.l.len 77 | else: 78 | raise newException(TypeError, fmt"Invalid data type for `len`: {v.kind}") 79 | 80 | proc `[]`*(v: RedisValue, idx: int): RedisValue = 81 | if v.isNil: 82 | raise newException(ValueError, "Redis value is nil") 83 | if v.kind != vkArray: 84 | raise newException(TypeError, fmt"Array expected but got {v.kind}") 85 | result = v.l[idx] 86 | 87 | iterator items*(v: RedisValue): RedisValue = 88 | if v.kind != vkArray: 89 | raise newException(TypeError, fmt"Array expected but got {v.kind}") 90 | for i in 0..