-module('Redis', package.seeall)
+local _G = _G
+local require, error, type, print = require, error, type, print
+local table, pairs, tostring, tonumber = table, pairs, tostring, tonumber
-require('socket') -- requires LuaSocket as a dependency
+module('Redis')
--- ############################################################################
+local socket = require('socket') -- requires LuaSocket as a dependency
-local protocol = {
- newline = '\r\n', ok = 'OK', err = 'ERR', null = 'nil',
-}
+local redis_commands = {}
+local network, request, response, utils = {}, {}, {}, {}, {}
--- ############################################################################
+local protocol = { newline = '\r\n', ok = 'OK', err = 'ERR', null = 'nil' }
+
+local function toboolean(value) return value == 1 end
+
+local function load_methods(proto, methods)
+ local redis = _G.setmetatable ({}, _G.getmetatable(proto))
+ for i, v in pairs(proto) do redis[i] = v end
-local function toboolean(value)
- return value == 1
+ for i, v in pairs(methods) do redis[i] = v end
+ return redis
end
-local function _write(self, buffer)
- local _, err = self.socket:send(buffer)
+-- ############################################################################
+
+function network.write(client, buffer)
+ local _, err = client.socket:send(buffer)
if err then error(err) end
end
-local function _read(self, len)
+function network.read(client, len)
if len == nil then len = '*l' end
- local line, err = self.socket:receive(len)
+ local line, err = client.socket:receive(len)
if not err then return line else error('Connection error: ' .. err) end
end
-- ############################################################################
-local function _read_response(self)
- if options and options.close == true then return end
-
- local res = _read(self)
+function response.read(client)
+ local res = network.read(client)
local prefix = res:sub(1, -#res)
local response_handler = protocol.prefixes[prefix]
if not response_handler then
error("Unknown response prefix: " .. prefix)
else
- return response_handler(self, res)
+ return response_handler(client, res)
+ end
+end
+
+function response.status(client, data)
+ local sub = data:sub(2)
+ if sub == protocol.ok then return true else return sub end
+end
+
+function response.error(client, data)
+ local err_line = data:sub(2)
+
+ if err_line:sub(1, 3) == protocol.err then
+ error("Redis error: " .. err_line:sub(5))
+ else
+ error("Redis error: " .. err_line)
+ end
+end
+
+function response.bulk(client, data)
+ local str = data:sub(2)
+ local len = tonumber(str)
+
+ if not len then
+ error('Cannot parse ' .. str .. ' as data length.')
+ else
+ if len == -1 then return nil end
+ local next_chunk = network.read(client, len + 2)
+ return next_chunk:sub(1, -3);
+ end
+end
+
+function response.multibulk(client, data)
+ local str = data:sub(2)
+
+ -- TODO: add a check if the returned value is indeed a number
+ local list_count = tonumber(str)
+
+ if list_count == -1 then
+ return nil
+ else
+ local list = {}
+
+ if list_count > 0 then
+ for i = 1, list_count do
+ table.insert(list, i, response.bulk(client, network.read(client)))
+ end
+ end
+
+ return list
end
end
+function response.integer(client, data)
+ local res = data:sub(2)
+ local number = tonumber(res)
-local function _send_raw(self, buffer)
+ if not number then
+ if res == protocol.null then
+ return nil
+ else
+ error('Cannot parse ' .. res .. ' as numeric response.')
+ end
+ end
+
+ return number
+end
+
+protocol.prefixes = {
+ ['+'] = response.status,
+ ['-'] = response.error,
+ ['$'] = response.bulk,
+ ['*'] = response.multibulk,
+ [':'] = response.integer,
+}
+
+-- ############################################################################
+
+function request.raw(client, buffer)
-- TODO: optimize
local bufferType = type(buffer)
if bufferType == 'string' then
- _write(self, buffer)
+ network.write(client, buffer)
elseif bufferType == 'table' then
- _write(self, table.concat(buffer))
+ network.write(client, table.concat(buffer))
else
error('Argument error: ' .. bufferType)
end
- return _read_response(self)
+ return response.read(client)
end
-local function _send_inline(self, command, ...)
+function request.inline(client, command, ...)
if arg.n == 0 then
- _write(self, command .. protocol.newline)
+ network.write(client, command .. protocol.newline)
else
local arguments = arg
arguments.n = nil
arguments = ''
end
- _write(self, command .. ' ' .. arguments .. protocol.newline)
+ network.write(client, command .. ' ' .. arguments .. protocol.newline)
end
- return _read_response(self)
+ return response.read(client)
end
-local function _send_bulk(self, command, ...)
+function request.bulk(client, command, ...)
local arguments = arg
local data = tostring(table.remove(arguments))
arguments.n = nil
arguments = ''
end
- return _send_raw(self, {
+ return request.raw(client, {
command, ' ', arguments, ' ', #data, protocol.newline, data, protocol.newline
})
end
+-- ############################################################################
-local function _read_line(self, response)
- return response:sub(2)
-end
-
-local function _read_error(self, response)
- local err_line = response:sub(2)
-
- if err_line:sub(1, 3) == protocol.err then
- error("Redis error: " .. err_line:sub(5))
- else
- error("Redis error: " .. err_line)
+local function custom(command, send, parse)
+ return function(self, ...)
+ local reply = send(self, command, ...)
+ if parse then
+ return parse(reply, command, ...)
+ else
+ return reply
+ end
end
end
-local function _read_bulk(self, response)
- local str = response:sub(2)
- local len = tonumber(str)
-
- if not len then
- error('Cannot parse ' .. str .. ' as data length.')
- else
- if len == -1 then return nil end
- local data = _read(self, len + 2)
- return data:sub(1, -3);
- end
+local function bulk(command, reader)
+ return custom(command, request.bulk, reader)
end
-local function _read_multibulk(self, response)
- local str = response:sub(2)
-
- -- TODO: add a check if the returned value is indeed a number
- local list_count = tonumber(str)
-
- if list_count == -1 then
- return nil
- else
- local list = {}
-
- if list_count > 0 then
- for i = 1, list_count do
- table.insert(list, i, _read_bulk(self, _read(self)))
- end
- end
-
- return list
- end
+local function inline(command, reader)
+ return custom(command, request.inline, reader)
end
-local function _read_integer(self, response)
- local res = response:sub(2)
- local number = tonumber(res)
+-- ############################################################################
- if not number then
- if res == protocol.null then
- return nil
- else
- error('Cannot parse ' .. res .. ' as numeric response.')
- end
+function connect(host, port)
+ local client_socket = socket.connect(host, port)
+ if not client_socket then
+ error('Could not connect to ' .. host .. ':' .. port)
end
- return number
-end
-
--- ############################################################################
+ local redis_client = {
+ socket = client_socket,
+ raw_cmd = function(self, buffer)
+ return request.raw(self, buffer .. protocol.newline)
+ end,
+ }
-protocol.prefixes = {
- ['+'] = _read_line,
- ['-'] = _read_error,
- ['$'] = _read_bulk,
- ['*'] = _read_multibulk,
- [':'] = _read_integer,
-}
+ return load_methods(redis_client, redis_commands)
+end
-- ############################################################################
-local methods = {
+redis_commands = {
-- miscellaneous commands
- ping = {
- 'PING', _send_inline, function(response)
+ ping = inline('PING',
+ function(response)
if response == 'PONG' then return true else return false end
end
- },
- echo = { 'ECHO', _send_bulk },
+ ),
+ echo = bulk('ECHO'),
-- TODO: the server returns an empty -ERR on authentication failure
- auth = { 'AUTH' },
+ auth = inline('AUTH'),
-- connection handling
- quit = { 'QUIT', function(self, command)
- _write(self, command .. protocol.newline)
+ quit = custom('QUIT',
+ function(client, command)
+ -- let's fire and forget! the connection is closed as soon
+ -- as the QUIT command is received by the server.
+ network.write(client, command .. protocol.newline)
end
- },
+ ),
-- commands operating on string values
- set = { 'SET', _send_bulk },
- set_preserve = { 'SETNX', _send_bulk, toboolean },
- get = { 'GET' },
- get_multiple = { 'MGET' },
- increment = { 'INCR' },
- increment_by = { 'INCRBY' },
- decrement = { 'DECR' },
- decrement_by = { 'DECRBY' },
- exists = { 'EXISTS', _send_inline, toboolean },
- delete = { 'DEL', _send_inline, toboolean },
- type = { 'TYPE' },
+ set = bulk('SET'),
+ set_preserve = bulk('SETNX', toboolean),
+ get = inline('GET'),
+ get_multiple = inline('MGET'),
+ increment = inline('INCR'),
+ increment_by = inline('INCRBY'),
+ decrement = inline('DECR'),
+ decrement_by = inline('DECRBY'),
+ exists = inline('EXISTS', toboolean),
+ delete = inline('DEL', toboolean),
+ type = inline('TYPE'),
-- commands operating on the key space
- keys = {
- 'KEYS', _send_inline, function(response)
+ keys = inline('KEYS',
+ function(response)
local keys = {}
response:gsub('%w+', function(key)
table.insert(keys, key)
end)
return keys
end
- },
- random_key = { 'RANDOMKEY' },
- rename = { 'RENAME' },
- rename_preserve = { 'RENAMENX' },
- database_size = { 'DBSIZE' },
+ ),
+ random_key = inline('RANDOMKEY'),
+ rename = inline('RENAME'),
+ rename_preserve = inline('RENAMENX'),
+ expire = inline('EXPIRE', toboolean),
+ database_size = inline('DBSIZE'),
-- commands operating on lists
- push_tail = { 'RPUSH', _send_bulk },
- push_head = { 'LPUSH', _send_bulk },
- list_length = { 'LLEN', _send_inline, function(response, key)
- --[[ TODO: redis seems to return a -ERR when the specified key does
- not hold a list value, but this behaviour is not
- consistent with the specs docs. This might be due to the
- -ERR response paradigm being new, which supersedes the
- check for negative numbers to identify errors. ]]
- if response == -2 then
- error('Key ' .. key .. ' does not hold a list value')
- end
- return response
- end
- },
- list_range = { 'LRANGE' },
- list_trim = { 'LTRIM' },
- list_index = { 'LINDEX' },
- list_set = { 'LSET', _send_bulk },
- list_remove = { 'LREM', _send_bulk },
- pop_first = { 'LPOP' },
- pop_last = { 'RPOP' },
+ push_tail = bulk('RPUSH'),
+ push_head = bulk('LPUSH'),
+ list_length = inline('LLEN'),
+ list_range = inline('LRANGE'),
+ list_trim = inline('LTRIM'),
+ list_index = inline('LINDEX'),
+ list_set = bulk('LSET'),
+ list_remove = bulk('LREM'),
+ pop_first = inline('LPOP'),
+ pop_last = inline('RPOP'),
-- commands operating on sets
- set_add = { 'SADD' },
- set_remove = { 'SREM' },
- set_cardinality = { 'SCARD' },
- set_is_member = { 'SISMEMBER' },
- set_intersection = { 'SINTER' },
- set_intersection_store = { 'SINTERSTORE' },
- set_members = { 'SMEMBERS' },
+ set_add = inline('SADD'),
+ set_remove = inline('SREM'),
+ set_cardinality = inline('SCARD'),
+ set_is_member = inline('SISMEMBER'),
+ set_intersection = inline('SINTER'),
+ set_intersection_store = inline('SINTERSTORE'),
+ set_members = inline('SMEMBERS'),
-- multiple databases handling commands
- select_database = { 'SELECT' },
- move_key = { 'MOVE' },
- flush_database = { 'FLUSHDB' },
- flush_databases = { 'FLUSHALL' },
+ select_database = inline('SELECT'),
+ move_key = inline('MOVE'),
+ flush_database = inline('FLUSHDB'),
+ flush_databases = inline('FLUSHALL'),
-- sorting
--[[
sort = { 'desc', 'alpha' }
}
--]]
- sort = { 'SORT' },
+ sort = custom('SORT',
+ function(client, command, params)
+ -- TODO: here we will put the logic needed to serialize the params
+ -- table to be sent as the argument of the SORT command.
+ return request.inline(client, command, params)
+ end
+ ),
-- persistence control commands
- save = { 'SAVE' },
- background_save = { 'BGSAVE' },
- last_save = { 'LASTSAVE' },
- shutdown = { 'SHUTDOWN', function(self, command)
- _write(self, command .. protocol.newline)
+ save = inline('SAVE'),
+ background_save = inline('BGSAVE'),
+ last_save = inline('LASTSAVE'),
+ shutdown = custom('SHUTDOWN',
+ function(client, command)
+ -- let's fire and forget! the connection is closed as soon
+ -- as the SHUTDOWN command is received by the server.
+ network.write(command .. protocol.newline)
end
- },
+ ),
-- remote server control commands
- info = {
- 'INFO', _send_inline, function(response)
+ info = inline('INFO',
+ function(response)
local info = {}
response:gsub('([^\r\n]*)\r\n', function(kv)
local k,v = kv:match(('([^:]*):([^:]*)'):rep(1))
end)
return info
end
- },
+ ),
}
-
-function connect(host, port)
- local client_socket = socket.connect(host, port)
-
- if not client_socket then
- error('Could not connect to ' .. host .. ':' .. port)
- end
-
- local redis_client = {
- socket = client_socket,
- raw_cmd = function(self, buffer)
- return _send_raw(self, buffer .. protocol.newline)
- end,
- }
-
- return setmetatable(redis_client, {
- __index = function(self, method)
- local redis_meth = methods[method]
- if redis_meth then
- return function(self, ...)
- if not redis_meth[2] then
- table.insert(redis_meth, 2, _send_inline)
- end
-
- local response = redis_meth[2](self, redis_meth[1], ...)
- if redis_meth[3] then
- return redis_meth[3](response, ...)
- else
- return response
- end
- end
- end
- end
- })
-end