1 /**
2 	Convenience wrappers types for accessing Redis keys.
3 
4 	Note that the API is still subject to change!
5 
6 	Copyright: © 2014 RejectedSoftware e.K.
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 : 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 	size_t remove(scope string[] fields...) { return cast(size_t)m_db.hdel(m_key, fields); }
375 	bool exists(string field) { return m_db.hexists(m_key, field); }
376 
377 	void opIndexAssign(T value, string field) { m_db.hset(m_key, field, value.toRedis()); }
378 	T opIndex(string field) { return m_db.hget!string(m_key, field).fromRedis!T(); }
379 
380 	T get(string field, T def_value)
381 	{
382 		import std.typecons;
383 		auto ret = m_db.hget!(Nullable!string)(m_key, field);
384 		return ret.isNull ? def_value : ret.fromRedis!T;
385 	}
386 
387 	bool setIfNotExist(string field, T value)
388 	{
389 		return m_db.hsetNX(m_key, field, value.toRedis());
390 	}
391 
392 	void opIndexOpAssign(string op)(T value, string field) if (op == "+") { m_db.hincr(m_key, field, value.toRedis()); }
393 
394 	int opApply(scope int delegate(string key, T value) del)
395 	{
396 		auto reply = m_db.hgetAll(m_key);
397 		while (reply.hasNext()) {
398 			auto key = reply.next!string();
399 			auto value = reply.next!string();
400 			if (auto ret = del(key, value.fromRedis!T))
401 				return ret;
402 		}
403 		return 0;
404 	}
405 
406 
407 	int opApply(scope int delegate(string key) del)
408 	{
409 		auto reply = m_db.hkeys(m_key);
410 		while (reply.hasNext()) {
411 			if (auto ret = del(reply.next!string()))
412 				return ret;
413 		}
414 		return 0;
415 	}
416 
417 	long length() { return m_db.hlen(m_key); }
418 
419 	// FIXME: support other types!
420 	void getMultiple(T[] dst, scope string[] fields...)
421 	{
422 		assert(dst.length == fields.length);
423 		auto reply = m_db.hmget(m_key, fields);
424 		size_t idx = 0;
425 		while (reply.hasNext())
426 			dst[idx++] = reply.next!string().fromRedis!T();
427 	}
428 
429 	// FIXME: support other types!
430 	/*void setMultiple(in string[] src, scope string[] fields...)
431 	{
432 		m_db.hmset(m_key, ...);
433 	}*/
434 
435 	//RedisReply hvals(string key) { return request!RedisReply("HVALS", key); }
436 }
437 
438 
439 /** Represents a Redis list value.
440 
441 	In addition to the methods specific to list values, all operations of
442 	$(D RedisValue) are available using an $(D alias this) declaration.
443 */
444 struct RedisList(T = string) {
445 	RedisValue value;
446 	alias value this;
447 
448 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
449 
450 	Dollar opDollar() { return Dollar(0); }
451 
452 	T opIndex(long index)
453 	{
454 		assert(index >= 0);
455 		return m_db.lindex!string(m_key, index).fromRedis!T;
456 	}
457 	T opIndex(Dollar index)
458 	{
459 		assert(index.offset < 0);
460 		return m_db.lindex!string(m_key, index.offset).fromRedis!T;
461 	}
462 	void opIndexAssign(T value, long index)
463 	{
464 		assert(index >= 0);
465 		m_db.lset(m_key, index, value.toRedis);
466 	}
467 	void opIndexAssign(T value, Dollar index)
468 	{
469 		assert(index.offset < 0);
470 		m_db.lset(m_key, index.offset, value.toRedis);
471 	}
472 	auto opSlice(S, E)(S start, E end)
473 		if ((is(S : long) || is(S == Dollar)) && (is(E : long) || is(E == Dollar)))
474 	{
475 		import std.algorithm;
476 		long s, e;
477 		static if (is(S == Dollar)) {
478 			assert(start.offset <= 0);
479 			s = start.offset;
480 		} else {
481 			assert(start >= 0);
482 			s = start;
483 		}
484 		static if (is(E == Dollar)) {
485 			assert(end.offset <= 0);
486 			e = end.offset - 1;
487 		} else {
488 			assert(end >= 0);
489 			e = end - 1;
490 		}
491 		return map!(e => e.fromRedis!T)(m_db.lrange(m_key, s, e));
492 	}
493 	auto opSlice()() { return this[0 .. $]; }
494 
495 	long length() { return m_db.llen(m_key); }
496 
497 	long insertBefore(T pivot, T value) { return m_db.linsertBefore(m_key, pivot.toRedis, value.toRedis); }
498 	long insertAfter(T pivot, T value) { return m_db.linsertAfter(m_key, pivot.toRedis, value.toRedis); }
499 
500 	long insertFront(T value) { return m_db.lpush(m_key, value.toRedis); }
501 	long insertFrontIfExists(T value) { return m_db.lpushX(m_key, value.toRedis); }
502 	long insertBack(T value) { return m_db.rpush(m_key, value.toRedis); }
503 	long insertBackIfExists(T value) { return m_db.rpushX(m_key, value.toRedis); }
504 
505 	long removeAll(T value) { return m_db.lrem(m_key, 0, value.toRedis); }
506 	long removeFirst(T value, long count = 1) { assert(count > 0); return m_db.lrem(m_key, count, value.toRedis); }
507 	long removeLast(T value, long count = 1) { assert(count > 0); return m_db.lrem(m_key, -count, value.toRedis); }
508 
509 	void trim(long start, long end) { m_db.ltrim(m_key, start, end); }
510 
511 	T removeFront() { return m_db.lpop!string(m_key).fromRedis!T; }
512 	T removeBack() { return m_db.rpop!string(m_key).fromRedis!T; }
513 	Nullable!T removeFrontBlock(Duration max_wait = 0.seconds) {
514 		assert(max_wait == 0.seconds || max_wait >= 1.seconds);
515 		auto r = m_db.blpop!string(m_key, max_wait.total!"seconds");
516 		return r.isNull ? Nullable!T.init : Nullable!T(r[1].fromRedis!T);
517 	}
518 
519 	struct Dollar {
520 		long offset = 0;
521 		Dollar opAdd(long off) { return Dollar(offset + off); }
522 		Dollar opSub(long off) { return Dollar(offset - off); }
523 	}
524 
525 	int opApply(scope int delegate(T) del)
526 	{
527 		foreach (v; this[0 .. $])
528 			if (auto ret = del(v))
529 				return ret;
530 		return 0;
531 	}
532 
533 	//RedisReply lrange(string key, long start, long stop) { return request!RedisReply("LRANGE",  key, start, stop); }
534 	//T rpoplpush(T : E[], E)(string key, string destination) { return request!T("RPOPLPUSH", key, destination); }
535 }
536 
537 
538 /** Represents a Redis set value.
539 
540 	In addition to the methods specific to set values, all operations of
541 	$(D RedisValue) are available using an $(D alias this) declaration.
542 */
543 struct RedisSet(T = string) {
544 	RedisValue value;
545 	alias value this;
546 
547 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
548 
549 	long insert(ARGS...)(ARGS args) { return m_db.sadd(m_key, args); }
550 	long remove(T value) { return m_db.srem(m_key, value.toRedis()); }
551 	void remove()() { value.remove(); }
552 	string pop() { return m_db.spop!string(m_key); }
553 	long length() { return m_db.scard(m_key); }
554 
555 	string getRandom() { return m_db.srandMember!string(m_key); }
556 
557 	//RedisReply sdiff(string[] keys...) { return request!RedisReply("SDIFF", keys); }
558 	//long sdiffStore(string destination, string[] keys...) { return request!long("SDIFFSTORE", destination, keys); }
559 	//RedisReply sinter(string[] keys) { return request!RedisReply("SINTER", keys); }
560 	//long sinterStore(string destination, string[] keys...) { return request!long("SINTERSTORE", destination, keys); }
561 	bool contains(T value) { return m_db.sisMember(m_key, value.toRedis()); }
562 
563 	int opApply(scope int delegate(T value) del)
564 	{
565 		foreach (m; m_db.smembers!string(m_key))
566 			if (auto ret = del(m.fromRedis!T()))
567 				return ret;
568 		return 0;
569 	}
570 
571 	bool intersects(scope RedisSet[] sets...)
572 	{
573 		import std.algorithm;
574 		import std.array;
575 		return !value.database.sinter(value.key ~ sets.map!(s => s.key).array).empty;
576 	}
577 
578 	auto getAll()
579 	{
580 		import std.algorithm;
581 		return map!(r => r.fromRedis!T)(value.database.smembers(value.key));
582 	}
583 
584 	//bool smove(T : E[], E)(string source, string destination, T member) { return request!bool("SMOVE", source, destination, member); }
585 	//RedisReply sunion(string[] keys...) { return request!RedisReply("SUNION", keys); }
586 	//long sunionStore(string[] keys...) { return request!long("SUNIONSTORE", keys); }
587 }
588 
589 
590 /** Represents a Redis sorted set value.
591 
592 	In addition to the methods specific to sorted set values, all operations of
593 	$(D RedisValue) are available using an $(D alias this) declaration.
594 */
595 struct RedisZSet(T = string) {
596 	RedisValue value;
597 	alias value this;
598 
599 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
600 
601 	long insert(ARGS...)(ARGS args) { return m_db.zadd(m_key, args); }
602 	long remove(ARGS...)(ARGS members) { return m_db.zrem(m_key, members); }
603 	long length() { return m_db.zcard(m_key); }
604 
605 	long count(string INT = "[]")(double min, double max)
606 		if (INT == "[]")
607 	{
608 		return m_db.zcount(m_key, min, max);
609 	}
610 
611 	long removeRangeByRank(long start, long end) { return m_db.zremRangeByRank(m_key, start, end); }
612 	long removeRangeByScore(string INT = "[]")(double min, double max) if (INT == "[]") { return m_db.zremRangeByScore(m_key, min, max); }
613 
614 	double opIndexOpAssign(string op)(double value, string member) if (op == "+") { return m_db.zincrby(m_key, value, member); }
615 
616 	long getRank(string member) { return m_db.zrank(m_key, member); }
617 	long getReverseRank(string member) { return m_db.zrevRank(m_key, member); }
618 
619 	//TODO: zinterstore
620 
621 	//RedisReply zrange(string key, long start, long end, bool withScores=false);
622 
623 	// TODO:
624 	// supports only inclusive intervals
625 	// see http://redis.io/commands/zrangebyscore
626 	//RedisReply zrangeByScore(string key, double start, double end, bool withScores=false);
627 
628 	// TODO:
629 	// supports only inclusive intervals
630 	// see http://redis.io/commands/zrangebyscore
631 	//RedisReply zrangeByScore(string key, double start, double end, long offset, long count, bool withScores=false);
632 
633 	//RedisReply zrevRange(string key, long start, long end, bool withScores=false);
634 
635 	// TODO:
636 	// supports only inclusive intervals
637 	// see http://redis.io/commands/zrangebyscore
638 	//RedisReply zrevRangeByScore(string key, double min, double max, bool withScores=false);
639 
640 	// TODO:
641 	// supports only inclusive intervals
642 	// see http://redis.io/commands/zrangebyscore
643 	//RedisReply zrevRangeByScore(string key, double min, double max, long offset, long count, bool withScores=false);
644 
645 	//RedisReply zscore(string key, string member) { return request!RedisReply("ZSCORE", key, member); }
646 	//TODO: zunionstore
647 }