diff --git a/example/module.c b/example/module.c index 295c6d2..a8a0101 100644 --- a/example/module.c +++ b/example/module.c @@ -1,55 +1,145 @@ #include "../redismodule.h" #include "../rmutil/util.h" #include "../rmutil/strings.h" +#include "../rmutil/test_util.h" -/* EXAMPLE.PARSE [SUM ] | [PROD ] -* Demonstrates the automatic arg parsing utility. +/* EXAMPLE.PARSE [SUM ] | [PROD ] +* Demonstrates the automatic arg parsing utility. * If the command receives "SUM " it returns their sum * If it receives "PROD " it returns their product */ int ParseCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { - - // we must have at least 4 args - if (argc < 4) { - return RedisModule_WrongArity(ctx); - } + // we must have at least 4 args + if (argc < 4) { + return RedisModule_WrongArity(ctx); + } - // init auto memory for created strings - RedisModule_AutoMemory(ctx); - long long x,y; - - - // If we got SUM - return the sum of 2 consecutive arguments - if (RMUtil_ParseArgsAfter("SUM", argv, argc, "ll", &x, &y) == REDISMODULE_OK) { - RedisModule_ReplyWithLongLong(ctx, x+y); - return REDISMODULE_OK; - } - // If we got PROD - return the product of 2 consecutive arguments - if (RMUtil_ParseArgsAfter("PROD", argv, argc, "ll", &x, &y) == REDISMODULE_OK) { - RedisModule_ReplyWithLongLong(ctx, x*y); - return REDISMODULE_OK; - } - - // something is fishy... - RedisModule_ReplyWithError(ctx, "Invalid arguments"); - + // init auto memory for created strings + RedisModule_AutoMemory(ctx); + long long x, y; + + // If we got SUM - return the sum of 2 consecutive arguments + if (RMUtil_ParseArgsAfter("SUM", argv, argc, "ll", &x, &y) == + REDISMODULE_OK) { + RedisModule_ReplyWithLongLong(ctx, x + y); return REDISMODULE_OK; + } + // If we got PROD - return the product of 2 consecutive arguments + if (RMUtil_ParseArgsAfter("PROD", argv, argc, "ll", &x, &y) == + REDISMODULE_OK) { + RedisModule_ReplyWithLongLong(ctx, x * y); + return REDISMODULE_ERR; + } + + // something is fishy... + RedisModule_ReplyWithError(ctx, "Invalid arguments"); + + return REDISMODULE_OK; } +/* +* example.HGETSET +* Atomically set a value in a HASH key to and return its value before +* the HSET. +* +* Basically atomic HGET + HSET +*/ +int HGetSetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { -int RedisModule_OnLoad(RedisModuleCtx *ctx) { - - if (RedisModule_Init(ctx,"example",1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) { - return REDISMODULE_ERR; - } + // we need EXACTLY 4 arguments + if (argc != 4) { + return RedisModule_WrongArity(ctx); + } + RedisModule_AutoMemory(ctx); + // open the key and make sure it's indeed a HASH and not empty + RedisModuleKey *key = + RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ | REDISMODULE_WRITE); + if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_HASH && + RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_EMPTY) { + return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); + } - if (RedisModule_CreateCommand(ctx, "example.parse", ParseCommand, "readonly", 1,1,1) - == REDISMODULE_ERR) { - return REDISMODULE_ERR; - } + // get the current value of the hash element + RedisModuleCallReply *rep = + RedisModule_Call(ctx, "HGET", "ss", argv[1], argv[2]); + RMUTIL_ASSERT_NOERROR(rep); + // set the new value of the element + RedisModuleCallReply *srep = + RedisModule_Call(ctx, "HSET", "sss", argv[1], argv[2], argv[3]); + RMUTIL_ASSERT_NOERROR(srep); + // if the value was null before - we just return null + if (RedisModule_CallReplyType(rep) == REDISMODULE_REPLY_NULL) { + RedisModule_ReplyWithNull(ctx); return REDISMODULE_OK; + } + + // forward the HGET reply to the client + RedisModule_ReplyWithCallReply(ctx, rep); + return REDISMODULE_OK; +} + +// Test the the PARSE command +int testParse(RedisModuleCtx *ctx) { + + RedisModuleCallReply *r = + RedisModule_Call(ctx, "example.parse", "ccc", "SUM", "5", "2"); + RMUtil_Assert(RedisModule_CallReplyType(r) == REDISMODULE_REPLY_INTEGER); + RMUtil_AssertReplyEquals(r, "7"); + + r = RedisModule_Call(ctx, "example.parse", "ccc", "PROD", "5", "2"); + RMUtil_Assert(RedisModule_CallReplyType(r) == REDISMODULE_REPLY_INTEGER); + RMUtil_AssertReplyEquals(r, "10"); + return 0; +} + +// test the HGETSET command +int testHgetSet(RedisModuleCtx *ctx) { + RedisModuleCallReply *r = RedisModule_Call(ctx, "example.hgetset", "ccc", "foo", "bar", "baz"); + RMUtil_Assert(RedisModule_CallReplyType(r) != REDISMODULE_REPLY_ERROR); + + r = RedisModule_Call(ctx, "example.hgetset", "ccc", "foo", "bar", "bag"); + RMUtil_Assert(RedisModule_CallReplyType(r) == REDISMODULE_REPLY_STRING); + RMUtil_AssertReplyEquals(r, "baz"); + r = RedisModule_Call(ctx, "example.hgetset", "ccc", "foo", "bar", "bang"); + RMUtil_AssertReplyEquals(r, "bag"); + return 0; +} + +// Unit test entry point for the module +int TestModule(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + RedisModule_AutoMemory(ctx); + + RMUtil_Test(testParse); + RMUtil_Test(testHgetSet); + + RedisModule_ReplyWithSimpleString(ctx, "PASS"); + return REDISMODULE_OK; +} + +int RedisModule_OnLoad(RedisModuleCtx *ctx) { + + // Register the module itself + if (RedisModule_Init(ctx, "example", 1, REDISMODULE_APIVER_1) == + REDISMODULE_ERR) { + return REDISMODULE_ERR; + } + + // register example.parse - the default registration syntax + if (RedisModule_CreateCommand(ctx, "example.parse", ParseCommand, "readonly", + 1, 1, 1) == REDISMODULE_ERR) { + return REDISMODULE_ERR; + } + + // register example.hgetset - using the shortened utility registration macro + RMUtil_RegisterWriteCmd(ctx, "example.hgetset", HGetSetCommand); + + + // register the unit test + RMUtil_RegisterWriteCmd(ctx, "example.test", TestModule); + + return REDISMODULE_OK; } \ No newline at end of file diff --git a/rmutil/test_util.h b/rmutil/test_util.h new file mode 100644 index 0000000..5a6273d --- /dev/null +++ b/rmutil/test_util.h @@ -0,0 +1,67 @@ +#ifndef __TEST_UTIL_H__ +#define __TEST_UTIL_H__ + +#include "util.h" +#include +#include +#include + + +#define RMUtil_Test(f) \ + if (argc < 2 || RMUtil_ArgExists(__STRING(f), argv, argc, 1)) { \ + int rc = f(ctx); \ + if (rc != REDISMODULE_OK) { \ + RedisModule_ReplyWithError(ctx, "Test " __STRING(f) " FAILED"); \ + return REDISMODULE_ERR;\ + }\ + } + + +#define RMUtil_Assert(expr) if (!(expr)) { fprintf (stderr, "Assertion '%s' Failed\n", __STRING(expr)); return REDISMODULE_ERR; } + +#define RMUtil_AssertReplyEquals(rep, cstr) RMUtil_Assert( \ + RMUtil_StringEquals(RedisModule_CreateStringFromCallReply(rep), RedisModule_CreateString(ctx, cstr, strlen(cstr))) \ + ) +# + +/** +* Create an arg list to pass to a redis command handler manually, based on the format in fmt. +* The accepted format specifiers are: +* c - for null terminated c strings +* s - for RedisModuleString* objects +* l - for longs +* +* Example: RMUtil_MakeArgs(ctx, &argc, "clc", "hello", 1337, "world"); +* +* Returns an array of RedisModuleString pointers. The size of the array is store in argcp +*/ +RedisModuleString **RMUtil_MakeArgs(RedisModuleCtx *ctx, int *argcp, const char *fmt, ...) { + + va_list ap; + va_start(ap, fmt); + RedisModuleString **argv = calloc(strlen(fmt), sizeof(RedisModuleString*)); + int argc = 0; + const char *p = fmt; + while(*p) { + if (*p == 'c') { + char *cstr = va_arg(ap,char*); + argv[argc++] = RedisModule_CreateString(ctx, cstr, strlen(cstr)); + } else if (*p == 's') { + argv[argc++] = va_arg(ap,void*);; + } else if (*p == 'l') { + long ll = va_arg(ap,long long); + argv[argc++] = RedisModule_CreateStringFromLongLong(ctx, ll); + } else { + goto fmterr; + } + p++; + } + *argcp = argc; + + return argv; +fmterr: + free(argv); + return NULL; +} + +#endif \ No newline at end of file diff --git a/rmutil/util.h b/rmutil/util.h index 7289edc..322da89 100644 --- a/rmutil/util.h +++ b/rmutil/util.h @@ -5,7 +5,7 @@ #include // make sure the response is not NULL or an error, and if it is sends the error to the client and exit the current function -#define REDIS_ASSERT_NOERROR(r) \ +#define RMUTIL_ASSERT_NOERROR(r) \ if (r == NULL) { \ return RedisModule_ReplyWithError(ctx,"ERR reply is NULL"); \ } else if (RedisModule_CallReplyType(r) == REDISMODULE_REPLY_ERROR) { \ @@ -14,6 +14,13 @@ } +#define __rmutil_register_cmd(ctx, cmd, f, mode) if (RedisModule_CreateCommand(ctx, cmd, f, mode, \ + 1, 1, 1) == REDISMODULE_ERR) return REDISMODULE_ERR; + +#define RMUtil_RegisterReadCmd(ctx, cmd, f) __rmutil_register_cmd(ctx, cmd, f, "readonly") } + +#define RMUtil_RegisterWriteCmd(ctx, cmd, f) __rmutil_register_cmd(ctx, cmd, f, "write") + /* RedisModule utilities. */ /* Return the offset of an arg if it exists in the arg list, or 0 if it's not there */