├── .gitignore ├── .travis.yml ├── .travis ├── platform.sh ├── setenv_lua.sh └── setup_lua.sh ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── examples ├── auth_pingpong.lua ├── pingpong.lua ├── publish.lua ├── server_info.lua └── subscribe.lua ├── rockspec └── nats-0.0.3-1.rockspec ├── src └── nats.lua └── tests └── test_client.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | sudo: required 4 | 5 | services: 6 | - docker 7 | 8 | env: 9 | global: 10 | - LUAROCKS=2.4.1 11 | matrix: 12 | - LUA=lua5.1 13 | - LUA=lua5.2 14 | - LUA=lua5.3 15 | - LUA=luajit 16 | - LUA=luajit2.0 17 | 18 | before_install: 19 | - docker-compose up -d nats 20 | - source .travis/setenv_lua.sh 21 | - luarocks install telescope 22 | 23 | install: 24 | - luarocks make rockspec/nats-0.0.3-1.rockspec 25 | 26 | script: 27 | - make test 28 | 29 | notifications: 30 | email: 31 | on_success: change 32 | on_failure: always -------------------------------------------------------------------------------- /.travis/platform.sh: -------------------------------------------------------------------------------- 1 | if [ -z "${PLATFORM:-}" ]; then 2 | PLATFORM=$TRAVIS_OS_NAME; 3 | fi 4 | 5 | if [ "$PLATFORM" == "osx" ]; then 6 | PLATFORM="macosx"; 7 | fi 8 | 9 | if [ -z "$PLATFORM" ]; then 10 | if [ "$(uname)" == "Linux" ]; then 11 | PLATFORM="linux"; 12 | else 13 | PLATFORM="macosx"; 14 | fi; 15 | fi -------------------------------------------------------------------------------- /.travis/setenv_lua.sh: -------------------------------------------------------------------------------- 1 | export PATH=${PATH}:$HOME/.lua:$HOME/.local/bin:${TRAVIS_BUILD_DIR}/install/luarocks/bin 2 | bash .travis/setup_lua.sh 3 | eval "$($HOME/.lua/luarocks path)" -------------------------------------------------------------------------------- /.travis/setup_lua.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # A script for setting up environment for travis-ci testing. 4 | # Sets up Lua and Luarocks. 5 | # LUA must be "lua5.1", "lua5.2" or "luajit". 6 | # luajit2.0 - master v2.0 7 | # luajit2.1 - master v2.1 8 | 9 | set -eufo pipefail 10 | 11 | LUAJIT_VERSION="2.0.5" 12 | if [ "$LUA" == "luajit2.1" ]; then 13 | LUAJIT_VERSION="2.1.0-beta3" 14 | fi 15 | LUAJIT_BASE="LuaJIT-$LUAJIT_VERSION" 16 | 17 | source .travis/platform.sh 18 | 19 | LUA_HOME_DIR=$TRAVIS_BUILD_DIR/install/lua 20 | 21 | LR_HOME_DIR=$TRAVIS_BUILD_DIR/install/luarocks 22 | 23 | mkdir $HOME/.lua 24 | 25 | LUAJIT="no" 26 | 27 | if [ "$PLATFORM" == "macosx" ]; then 28 | if [ "$LUA" == "luajit" ]; then 29 | LUAJIT="yes"; 30 | fi 31 | if [ "$LUA" == "luajit2.0" ]; then 32 | LUAJIT="yes"; 33 | fi 34 | if [ "$LUA" == "luajit2.1" ]; then 35 | LUAJIT="yes"; 36 | fi; 37 | elif [ "$(expr substr $LUA 1 6)" == "luajit" ]; then 38 | LUAJIT="yes"; 39 | fi 40 | 41 | mkdir -p "$LUA_HOME_DIR" 42 | 43 | if [ "$LUAJIT" == "yes" ]; then 44 | 45 | if [ "$LUA" == "luajit" ]; then 46 | curl --location https://github.com/LuaJIT/LuaJIT/archive/v$LUAJIT_VERSION.tar.gz | tar xz; 47 | else 48 | git clone https://github.com/LuaJIT/LuaJIT.git $LUAJIT_BASE; 49 | fi 50 | 51 | cd $LUAJIT_BASE 52 | 53 | if [ "$LUA" == "luajit2.1" ]; then 54 | git checkout v2.1; 55 | # force the INSTALL_TNAME to be luajit 56 | perl -i -pe 's/INSTALL_TNAME=.+/INSTALL_TNAME= luajit/' Makefile 57 | fi 58 | 59 | make && make install PREFIX="$LUA_HOME_DIR" 60 | 61 | ln -s "$LUA_HOME_DIR/bin/luajit" $HOME/.lua/luajit 62 | ln -s "$LUA_HOME_DIR/bin/luajit" $HOME/.lua/lua; 63 | 64 | else 65 | 66 | if [ "$LUA" == "lua5.1" ]; then 67 | curl http://www.lua.org/ftp/lua-5.1.5.tar.gz | tar xz 68 | cd lua-5.1.5; 69 | elif [ "$LUA" == "lua5.2" ]; then 70 | curl http://www.lua.org/ftp/lua-5.2.4.tar.gz | tar xz 71 | cd lua-5.2.4; 72 | elif [ "$LUA" == "lua5.3" ]; then 73 | curl http://www.lua.org/ftp/lua-5.3.2.tar.gz | tar xz 74 | cd lua-5.3.2; 75 | fi 76 | 77 | # Build Lua without backwards compatibility for testing 78 | perl -i -pe 's/-DLUA_COMPAT_(ALL|5_2)//' src/Makefile 79 | make $PLATFORM 80 | make INSTALL_TOP="$LUA_HOME_DIR" install; 81 | 82 | ln -s $LUA_HOME_DIR/bin/lua $HOME/.lua/lua 83 | ln -s $LUA_HOME_DIR/bin/luac $HOME/.lua/luac; 84 | 85 | fi 86 | 87 | cd $TRAVIS_BUILD_DIR 88 | 89 | lua -v 90 | 91 | LUAROCKS_BASE=luarocks-$LUAROCKS 92 | 93 | curl --location http://luarocks.org/releases/$LUAROCKS_BASE.tar.gz | tar xz 94 | 95 | cd $LUAROCKS_BASE 96 | 97 | if [ "$LUA" == "luajit" ]; then 98 | ./configure --lua-suffix=jit --with-lua-include="$LUA_HOME_DIR/include/luajit-2.0" --prefix="$LR_HOME_DIR"; 99 | elif [ "$LUA" == "luajit2.0" ]; then 100 | ./configure --lua-suffix=jit --with-lua-include="$LUA_HOME_DIR/include/luajit-2.0" --prefix="$LR_HOME_DIR"; 101 | elif [ "$LUA" == "luajit2.1" ]; then 102 | ./configure --lua-suffix=jit --with-lua-include="$LUA_HOME_DIR/include/luajit-2.1" --prefix="$LR_HOME_DIR"; 103 | else 104 | ./configure --with-lua="$LUA_HOME_DIR" --prefix="$LR_HOME_DIR" 105 | fi 106 | 107 | make build && make install 108 | 109 | ln -s $LR_HOME_DIR/bin/luarocks $HOME/.lua/luarocks 110 | 111 | cd $TRAVIS_BUILD_DIR 112 | 113 | luarocks --version 114 | 115 | rm -rf $LUAROCKS_BASE 116 | 117 | if [ "$LUAJIT" == "yes" ]; then 118 | rm -rf $LUAJIT_BASE; 119 | elif [ "$LUA" == "lua5.1" ]; then 120 | rm -rf lua-5.1.5; 121 | elif [ "$LUA" == "lua5.2" ]; then 122 | rm -rf lua-5.2.4; 123 | elif [ "$LUA" == "lua5.3" ]; then 124 | rm -rf lua-5.3.2; 125 | fi -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Eric Pinto 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | tsc -f tests/*.lua 3 | 4 | test-deps: 5 | luarocks install telescope 6 | 7 | deps: 8 | luarocks install luasocket 9 | luarocks install lua-cjson 10 | luarocks install uuid 11 | 12 | .PHONY: test test-deps deps 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NATS Lua library 2 | ================ 3 | 4 | ![travis status](https://travis-ci.org/OystParis/lua-nats.svg?branch=master) 5 | 6 | LUA client for NATS messaging system. https://nats.io 7 | 8 | > Note: lua-nats is under heavy development. 9 | 10 | by [dawnangel][github] 11 | 12 | [github]: http://github.com/dawnangel/ "Github repositories" 13 | 14 | [![License](http://img.shields.io/:license-mit-blue.svg)](http://mit-license.org) 15 | 16 | Requirements 17 | ------------ 18 | 19 | * Lua >= 5.1 20 | * [luasocket](https://github.com/diegonehab/luasocket) 21 | * [lua-cjson](https://github.com/mpx/lua-cjson) 22 | * [uuid](https://github.com/Tieske/uuid) 23 | * [nats](https://github.com/derekcollison/nats) or [gnatsd](https://github.com/apcera/gnatsd) 24 | 25 | This is a NATS Lua library for Lua 5.1, 5.2 and 5.3. The 26 | libraries are copyright by their author 2015 (see the Creators 27 | file for details), and released under the MIT license (the same 28 | license as Lua itself). There is no warranty. 29 | 30 | 31 | Usage 32 | ----- 33 | 34 | ### Basic usage: Subscribe / Unsubscribe 35 | 36 | ```lua 37 | local nats = require 'nats' 38 | 39 | local client = nats.connect({ 40 | host = '127.0.0.1', 41 | port = 4222, 42 | }) 43 | 44 | -- connect to the server 45 | client:connect() 46 | 47 | -- callback function for subscriptions 48 | local function subscribe_callback(payload) 49 | print('Received data: ' .. payload) 50 | end 51 | 52 | -- subscribe to a subject 53 | local subscribe_id = client:subscribe('foo', subscribe_callback) 54 | 55 | -- wait until 2 messages come 56 | client:wait(2) 57 | 58 | -- unsubscribe from the subject 59 | client:unsubscribe(subscribe_id) 60 | ``` 61 | 62 | ### Basic usage: Publish 63 | 64 | ```lua 65 | local nats = require 'nats' 66 | 67 | local client = nats.connect({ 68 | host = '127.0.0.1', 69 | port = 4222, 70 | }) 71 | 72 | -- connect to the server 73 | client:connect() 74 | 75 | -- publish to a subject 76 | local subscribe_id = client:publish('foo', 'message to be published') 77 | ``` 78 | 79 | ### Basic usage: User authentication 80 | 81 | ```lua 82 | local nats = require 'nats' 83 | 84 | local client = nats.connect({ 85 | host = '127.0.0.1', 86 | port = 4222, 87 | }) 88 | 89 | -- user authentication 90 | local user, password = 'user', 'password' 91 | client:set_auth(user, password) 92 | 93 | -- connect to the server 94 | client:connect() 95 | ``` 96 | 97 | Developer's Information 98 | ----------------------- 99 | 100 | ### Installation of the libraries 101 | 102 | To install the required libraries you can execute `make deps`. 103 | 104 | ### Tests 105 | 106 | Tests are in the `tests` folder. 107 | To run them, you require: 108 | 109 | - `luarocks` and `telescope` installed. 110 | - execute `make test`. 111 | 112 | Creators 113 | -------- 114 | 115 | **Eric Pinto** 116 | 117 | - 118 | - 119 | 120 | Bug reports and code contributions 121 | ---------------------------------- 122 | 123 | These libraries are written and maintained by their users. Please make 124 | bug report and suggestions on GitHub (see URL at top of file). Pull 125 | requests are especially appreciated. 126 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | 4 | nats: 5 | image: nats 6 | ports: 7 | - "4222:4222" -------------------------------------------------------------------------------- /examples/auth_pingpong.lua: -------------------------------------------------------------------------------- 1 | package.path = '../src/?.lua;src/?.lua;' .. package.path 2 | pcall(require, 'luarocks.require') 3 | 4 | local nats = require 'nats' 5 | 6 | local params = { 7 | host = '127.0.0.1', 8 | port = 4222, 9 | } 10 | 11 | local client = nats.connect(params) 12 | 13 | client:enable_trace() 14 | client:set_auth('user', 'password') 15 | 16 | client:connect() 17 | client:ping() 18 | -------------------------------------------------------------------------------- /examples/pingpong.lua: -------------------------------------------------------------------------------- 1 | package.path = '../src/?.lua;src/?.lua;' .. package.path 2 | pcall(require, 'luarocks.require') 3 | 4 | local nats = require 'nats' 5 | 6 | local params = { 7 | host = '127.0.0.1', 8 | port = 4222, 9 | } 10 | 11 | local client = nats.connect(params) 12 | 13 | -- client:enable_trace() 14 | client:connect() 15 | client:ping() 16 | -------------------------------------------------------------------------------- /examples/publish.lua: -------------------------------------------------------------------------------- 1 | package.path = '../src/?.lua;src/?.lua;' .. package.path 2 | pcall(require, 'luarocks.require') 3 | 4 | local nats = require 'nats' 5 | 6 | local params = { 7 | host = '127.0.0.1', 8 | port = 4222, 9 | } 10 | 11 | local client = nats.connect(params) 12 | 13 | -- client:enable_trace() 14 | client:connect() 15 | 16 | client:publish('foo', 'bar A') 17 | client:publish('foo', 'bar B') 18 | -------------------------------------------------------------------------------- /examples/server_info.lua: -------------------------------------------------------------------------------- 1 | package.path = '../src/?.lua;src/?.lua;' .. package.path 2 | pcall(require, 'luarocks.require') 3 | 4 | local nats = require 'nats' 5 | 6 | local params = { 7 | host = '127.0.0.1', 8 | port = 4222, 9 | } 10 | 11 | local client = nats.connect(params) 12 | 13 | -- client:enable_trace() 14 | client:connect() 15 | local server_info = client:get_server_info() 16 | 17 | for k, v in pairs(server_info) do 18 | print(k, v) 19 | end 20 | -------------------------------------------------------------------------------- /examples/subscribe.lua: -------------------------------------------------------------------------------- 1 | package.path = '../src/?.lua;src/?.lua;' .. package.path 2 | pcall(require, 'luarocks.require') 3 | 4 | local nats = require 'nats' 5 | 6 | local params = { 7 | host = '127.0.0.1', 8 | port = 4222, 9 | } 10 | 11 | local client = nats.connect(params) 12 | 13 | local function subscribe_callback(payload) 14 | print('Received data: ' .. payload) 15 | end 16 | 17 | -- client:enable_trace() 18 | client:connect() 19 | local subscribe_id = client:subscribe('foo', subscribe_callback) 20 | client:wait(2) 21 | client:unsubscribe(subscribe_id) 22 | -------------------------------------------------------------------------------- /rockspec/nats-0.0.3-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "nats" 2 | version = "0.0.3-1" 3 | 4 | source = { 5 | url = "git://github.com/DawnAngel/lua-nats.git", 6 | tag = "0.0.3" 7 | } 8 | 9 | description = { 10 | summary = "LUA client for NATS messaging system. https://nats.io", 11 | detailed = [[ 12 | LUA client for NATS messaging system. https://nats.io 13 | ]], 14 | homepage = "http://github.com/DawnAngel/lua-nats", 15 | maintainer = "Eric Pinto ", 16 | license = "MIT" 17 | } 18 | 19 | dependencies = { 20 | "lua >= 5.1", 21 | "luasocket", 22 | "lua-cjson", 23 | "uuid" 24 | } 25 | 26 | build = { 27 | type = "none", 28 | install = { 29 | lua = { 30 | "src/nats.lua" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/nats.lua: -------------------------------------------------------------------------------- 1 | local nats = { 2 | _VERSION = 'lua-nats 0.0.2', 3 | _DESCRIPTION = 'LUA client for NATS messaging system. https://nats.io', 4 | _COPYRIGHT = 'Copyright (C) 2015 Eric Pinto', 5 | } 6 | 7 | 8 | -- ### Library requirements ### 9 | 10 | local cjson = require('cjson') 11 | local uuid = require('uuid') 12 | 13 | 14 | -- ### Local properties ### 15 | 16 | local unpack = _G.unpack or table.unpack 17 | local network, request, response, command = {}, {}, {}, {} 18 | 19 | local client_prototype = { 20 | user = nil, 21 | pass = nil, 22 | lang = 'lua', 23 | version = '0.0.2', 24 | verbose = false, 25 | pedantic = false, 26 | trace = false, 27 | reconnect = true, 28 | subscriptions = {}, 29 | information = {}, 30 | } 31 | 32 | local defaults = { 33 | host = '127.0.0.1', 34 | port = 4222, 35 | tcp_nodelay = true, 36 | path = nil, 37 | } 38 | 39 | -- ### Create a properly formatted inbox subject. 40 | 41 | local function create_inbox() 42 | return '_INBOX.' .. uuid() 43 | end 44 | 45 | -- ### Local methods ### 46 | 47 | local function merge_defaults(parameters) 48 | if parameters == nil then 49 | parameters = {} 50 | end 51 | for k, v in pairs(defaults) do 52 | if parameters[k] == nil then 53 | parameters[k] = defaults[k] 54 | end 55 | end 56 | return parameters 57 | end 58 | 59 | local function load_methods(proto, commands) 60 | -- inherit client metatable from client_proto metatable 61 | local client = setmetatable({}, getmetatable(proto)) 62 | 63 | -- Assign commands functions to the client 64 | for cmd, fn in pairs(commands) do 65 | if type(fn) ~= 'function' then 66 | nats.error('invalid type for command ' .. cmd .. '(must be a function)') 67 | end 68 | client[cmd] = fn 69 | end 70 | 71 | -- assing client properties and methods from client_proto 72 | for i, v in pairs(proto) do 73 | client[i] = v 74 | end 75 | 76 | return client 77 | end 78 | 79 | local function create_client(client_proto, client_socket, commands) 80 | local client = load_methods(client_proto, commands) 81 | -- assign client error handler 82 | client.error = nats.error 83 | -- assign client network methods 84 | client.network = { 85 | socket = client_socket, 86 | read = network.read, 87 | write = network.write, 88 | lread = nil, 89 | lwrite = nil, 90 | } 91 | -- assign client requests methods 92 | client.requests = { 93 | multibulk = request.raw, 94 | } 95 | uuid.seed() 96 | return client 97 | end 98 | 99 | -- ### Network methods ### 100 | 101 | function network.write(client, buffer) 102 | if client.trace then print('->> '..buffer:sub(1,-3)) end 103 | local _, err = client.network.socket:send(buffer) 104 | if not err then 105 | client.network.lwrite = buffer 106 | else 107 | client.error(err) 108 | end 109 | end 110 | 111 | function network.read(client, len) 112 | if len == nil then len = '*l' end 113 | local line, err = client.network.socket:receive(len) 114 | if client.trace then print('<<- '..line) end 115 | if not err then 116 | client.network.lread = line 117 | return line 118 | else 119 | client.error('connection error: ' .. err) 120 | end 121 | end 122 | 123 | -- ### Response methods ### 124 | 125 | -- Client response reader 126 | function response.read(client) 127 | local payload = client.network.read(client) 128 | local slices = {} 129 | local data = {} 130 | 131 | for slice in payload:gmatch('[^%s]+') do 132 | table.insert(slices, slice) 133 | end 134 | 135 | -- PING 136 | if slices[1] == 'PING' then 137 | data.action = 'PING' 138 | 139 | -- PONG 140 | elseif slices[1] == 'PONG' then 141 | data.action = 'PONG' 142 | 143 | -- MSG 144 | elseif slices[1] == 'MSG' then 145 | data.action = 'MSG' 146 | data.subject = slices[2] 147 | data.unique_id = slices[3] 148 | -- ask for line ending chars and remove them 149 | if #slices == 4 then 150 | data.content = client.network.read(client, slices[4]+2):sub(1, -3) 151 | else 152 | data.reply = slices[4] 153 | data.content = client.network.read(client, slices[5]+2):sub(1, -3) 154 | end 155 | 156 | -- INFO 157 | elseif slices[1] == 'INFO' then 158 | data.action = 'INFO' 159 | data.content = slices[2] 160 | 161 | -- INFO 162 | elseif slices[1] == '+OK' then 163 | data.action = 'OK' 164 | 165 | -- INFO 166 | elseif slices[1] == '-ERR' then 167 | data.action = 'ERROR' 168 | -- data.content = slices[2] 169 | 170 | -- unknown type of reply 171 | else 172 | data = client.error('unknown response: ' .. payload) 173 | end 174 | 175 | return data 176 | end 177 | 178 | 179 | -- ### Request methods ### 180 | 181 | -- Client request sender (RAW) 182 | function request.raw(client, buffer) 183 | local bufferType = type(buffer) 184 | 185 | if bufferType == 'table' then 186 | client.network.write(client, table.concat(buffer)) 187 | elseif bufferType == 'string' then 188 | client.network.write(client, buffer) 189 | else 190 | client.error('argument error: ' .. bufferType) 191 | end 192 | end 193 | 194 | 195 | -- ### Client prototype methods ### 196 | 197 | client_prototype.raw_cmd = function(client, buffer) 198 | request.raw(client, buffer .. '\r\n') 199 | return response.read(client) 200 | end 201 | 202 | client_prototype.set_auth = function(client, user, pass) 203 | client.user = user 204 | client.pass = pass 205 | end 206 | 207 | client_prototype.enable_trace = function(client) 208 | client.trace = true 209 | end 210 | 211 | client_prototype.set_verbose = function(client, verbose) 212 | client.verbose = verbose 213 | end 214 | 215 | client_prototype.set_pedantic = function(client, pedantic) 216 | client.pedantic = pedantic 217 | end 218 | 219 | client_prototype.count_subscriptions = function(client) 220 | return #client.subscriptions 221 | end 222 | 223 | client_prototype.get_server_info = function(client) 224 | return client.information 225 | end 226 | 227 | client_prototype.shutdown = function(client) 228 | client.network.socket:shutdown() 229 | end 230 | 231 | -- ### Socket connection methods ### 232 | 233 | local function connect_tcp(socket, parameters) 234 | local host, port = parameters.host, tonumber(parameters.port) 235 | if parameters.timeout then 236 | socket:settimeout(parameters.timeout, 't') 237 | end 238 | 239 | local ok, err = socket:connect(host, port) 240 | if not ok then 241 | nats.error('could not connect to '..host..':'..port..' ['..err..']') 242 | end 243 | socket:setoption('tcp-nodelay', parameters.tcp_nodelay) 244 | return socket 245 | end 246 | 247 | local function connect_unix(socket, parameters) 248 | local ok, err = socket:connect(parameters.path) 249 | if not ok then 250 | nats.error('could not connect to '..parameters.path..' ['..err..']') 251 | end 252 | return socket 253 | end 254 | 255 | local function create_connection(parameters) 256 | if parameters.socket then 257 | return parameters.socket 258 | end 259 | 260 | local perform_connection, socket 261 | 262 | if parameters.scheme == 'unix' then 263 | perform_connection, socket = connect_unix, require('socket.unix') 264 | assert(socket, 'your build of LuaSocket does not support UNIX domain sockets') 265 | else 266 | if parameters.scheme then 267 | local scheme = parameters.scheme 268 | assert(scheme == 'nats' or scheme == 'tcp', 'invalid scheme: '..scheme) 269 | end 270 | perform_connection, socket = connect_tcp, require('socket').tcp 271 | end 272 | 273 | return perform_connection(socket(), parameters) 274 | end 275 | 276 | -- ### Nats library methods ### 277 | 278 | function nats.error(message, level) 279 | error(message, (level or 1) + 1) 280 | end 281 | 282 | function nats.connect(...) 283 | local args, parameters = {...}, nil 284 | 285 | if #args == 1 then 286 | parameters = args[1] 287 | elseif #args > 1 then 288 | local host, port, timeout = unpack(args) 289 | parameters = { host = host, port = port, timeout = tonumber(timeout) } 290 | end 291 | 292 | local commands = nats.commands or {} 293 | if type(commands) ~= 'table' then 294 | nats.error('invalid type for the commands table') 295 | end 296 | 297 | local socket = create_connection(merge_defaults(parameters)) 298 | local client = create_client(client_prototype, socket, commands) 299 | 300 | return client 301 | end 302 | 303 | function nats.command(cmd, opts) 304 | return command(cmd, opts) 305 | end 306 | 307 | -- ### Command methods ### 308 | 309 | function command.connect(client) 310 | local config = { 311 | lang = client.lang, 312 | version = client.version, 313 | verbose = client.verbose, 314 | pedantic = client.pedantic, 315 | } 316 | 317 | if client.user ~= nil and client.pass ~= nil then 318 | config.user = client.user 319 | config.pass = client.pass 320 | end 321 | 322 | request.raw(client, 'CONNECT '..cjson.encode(config)..'\r\n') 323 | 324 | -- gather the server information 325 | local data = response.read(client) 326 | if data.action == 'INFO' then 327 | client.information = cjson.decode(data.content) 328 | end 329 | end 330 | 331 | function command.ping(client) 332 | request.raw(client, 'PING\r\n') 333 | 334 | -- wait for the server pong 335 | local data = response.read(client) 336 | if data.action == 'PONG' then 337 | return true 338 | else 339 | return false 340 | end 341 | 342 | end 343 | 344 | function command.pong(client) 345 | request.raw(client, 'PONG\r\n') 346 | end 347 | 348 | function command.request(client, subject, payload, callback) 349 | local inbox = create_inbox() 350 | unique_id = client:subscribe(inbox, function(message, reply) 351 | client:unsubscribe(unique_id) 352 | callback(message, reply) 353 | end) 354 | client:publish(subject, payload, inbox) 355 | return unique_id, inbox 356 | end 357 | 358 | function command.subscribe(client, subject, callback) 359 | local unique_id = uuid() 360 | request.raw(client, 'SUB '..subject..' '..unique_id..'\r\n') 361 | client.subscriptions[unique_id] = callback 362 | 363 | return unique_id 364 | end 365 | 366 | function command.unsubscribe(client, unique_id) 367 | request.raw(client, 'UNSUB '..unique_id..'\r\n') 368 | client.subscriptions[unique_id] = nil 369 | end 370 | 371 | function command.publish(client, subject, payload, reply) 372 | if reply ~= nil then 373 | reply = ' '..reply 374 | else 375 | reply = '' 376 | end 377 | request.raw(client, { 378 | 'PUB '..subject..reply..' '..#payload..'\r\n', 379 | payload..'\r\n', 380 | }) 381 | end 382 | 383 | function command.wait(client, quantity) 384 | quantity = quantity or 0 385 | 386 | local count = 0 387 | repeat 388 | local data = response.read(client) 389 | 390 | if data.action == 'PING' then 391 | command.pong(client) 392 | 393 | elseif data.action == 'MSG' then 394 | count = count + 1 395 | client.subscriptions[data.unique_id](data.content, data.reply) 396 | end 397 | until quantity > 0 and count >= quantity 398 | end 399 | 400 | -- Commands defined in this table do not take the precedence over 401 | -- methods defined in the client prototype table. 402 | 403 | nats.commands = { 404 | connect = command.connect, 405 | ping = command.ping, 406 | pong = command.pong, 407 | request = command.request, 408 | subscribe = command.subscribe, 409 | unsubscribe = command.unsubscribe, 410 | publish = command.publish, 411 | wait = command.wait, 412 | } 413 | 414 | return nats 415 | -------------------------------------------------------------------------------- /tests/test_client.lua: -------------------------------------------------------------------------------- 1 | package.path = '../src/?.lua;src/?.lua;' .. package.path 2 | 3 | pcall(require, 'luarocks.require') 4 | 5 | local telescope = require 'telescope' 6 | local nats = require 'nats' 7 | 8 | local settings = { 9 | host = '127.0.0.1', 10 | port = 4222, 11 | } 12 | 13 | -- ### Basic table handling methods ### 14 | 15 | function table.keys(self) 16 | local keys = {} 17 | for k, _ in pairs(self) do table.insert(keys, k) end 18 | return keys 19 | end 20 | 21 | function table.values(self) 22 | local values = {} 23 | for _, v in pairs(self) do table.insert(values, v) end 24 | return values 25 | end 26 | 27 | function table.contains(self, value) 28 | for _, v in pairs(self) do 29 | if v == value then return true end 30 | end 31 | return false 32 | end 33 | 34 | telescope.make_assertion('error_message', 'result to be an error with the expected message', function(msg, fn) 35 | local ok, err = pcall(fn) 36 | return not ok and err:match(msg) 37 | end) 38 | 39 | -- ### Tests ### 40 | 41 | context('Client initialization', function() 42 | test('* Can connect successfully', function() 43 | local client = nats.connect(settings.host, settings.port) 44 | assert_type(client, 'table') 45 | assert_true(table.contains(table.keys(client.network), 'socket')) 46 | -- Before the connection, the server information is not found in the client 47 | assert_false(table.contains(table.keys(client.information), 'host')) 48 | assert_false(table.contains(table.keys(client.information), 'port')) 49 | 50 | client:connect() 51 | -- After the connection, the server information is stored in the client 52 | assert_true(table.contains(table.keys(client.information), 'host')) 53 | assert_true(table.contains(table.keys(client.information), 'port')) 54 | end) 55 | 56 | test('* Accepts a table for connection parameters', function() 57 | local client = nats.connect(settings) 58 | assert_type(client, 'table') 59 | end) 60 | 61 | test('* Can handle connection failures', function() 62 | assert_error_message('could not connect to .*:%d+ %[connection refused%]', function() 63 | nats.connect(settings.host, settings.port + 50) 64 | end) 65 | end) 66 | end) 67 | 68 | context('NATS commands', function() 69 | before(function() 70 | client = nats.connect(settings) 71 | client:connect() 72 | end) 73 | 74 | test('* CONNECT (client:connect)', function() 75 | assert_equal(client.network.lwrite:sub(1,7), 'CONNECT') 76 | assert_equal(client.network.lread:sub(1,4), 'INFO') 77 | end) 78 | 79 | test('* PING (client:ping)', function() 80 | assert_true(client:ping()) 81 | assert_equal(client.network.lwrite:sub(1,4), 'PING') 82 | assert_equal(client.network.lread:sub(1,4), 'PONG') 83 | end) 84 | 85 | test('* PONG (client:pong)', function() 86 | client:pong() 87 | assert_equal(client.network.lwrite:sub(1,4), 'PONG') 88 | end) 89 | 90 | test('* SUB (client:subscribe)', function() 91 | local subject, callback = 'subject', function() print('callback') end 92 | local subscribe_id = client:subscribe(subject, callback) 93 | 94 | assert_true(subscribe_id ~= '') 95 | assert_equal(client.network.lwrite:sub(1,3), 'SUB') 96 | end) 97 | 98 | test('* UNSUB (client:unsubscribe)', function() 99 | local subject, callback = 'subject', function() return end 100 | local subscribe_id = client:subscribe(subject, callback) 101 | client:unsubscribe(subscribe_id) 102 | 103 | assert_equal(client.network.lwrite:sub(1,5), 'UNSUB') 104 | end) 105 | 106 | test('* PUB (client:publish)', function() 107 | local subject, payload = 'subject', 'payload' 108 | client:publish(subject, payload) 109 | 110 | assert_equal(client.network.lwrite:sub(1,3), 'PUB') 111 | end) 112 | 113 | test('* client:request', function() 114 | local init_message = 'init message' 115 | local reply_message = 'reply message' 116 | local equal_init_message, equal_reply_message 117 | 118 | client:subscribe('foo', function(message, reply) 119 | equal_init_message = message == init_message 120 | client:publish(reply, reply_message) 121 | end) 122 | 123 | client:request('foo', init_message, function(message) 124 | equal_reply_message = message == reply_message 125 | end) 126 | 127 | client:wait(2) 128 | 129 | assert_true(equal_init_message) 130 | assert_true(equal_reply_message) 131 | end) 132 | end) 133 | --------------------------------------------------------------------------------