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 }