diff --git a/Makefile b/Makefile
index a44ea40..3408b08 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,8 @@
OUT=turnip
HIREDIS_OBJ=hiredis/hiredis.o hiredis/sds.o hiredis/net.o hiredis/async.o
JANSSON_OBJ=jansson/src/dump.o jansson/src/error.o jansson/src/hashtable.o jansson/src/load.o jansson/src/strbuffer.o jansson/src/utf.o jansson/src/value.o jansson/src/variadic.o
-OBJS=turnip.o conf.o json.o cmd.o server.o $(HIREDIS_OBJ) $(JANSSON_OBJ)
+FORMAT_OBJS=formats/json.o formats/raw.o
+OBJS=turnip.o conf.o $(FORMAT_OBJS) cmd.o server.o $(HIREDIS_OBJ) $(JANSSON_OBJ)
CFLAGS=-O0 -ggdb -Wall -Wextra -I. -Ijansson/src
LDFLAGS=-levent
diff --git a/README.markdown b/README.markdown
index aa3b9f2..28cc259 100644
--- a/README.markdown
+++ b/README.markdown
@@ -18,13 +18,13 @@ curl -d "GET/hello" http://127.0.0.1:7379/
# Features
* GET and POST are supported.
* JSON output, optional JSONP parameter.
+* Raw Redis 2.0 protocol output with `?format=raw`
* HTTP 1.1 pipelining (45 kqps on a desktop Linux machine.)
* Connects to Redis using a TCP or UNIX socket.
# Ideas
* Add meta-data info per key (MIME type in a second key, for instance).
-* Add a “raw” output format, and find a way to format multi-bulk data in that format.
* Support PUT, DELETE, HEAD?
* Support pub/sub.
* Disable MULTI/EXEC/DISCARD/WATCH.
@@ -34,40 +34,73 @@ curl -d "GET/hello" http://127.0.0.1:7379/
* Restrict commands by IP range
* Send your ideas using the github tracker or on twitter [@yowgi](http://twitter.com/yowgi).
-# HTTP error codes that could be used (although that's not the case at the moment)
-* Missing key: 404 Not Found
-* Timeout on the redis side: 503 Service Unavailable
-* Unknown verb: 405 Method Not Allowed
+# HTTP error codes
+* Unknown HTTP verb: 405 Method Not Allowed
+* Disconnected from redis: 503 Service Unavailable
+* Could also be used:
+ * Timeout on the redis side: 503 Service Unavailable
+ * Missing key: 404 Not Found
# JSON output
The URI `/COMMAND/arg0/arg1/.../argN` returns a JSON object with the command as a key and the result as a value.
+JSON is the default output format.
**Examples:**
// string
-$ curl http://127.0.0.1:7379/GET/y
+$ curl http://127.0.0.1:7379/GET/y
{"GET":"41"}
// number
-$ curl http://127.0.0.1:7379/INCR/y
+$ curl http://127.0.0.1:7379/INCR/y
{"INCR":42}
// list
-$ curl http://127.0.0.1:7379/LRANGE/x/0/1
+$ curl http://127.0.0.1:7379/LRANGE/x/0/1
{"LRANGE":["abc","def"]}
// status
-$ curl http://127.0.0.1:7379/TYPE/y
+$ curl http://127.0.0.1:7379/TYPE/y
{"TYPE":[true,"string"]}
// error, which is basically a status
-$ curl http://127.0.0.1:7379/MAKE-ME-COFFEE
+$ curl http://127.0.0.1:7379/MAKE-ME-COFFEE
{"MAKE-ME-COFFEE":[false,"ERR unknown command 'MAKE-ME-COFFEE'"]}
-
// JSONP callback:
$ curl "http://127.0.0.1:7379/TYPE/y?jsonp=myCustomFunction"
myCustomFunction({"TYPE":[true,"string"]})
+
+
+
+# RAW output
+This is the raw Redis output.
+
+
+// string
+$ curl http://127.0.0.1:7379/GET/z?format=raw
+hello
+
+// number
+curl http://127.0.0.1:7379/INCR/a?format=raw
+:2
+
+// list
+$ curl http://127.0.0.1:7379/LRANGE/x/0/-1?format=raw
+*2
+$3
+abc
+$3
+def
+
+// status
+$ curl http://127.0.0.1:7379/TYPE/y?format=raw
++zset
+
+// error, which is basically a status
+$ curl http://127.0.0.1:7379/MAKE-ME-COFFEE?format=raw
+-ERR unknown command 'ABC'
+
diff --git a/cmd.c b/cmd.c
index 6ff774c..0c3053a 100644
--- a/cmd.c
+++ b/cmd.c
@@ -1,5 +1,8 @@
#include "cmd.h"
-#include "json.h"
+#include "server.h"
+
+#include "formats/json.h"
+#include "formats/raw.h"
#include
#include
@@ -30,17 +33,17 @@ cmd_free(struct cmd *c) {
}
void
-cmd_run(redisAsyncContext *c, struct evhttp_request *rq,
+cmd_run(struct server *s, struct evhttp_request *rq,
const char *uri, size_t uri_len) {
char *qmark = strchr(uri, '?');
char *slash = strchr(uri, '/');
+ const char *p;
int cmd_len;
int param_count = 0, cur_param = 1;
struct cmd *cmd;
-
- const char *p;
+ formatting_fun fun;
/* count arguments */
if(qmark) {
@@ -61,12 +64,15 @@ cmd_run(redisAsyncContext *c, struct evhttp_request *rq,
/* parse URI parameters */
evhttp_parse_query(uri, &cmd->uri_params);
+ /* get output formatting function */
+ fun = get_formatting_funtion(&cmd->uri_params);
+
/* there is always a first parameter, it's the command name */
cmd->argv[0] = uri;
cmd->argv_len[0] = cmd_len;
if(!slash) {
- redisAsyncCommandArgv(c, json_reply, cmd, 1, cmd->argv, cmd->argv_len);
+ redisAsyncCommandArgv(s->ac, fun, cmd, 1, cmd->argv, cmd->argv_len);
return;
}
p = slash + 1;
@@ -90,6 +96,27 @@ cmd_run(redisAsyncContext *c, struct evhttp_request *rq,
cur_param++;
}
- redisAsyncCommandArgv(c, json_reply, cmd, param_count, cmd->argv, cmd->argv_len);
+ redisAsyncCommandArgv(s->ac, fun, cmd, param_count, cmd->argv, cmd->argv_len);
}
+
+
+formatting_fun
+get_formatting_funtion(struct evkeyvalq *params) {
+
+ struct evkeyval *kv;
+
+ /* check for JSONP */
+ TAILQ_FOREACH(kv, params, next) {
+ if(strcmp(kv->key, "format") == 0) {
+ if(strcmp(kv->value, "raw") == 0) {
+ return raw_reply;
+ } else if(strcmp(kv->value, "json") == 0) {
+ return json_reply;
+ }
+ break;
+ }
+ }
+
+ return json_reply;
+}
diff --git a/cmd.h b/cmd.h
index 3dd470d..74af65c 100644
--- a/cmd.h
+++ b/cmd.h
@@ -8,6 +8,9 @@
#include
struct evhttp_request;
+struct server;
+
+typedef void (*formatting_fun)(redisAsyncContext *, void *, void *);
struct cmd {
@@ -26,7 +29,10 @@ void
cmd_free(struct cmd *c);
void
-cmd_run(redisAsyncContext *c, struct evhttp_request *rq,
+cmd_run(struct server *s, struct evhttp_request *rq,
const char *uri, size_t uri_len);
+formatting_fun
+get_formatting_funtion(struct evkeyvalq *params);
+
#endif
diff --git a/json.c b/formats/json.c
similarity index 100%
rename from json.c
rename to formats/json.c
diff --git a/json.h b/formats/json.h
similarity index 100%
rename from json.h
rename to formats/json.h
diff --git a/formats/raw.c b/formats/raw.c
new file mode 100644
index 0000000..d7d0033
--- /dev/null
+++ b/formats/raw.c
@@ -0,0 +1,146 @@
+#include "raw.h"
+#include "cmd.h"
+
+#include
+#include
+#include
+#include
+#include
+
+static char *
+raw_wrap(const redisReply *r, size_t *sz);
+
+void
+raw_reply(redisAsyncContext *c, void *r, void *privdata) {
+
+ (void)c;
+ struct evbuffer *body;
+ redisReply *reply = r;
+ struct cmd *cmd = privdata;
+ char *raw_out;
+ size_t sz;
+
+ if (reply == NULL) {
+ evhttp_send_reply(cmd->rq, 404, "Not Found", NULL);
+ return;
+ }
+
+ raw_out = raw_wrap(r, &sz);
+
+ /* send reply */
+ body = evbuffer_new();
+ evbuffer_add(body, raw_out, sz);
+ evhttp_add_header(cmd->rq->output_headers, "Content-Type", "text/plain");
+ evhttp_send_reply(cmd->rq, 200, "OK", body);
+
+ /* cleanup */
+ evbuffer_free(body);
+ freeReplyObject(r);
+ cmd_free(cmd);
+ free(raw_out);
+}
+
+static int
+integer_length(long long int i) {
+ int sz = 0;
+ int ci = abs(i);
+ while (ci > 0) {
+ ci = (ci/10);
+ sz += 1;
+ }
+ if(i == 0) { /* log 0 doesn't make sense. */
+ sz = 1;
+ } else if(i < 0) { /* allow for neg sign as well. */
+ sz++;
+ }
+ return sz;
+}
+
+static char *
+raw_array(const redisReply *r, size_t *sz) {
+
+ unsigned int i;
+ char *ret, *p;
+
+ /* compute size */
+ *sz = 0;
+ *sz += 1 + integer_length(r->elements) + 1;
+ for(i = 0; i < r->elements; ++i) {
+ redisReply *e = r->element[i];
+ switch(e->type) {
+ case REDIS_REPLY_STRING:
+ *sz += 1 + integer_length(e->len) + 1
+ + e->len + 1;
+ break;
+ case REDIS_REPLY_INTEGER:
+ *sz += 1 + integer_length(integer_length(e->integer)) + 1
+ + integer_length(e->integer) + 1;
+ break;
+
+ }
+ }
+
+ /* allocate */
+ p = ret = malloc(*sz);
+ p += sprintf(p, "*%zd\n", r->elements);
+
+ /* copy */
+ for(i = 0; i < r->elements; ++i) {
+ redisReply *e = r->element[i];
+ switch(e->type) {
+ case REDIS_REPLY_STRING:
+ p += sprintf(p, "$%d\n", e->len);
+ memcpy(p, e->str, e->len);
+ p += e->len;
+ *p = '\n';
+ p++;
+ break;
+ case REDIS_REPLY_INTEGER:
+ p += sprintf(p, "$%d\n%lld\n",
+ integer_length(e->integer), e->integer);
+ break;
+ }
+ }
+
+ return ret;
+}
+
+static char *
+raw_wrap(const redisReply *r, size_t *sz) {
+
+ char *ret;
+
+ switch(r->type) {
+ case REDIS_REPLY_STATUS:
+ case REDIS_REPLY_ERROR:
+ *sz = 2 + r->len;
+ ret = malloc(*sz);
+ ret[0] = (r->type == REDIS_REPLY_STATUS?'+':'-');
+ memcpy(ret+1, r->str, *sz-2);
+ memcpy(ret+*sz - 1, "\n", 1);
+ return ret;
+
+ case REDIS_REPLY_STRING:
+ *sz = 1 + r->len;
+ ret = malloc(*sz);
+ memcpy(ret, r->str, *sz - 1);
+ memcpy(ret + *sz - 1, "\n", 1);
+ return ret;
+
+ case REDIS_REPLY_INTEGER:
+ *sz = 2 + integer_length(r->integer);
+ ret = malloc(3+*sz);
+ sprintf(ret, ":%lld\n", r->integer);
+ return ret;
+
+ case REDIS_REPLY_ARRAY:
+ return raw_array(r, sz);
+
+ default:
+ *sz = 4;
+ ret = malloc(*sz);
+ memcpy(ret, "$-1\n", 4);
+ return ret;
+ }
+}
+
diff --git a/formats/raw.h b/formats/raw.h
new file mode 100644
index 0000000..f25607f
--- /dev/null
+++ b/formats/raw.h
@@ -0,0 +1,13 @@
+#ifndef RAW_H
+#define RAW_H
+
+#include
+#include
+
+struct cmd;
+
+void
+raw_reply(redisAsyncContext *c, void *r, void *privdata);
+
+
+#endif
diff --git a/turnip.c b/turnip.c
index 50342ed..55c6f12 100644
--- a/turnip.c
+++ b/turnip.c
@@ -26,11 +26,11 @@ on_request(struct evhttp_request *rq, void *ctx) {
switch(rq->type) {
case EVHTTP_REQ_GET:
- cmd_run(s->ac, rq, 1+uri, strlen(uri)-1);
+ cmd_run(s, rq, 1+uri, strlen(uri)-1);
break;
case EVHTTP_REQ_POST:
- cmd_run(s->ac, rq,
+ cmd_run(s, rq,
(const char*)EVBUFFER_DATA(rq->input_buffer),
EVBUFFER_LENGTH(rq->input_buffer));
break;