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 }