1 /**
2 	Convenience wrappers types for accessing Redis keys.
3 
4 	Note that the API is still subject to change!
5 
6 	Copyright: © 2014 Sönke Ludwig
7 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
8 	Authors: Sönke Ludwig
9 */
10 module vibe.db.redis.types;
11 
12 import vibe.db.redis.redis;
13 
14 import std.conv : to;
15 import std.datetime : SysTime;
16 import std.typecons : apply, Nullable;
17 import core.time : Duration, msecs, seconds;
18 
19 
20 /** Returns a handle to a string type value.
21 */
22 RedisString!T getAsString(T = string)(RedisDatabase db, string key)
23 {
24 	return RedisString!T(db, key);
25 }
26 
27 ///
28 unittest {
29 	void test()
30 	{
31 		auto db = connectRedis("127.0.0.1").getDatabase(0);
32 		auto str = db.getAsString("some_string");
33 		str = "test";
34 	}
35 }
36 
37 
38 
39 /** Returns a handle to a set type value.
40 */
41 RedisSet!T getAsSet(T = string)(RedisDatabase db, string key)
42 {
43 	return RedisSet!T(db, key);
44 }
45 
46 ///
47 unittest {
48 	void test()
49 	{
50 		auto db = connectRedis("127.0.0.1").getDatabase(0);
51 		auto set = db.getAsSet("some_set");
52 		set.insert("test");
53 	}
54 }
55 
56 
57 /** Returns a handle to a set type value.
58 */
59 RedisZSet!T getAsZSet(T = string)(RedisDatabase db, string key)
60 {
61 	return RedisZSet!T(db, key);
62 }
63 
64 ///
65 unittest {
66 	void test()
67 	{
68 		auto db = connectRedis("127.0.0.1").getDatabase(0);
69 		auto set = db.getAsZSet("some_sorted_set");
70 		set.insert(1, "test");
71 	}
72 }
73 
74 
75 /** Returns a handle to a hash type value.
76 */
77 RedisHash!T getAsHash(T = string)(RedisDatabase db, string key)
78 {
79 	return RedisHash!T(db, key);
80 }
81 
82 ///
83 unittest {
84 	void test()
85 	{
86 		auto db = connectRedis("127.0.0.1").getDatabase(0);
87 		auto hash = db.getAsHash("some_hash");
88 		hash["test"] = "123";
89 	}
90 }
91 
92 
93 /** Returns a handle to a list type value.
94 */
95 RedisList!T getAsList(T = string)(RedisDatabase db, string key)
96 {
97 	return RedisList!T(db, key);
98 }
99 
100 ///
101 unittest {
102 	void test()
103 	{
104 		auto db = connectRedis("127.0.0.1").getDatabase(0);
105 		auto list = db.getAsList!long("some_list");
106 		list.insertFront(123);
107 	}
108 }
109 
110 
111 /**
112 	Converts the given value to a binary/string representation suitable for
113 	Redis storage.
114 
115 	These functions are used by the proxy types of this module to convert
116 	between Redis and D.
117 
118 	See_also: $(D fromRedis)
119 */
120 string toRedis(T)(T value)
121 {
122 	import std.format;
123 	import std.traits;
124 	import vibe.data.serialization;
125 	static if (is(T == bool)) return value ? "1": "0";
126 	else static if (is(T : long) || is(T : double)) return value.to!string;
127 	else static if (isSomeString!T) return value.to!string;
128 	else static if (is(T : const(ubyte)[])) return cast(string)value;
129 	else static if (isISOExtStringSerializable!T) return value == T.init ? null : value.toISOExtString();
130 	else static if (isStringSerializable!T) return value.toString();
131 	else static assert(false, "Unsupported type: "~T.stringof);
132 }
133 /// ditto
134 void toRedis(R, T)(ref R dst, T value)
135 {
136 	import std.format;
137 	import std.traits;
138 	import vibe.data.serialization;
139 	static if (is(T == bool)) dst.put(value ? '1' : '0');
140 	else static if (is(T : long)) dst.formattedWrite("%s", value);
141 	else static if (isSomeString!T) dst.formattedWrite("%s", value);
142 	else static if(is(T : const(ubyte)[])) dst.put(value);
143 	else static if (isISOExtStringSerializable!T) dst.put(value == T.init ? null : value.toISOExtString());
144 	else static if (isStringSerializable!T) dst.put(value.toString());
145 	else static assert(false, "Unsupported type: "~T.stringof);
146 }
147 
148 
149 /**
150 	Converts a Redis value back to its original representation.
151 
152 	These functions are used by the proxy types of this module to convert
153 	between Redis and D.
154 
155 	See_also: $(D toRedis)
156 */
157 T fromRedis(T)(string value)
158 {
159 	import std.conv;
160 	import std.traits;
161 	import vibe.data.serialization;
162 	static if (is(T == bool)) return value != "0" && value != "false";
163 	else static if (is(T : long) || is(T : double)) return value.to!T;
164 	else static if (isSomeString!T) return value.to!T;
165 	else static if (is(T : const(ubyte)[])) return cast(T)value;
166 	else static if (isISOExtStringSerializable!T) return value.length ? T.fromISOExtString(value) : T.init;
167 	else static if (isStringSerializable!T) return T.fromString(value);
168 	else static assert(false, "Unsupported type: "~T.stringof);
169 }
170 
171 
172 /** The type of a Redis key.
173 */
174 enum RedisType {
175 	none,    /// Non-existent key
176 	string,  /// String/binary value
177 	list,    /// Linked list
178 	set,     /// Unsorted set
179 	zset,    /// Sorted set
180 	hash     /// Unsorted map
181 }
182 
183 
184 /** Represents a generic Redis value.
185 */
186 struct RedisValue {
187 	private {
188 		RedisDatabase m_db;
189 		string m_key;
190 	}
191 
192 	this(RedisDatabase db, string key) { m_db = db; m_key = key; }
193 
194 	/** The database in which the key is stored.
195 	*/
196 	@property inout(RedisDatabase) database() inout { return m_db; }
197 
198 	/** Name of the corresponding key.
199 	*/
200 	@property string key() const { return m_key; }
201 
202 	/** Remaining time-to-live.
203 
204 		Returns:
205 			The time until the key expires, if applicable. Returns
206 			$(D Duration.max) otherwise.
207 
208 		See_also: $(LINK2 http://redis.io/commands/pttl, PTTL)
209 	*/
210 	@property Duration ttl()
211 	{
212 		auto ret = m_db.pttl(m_key);
213 		return ret >= 0 ? ret.msecs : Duration.max;
214 	}
215 
216 	/** The data type of the referenced value.
217 
218 		Queries the actual type of the value that is referenced by this
219 		key.
220 
221 		See_also: $(LINK2 http://redis.io/commands/type, TYPE)
222 	*/
223 	@property RedisType type() { import std.conv; return m_db.type(m_key).to!RedisType; }
224 
225 	/** Checks if the referenced key exists.
226 
227 		See_also: $(LINK2 http://redis.io/commands/exists, EXISTS)
228 	*/
229 	@property bool exists() { return m_db.exists(m_key); }
230 
231 	/** Removes the referenced key.
232 
233 		Returns: $(D true) $(I iff) the key was successfully removed.
234 
235 		See_also: $(LINK2 http://redis.io/commands/del, DEL)
236 	*/
237 	bool remove() { return m_db.del(m_key) > 0; }
238 
239 	/** Sets the key for expiration after the given timeout.
240 
241 		Note that Redis handles timeouts in second resolution, so that the
242 		timeout must be at least one second.
243 
244 		Returns: $(D true) $(I iff) the expiration time was successfully set.
245 
246 		See_also: $(LINK2 http://redis.io/commands/expire, EXPIRE)
247 	*/
248 	bool expire(Duration expire_time) { assert(expire_time >= 1.seconds); return m_db.expire(m_key, expire_time.total!"seconds"); }
249 
250 	/** Sets the key for expiration at the given point in time.
251 
252 		Note that Redis handles timeouts in second resolution, so that any
253 		fractional seconds of the given $(D expire_time) will be truncated.
254 
255 		Returns: $(D true) $(I iff) the expiration time was successfully set.
256 
257 		See_also: $(LINK2 http://redis.io/commands/expireat, EXPIREAT)
258 	*/
259 	bool expireAt(SysTime expire_time) { return m_db.expireAt(m_key, expire_time.toUnixTime()); }
260 
261 	/** Removes any existing expiration time for the key.
262 
263 		Returns:
264 			$(D true) $(I iff) the key exists and an existing timeout was removed.
265 
266 		See_also: $(LINK2 http://redis.io/commands/persist, PERSIST)
267 	*/
268 	bool persist() { return m_db.persist(m_key); }
269 
270 	/** Moves this key to a different database.
271 
272 		Existing keys will not be overwritten.
273 
274 		Returns:
275 			$(D true) $(I iff) the key exists and was successfully moved to the
276 			destination database.
277 
278 		See_also: $(LINK2 http://redis.io/commands/move, MOVE)
279 	*/
280 	bool moveTo(long dst_database) { return m_db.move(m_key, dst_database); }
281 
282 	/** Renames the referenced key.
283 
284 		This method will also update this instance to refer to the renamed
285 		key.
286 
287 		See_also: $(LINK2 http://redis.io/commands/rename, RENAME), $(D renameIfNotExist)
288 	*/
289 	void rename(string new_name) { m_db.rename(m_key, new_name); m_key = new_name; }
290 
291 	/** Renames the referenced key if the destination key doesn't exist.
292 
293 		This method will also update this instance to refer to the renamed
294 		key if the rename was successful.
295 
296 		Returns:
297 			$(D true) $(I iff) the source key exists and the destination key doesn't
298 			exist.
299 
300 		See_also: $(LINK2 http://redis.io/commands/renamenx, RENAMENX), $(D rename)
301 	*/
302 	bool renameIfNotExist(string new_name)
303 	{
304 		if (m_db.renameNX(m_key, new_name)) {
305 			m_key = new_name;
306 			return true;
307 		}
308 		return false;
309 	}
310 
311 	//TODO sort
312 }
313 
314 
315 /** Represents a Redis string value.
316 
317 	In addition to the methods specific to string values, all operations of
318 	$(D RedisValue) are available using an $(D alias this) declaration.
319 */
320 struct RedisString(T = string) {
321 	RedisValue value;
322 	alias value this;
323 
324 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
325 
326 	/** The length in bytes of the string.
327 
328 		See_also: $(LINK2 http://redis.io/commands/strlen, STRLEN)
329 	*/
330 	@property long length() { return m_db.strlen(m_key); }
331 
332 	T get() { return m_db.get!string(m_key).fromRedis!T; }
333 
334 	T getSet(T value) { return m_db.getSet(m_key, value.toRedis).fromRedis!T; }
335 	bool getBit(long offset) { return m_db.getBit(m_key, offset); }
336 	bool setBit(long offset, bool value) { return m_db.setBit(m_key, offset, value); }
337 	void setExpire(T value, Duration expire_time) { assert(expire_time >= 1.seconds); m_db.setEX(m_key, expire_time.total!"seconds", value.toRedis); }
338 	bool setIfNotExist(T value) { return m_db.setNX(m_key, value.toRedis); }
339 
340 	string getSubString(long start, long end) { return m_db.getRange!string(m_key, start, end); }
341 	long setSubString(long offset, string value) { return m_db.setRange(m_key, offset, value); }
342 
343 	void opAssign(T value) { m_db.set(m_key, value.toRedis); }
344 
345 	long opOpAssign(string OP)(string value) if (OP == "~") { return m_db.append(m_key, value); }
346 	long opUnary(string OP)() if (OP == "++") { return m_db.incr(m_key); }
347 	long opUnary(string OP)() if (OP == "--") { return m_db.decr(m_key); }
348 	long opOpAssign(string OP)(long value) if (OP == "+") {
349 		assert(value != 0);
350 		if (value > 0) return m_db.incr(m_key, value);
351 		else return m_db.decr(m_key, -value);
352 	}
353 	long opOpAssign(string OP)(long value) if (OP == "-") {
354 		assert(value != 0);
355 		if (value > 0) return m_db.incr(m_key, value);
356 		else return m_db.decr(m_key, -value);
357 	}
358 	long opOpAssign(string OP)(double value) if (OP == "+") { return m_db.incr(m_key, value); }
359 	long opOpAssign(string OP)(double value) if (OP == "-") { return m_db.incr(m_key, -value); }
360 }
361 
362 
363 /** Represents a Redis hash value.
364 
365 	In addition to the methods specific to hash values, all operations of
366 	$(D RedisValue) are available using an $(D alias this) declaration.
367 */
368 struct RedisHash(T = string) {
369 	RedisValue value;
370 	alias value this;
371 
372 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
373 
374 	bool remove() { return value.remove(); }
375 	size_t remove(scope string[] fields...) { return cast(size_t)m_db.hdel(m_key, fields); }
376 	bool exists(string field) { return m_db.hexists(m_key, field); }
377 	bool exists() { return value.exists; }
378 
379 	void opIndexAssign(T value, string field) { m_db.hset(m_key, field, value.toRedis()); }
380 	T opIndex(string field) { return m_db.hget!string(m_key, field).fromRedis!T(); }
381 
382 	T get(string field, T def_value)
383 	{
384 		import std.typecons;
385 		auto ret = m_db.hget!(Nullable!string)(m_key, field);
386 		return ret.isNull ? def_value : ret.get().fromRedis!T;
387 	}
388 
389 	bool setIfNotExist(string field, T value)
390 	{
391 		return m_db.hsetNX(m_key, field, value.toRedis());
392 	}
393 
394 	void opIndexOpAssign(string op)(T value, string field) if (op == "+") { m_db.hincr(m_key, field, value); }
395 	void opIndexOpAssign(string op)(T value, string field) if (op == "-") { m_db.hincr(m_key, field, -value); }
396 
397 	int opApply(scope int delegate(string key, T value) del)
398 	{
399 		auto reply = m_db.hgetAll(m_key);
400 		while (reply.hasNext()) {
401 			auto key = reply.next!string();
402 			auto value = reply.next!string();
403 			if (auto ret = del(key, value.fromRedis!T))
404 				return ret;
405 		}
406 		return 0;
407 	}
408 
409 
410 	int opApply(scope int delegate(string key) del)
411 	{
412 		auto reply = m_db.hkeys(m_key);
413 		while (reply.hasNext()) {
414 			if (auto ret = del(reply.next!string()))
415 				return ret;
416 		}
417 		return 0;
418 	}
419 
420 	long length() { return m_db.hlen(m_key); }
421 
422 	// FIXME: support other types!
423 	void getMultiple(T[] dst, scope string[] fields...)
424 	{
425 		assert(dst.length == fields.length);
426 		auto reply = m_db.hmget(m_key, fields);
427 		size_t idx = 0;
428 		while (reply.hasNext())
429 			dst[idx++] = reply.next!string().fromRedis!T();
430 	}
431 
432 	// FIXME: support other types!
433 	/*void setMultiple(in string[] src, scope string[] fields...)
434 	{
435 		m_db.hmset(m_key, ...);
436 	}*/
437 
438 	//RedisReply hvals(string key) { return request!RedisReply("HVALS", key); }
439 }
440 
441 
442 /** Represents a Redis list value.
443 
444 	In addition to the methods specific to list values, all operations of
445 	$(D RedisValue) are available using an $(D alias this) declaration.
446 */
447 struct RedisList(T = string) {
448 	RedisValue value;
449 	alias value this;
450 
451 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
452 
453 	Dollar opDollar() { return Dollar(0); }
454 
455 	T opIndex(long index)
456 	{
457 		assert(index >= 0);
458 		return m_db.lindex!string(m_key, index).fromRedis!T;
459 	}
460 	T opIndex(Dollar index)
461 	{
462 		assert(index.offset < 0);
463 		return m_db.lindex!string(m_key, index.offset).fromRedis!T;
464 	}
465 	void opIndexAssign(T value, long index)
466 	{
467 		assert(index >= 0);
468 		m_db.lset(m_key, index, value.toRedis);
469 	}
470 	void opIndexAssign(T value, Dollar index)
471 	{
472 		assert(index.offset < 0);
473 		m_db.lset(m_key, index.offset, value.toRedis);
474 	}
475 	auto opSlice(S, E)(S start, E end)
476 		if ((is(S : long) || is(S == Dollar)) && (is(E : long) || is(E == Dollar)))
477 	{
478 		import std.algorithm;
479 		long s, e;
480 		static if (is(S == Dollar)) {
481 			assert(start.offset <= 0);
482 			s = start.offset;
483 		} else {
484 			assert(start >= 0);
485 			s = start;
486 		}
487 		static if (is(E == Dollar)) {
488 			assert(end.offset <= 0);
489 			e = end.offset - 1;
490 		} else {
491 			assert(end >= 0);
492 			e = end - 1;
493 		}
494 		return map!(e => e.fromRedis!T)(m_db.lrange(m_key, s, e));
495 	}
496 	auto opSlice()() { return this[0 .. $]; }
497 
498 	long length() { return m_db.llen(m_key); }
499 
500 	long insertBefore(T pivot, T value) { return m_db.linsertBefore(m_key, pivot.toRedis, value.toRedis); }
501 	long insertAfter(T pivot, T value) { return m_db.linsertAfter(m_key, pivot.toRedis, value.toRedis); }
502 
503 	long insertFront(T value) { return m_db.lpush(m_key, value.toRedis); }
504 	long insertFrontIfExists(T value) { return m_db.lpushX(m_key, value.toRedis); }
505 	long insertBack(T value) { return m_db.rpush(m_key, value.toRedis); }
506 	long insertBackIfExists(T value) { return m_db.rpushX(m_key, value.toRedis); }
507 
508 	long removeAll(T value) { return m_db.lrem(m_key, 0, value.toRedis); }
509 	long removeFirst(T value, long count = 1) { assert(count > 0); return m_db.lrem(m_key, count, value.toRedis); }
510 	long removeLast(T value, long count = 1) { assert(count > 0); return m_db.lrem(m_key, -count, value.toRedis); }
511 
512 	void trim(long start, long end) { m_db.ltrim(m_key, start, end); }
513 
514 	T removeFront() { return m_db.lpop!string(m_key).fromRedis!T; }
515 	T removeBack() { return m_db.rpop!string(m_key).fromRedis!T; }
516 	Nullable!T removeFrontBlock(Duration max_wait = 0.seconds) {
517 		assert(max_wait == 0.seconds || max_wait >= 1.seconds);
518 		auto r = m_db.blpop!string(m_key, max_wait.total!"seconds");
519 		return r.apply!(r => r[1].fromRedis!T);
520 	}
521 
522 	struct Dollar {
523 		long offset = 0;
524 		Dollar opAdd(long off) { return Dollar(offset + off); }
525 		Dollar opSub(long off) { return Dollar(offset - off); }
526 	}
527 
528 	int opApply(scope int delegate(T) del)
529 	{
530 		foreach (v; this[0 .. $])
531 			if (auto ret = del(v))
532 				return ret;
533 		return 0;
534 	}
535 
536 	//RedisReply lrange(string key, long start, long stop) { return request!RedisReply("LRANGE",  key, start, stop); }
537 	//T rpoplpush(T : E[], E)(string key, string destination) { return request!T("RPOPLPUSH", key, destination); }
538 }
539 
540 
541 /** Represents a Redis set value.
542 
543 	In addition to the methods specific to set values, all operations of
544 	$(D RedisValue) are available using an $(D alias this) declaration.
545 */
546 struct RedisSet(T = string) {
547 	RedisValue value;
548 	alias value this;
549 
550 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
551 
552 	long insert(ARGS...)(ARGS args) { return m_db.sadd(m_key, args); }
553 	long remove(T value) { return m_db.srem(m_key, value.toRedis()); }
554 	bool remove() { return value.remove(); }
555 	string pop() { return m_db.spop!string(m_key); }
556 	long length() { return m_db.scard(m_key); }
557 
558 	string getRandom() { return m_db.srandMember!string(m_key); }
559 
560 	//RedisReply sdiff(string[] keys...) { return request!RedisReply("SDIFF", keys); }
561 	//long sdiffStore(string destination, string[] keys...) { return request!long("SDIFFSTORE", destination, keys); }
562 	//RedisReply sinter(string[] keys) { return request!RedisReply("SINTER", keys); }
563 	//long sinterStore(string destination, string[] keys...) { return request!long("SINTERSTORE", destination, keys); }
564 	bool contains(T value) { return m_db.sisMember(m_key, value.toRedis()); }
565 
566 	int opApply(scope int delegate(T value) del)
567 	{
568 		foreach (m; m_db.smembers!string(m_key))
569 			if (auto ret = del(m.fromRedis!T()))
570 				return ret;
571 		return 0;
572 	}
573 
574 	bool intersects(scope RedisSet[] sets...)
575 	{
576 		import std.algorithm;
577 		import std.array;
578 		return !value.database.sinter(value.key ~ sets.map!(s => s.key).array).empty;
579 	}
580 
581 	auto getAll()
582 	{
583 		import std.algorithm;
584 		return map!(r => r.fromRedis!T)(value.database.smembers(value.key));
585 	}
586 
587 	//bool smove(T : E[], E)(string source, string destination, T member) { return request!bool("SMOVE", source, destination, member); }
588 	//RedisReply sunion(string[] keys...) { return request!RedisReply("SUNION", keys); }
589 	//long sunionStore(string[] keys...) { return request!long("SUNIONSTORE", keys); }
590 }
591 
592 
593 /** Represents a Redis sorted set value.
594 
595 	In addition to the methods specific to sorted set values, all operations of
596 	$(D RedisValue) are available using an $(D alias this) declaration.
597 */
598 struct RedisZSet(T = string) {
599 	RedisValue value;
600 	alias value this;
601 
602 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
603 
604 	long insert(ARGS...)(ARGS args) { return m_db.zadd(m_key, args); }
605 	long remove(ARGS...)(ARGS members) { return m_db.zrem(m_key, members); }
606 	bool remove() { return value.remove(); }
607 	long length() { return m_db.zcard(m_key); }
608 
609 	long count(string INT = "[]")(double min, double max)
610 		if (INT == "[]")
611 	{
612 		return m_db.zcount(m_key, min, max);
613 	}
614 
615 	long removeRangeByRank(long start, long end) { return m_db.zremRangeByRank(m_key, start, end); }
616 	long removeRangeByScore(string INT = "[]")(double min, double max) if (INT == "[]") { return m_db.zremRangeByScore(m_key, min, max); }
617 
618 	double opIndexOpAssign(string op)(double value, string member) if (op == "+") { return m_db.zincrby(m_key, value, member); }
619 
620 	long getRank(string member) { return m_db.zrank(m_key, member); }
621 	long getReverseRank(string member) { return m_db.zrevRank(m_key, member); }
622 
623 	long countByLex(string min, string max) { return m_db.zlexCount(m_key, min, max); }
624 
625 	//TODO: zinterstore
626 
627 	//RedisReply zrange(string key, long start, long end, bool withScores=false);
628 
629 	// TODO:
630 	// supports only inclusive intervals
631 	// see http://redis.io/commands/zrangebyscore
632 	//RedisReply zrangeByScore(string key, double start, double end, bool withScores=false);
633 
634 	// TODO:
635 	// supports only inclusive intervals
636 	// see http://redis.io/commands/zrangebyscore
637 	//RedisReply zrangeByScore(string key, double start, double end, long offset, long count, bool withScores=false);
638 
639 	//RedisReply zrevRange(string key, long start, long end, bool withScores=false);
640 
641 	// TODO:
642 	// supports only inclusive intervals
643 	// see http://redis.io/commands/zrangebyscore
644 	//RedisReply zrevRangeByScore(string key, double min, double max, bool withScores=false);
645 
646 	auto rangeByLex(T = string)(string min = "-", string max = "+", long offset = 0, long count = -1)
647 	{
648 		return m_db.zrangeByLex!T(m_key, min, max, offset, count);
649 	}
650 
651 	// TODO:
652 	// supports only inclusive intervals
653 	// see http://redis.io/commands/zrangebyscore
654 	//RedisReply zrevRangeByScore(string key, double min, double max, long offset, long count, bool withScores=false);
655 
656 	//RedisReply zscore(string key, string member) { return request!RedisReply("ZSCORE", key, member); }
657 	//TODO: zunionstore
658 }