]> git.saurik.com Git - redis.git/blobdiff - client-libraries/lua/redis.lua
Lua client updated
[redis.git] / client-libraries / lua / redis.lua
index 757aae1139561081b60f5cee992417bf288f0bae..2455ceb3d17b8093aa03c3a2dd77cc8c6070f469 100644 (file)
-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
@@ -70,13 +150,13 @@ local function _send_inline(self, command, ...)
             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
@@ -88,169 +168,127 @@ local function _send_bulk(self, command, ...)
         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
     --[[
@@ -262,20 +300,29 @@ local methods = {
                     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))
@@ -283,40 +330,5 @@ local methods = {
             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