1 module redis.encoder; 2 3 import std.array : appender; 4 import std.traits : isSomeChar, isSomeString, isArray; 5 import std.conv : to, text; 6 7 public: 8 9 alias encode = toMultiBulk; 10 11 /** 12 Take an array of (w|d)string arguments and concat them to a single Multibulk 13 14 Examples: 15 16 --- 17 toMultiBulk("SADD", ["fruits", "apple", "banana"]) == toMultiBulk("SADD fruits apple banana") 18 --- 19 */ 20 21 @trusted auto toMultiBulk(C, T)(const C[] command, T[][] args) if (isSomeChar!C && isSomeChar!T) 22 { 23 auto buffer = appender!(C[])(); 24 buffer.reserve(command.length + args.length * 70); //guesstimate 25 26 buffer ~= "*" ~ to!(C[])(args.length + 1) ~ "\r\n" ~ toBulk(command); 27 28 foreach (c; args) { 29 buffer ~= toBulk(c); 30 } 31 32 return buffer.data; 33 } 34 35 /** 36 Take an array of varargs and concat them to a single Multibulk 37 38 Examples: 39 40 --- 41 toMultiBulk("SADD", "random", 1, 1.5, 'c') == toMultiBulk("SADD random 1 1.5 c") 42 --- 43 */ 44 @trusted auto toMultiBulk(C, T...)(const C[] command, T args) if (isSomeChar!C) 45 { 46 auto buffer = appender!(C[])(); 47 auto l = accumulator!(C,T)(buffer, args); 48 auto str = "*" ~ to!(C[])(l + 1) ~ "\r\n" ~ toBulk(command) ~ buffer.data; 49 return str; 50 } 51 52 /** 53 Take an array of strings and concat them to a single Multibulk 54 55 Examples: 56 57 --- 58 toMultiBulk(["SET", "name", "adil"]) == toMultiBulk("SET name adil") 59 --- 60 */ 61 @trusted auto toMultiBulk(C)(const C[][] commands) if (isSomeChar!C) 62 { 63 auto buffer = appender!(C[])(); 64 buffer.reserve(commands.length * 100); 65 66 buffer ~= "*" ~ to!(C[])(commands.length) ~ "\r\n"; 67 68 foreach(c; commands) { 69 buffer ~= toBulk(c); 70 } 71 72 return buffer.data; 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 114 // c is a ' ' (space) here 115 if(i == start) { 116 start++; 117 end++; 118 continue; 119 } 120 121 MULTIBULK_PROCESS: 122 end = i; 123 buffer ~= toBulk(str[start .. end]); 124 start = end + 1; 125 bulk_count++; 126 } 127 128 //Nothing found? That means the string is just one Bulk 129 if(!buffer.data.length) { 130 buffer ~= toBulk(str); 131 bulk_count++; 132 } 133 //If there's anything leftover, push it 134 else if(end+1 < str.length) { 135 buffer ~= toBulk(str[end+1 .. $]); 136 bulk_count++; 137 } 138 139 import std..string : format; 140 return format!(C)("*%d\r\n%s", bulk_count, buffer.data); 141 } 142 143 @trusted auto toBulk(C)(const C[] str) if (isSomeChar!C) 144 { 145 import std..string : format; 146 return format!(C)("$%d\r\n%s\r\n", str.length, str); 147 } 148 149 debug(redis) @trusted C[] escape(C)(C[] str) if (isSomeChar!C) 150 { 151 import std..string : replace; 152 return replace(str,"\r\n","\\r\\n"); 153 } 154 155 private : 156 157 import std.array : Appender; 158 159 @trusted uint accumulator(C, T...)(Appender!(C[]) w, T args) 160 { 161 uint ctr = 0; 162 163 foreach (i, arg; args) { 164 static if(isSomeString!(typeof(arg))) { 165 w ~= toBulk(arg); 166 ctr++; 167 } else static if(isArray!(typeof(arg))) { 168 foreach(a; arg) { 169 ctr += accumulator(w, a); 170 } 171 } else { 172 w ~= toBulk(text(arg)); 173 ctr++; 174 } 175 } 176 177 return ctr; 178 } 179 180 unittest { 181 182 assert(toBulk("$2") == "$2\r\n$2\r\n"); 183 assert(encode("GET *2") == "*2\r\n$3\r\nGET\r\n$2\r\n*2\r\n"); 184 assert(encode("TTL myset") == "*2\r\n$3\r\nTTL\r\n$5\r\nmyset\r\n"); 185 assert(encode("TTL", "myset") == "*2\r\n$3\r\nTTL\r\n$5\r\nmyset\r\n"); 186 187 auto lua = "return redis.call('set','foo','bar')"; 188 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"); 189 190 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"); 191 assert(encode("eval \"" ~ lua ~ "\" " ~ "0") == encode("eval", lua, 0)); 192 193 assert(encode("SREM", ["myset", "$3", "$4", "two words"]) == encode("SREM myset $3 $4 'two words'")); 194 assert(encode("SREM", "myset", "$3", "$4", "two words") == encode("SREM myset $3 $4 'two words'")); 195 assert(encode(["SREM", "myset", "$3", "$4", "two words"]) == encode("SREM myset $3 $4 'two words'")); 196 197 assert(encode("SADD", "numbers", [1,2,3]) == encode("SADD numbers 1 2 3")); 198 assert(encode("SADD", "numbers", 1,2,3, [4,5]) == encode("SADD numbers 1 2 3 4 5")); 199 assert(encode("TTL", "myset") == encode("TTL myset")); 200 assert(encode("TTL", "myset") == encode("TTL", ["myset"])); 201 202 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"); 203 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"); 204 }