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 }