├── .gitignore ├── LICENSE ├── README.md ├── example ├── process.lua └── server.lua ├── libs ├── redis-client.lua ├── redis-codec.lua └── websocket-client.lua └── test.lua /.gitignore: -------------------------------------------------------------------------------- 1 | deps 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tim Caswell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redis-luvit 2 | 3 | A [Redis][] protocol codec for [Luvit][] 4 | 5 | ## Installing 6 | 7 | Simply install using lit directly: 8 | 9 | ```sh 10 | lit install creationix/redis-client 11 | ``` 12 | 13 | Or add to your dependencies list. 14 | 15 | ```lua 16 | exports.dependencies = { 17 | "creationix/redis-client" 18 | } 19 | ``` 20 | 21 | ## Usage 22 | 23 | The redis client library wraps the raw codec in an easy to use coroutine based 24 | interface using luvit streams. 25 | 26 | Send multiple strings to make a query and the response will come back 27 | pre-decoded. 28 | 29 | ```lua 30 | local connect = require('redis-client') 31 | 32 | coroutine.wrap(function () 33 | -- Connect to redis 34 | local send = connect { host = "localhost", port = 6379 } 35 | 36 | -- Send some commands 37 | send("set", "name", "Tim") 38 | local name = send("get", "name") 39 | assert(name == "Tim") 40 | 41 | -- Close the connection 42 | send() 43 | end)() 44 | ``` 45 | 46 | ## Using the raw codec directly 47 | 48 | This codec is transport agnostic. I like to use it with the coro friendly 49 | libraries, but it can be used with the node style streams in luvit or even 50 | with a non-luvit lua project. 51 | 52 | It is a simple encoder/decoder for talking [RESP][] over a socket. 53 | 54 | The encoder is a simple function that accepts a table of strings and encodes 55 | it as a RESP list. The decoder accepts a chunk of raw data string and tries to 56 | consume one message. 57 | 58 | If there is not enough data, it simply returns nothing. If there is enough, it 59 | returns the parsed value as well as the leftover data. 60 | 61 | ```lua 62 | local codec = require('redis-codec') 63 | 64 | local encoded = codec.encode({"set", "name", "Tim"}) 65 | 66 | local message, extra = codec.decode("$5\r\nHello\r\n+More\r\n") 67 | ``` 68 | 69 | [Redis]: http://redis.io/ 70 | [Luvit]: https://luvit.io/ 71 | [RESP]: http://redis.io/topics/protocol 72 | -------------------------------------------------------------------------------- /example/process.lua: -------------------------------------------------------------------------------- 1 | local redisConnect = require('redis-client') 2 | 3 | local websocketConnect = require('websocket-client') 4 | local jsonParse = require('json').parse 5 | 6 | coroutine.wrap(function () 7 | -- Connect to redis 8 | local send = redisConnect { host = "localhost", port = 6379 } 9 | 10 | local read, write = websocketConnect "ws://stream.meetup.com/2/rsvps" 11 | 12 | for message in read do 13 | if message.opcode == 1 then 14 | local topics = jsonParse(message.payload).group.group_topics 15 | for i = 1, #topics do 16 | local topic = topics[i] 17 | p(topic) 18 | if send("exists", topic.urlkey) == 0 then 19 | send("set", topic.urlkey, topic.topic_name) 20 | end 21 | send("zincrby", "frequency", 1, topic.urlkey) 22 | end 23 | end 24 | end 25 | write() 26 | 27 | -- -- Send some commands 28 | -- p(send("set", "name", "Tim")) 29 | -- p(send("get", "name")) 30 | -- local name = send("get", "name") 31 | -- assert(name == "Tim") 32 | -- 33 | -- -- Close the connection 34 | -- send() 35 | end)() 36 | -------------------------------------------------------------------------------- /example/server.lua: -------------------------------------------------------------------------------- 1 | local jsonStringify = require('json').stringify 2 | local send 3 | 4 | local function handleSocket(req, read, write) 5 | if not send then 6 | send = require('redis-client')() 7 | end 8 | p(req) 9 | for message in read do 10 | if message.opcode == 1 then 11 | p(message.payload) 12 | local list = {} 13 | for part in message.payload:gmatch("%w+") do 14 | list[#list + 1] = part 15 | end 16 | local res = send(unpack(list)) 17 | p(list, res) 18 | write { 19 | opcode = 1, 20 | payload = jsonStringify(res) 21 | } 22 | end 23 | end 24 | end 25 | 26 | require('weblit-websocket') 27 | require('weblit-app') 28 | 29 | .bind({ 30 | host = "0.0.0.0", 31 | port = 8080 32 | }) 33 | 34 | .use(require('weblit-logger')) 35 | .use(require('weblit-auto-headers')) 36 | .use(require('weblit-etag-cache')) 37 | 38 | .websocket({ 39 | path = "/", 40 | protocol = "resp", 41 | }, handleSocket) 42 | 43 | .start() 44 | -------------------------------------------------------------------------------- /libs/redis-client.lua: -------------------------------------------------------------------------------- 1 | --[[lit-meta 2 | name = "creationix/redis-client" 3 | version = "3.0.0" 4 | description = "A coroutine based client for Redis" 5 | tags = {"coro", "redis"} 6 | license = "MIT" 7 | author = { name = "Tim Caswell" } 8 | homepage = "https://github.com/creationix/redis-luvit" 9 | dependencies = { 10 | "creationix/redis-codec@3.0.0", 11 | "creationix/coro-net@3.0.0", 12 | } 13 | ]] 14 | 15 | local codec = require('redis-codec') 16 | local connect = require('coro-net').connect 17 | 18 | return function (config) 19 | if not config then config = {} end 20 | 21 | local read, write = assert(connect{ 22 | host = config.host or "localhost", 23 | port = config.port or 6379, 24 | encode = codec.encode, 25 | decode = codec.decode, 26 | }) 27 | 28 | return function (command, ...) 29 | if not command then return write() end 30 | write {command, ...} 31 | local res = read() 32 | if type(res) == "table" and res.error then 33 | error(res.error) 34 | end 35 | return res 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /libs/redis-codec.lua: -------------------------------------------------------------------------------- 1 | --[[lit-meta 2 | name = "creationix/redis-codec" 3 | version = "3.0.0" 4 | description = "Pure Lua codec for RESP (REdis Serialization Protocol)" 5 | tags = {"codec", "redis"} 6 | license = "MIT" 7 | author = { name = "Tim Caswell" } 8 | homepage = "https://github.com/creationix/redis-luvit" 9 | ]] 10 | 11 | local function encode(list) 12 | local len = #list 13 | local parts = {"*" .. len .. '\r\n'} 14 | for i = 1, len do 15 | local str = tostring(list[i]) 16 | parts[i + 1] = "$" .. #str .. "\r\n" .. str .. "\r\n" 17 | end 18 | return table.concat(parts) 19 | end 20 | 21 | local byte = string.byte 22 | local find = string.find 23 | local sub = string.sub 24 | 25 | local function decode(chunk, index) 26 | if #chunk < 1 then return end 27 | local first = byte(chunk, index) 28 | if first == 43 then -- '+' Simple string 29 | local start = find(chunk, "\r\n", index, true) 30 | if not start then return end 31 | return sub(chunk, index + 1, start - 1), start + 2 32 | elseif first == 45 then -- '-' Error 33 | local start = find(chunk, "\r\n", index, true) 34 | if not start then return end 35 | return {error=sub(chunk, index + 1, start - 1)}, start + 2 36 | elseif first == 58 then -- ':' Integer 37 | local start = find(chunk, "\r\n", index, true) 38 | if not start then return end 39 | return tonumber(sub(chunk, index + 1, start - 1)), start + 2 40 | elseif first == 36 then -- '$' Bulk String 41 | local start = find(chunk, "\r\n", index, true) 42 | if not start then return end 43 | local len = tonumber(sub(chunk, index + 1, start - 1)) 44 | if len == -1 then 45 | return nil, start + 2 46 | end 47 | if #chunk < start + 3 + len then return end 48 | return sub(chunk, start + 2, start + 1 + len), start + 4 + len 49 | elseif first == 42 then -- '*' List 50 | local start = find(chunk, "\r\n", index, true) 51 | if not start then return end 52 | local len = tonumber(sub(chunk, index + 1, start - 1)) 53 | if len == -1 then 54 | return nil, start + 2 55 | end 56 | local list = {} 57 | index = start + 2 58 | for i = 1, len do 59 | local value 60 | value, index = decode(chunk, index) 61 | if not value then return end 62 | list[i] = value 63 | end 64 | return list, index 65 | else 66 | local list = {} 67 | local stop = find(chunk, "\r\n", index, true) 68 | if not stop then return end 69 | while index < stop do 70 | local e = find(chunk, " ", index, true) 71 | if not e then 72 | list[#list + 1] = sub(chunk, index, stop - 1) 73 | break 74 | end 75 | list[#list + 1] = sub(chunk, index, e - 1) 76 | index = e + 1 77 | end 78 | return list, stop + 2 79 | end 80 | end 81 | 82 | return { 83 | encode = encode, 84 | decode = decode, 85 | } 86 | -------------------------------------------------------------------------------- /libs/websocket-client.lua: -------------------------------------------------------------------------------- 1 | --[[lit-meta 2 | name = "creationix/websocket-client" 3 | version = "4.0.0" 4 | description = "A coroutine based client for Websockets" 5 | tags = {"coro", "websocket"} 6 | license = "MIT" 7 | author = { name = "Tim Caswell" } 8 | homepage = "https://github.com/creationix/redis-luvit" 9 | dependencies = { 10 | "luvit/http-codec@3.0.0", 11 | "creationix/websocket-codec@3.0.0", 12 | "creationix/coro-net@3.0.0", 13 | } 14 | ]] 15 | 16 | local connect = require('coro-net').connect 17 | local websocketCodec = require('websocket-codec') 18 | local httpCodec = require('http-codec') 19 | 20 | return function (url, subprotocol, options) 21 | options = options or {} 22 | local headers = options.headers or {} 23 | 24 | local protocol, host, port, path = string.match(url, "^(wss?)://([^:/]+):?(%d*)(/?[^#]*)") 25 | local tls = options.tls 26 | if protocol == "ws" then 27 | port = tonumber(port) or 80 28 | elseif protocol == "wss" then 29 | port = tonumber(port) or 443 30 | if not tls then tls = {} end 31 | else 32 | error("Sorry, only ws:// or wss:// protocols supported") 33 | end 34 | if #path == 0 then path = "/" end 35 | 36 | local read, write, socket, updateDecoder, updateEncoder = assert(connect{ 37 | tls = tls, 38 | host = host, 39 | port = port, 40 | encode = httpCodec.encoder(), 41 | decode = httpCodec.decoder(), 42 | }) 43 | 44 | -- Perform the websocket handshake 45 | assert(websocketCodec.handshake({ 46 | host = host, 47 | path = path, 48 | protocol = subprotocol 49 | }, function (req) 50 | for _ = 1, #headers do 51 | req[#req + 1] = headers[1] 52 | end 53 | write(req) 54 | local res = read() 55 | if not res then error("Missing server response") end 56 | if res.code == 400 then 57 | -- p { req = req, res = res } 58 | local reason = read() or res.reason 59 | error("Invalid request: " .. reason) 60 | end 61 | return res 62 | end)) 63 | 64 | -- Upgrade the protocol to websocket 65 | updateDecoder(websocketCodec.decode) 66 | updateEncoder(websocketCodec.encode) 67 | 68 | return read, write, socket 69 | end 70 | -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | local codec = require('redis-codec') 2 | local jsonStringify = require('json').stringify 3 | 4 | local function test(str, extra, expected) 5 | local result, e = codec.decode(str, 1) 6 | p(str) 7 | e = str and extra and str:sub(e) 8 | p(e, extra) 9 | p(result, expected) 10 | assert(extra == e) 11 | assert(jsonStringify(result) == jsonStringify(expected)) 12 | end 13 | 14 | test("*2\r\n*1\r\n+Hello\r\n+World\r\n", "", {{"Hello"},"World"}) 15 | test("*2\r\n*1\r\n$5\r\nHello\r\n$5\r\nWorld\r\n", "", {{"Hello"},"World"}) 16 | test("set language Lua\r\n", "", {"set", "language", "Lua"}) 17 | test("$5\r\n12345\r\n", "", "12345") 18 | test("$5\r\n12345\r") 19 | test("$5\r\n12345\r\nabc", "abc", "12345") 20 | test("+12") 21 | test("+1234\r") 22 | test("+1235\r\n", "", "1235") 23 | test("+1235\r\n1234", "1234", "1235") 24 | test(":45\r") 25 | test(":45\r\n", "", 45) 26 | test("*-1\r\nx", "x", nil) 27 | test("-FATAL, YIKES\r\n", "", {error="FATAL, YIKES"}) 28 | test("*12\r\n$4\r\n2048\r\n$1\r\n0\r\n$4\r\n1024\r\n$2\r\n42\r\n$1\r\n5\r\n$1\r\n7\r\n$1\r\n5\r\n$1\r\n7\r\n$1\r\n5\r\n$1\r\n7\r\n$1\r\n5\r\n$1\r\n7\r\n", 29 | "", { '2048', '0', '1024', '42', '5', '7', '5', '7', '5', '7', '5', '7' }) 30 | --------------------------------------------------------------------------------