1 module tinyredis.encoder; 2 3 /** 4 * Authors: Adil Baig, adil.baig@aidezigns.com 5 */ 6 7 import std.array : Appender, appender; 8 import std.conv : to, text; 9 import std.format : format; 10 import std.traits : isSomeChar, isSomeString, isArray; 11 12 alias encode = toMultiBulk; 13 14 /** 15 Take an array of (w|d)string arguments and concat them to a single Multibulk 16 17 Examples: 18 19 --- 20 toMultiBulk("SADD", ["fruits", "apple", "banana"]) == toMultiBulk("SADD fruits apple banana") 21 --- 22 */ 23 24 @trusted auto toMultiBulk(C, T)(const C[] command, T[][] args) if (isSomeChar!C && isSomeChar!T) 25 { 26 auto buffer = appender!(C[])(); 27 buffer.reserve(command.length + args.length * 70); //guesstimate 28 29 buffer ~= "*" ~ to!(C[])(args.length + 1) ~ "\r\n" ~ toBulk(command); 30 31 foreach (c; args) 32 buffer ~= toBulk(c); 33 34 return buffer[]; 35 } 36 37 /** 38 Take an array of varargs and concat them to a single Multibulk 39 40 Examples: 41 42 --- 43 toMultiBulk("SADD", "random", 1, 1.5, 'c') == toMultiBulk("SADD random 1 1.5 c") 44 --- 45 */ 46 @trusted auto toMultiBulk(C, T...)(const C[] command, T args) if (isSomeChar!C) 47 { 48 auto buffer = appender!(C[])(); 49 auto l = accumulator!(C,T)(buffer, args); 50 return "*" ~ to!(C[])(l + 1) ~ "\r\n" ~ toBulk(command) ~ buffer[]; 51 } 52 53 /** 54 Take an array of strings and concat them to a single Multibulk 55 56 Examples: 57 58 --- 59 toMultiBulk(["SET", "name", "adil"]) == toMultiBulk("SET name adil") 60 --- 61 */ 62 @trusted auto toMultiBulk(C)(const C[][] commands) if (isSomeChar!C) 63 { 64 auto buffer = appender!(C[])(); 65 buffer.reserve(commands.length * 50); 66 67 buffer ~= "*" ~ to!(C[])(commands.length) ~ "\r\n"; 68 69 foreach(c; commands) 70 buffer ~= toBulk(c); 71 72 return buffer[]; 73 } 74 75 /** 76 * Take a Redis command (w|d)string and convert it to a MultiBulk 77 */ 78 @trusted auto toMultiBulk(C)(const C[] command) if (isSomeChar!C) 79 { 80 alias command str; 81 82 size_t 83 start, 84 end, 85 bulk_count; 86 87 auto buffer = appender!(C[])(); 88 buffer.reserve(cast(size_t)(command.length * 1.2)); //Reserve for 20% overhead. 89 90 C c; 91 92 for(size_t i = 0; i < str.length; i++) { 93 c = str[i]; 94 95 /** 96 * Special support for quoted string so that command line support for 97 proper use of EVAL is available. 98 */ 99 if(c == '"' || c == '\'') { 100 start = i+1; 101 102 //Circuit breaker to avoid RangeViolation 103 while(++i < str.length 104 && (str[i] != c || (str[i] == c && str[i-1] == '\\')) 105 ){} 106 107 goto MULTIBULK_PROCESS; 108 } 109 110 if(c != ' ') 111 continue; 112 113 // c is a ' ' (space) here 114 if(i == start) { 115 start++; 116 end++; 117 continue; 118 } 119 120 MULTIBULK_PROCESS: 121 end = i; 122 buffer ~= toBulk(str[start .. end]); 123 start = end + 1; 124 bulk_count++; 125 } 126 127 //Nothing found? That means the string is just one Bulk 128 if(!buffer[].length) { 129 buffer ~= toBulk(str); 130 bulk_count++; 131 } 132 //If there's anything leftover, push it 133 else if(end+1 < str.length) { 134 buffer ~= toBulk(str[end+1 .. $]); 135 bulk_count++; 136 } 137 138 return "*%d\r\n%s".format(bulk_count, buffer[]); 139 } 140 141 @trusted auto toBulk(C)(const C[] str) if (isSomeChar!C) 142 { 143 return "$%d\r\n%s\r\n".format(str.length, str); 144 } 145 146 debug(tinyredis) @trusted C[] escape(C)(C[] str) if (isSomeChar!C) 147 { 148 import std.string : replace; 149 return str.replace("\r\n", "\\r\\n"); 150 } 151 152 private: 153 154 @trusted uint accumulator(C, T...)(Appender!(C[]) w, T args) 155 { 156 uint ctr; 157 158 static foreach (i, arg; args) { 159 static if(isSomeString!(T[i])) { 160 w ~= toBulk(arg); 161 ctr++; 162 } else static if(isArray!(T[i])) { 163 foreach(a; arg) 164 ctr += accumulator(w, a); 165 } else { 166 w ~= toBulk(text(arg)); 167 ctr++; 168 } 169 } 170 171 return ctr; 172 } 173 174 unittest { 175 176 assert(toBulk("$2") == "$2\r\n$2\r\n"); 177 assert(encode("GET *2") == "*2\r\n$3\r\nGET\r\n$2\r\n*2\r\n"); 178 assert(encode("TTL myset") == "*2\r\n$3\r\nTTL\r\n$5\r\nmyset\r\n"); 179 assert(encode("TTL", "myset") == "*2\r\n$3\r\nTTL\r\n$5\r\nmyset\r\n"); 180 181 enum lua = "return redis.call('set','foo','bar')"; 182 assert(encode("EVAL \"" ~ lua ~ "\" 0") == "*3\r\n$4\r\nEVAL\r\n$"~to!string(lua.length)~"\r\n"~lua~"\r\n$1\r\n0\r\n"); 183 184 assert(encode("\"" ~ lua ~ "\" \"" ~ lua ~ "\" ") == "*2\r\n$"~to!string(lua.length)~"\r\n"~lua~"\r\n$"~to!string(lua.length)~"\r\n"~lua~"\r\n"); 185 assert(encode("eval \"" ~ lua ~ "\" " ~ "0") == encode("eval", lua, 0)); 186 187 assert(encode("SREM", ["myset", "$3", "$4", "two words"]) == encode("SREM myset $3 $4 'two words'")); 188 assert(encode("SREM", "myset", "$3", "$4", "two words") == encode("SREM myset $3 $4 'two words'")); 189 assert(encode(["SREM", "myset", "$3", "$4", "two words"]) == encode("SREM myset $3 $4 'two words'")); 190 191 assert(encode("SADD", "numbers", [1,2,3]) == encode("SADD numbers 1 2 3")); 192 assert(encode("SADD", "numbers", 1,2,3, [4,5]) == encode("SADD numbers 1 2 3 4 5")); 193 assert(encode("TTL", "myset") == encode("TTL myset")); 194 assert(encode("TTL", "myset") == encode("TTL", ["myset"])); 195 196 assert(encode("ZADD", "mysortedset", 1, "{\"a\": \"b\"}") == "*4\r\n$4\r\nZADD\r\n$11\r\nmysortedset\r\n$1\r\n1\r\n$10\r\n{\"a\": \"b\"}\r\n"); 197 assert(encode("ZADD", "mysortedset", "1", "{\"a\": \"b\"}") == "*4\r\n$4\r\nZADD\r\n$11\r\nmysortedset\r\n$1\r\n1\r\n$10\r\n{\"a\": \"b\"}\r\n"); 198 }