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 }