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