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 }