1 module redis.redis; 2 3 import std.stdio; 4 import std.conv; 5 import std.traits; 6 import std.json; 7 import std.exception; 8 import std.variant; 9 import std.array; 10 import std..string; 11 import std.typetuple; 12 import std.exception; 13 14 public import redis; 15 import std.socket; 16 public class Redis 17 { 18 import std.socket : TcpSocket, InternetAddress; 19 20 private: 21 TcpSocket[string] conns; 22 string addr; 23 string password; 24 public: 25 26 mixin keyCommands; 27 mixin stringCommands; 28 mixin hashCommands; 29 mixin listCommands; 30 mixin setCommands; 31 mixin sortedsetCommands; 32 mixin serverCommands; 33 34 /** 35 * Create a new connection to the Redis server 36 */ 37 this(string host = "127.0.0.1", ushort port = 6379,string password = "") 38 { 39 auto conn = new TcpSocket(new InternetAddress(host, port)); 40 addr = host~":"~to!string(port); 41 conns[addr] = conn; 42 writeln(password); 43 writeln("AUTH "~password); 44 this.password = password; 45 if(password.length > 0){ 46 conn.send(toMultiBulk("AUTH", password)); 47 Response[] r = receiveResponses(conn, 1); 48 } 49 } 50 51 override string toString() 52 { 53 return "redis"; 54 } 55 56 auto opDispatch(string name,T...)(T args) 57 { 58 //try{ 59 auto result = send(name,args); 60 //} catch (exception e){ 61 //return e.msg; 62 //} 63 return result; 64 } 65 66 /** 67 * Call Redis using any type T that can be converted to a string 68 * 69 * Examples: 70 * 71 * --- 72 * send("SET name Adil") 73 * send("SADD", "myset", 1) 74 * send("SADD", "myset", 1.2) 75 * send("SADD", "myset", true) 76 * send("SADD", "myset", "Batman") 77 * send("SREM", "myset", ["$3", "$4"]) 78 * send("SADD", "myset", object) //provided 'object' implements toString() 79 * send("GET", "*") == send("GET *") 80 * send("ZADD", "my_unique_json", 1, json.toString()); 81 * send("EVAL", "return redis.call('set','lua','LUA')", 0); 82 * --- 83 */ 84 R send(R = Response, T...)(string key, T args) 85 { 86 //Implement a write queue here. 87 // All encoded responses are put into a write queue and flushed 88 // For a send request, flush the queue and listen to a response 89 // For async calls, just flush the queue 90 // This automatically gives us PubSub 91 92 SENDS: 93 debug(redis) { writeln(__FUNCTION__,"\t",__LINE__,escape(toMultiBulk(key, args)));} 94 conn.send(toMultiBulk(key, args)); 95 Response[] r = receiveResponses(conn, 1); 96 if(r.length && r[0].isMoved) 97 { 98 addr = ((split(r[0].toString," "))[2]); 99 goto SENDS; 100 } 101 return cast(R)(r[0]); 102 } 103 104 R send(R = Response)(string cmd) 105 { 106 SEND: 107 debug(redis) { writeln(escape(toMultiBulk(cmd)));} 108 conn.send(toMultiBulk(cmd)); 109 Response[] r = receiveResponses(conn, 1); 110 if(r.length && r[0].isMoved) 111 { 112 addr = ((split(r[0].toString," "))[2]); 113 goto SEND; 114 } 115 return cast(R)(r[0]); 116 } 117 118 /** 119 * Send a string that is already encoded in the Redis protocol 120 */ 121 R sendRaw(R = Response)(string cmd) 122 { 123 SEND: 124 125 debug(redis) { writeln(escape(cmd));} 126 127 conn.send(cmd); 128 Response[] r = receiveResponses(conn, 1); 129 if(r.length && r[0].isMoved) 130 { 131 addr = ((split(r[0].toString," "))[2]); 132 goto SEND; 133 } 134 return cast(R)(r[0]); 135 } 136 137 /** 138 * Send a series of commands as a pipeline 139 * 140 * Examples: 141 * 142 * --- 143 * pipeline(["SADD shopping_cart Shirt", "SADD shopping_cart Pant", "SADD shopping_cart Boots"]) 144 * --- 145 */ 146 import std.traits : isSomeChar; 147 Response[] pipeline(C)(C[][] commands) if (isSomeChar!C) 148 { 149 import std.array : appender; 150 151 auto app = appender!(C[])(); 152 foreach(c; commands) { 153 app ~= encode(c); 154 } 155 156 conn.send(app.data); 157 return receiveResponses(conn, commands.length); 158 } 159 160 /** 161 * Execute commands in a MULTI/EXEC block. 162 * 163 * @param all - (Default: false) - By default, only the results of a transaction are returned. If set to "true", the results of each queuing step is also returned. 164 * 165 * Examples: 166 * 167 * --- 168 * transaction(["SADD shopping_cart Shirt", "INCR shopping_cart_ctr"]) 169 * --- 170 */ 171 Response[] transaction(string[] commands, bool all = false) 172 { 173 auto cmd = ["MULTI"]; 174 cmd ~= commands; 175 cmd ~= "EXEC"; 176 auto rez = pipeline(cmd); 177 178 if(all) { 179 return rez; 180 } 181 182 auto resp = rez[$ - 1]; 183 if(resp.isError()) { 184 throw new RedisResponseException(resp.value); 185 } 186 187 return resp.values; 188 } 189 190 /** 191 * Simplified call to EVAL 192 * 193 * Examples: 194 * 195 * --- 196 * Response r = eval("return redis.call('set','lua','LUA_AGAIN')"); 197 * r.value == "LUA_AGAIN"; 198 * 199 * Response r1 = redis.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", ["key1", "key2"], ["first", "second"]); 200 * writeln(r1); // [key1, key2, first, second] 201 * 202 * Response r1 = redis.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", [1, 2]); 203 * writeln(r1); // [1, 2] 204 * --- 205 */ 206 Response eval(K = string, A = string)(string lua_script, K[] keys = [], A[] args = []) 207 { 208 conn.send(toMultiBulk("EVAL", lua_script, keys.length, keys, args)); 209 Response[] r = receiveResponses(conn, 1); 210 return (r[0]); 211 } 212 213 Response evalSha(K = string, A = string)(string sha1, K[] keys = [], A[] args = []) 214 { 215 conn.send(toMultiBulk("EVALSHA", sha1, keys.length, keys, args)); 216 Response[] r = receiveResponses(conn, 1); 217 return (r[0]); 218 } 219 220 TcpSocket conn() 221 { 222 if(addr !in conns){ 223 auto arr = split(addr,":"); 224 conns[addr] = new TcpSocket(new InternetAddress(arr[0], arr[1].to!ushort)); 225 if(password.length > 0){ 226 conns[addr].send(toMultiBulk("AUTH", password)); 227 Response[] r = receiveResponses(conns[addr], 1); 228 } 229 } 230 231 int error; 232 conns[addr].getOption(SocketOptionLevel.SOCKET, SocketOption.ERROR, error); 233 if (error != 0) 234 { 235 conns[addr].close(); 236 auto arr = split(addr,":"); 237 conns[addr] = new TcpSocket(new InternetAddress(arr[0], arr[1].to!ushort)); 238 if(password.length > 0){ 239 conns[addr].send(toMultiBulk("AUTH", password)); 240 Response[] r = receiveResponses(conns[addr], 1); 241 } 242 throw new ConnectionException("Error while sending request, error " ~ to!string(error)); 243 244 } 245 return conns[addr]; 246 } 247 248 } 249 250 251 252 unittest 253 { 254 auto redis = new Redis(); 255 //debug(redis) { writeln("\n\n\nredis commands test.....");} 256 //assert(redis.ping() == "PONG"); 257 redis.flushall(); 258 assert(redis.set("xxkey","10") == true); 259 assert(redis.get("xxkey") == "10"); 260 assert(redis.exists("xxkey") == true); 261 //assert(redis.dump("xxkey") == "\u0000\xC0\n\b\u0000ײ\xBB\xFA\xA7\xB7\xE9\x83"); 262 assert(redis.del("xxkey") == 1); 263 assert(redis.set("xxkey","10") == true); 264 assert(redis.expire("xxkey",20) == 1); 265 assert(redis.expire("yykey",20) == 0); 266 assert(redis.expireat("xxkey",1798736461) == 1); 267 assert(redis.expireat("yykey",1798736461) == 0); 268 redis.set("ssoxx","1"); 269 redis.set("bboxx","2"); 270 redis.set("ssmxx","3"); 271 assert(redis.keys("*m*") == ["ssmxx"]); 272 redis.set("abc","test",10); 273 assert(redis.ttl("abc") == 10); 274 redis.set("incrkey","10"); 275 assert(redis.incr("incrkey") == 11); 276 277 assert(redis.hset("website","google","google.com") == 1); 278 assert(redis.hset("website","baidu","baidu.com") == 1); 279 assert(redis.hset("website","putao","putao.com") == 1); 280 assert(redis.hset("website","google","google.com") == 0); 281 assert(redis.hlen("website") == 3); 282 assert(redis.hget("website","google") == "google.com"); 283 assert(redis.hdel("website","google") == 1); 284 assert(redis.hlen("website") == 2); 285 assert(redis.hdel("website","baidu","putao") == 2); 286 assert(redis.hlen("website") == 0); 287 288 assert(redis.lpush("language","c") == 1); 289 assert(redis.lpush("language","php") == 2); 290 assert(redis.llen("language") == 2); 291 assert(redis.lset("language",0,"d") == true); 292 assert(redis.lpop("language") == "d"); 293 assert(redis.llen("language") == 1); 294 assert(redis.lrange("language",0,1) == ["c"]); 295 296 assert(redis.sadd("bbs","baidu.com","google.com") == 2); 297 assert(redis.sadd("bbs","douban.com") == 1); 298 assert(redis.scard("bbs") == 3); 299 assert(redis.sadd("net","douban.com") == 1); 300 assert(redis.spop("net") == "douban.com"); 301 302 debug(redis){writeln("redis commands test end\n\n\n");} 303 304 auto response = redis.send("LASTSAVE"); 305 assert(response.type == ResponseType.Integer); 306 307 assert(redis.send!(bool)("SET", "name", "adil baig")); 308 309 redis.send("SET emptystring ''"); 310 response = redis.send("GET emptystring"); 311 assert(response.value == ""); 312 313 response = redis.send("GET name"); 314 assert(response.type == ResponseType.Bulk); 315 assert(response.value == "adil baig"); 316 317 /* START Test casting byte[] */ 318 assert(cast(byte[])response == "adil baig"); //Test casting to byte[] 319 assert(cast(byte[])response == [97, 100, 105, 108, 32, 98, 97, 105, 103]); 320 321 redis.send("SET mykey 10"); 322 response = redis.send("INCR mykey"); 323 assert(response.type == ResponseType.Integer); 324 assert(response.intval == 11); 325 auto bytes = (cast(ubyte[])response); 326 assert(bytes.length == response.intval.sizeof); 327 assert(bytes[0] == 11); 328 /* END Test casting byte[] */ 329 330 assert(redis.send!(string)("GET name") == "adil baig"); 331 332 response = redis.send("GET nonexistentkey"); 333 assert(response.type == ResponseType.Nil); 334 assert(cast(ubyte[])response == []); 335 336 redis.send("DEL myset"); 337 redis.send("SADD", "myset", 1.2); 338 redis.send("SADD", "myset", 1); 339 redis.send("SADD", "myset", true); 340 redis.send("SADD", "myset", "adil"); 341 redis.send("SADD", "myset", 350001939); 342 redis.send("SADD", ["myset","$4"]); 343 auto r = redis.send("SMEMBERS myset"); 344 assert(r.type == ResponseType.MultiBulk); 345 assert(r.values.length == 6); 346 347 //Check pipeline 348 redis.send("DEL ctr"); 349 auto responses = redis.pipeline(["SET ctr 1", "INCR ctr", "INCR ctr", "INCR ctr", "INCR ctr"]); 350 351 assert(responses.length == 5); 352 assert(responses[0].type == ResponseType.Status); 353 assert(responses[1].intval == 2); 354 assert(responses[2].intval == 3); 355 assert(responses[3].intval == 4); 356 assert(responses[4].intval == 5); 357 358 redis.send("DEL buddies"); 359 auto buddiesQ = ["SADD buddies Batman", "SADD buddies Spiderman", "SADD buddies Hulk", "SMEMBERS buddies"]; 360 Response[] buddies = redis.pipeline(buddiesQ); 361 assert(buddies.length == buddiesQ.length); 362 assert(buddies[0].type == ResponseType.Integer); 363 assert(buddies[1].type == ResponseType.Integer); 364 assert(buddies[2].type == ResponseType.Integer); 365 assert(buddies[3].type == ResponseType.MultiBulk); 366 assert(buddies[3].values.length == 3); 367 368 //Check transaction 369 redis.send("DEL ctr"); 370 responses = redis.transaction(["SET ctr 1", "INCR ctr", "INCR ctr"], true); 371 assert(responses.length == 5); 372 assert(responses[0].type == ResponseType.Status); 373 assert(responses[1].type == ResponseType.Status); 374 assert(responses[2].type == ResponseType.Status); 375 assert(responses[3].type == ResponseType.Status); 376 assert(responses[4].type == ResponseType.MultiBulk); 377 assert(responses[4].values[0].type == ResponseType.Status); 378 assert(responses[4].values[1].intval == 2); 379 assert(responses[4].values[2].intval == 3); 380 381 redis.send("DEL ctr"); 382 responses = redis.transaction(["SET ctr 1", "INCR ctr", "INCR ctr"]); 383 assert(responses.length == 3); 384 assert(responses[0].type == ResponseType.Status); 385 assert(responses[1].intval == 2); 386 assert(responses[2].intval == 3); 387 388 response = redis.send("EVAL", "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", 2, "key1", "key2", "first", "second"); 389 assert(response.values.length == 4); 390 assert(response.values[0].value == "key1"); 391 assert(response.values[1].value == "key2"); 392 assert(response.values[2].value == "first"); 393 assert(response.values[3].value == "second"); 394 395 //Same as above, but simpler 396 response = redis.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", ["key1", "key2"], ["first", "second"]); 397 assert(response.values.length == 4); 398 assert(response.values[0].value == "key1"); 399 assert(response.values[1].value == "key2"); 400 assert(response.values[2].value == "first"); 401 assert(response.values[3].value == "second"); 402 403 response = redis.eval("return redis.call('set','lua','LUA_AGAIN')"); 404 assert(cast(string)redis.send("GET lua") == "LUA_AGAIN"); 405 406 // A BLPOP times out to a Nil multibulk 407 response = redis.send("BLPOP nonExistentList 1"); 408 assert(response.isNil()); 409 }