1 module tinyredis.redis; 2 3 /** 4 * Authors: Adil Baig, adil.baig@aidezigns.com 5 */ 6 7 private: 8 import std.array : appender; 9 import std.socket : TcpSocket, InternetAddress; 10 import std.traits; 11 12 public : 13 import tinyredis.connection; 14 import tinyredis.encoder; 15 import tinyredis.response; 16 import tinyredis.parser : RedisResponseException; 17 18 class Redis 19 { 20 private: 21 TcpSocket conn; 22 23 public: 24 25 /** 26 * Create a new connection to the Redis server 27 */ 28 this(string host = "127.0.0.1", ushort port = 6379) 29 { 30 conn = new TcpSocket(new InternetAddress(host, port)); 31 } 32 33 /** 34 * Call Redis using any type T that can be converted to a string 35 * 36 * Examples: 37 * 38 * --- 39 * send("SET name Adil") 40 * send("SADD", "myset", 1) 41 * send("SADD", "myset", 1.2) 42 * send("SADD", "myset", true) 43 * send("SADD", "myset", "Batman") 44 * send("SREM", "myset", ["$3", "$4"]) 45 * send("SADD", "myset", object) //provided 'object' implements toString() 46 * send("GET", "*") == send("GET *") 47 * send("ZADD", "my_unique_json", 1, json.toString()); 48 * send("EVAL", "return redis.call('set','lua','LUA')", 0); 49 * --- 50 */ 51 R send(R = Response, T...)(string key, T args) 52 { 53 //Implement a write queue here. 54 // All encoded responses are put into a write queue and flushed 55 // For a send request, flush the queue and listen to a response 56 // For async calls, just flush the queue 57 // This automatically gives us PubSub 58 59 debug(tinyredis) { writeln(escape(toMultiBulk(key, args)));} 60 61 conn.send(toMultiBulk(key, args)); 62 Response[] r = receiveResponses(conn, 1); 63 return cast(R)(r[0]); 64 } 65 66 R send(R = Response)(string cmd) 67 { 68 debug(tinyredis) { writeln(escape(toMultiBulk(cmd)));} 69 70 conn.send(toMultiBulk(cmd)); 71 Response[] r = receiveResponses(conn, 1); 72 return cast(R)(r[0]); 73 } 74 75 /** 76 * Send a string that is already encoded in the Redis protocol 77 */ 78 R sendRaw(R = Response)(string cmd) 79 { 80 debug(tinyredis) { writeln(escape(cmd));} 81 82 conn.send(cmd); 83 Response[] r = receiveResponses(conn, 1); 84 return cast(R)(r[0]); 85 } 86 87 /** 88 * Send a series of commands as a pipeline 89 * 90 * Examples: 91 * 92 * --- 93 * pipeline(["SADD shopping_cart Shirt", "SADD shopping_cart Pant", "SADD shopping_cart Boots"]) 94 * --- 95 */ 96 Response[] pipeline(C)(C[][] commands) if (isSomeChar!C) 97 { 98 auto appender = appender!(C[])(); 99 foreach(c; commands) { 100 appender ~= encode(c); 101 } 102 103 conn.send(appender.data); 104 return receiveResponses(conn, commands.length); 105 } 106 107 /** 108 * Execute commands in a MULTI/EXEC block. 109 * 110 * @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. 111 * 112 * Examples: 113 * 114 * --- 115 * transaction(["SADD shopping_cart Shirt", "INCR shopping_cart_ctr"]) 116 * --- 117 */ 118 Response[] transaction(string[] commands, bool all = false) 119 { 120 auto cmd = ["MULTI"]; 121 cmd ~= commands; 122 cmd ~= "EXEC"; 123 auto rez = pipeline(cmd); 124 125 if(all) { 126 return rez; 127 } 128 129 auto resp = rez[$ - 1]; 130 if(resp.isError()) { 131 throw new RedisResponseException(resp.value); 132 } 133 134 return resp.values; 135 } 136 137 /** 138 * Simplified call to EVAL 139 * 140 * Examples: 141 * 142 * --- 143 * Response r = eval("return redis.call('set','lua','LUA_AGAIN')"); 144 * r.value == "LUA_AGAIN"; 145 * 146 * Response r1 = redis.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", ["key1", "key2"], ["first", "second"]); 147 * writeln(r1); // [key1, key2, first, second] 148 * 149 * Response r1 = redis.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", [1, 2]); 150 * writeln(r1); // [1, 2] 151 * --- 152 */ 153 Response eval(K = string, A = string)(string lua_script, K[] keys = [], A[] args = []) 154 { 155 conn.send(toMultiBulk("EVAL", lua_script, keys.length, keys, args)); 156 Response[] r = receiveResponses(conn, 1); 157 return (r[0]); 158 } 159 160 Response evalSha(K = string, A = string)(string sha1, K[] keys = [], A[] args = []) 161 { 162 conn.send(toMultiBulk("EVALSHA", sha1, keys.length, keys, args)); 163 Response[] r = receiveResponses(conn, 1); 164 return (r[0]); 165 } 166 167 } 168 169 170 171 unittest 172 { 173 auto redis = new Redis(); 174 auto response = redis.send("LASTSAVE"); 175 assert(response.type == ResponseType.Integer); 176 177 assert(redis.send!(bool)("SET", "name", "adil baig")); 178 179 redis.send("SET emptystring ''"); 180 response = redis.send("GET emptystring"); 181 assert(response.value == ""); 182 183 response = redis.send("GET name"); 184 assert(response.type == ResponseType.Bulk); 185 assert(response.value == "adil baig"); 186 187 /* START Test casting byte[] */ 188 assert(cast(byte[])response == "adil baig"); //Test casting to byte[] 189 assert(cast(byte[])response == [97, 100, 105, 108, 32, 98, 97, 105, 103]); 190 191 redis.send("SET mykey 10"); 192 response = redis.send("INCR mykey"); 193 assert(response.type == ResponseType.Integer); 194 assert(response.intval == 11); 195 auto bytes = (cast(ubyte[])response); 196 assert(bytes.length == response.intval.sizeof); 197 assert(bytes[0] == 11); 198 /* END Test casting byte[] */ 199 200 assert(redis.send!(string)("GET name") == "adil baig"); 201 202 response = redis.send("GET nonexistentkey"); 203 assert(response.type == ResponseType.Nil); 204 assert(cast(ubyte[])response == []); 205 206 redis.send("DEL myset"); 207 redis.send("SADD", "myset", 1.2); 208 redis.send("SADD", "myset", 1); 209 redis.send("SADD", "myset", true); 210 redis.send("SADD", "myset", "adil"); 211 redis.send("SADD", "myset", 350001939); 212 redis.send("SADD", ["myset","$4"]); 213 auto r = redis.send("SMEMBERS myset"); 214 assert(r.type == ResponseType.MultiBulk); 215 assert(r.values.length == 6); 216 217 //Check pipeline 218 redis.send("DEL ctr"); 219 auto responses = redis.pipeline(["SET ctr 1", "INCR ctr", "INCR ctr", "INCR ctr", "INCR ctr"]); 220 221 assert(responses.length == 5); 222 assert(responses[0].type == ResponseType.Status); 223 assert(responses[1].intval == 2); 224 assert(responses[2].intval == 3); 225 assert(responses[3].intval == 4); 226 assert(responses[4].intval == 5); 227 228 redis.send("DEL buddies"); 229 auto buddiesQ = ["SADD buddies Batman", "SADD buddies Spiderman", "SADD buddies Hulk", "SMEMBERS buddies"]; 230 Response[] buddies = redis.pipeline(buddiesQ); 231 assert(buddies.length == buddiesQ.length); 232 assert(buddies[0].type == ResponseType.Integer); 233 assert(buddies[1].type == ResponseType.Integer); 234 assert(buddies[2].type == ResponseType.Integer); 235 assert(buddies[3].type == ResponseType.MultiBulk); 236 assert(buddies[3].values.length == 3); 237 238 //Check transaction 239 redis.send("DEL ctr"); 240 responses = redis.transaction(["SET ctr 1", "INCR ctr", "INCR ctr"], true); 241 assert(responses.length == 5); 242 assert(responses[0].type == ResponseType.Status); 243 assert(responses[1].type == ResponseType.Status); 244 assert(responses[2].type == ResponseType.Status); 245 assert(responses[3].type == ResponseType.Status); 246 assert(responses[4].type == ResponseType.MultiBulk); 247 assert(responses[4].values[0].type == ResponseType.Status); 248 assert(responses[4].values[1].intval == 2); 249 assert(responses[4].values[2].intval == 3); 250 251 redis.send("DEL ctr"); 252 responses = redis.transaction(["SET ctr 1", "INCR ctr", "INCR ctr"]); 253 assert(responses.length == 3); 254 assert(responses[0].type == ResponseType.Status); 255 assert(responses[1].intval == 2); 256 assert(responses[2].intval == 3); 257 258 response = redis.send("EVAL", "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", 2, "key1", "key2", "first", "second"); 259 assert(response.values.length == 4); 260 assert(response.values[0].value == "key1"); 261 assert(response.values[1].value == "key2"); 262 assert(response.values[2].value == "first"); 263 assert(response.values[3].value == "second"); 264 265 //Same as above, but simpler 266 response = redis.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", ["key1", "key2"], ["first", "second"]); 267 assert(response.values.length == 4); 268 assert(response.values[0].value == "key1"); 269 assert(response.values[1].value == "key2"); 270 assert(response.values[2].value == "first"); 271 assert(response.values[3].value == "second"); 272 273 response = redis.eval("return redis.call('set','lua','LUA_AGAIN')"); 274 assert(cast(string)redis.send("GET lua") == "LUA_AGAIN"); 275 276 // A BLPOP times out to a Nil multibulk 277 response = redis.send("BLPOP nonExistentList 1"); 278 assert(response.isNil()); 279 }