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