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 @safe:
193 	this(RedisDatabase db, string key) { m_db = db; m_key = key; }
194 
195 	/** The database in which the key is stored.
196 	*/
197 	@property inout(RedisDatabase) database() inout { return m_db; }
198 
199 	/** Name of the corresponding key.
200 	*/
201 	@property string key() const { return m_key; }
202 
203 	/** Remaining time-to-live.
204 
205 		Returns:
206 			The time until the key expires, if applicable. Returns
207 			$(D Duration.max) otherwise.
208 
209 		See_also: $(LINK2 http://redis.io/commands/pttl, PTTL)
210 	*/
211 	@property Duration ttl()
212 	{
213 		auto ret = m_db.pttl(m_key);
214 		return ret >= 0 ? ret.msecs : Duration.max;
215 	}
216 
217 	/** The data type of the referenced value.
218 
219 		Queries the actual type of the value that is referenced by this
220 		key.
221 
222 		See_also: $(LINK2 http://redis.io/commands/type, TYPE)
223 	*/
224 	@property RedisType type() { import std.conv; return m_db.type(m_key).to!RedisType; }
225 
226 	/** Checks if the referenced key exists.
227 
228 		See_also: $(LINK2 http://redis.io/commands/exists, EXISTS)
229 	*/
230 	@property bool exists() { return m_db.exists(m_key); }
231 
232 	/** Removes the referenced key.
233 
234 		Returns: $(D true) $(I iff) the key was successfully removed.
235 
236 		See_also: $(LINK2 http://redis.io/commands/del, DEL)
237 	*/
238 	bool remove() { return m_db.del(m_key) > 0; }
239 
240 	/** Sets the key for expiration after the given timeout.
241 
242 		Note that Redis handles timeouts in second resolution, so that the
243 		timeout must be at least one second.
244 
245 		Returns: $(D true) $(I iff) the expiration time was successfully set.
246 
247 		See_also: $(LINK2 http://redis.io/commands/expire, EXPIRE)
248 	*/
249 	bool expire(Duration expire_time) { assert(expire_time >= 1.seconds); return m_db.expire(m_key, expire_time.total!"seconds"); }
250 
251 	/** Sets the key for expiration at the given point in time.
252 
253 		Note that Redis handles timeouts in second resolution, so that any
254 		fractional seconds of the given $(D expire_time) will be truncated.
255 
256 		Returns: $(D true) $(I iff) the expiration time was successfully set.
257 
258 		See_also: $(LINK2 http://redis.io/commands/expireat, EXPIREAT)
259 	*/
260 	bool expireAt(SysTime expire_time) { return m_db.expireAt(m_key, expire_time.toUnixTime()); }
261 
262 	/** Removes any existing expiration time for the key.
263 
264 		Returns:
265 			$(D true) $(I iff) the key exists and an existing timeout was removed.
266 
267 		See_also: $(LINK2 http://redis.io/commands/persist, PERSIST)
268 	*/
269 	bool persist() { return m_db.persist(m_key); }
270 
271 	/** Moves this key to a different database.
272 
273 		Existing keys will not be overwritten.
274 
275 		Returns:
276 			$(D true) $(I iff) the key exists and was successfully moved to the
277 			destination database.
278 
279 		See_also: $(LINK2 http://redis.io/commands/move, MOVE)
280 	*/
281 	bool moveTo(long dst_database) { return m_db.move(m_key, dst_database); }
282 
283 	/** Renames the referenced key.
284 
285 		This method will also update this instance to refer to the renamed
286 		key.
287 
288 		See_also: $(LINK2 http://redis.io/commands/rename, RENAME), $(D renameIfNotExist)
289 	*/
290 	void rename(string new_name) { m_db.rename(m_key, new_name); m_key = new_name; }
291 
292 	/** Renames the referenced key if the destination key doesn't exist.
293 
294 		This method will also update this instance to refer to the renamed
295 		key if the rename was successful.
296 
297 		Returns:
298 			$(D true) $(I iff) the source key exists and the destination key doesn't
299 			exist.
300 
301 		See_also: $(LINK2 http://redis.io/commands/renamenx, RENAMENX), $(D rename)
302 	*/
303 	bool renameIfNotExist(string new_name)
304 	{
305 		if (m_db.renameNX(m_key, new_name)) {
306 			m_key = new_name;
307 			return true;
308 		}
309 		return false;
310 	}
311 
312 	//TODO sort
313 }
314 
315 
316 /** Represents a Redis string value.
317 
318 	In addition to the methods specific to string values, all operations of
319 	$(D RedisValue) are available using an $(D alias this) declaration.
320 */
321 struct RedisString(T = string) {
322 	RedisValue value;
323 	alias value this;
324 
325 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
326 
327 	/** The length in bytes of the string.
328 
329 		See_also: $(LINK2 http://redis.io/commands/strlen, STRLEN)
330 	*/
331 	@property long length() { return m_db.strlen(m_key); }
332 
333 	T get() { return m_db.get!string(m_key).fromRedis!T; }
334 
335 	T getSet(T value) { return m_db.getSet(m_key, value.toRedis).fromRedis!T; }
336 	bool getBit(long offset) { return m_db.getBit(m_key, offset); }
337 	bool setBit(long offset, bool value) { return m_db.setBit(m_key, offset, value); }
338 	void setExpire(T value, Duration expire_time) { assert(expire_time >= 1.seconds); m_db.setEX(m_key, expire_time.total!"seconds", value.toRedis); }
339 	bool setIfNotExist(T value) { return m_db.setNX(m_key, value.toRedis); }
340 
341 	string getSubString(long start, long end) { return m_db.getRange!string(m_key, start, end); }
342 	long setSubString(long offset, string value) { return m_db.setRange(m_key, offset, value); }
343 
344 	void opAssign(T value) { m_db.set(m_key, value.toRedis); }
345 
346 	long opOpAssign(string OP)(string value) if (OP == "~") { return m_db.append(m_key, value); }
347 	long opUnary(string OP)() if (OP == "++") { return m_db.incr(m_key); }
348 	long opUnary(string OP)() if (OP == "--") { return m_db.decr(m_key); }
349 	long opOpAssign(string OP)(long value) if (OP == "+") {
350 		assert(value != 0);
351 		if (value > 0) return m_db.incr(m_key, value);
352 		else return m_db.decr(m_key, -value);
353 	}
354 	long opOpAssign(string OP)(long value) if (OP == "-") {
355 		assert(value != 0);
356 		if (value > 0) return m_db.incr(m_key, value);
357 		else return m_db.decr(m_key, -value);
358 	}
359 	long opOpAssign(string OP)(double value) if (OP == "+") { return m_db.incr(m_key, value); }
360 	long opOpAssign(string OP)(double value) if (OP == "-") { return m_db.incr(m_key, -value); }
361 }
362 
363 
364 /** Represents a Redis hash value.
365 
366 	In addition to the methods specific to hash values, all operations of
367 	$(D RedisValue) are available using an $(D alias this) declaration.
368 */
369 struct RedisHash(T = string) {
370 	RedisValue value;
371 	alias value this;
372 
373 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
374 
375 	bool remove() { return value.remove(); }
376 	size_t remove(scope string[] fields...) { return cast(size_t)m_db.hdel(m_key, fields); }
377 	bool exists(string field) { return m_db.hexists(m_key, field); }
378 	bool exists() { return value.exists; }
379 
380 	void opIndexAssign(T value, string field) { m_db.hset(m_key, field, value.toRedis()); }
381 	T opIndex(string field) { return m_db.hget!string(m_key, field).fromRedis!T(); }
382 
383 	T get(string field, T def_value)
384 	{
385 		import std.typecons;
386 		auto ret = m_db.hget!(Nullable!string)(m_key, field);
387 		return ret.isNull ? def_value : ret.get().fromRedis!T;
388 	}
389 
390 	bool setIfNotExist(string field, T value)
391 	{
392 		return m_db.hsetNX(m_key, field, value.toRedis());
393 	}
394 
395 	void opIndexOpAssign(string op)(T value, string field) if (op == "+") { m_db.hincr(m_key, field, value); }
396 	void opIndexOpAssign(string op)(T value, string field) if (op == "-") { m_db.hincr(m_key, field, -value); }
397 
398 	int opApply(scope int delegate(string key, T value) @safe del)
399 	{
400 		auto reply = m_db.hgetAll(m_key);
401 		while (reply.hasNext()) {
402 			auto key = reply.next!string();
403 			auto value = reply.next!string();
404 			if (auto ret = del(key, value.fromRedis!T))
405 				return ret;
406 		}
407 		return 0;
408 	}
409 
410 
411 	int opApply(scope int delegate(string key) @safe del)
412 	{
413 		auto reply = m_db.hkeys(m_key);
414 		while (reply.hasNext()) {
415 			if (auto ret = del(reply.next!string()))
416 				return ret;
417 		}
418 		return 0;
419 	}
420 
421 	long length() { return m_db.hlen(m_key); }
422 
423 	// FIXME: support other types!
424 	void getMultiple(T[] dst, scope string[] fields...)
425 	{
426 		assert(dst.length == fields.length);
427 		auto reply = m_db.hmget(m_key, fields);
428 		size_t idx = 0;
429 		while (reply.hasNext())
430 			dst[idx++] = reply.next!string().fromRedis!T();
431 	}
432 
433 	// FIXME: support other types!
434 	/*void setMultiple(in string[] src, scope string[] fields...)
435 	{
436 		m_db.hmset(m_key, ...);
437 	}*/
438 
439 	//RedisReply hvals(string key) { return request!RedisReply("HVALS", key); }
440 }
441 
442 
443 /** Represents a Redis list value.
444 
445 	In addition to the methods specific to list values, all operations of
446 	$(D RedisValue) are available using an $(D alias this) declaration.
447 */
448 struct RedisList(T = string) {
449 	RedisValue value;
450 	alias value this;
451 
452 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
453 
454 	Dollar opDollar() { return Dollar(0); }
455 
456 	T opIndex(long index)
457 	{
458 		assert(index >= 0);
459 		return m_db.lindex!string(m_key, index).fromRedis!T;
460 	}
461 	T opIndex(Dollar index)
462 	{
463 		assert(index.offset < 0);
464 		return m_db.lindex!string(m_key, index.offset).fromRedis!T;
465 	}
466 	void opIndexAssign(T value, long index)
467 	{
468 		assert(index >= 0);
469 		m_db.lset(m_key, index, value.toRedis);
470 	}
471 	void opIndexAssign(T value, Dollar index)
472 	{
473 		assert(index.offset < 0);
474 		m_db.lset(m_key, index.offset, value.toRedis);
475 	}
476 	auto opSlice(S, E)(S start, E end)
477 		if ((is(S : long) || is(S == Dollar)) && (is(E : long) || is(E == Dollar)))
478 	{
479 		import std.algorithm;
480 		long s, e;
481 		static if (is(S == Dollar)) {
482 			assert(start.offset <= 0);
483 			s = start.offset;
484 		} else {
485 			assert(start >= 0);
486 			s = start;
487 		}
488 		static if (is(E == Dollar)) {
489 			assert(end.offset <= 0);
490 			e = end.offset - 1;
491 		} else {
492 			assert(end >= 0);
493 			e = end - 1;
494 		}
495 		return map!(e => e.fromRedis!T)(m_db.lrange(m_key, s, e));
496 	}
497 	auto opSlice()() { return this[0 .. $]; }
498 
499 	long length() { return m_db.llen(m_key); }
500 
501 	long insertBefore(T pivot, T value) { return m_db.linsertBefore(m_key, pivot.toRedis, value.toRedis); }
502 	long insertAfter(T pivot, T value) { return m_db.linsertAfter(m_key, pivot.toRedis, value.toRedis); }
503 
504 	long insertFront(T value) { return m_db.lpush(m_key, value.toRedis); }
505 	long insertFrontIfExists(T value) { return m_db.lpushX(m_key, value.toRedis); }
506 	long insertBack(T value) { return m_db.rpush(m_key, value.toRedis); }
507 	long insertBackIfExists(T value) { return m_db.rpushX(m_key, value.toRedis); }
508 
509 	long removeAll(T value) { return m_db.lrem(m_key, 0, value.toRedis); }
510 	long removeFirst(T value, long count = 1) { assert(count > 0); return m_db.lrem(m_key, count, value.toRedis); }
511 	long removeLast(T value, long count = 1) { assert(count > 0); return m_db.lrem(m_key, -count, value.toRedis); }
512 
513 	void trim(long start, long end) { m_db.ltrim(m_key, start, end); }
514 
515 	T removeFront() { return m_db.lpop!string(m_key).fromRedis!T; }
516 	T removeBack() { return m_db.rpop!string(m_key).fromRedis!T; }
517 	Nullable!T removeFrontBlock(Duration max_wait = 0.seconds) {
518 		assert(max_wait == 0.seconds || max_wait >= 1.seconds);
519 		auto r = m_db.blpop!string(m_key, max_wait.total!"seconds");
520 		return r.apply!(r => r[1].fromRedis!T);
521 	}
522 
523 	struct Dollar {
524 		long offset = 0;
525 		Dollar opAdd(long off) { return Dollar(offset + off); }
526 		Dollar opSub(long off) { return Dollar(offset - off); }
527 	}
528 
529 	int opApply(scope int delegate(T) @safe del)
530 	{
531 		foreach (v; this[0 .. $])
532 			if (auto ret = del(v))
533 				return ret;
534 		return 0;
535 	}
536 
537 	//RedisReply lrange(string key, long start, long stop) { return request!RedisReply("LRANGE",  key, start, stop); }
538 	//T rpoplpush(T : E[], E)(string key, string destination) { return request!T("RPOPLPUSH", key, destination); }
539 }
540 
541 
542 /** Represents a Redis set value.
543 
544 	In addition to the methods specific to set values, all operations of
545 	$(D RedisValue) are available using an $(D alias this) declaration.
546 */
547 struct RedisSet(T = string) {
548 	RedisValue value;
549 	alias value this;
550 
551 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
552 
553 	long insert(ARGS...)(ARGS args) { return m_db.sadd(m_key, args); }
554 	long remove(T value) { return m_db.srem(m_key, value.toRedis()); }
555 	bool remove() { return value.remove(); }
556 	string pop() { return m_db.spop!string(m_key); }
557 	long length() { return m_db.scard(m_key); }
558 
559 	string getRandom() { return m_db.srandMember!string(m_key); }
560 
561 	//RedisReply sdiff(string[] keys...) { return request!RedisReply("SDIFF", keys); }
562 	//long sdiffStore(string destination, string[] keys...) { return request!long("SDIFFSTORE", destination, keys); }
563 	//RedisReply sinter(string[] keys) { return request!RedisReply("SINTER", keys); }
564 	//long sinterStore(string destination, string[] keys...) { return request!long("SINTERSTORE", destination, keys); }
565 	bool contains(T value) { return m_db.sisMember(m_key, value.toRedis()); }
566 
567 	int opApply(scope int delegate(T value) @safe del)
568 	{
569 		foreach (m; m_db.smembers!string(m_key))
570 			if (auto ret = del(m.fromRedis!T()))
571 				return ret;
572 		return 0;
573 	}
574 
575 	bool intersects(scope RedisSet[] sets...)
576 	{
577 		import std.algorithm;
578 		import std.array;
579 		return !value.database.sinter(value.key ~ sets.map!(s => s.key).array).empty;
580 	}
581 
582 	auto getAll()
583 	{
584 		import std.algorithm;
585 		return map!(r => r.fromRedis!T)(value.database.smembers(value.key));
586 	}
587 
588 	//bool smove(T : E[], E)(string source, string destination, T member) { return request!bool("SMOVE", source, destination, member); }
589 	//RedisReply sunion(string[] keys...) { return request!RedisReply("SUNION", keys); }
590 	//long sunionStore(string[] keys...) { return request!long("SUNIONSTORE", keys); }
591 }
592 
593 
594 /** Represents a Redis sorted set value.
595 
596 	In addition to the methods specific to sorted set values, all operations of
597 	$(D RedisValue) are available using an $(D alias this) declaration.
598 */
599 struct RedisZSet(T = string) {
600 	RedisValue value;
601 	alias value this;
602 
603 	this(RedisDatabase db, string key) { value = RedisValue(db, key); }
604 
605 	long insert(ARGS...)(ARGS args) { return m_db.zadd(m_key, args); }
606 	long remove(ARGS...)(ARGS members) { return m_db.zrem(m_key, members); }
607 	bool remove() { return value.remove(); }
608 	long length() { return m_db.zcard(m_key); }
609 
610 	long count(string INT = "[]")(double min, double max)
611 		if (INT == "[]")
612 	{
613 		return m_db.zcount(m_key, min, max);
614 	}
615 
616 	long removeRangeByRank(long start, long end) { return m_db.zremRangeByRank(m_key, start, end); }
617 	long removeRangeByScore(string INT = "[]")(double min, double max) if (INT == "[]") { return m_db.zremRangeByScore(m_key, min, max); }
618 
619 	double opIndexOpAssign(string op)(double value, string member) if (op == "+") { return m_db.zincrby(m_key, value, member); }
620 
621 	long getRank(string member) { return m_db.zrank(m_key, member); }
622 	long getReverseRank(string member) { return m_db.zrevRank(m_key, member); }
623 
624 	long countByLex(string min, string max) { return m_db.zlexCount(m_key, min, max); }
625 
626 	//TODO: zinterstore
627 
628 	//RedisReply zrange(string key, long start, long end, bool withScores=false);
629 
630 	// TODO:
631 	// supports only inclusive intervals
632 	// see http://redis.io/commands/zrangebyscore
633 	//RedisReply zrangeByScore(string key, double start, double end, bool withScores=false);
634 
635 	// TODO:
636 	// supports only inclusive intervals
637 	// see http://redis.io/commands/zrangebyscore
638 	//RedisReply zrangeByScore(string key, double start, double end, long offset, long count, bool withScores=false);
639 
640 	//RedisReply zrevRange(string key, long start, long end, bool withScores=false);
641 
642 	// TODO:
643 	// supports only inclusive intervals
644 	// see http://redis.io/commands/zrangebyscore
645 	//RedisReply zrevRangeByScore(string key, double min, double max, bool withScores=false);
646 
647 	auto rangeByLex(T = string)(string min = "-", string max = "+", long offset = 0, long count = -1)
648 	{
649 		return m_db.zrangeByLex!T(m_key, min, max, offset, count);
650 	}
651 
652 	// TODO:
653 	// supports only inclusive intervals
654 	// see http://redis.io/commands/zrangebyscore
655 	//RedisReply zrevRangeByScore(string key, double min, double max, long offset, long count, bool withScores=false);
656 
657 	//RedisReply zscore(string key, string member) { return request!RedisReply("ZSCORE", key, member); }
658 	//TODO: zunionstore
659 }