1 module tinyredis.response;
2 
3 /**
4  * Authors: Adil Baig, adil.baig@aidezigns.com
5  */
6 
7 package enum CRLF = "\r\n";
8 
9 enum ResponseType : byte
10 {
11 	Invalid,
12 	Status,
13 	Error,
14 	Integer,
15 	Bulk,
16 	MultiBulk,
17 	Nil
18 }
19 
20 /**
21  * The Response struct represents returned data from Redis.
22  *
23  * Stores values true to form. Allows user code to query, cast, iterate, print, and log strings, ints, errors and all other return types.
24  *
25  * The role of the Response struct is to make it simple, yet accurate to retrieve returned values from Redis. To aid this
26  * it implements D op* functions as well as little helper methods that simplify user facing code.
27  */
28 struct Response
29 {
30 	ResponseType type;
31 	union {
32 		struct {
33 			Response[] values;
34 			int count; //Used for multibulk only. -1 is a valid multibulk. Indicates nil
35 		}
36 		struct {
37 			size_t length;
38 			union {
39 				string value;
40 				long intval;
41 			}
42 		}
43 	}
44 
45 	alias values this;
46 
47 	@property nothrow @nogc {
48 		bool isString() const { return type == ResponseType.Bulk; }
49 
50 		bool isInt() const { return type == ResponseType.Integer; }
51 
52 		bool isArray() const { return type == ResponseType.MultiBulk; }
53 
54 		bool isError() const { return type == ResponseType.Error; }
55 
56 		bool isNil() const { return type == ResponseType.Nil; }
57 
58 		bool isStatus() const { return type == ResponseType.Status; }
59 
60 		bool isValid() const { return type != ResponseType.Invalid; }
61 
62 		// Response is a ForwardRange
63 		auto save()
64 		{
65 			// Returning a copy of this struct object
66 			return this;
67 		}
68 	}
69 
70 	/**
71 	 * Parse a char array into a Response struct.
72 	 *
73 	 * The parser works to identify a minimum complete Response. If successful, it removes that chunk from "mb" and returns a Response struct.
74 	 * On failure it returns a `ResponseType.Invalid` Response and leaves "mb" untouched.
75 	 */
76 	static Response parse(ref char[] mb) nothrow @nogc @trusted
77 	{
78 		Response resp;
79 		if(mb.length < 4)
80 			return resp;
81 
82 		char c = mb[0];
83 		mb = mb[1..$];
84 		switch(c)
85 		{
86 			case '+':
87 				if (!mb.tryParse(resp.value))
88 					return resp;
89 
90 				resp.type = ResponseType.Status;
91 				break;
92 
93 			case '-':
94 				if (!mb.tryParse(resp.value))
95 					return resp;
96 
97 				resp.type = ResponseType.Error;
98 				break;
99 
100 			case ':':
101 				if (!mb.tryParse(resp.intval))
102 					return resp;
103 
104 				resp.type = ResponseType.Integer;
105 				break;
106 
107 			case '$':
108 				int l = void;
109 				if (!mb.tryParse(l))
110 					return resp;
111 
112 				if(l == -1)
113 				{
114 					resp.type = ResponseType.Nil;
115 					break;
116 				}
117 
118 				if(l + 2 > mb.length) //We don't have enough data. Let's return an invalid resp.
119 					return resp;
120 
121 				resp.value = cast(string)mb[0..l];
122 				resp.type = ResponseType.Bulk;
123 				mb = mb[l+2..$];
124 				break;
125 
126 			case '*':
127 				int l = void;
128 				if (!mb.tryParse(l))
129 					return resp;
130 
131 				if(l == -1)
132 				{
133 					resp.type = ResponseType.Nil;
134 					break;
135 				}
136 
137 				resp.type = ResponseType.MultiBulk;
138 				resp.count = l;
139 				break;
140 
141 			default:
142 				break;
143 		}
144 
145 		return resp;
146 	}
147 
148 	/**
149 	 * Attempts to check for truthiness of a Response.
150 	 *
151 	 * Returns false on failure.
152 	 */
153 	T opCast(T : bool)() {
154 		switch(type) with(ResponseType)
155 		{
156 			case Integer:	return intval > 0;
157 			case Status:	return value == "OK";
158 			case Bulk:		return value.length != 0;
159 			case MultiBulk: return values.length != 0;
160 			default:		return false;
161 		}
162 	}
163 
164 	/**
165 	 * Allows casting a Response to an integral or string
166 	 */
167 	T opCast(T)() if(is(T : long) || is(T == string))
168 	{
169 		static if(is(T : long))
170 			return toInt!T;
171 		else
172 			return toString;
173 	}
174 
175 	/**
176 	 * Attempts to convert a response to an array of bytes
177 	 *
178 	 * For intvals - converts to an array of bytes that is Response.intval.sizeof long
179 	 * For Bulk - casts the string to C[]
180 	 *
181 	 * Returns an empty array in all other cases;
182 	 */
183 	C[] opCast(C : C[])() if(is(C == byte) || is(C == ubyte))
184 	{
185 		import std.array;
186 
187 		switch(type)
188 		{
189 			case ResponseType.Integer:
190 				C[] ret = uninitializedArray!(C[])(intval.sizeof);
191 				*cast(long*)ret.ptr = intval;
192 				return ret;
193 
194 			case ResponseType.Bulk:
195 				return cast(C[])value;
196 
197 			default:
198 				return [];
199 		}
200 	}
201 
202 	/**
203 	 * Converts a Response to an integral (byte to long)
204 	 *
205 	 * Only works with ResponseType.Integer and ResponseType.Bulk
206 	 *
207 	 * Throws : ConvOverflowException, RedisCastException
208 	 */
209 	T toInt(T = int)() if(is(T : long))
210 	{
211 		import std.conv;
212 
213 		switch(type)
214 		{
215 			case ResponseType.Integer:
216 				if(intval <= T.max)
217 					return cast(T)intval;
218 				throw new ConvOverflowException("Cannot convert " ~ intval.to!string ~ " to " ~ T.stringof);
219 
220 			case ResponseType.Bulk:
221 				try
222 					return value.to!T;
223 				catch(ConvOverflowException e)
224 				{
225 					e.msg = "Cannot convert " ~ value ~ " to " ~ T.stringof;
226 					throw e;
227 				}
228 
229 			default:
230 				throw new RedisCastException("Cannot cast " ~ type ~ " to " ~ T.stringof);
231 		}
232 	}
233 
234 @property @trusted:
235 	/**
236 	 * Returns the value of this Response as a string
237 	 */
238 	string toString()
239 	{
240 		import std.conv;
241 
242 		switch(type) with(ResponseType)
243 		{
244 			case Integer:
245 				return intval.to!string;
246 
247 			case Error:
248 			case Status:
249 			case Bulk:
250 				return value;
251 
252 			case MultiBulk:
253 				return text(values);
254 
255 			default:
256 				return "";
257 		}
258 	}
259 
260 	/**
261 	 * Returns the value of this Response as a string, along with type information
262 	 */
263 	string toDiagnosticString()
264 	{
265 		import std.array : appender;
266 		auto app = appender!string;
267 		toDiagnosticString(app);
268 		return app[];
269 	}
270 
271 	void toDiagnosticString(R)(ref R appender)
272 	{
273 		import std.conv : to;
274 		final switch(type) with(ResponseType)
275 		{
276 		case Invalid:	appender.put("(Invalid)");	break;
277 		case Nil:		appender.put("(Nil)");		break;
278 		case Error:
279 			appender.put("(Err) ");
280 			goto case Bulk;
281 		case Integer:
282 			appender.put("(Integer) ");
283 			appender.put(intval.to!string);
284 			break;
285 		case Status:
286 			appender.put("(Status) ");
287 			goto case;
288 		case Bulk:
289 			appender.put(value);
290 			break;
291 		case MultiBulk:
292 			foreach(v; values)
293 				v.toDiagnosticString(appender);
294 			break;
295 		}
296 	}
297 }
298 
299 unittest
300 {
301 	import std.range : isInputRange, isForwardRange, isBidirectionalRange;
302 
303 	//Testing ranges for Response
304 	static assert(isInputRange!Response);
305 	static assert(isForwardRange!Response);
306 	static assert(isBidirectionalRange!Response);
307 }
308 
309 /* ----------- EXCEPTIONS ------------- */
310 
311 class RedisCastException : Exception {
312 	this(string msg) { super(msg); }
313 }
314 
315 import std.traits;
316 
317 bool tryParse(T)(ref char[] data, out T x) if(isIntegral!T)
318 in (data.length) {
319 	T f = 1;
320 	size_t i;
321 	char c = data[0];
322 	if (c == '-') {
323 		f = -1;
324 		i = 1;
325 	}
326 	for(; i < data.length; ++i) {
327 		c = data[i];
328 		if (c < '0' || c > '9')
329 			break;
330 		x = x * 10 + (c ^ '0');
331 		if (x < 0)
332 			return false;
333 	}
334 	x *= f;
335 	++i;
336 	if(c != '\r' || i >= data.length || data[i] != '\n')
337 		return false;
338 
339 	data = data[i+1..$];
340 	return true;
341 }
342 
343 bool tryParse(T)(ref char[] data, out T x) if(isSomeString!T) {
344 	import std.string;
345 
346 	auto i = indexOf(data, '\r');
347 	if (i < 0 || i + 1 >= data.length || data[i + 1] != '\n')
348 		return false;
349 
350 	x = cast(T)data[0..i];
351 	data = data[i+2..$];
352 	return true;
353 }
354 
355 unittest
356 {
357 	alias parse = Response.parse;
358 
359 	//Test Nil bulk
360 	auto stream = cast(char[])"$-1\r\n";
361 	auto resp = parse(stream);
362 	assert(resp.toString == "");
363 	assert(!resp);
364 	try{
365 		cast(int)resp;
366 		assert(0);
367 	}
368 	catch(RedisCastException) {}
369 
370 	//Test Nil multibulk
371 	stream = cast(char[])"*-1\r\n";
372 	resp = parse(stream);
373 	assert(resp.toString == "");
374 	assert(!resp);
375 	try{
376 		cast(int)resp;
377 		assert(0);
378 	}
379 	catch(RedisCastException) {}
380 
381 	//Empty Bulk
382 	stream = cast(char[])"$0\r\n\r\n";
383 	resp = parse(stream);
384 	assert(resp.toString == "");
385 	assert(!resp);
386 
387 	stream = cast(char[])"*4\r\n$3\r\nGET\r\n$1\r\n*\r\n:123\r\n+A Status Message\r\n";
388 
389 	resp = parse(stream);
390 	assert(resp.type == ResponseType.MultiBulk);
391 	assert(resp.count == 4);
392 	assert(resp.values.length == 0);
393 
394 	resp = parse(stream);
395 	assert(resp.type == ResponseType.Bulk);
396 	assert(resp.value == "GET");
397 	assert(cast(string)resp == "GET");
398 
399 	resp = parse(stream);
400 	assert(resp.type == ResponseType.Bulk);
401 	assert(resp.value == "*");
402 	assert(resp);
403 
404 	resp = parse(stream);
405 	assert(resp.type == ResponseType.Integer);
406 	assert(resp.intval == 123);
407 	assert(cast(string)resp == "123");
408 	assert(cast(int)resp == 123);
409 
410 	resp = parse(stream);
411 	assert(resp.type == ResponseType.Status);
412 	assert(resp.value == "A Status Message");
413 	assert(cast(string)resp == "A Status Message");
414 	try {
415 		cast(int)resp;
416 		assert(0, "Tried to convert string to int");
417 	} catch(RedisCastException) {}
418 
419 	//Stream should have been used up, verify
420 	assert(stream.length == 0);
421 	assert(parse(stream).type == ResponseType.Invalid);
422 
423 	import std.conv : ConvOverflowException;
424 	//Long overflow checking
425 	stream = cast(char[])":9223372036854775808\r\n";
426 	resp = parse(stream);
427 	assert(resp.type == ResponseType.Invalid, "Tried to convert long.max+1 to long");
428 
429 	Response r = {type : ResponseType.Bulk, value : "9223372036854775807"};
430 	try{
431 		r.toInt(); //Default int
432 		assert(0, "Tried to convert long.max to int");
433 	}
434 	catch(ConvOverflowException) {}
435 
436 	r.value = "127";
437 	assert(r.toInt!byte() == 127);
438 	assert(r.toInt!short() == 127);
439 	assert(r.toInt() == 127);
440 	assert(r.toInt!long() == 127);
441 
442 	stream = cast(char[])"*0\r\n";
443 	resp = parse(stream);
444 	assert(resp.count == 0);
445 	assert(resp.values.length == 0);
446 	assert(resp.values == []);
447 	assert(resp.toString == "[]");
448 	assert(!resp);
449 	try
450 		cast(int)resp;
451 	catch(RedisCastException) {}
452 
453 	//Testing opApply
454 	stream = cast(char[])"*0\r\n";
455 	resp = parse(stream);
456 	foreach(k, v; resp)
457 		assert(0, "opApply is broken");
458 	foreach(v; resp)
459 		assert(0, "opApply is broken");
460 
461 	stream = cast(char[])"$2\r\n$2\r\n";
462 	resp = parse(stream);
463 	foreach(k, v; resp)
464 		assert(0, "opApply is broken");
465 	foreach(v; resp)
466 		assert(0, "opApply is broken");
467 
468 	stream = cast(char[])":1000\r\n";
469 	resp = parse(stream);
470 	foreach(k, v; resp)
471 		assert(0, "opApply is broken");
472 	foreach(v; resp)
473 		assert(0, "opApply is broken");
474 
475 	//Testing opApplyReverse
476 	stream = cast(char[])"*0\r\n";
477 	resp = parse(stream);
478 	foreach_reverse(k, v; resp)
479 		assert(0, "opApplyReverse is broken");
480 	foreach_reverse(v; resp)
481 		assert(0, "opApplyReverse is broken");
482 }