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 }