├── LICENSE ├── Makefile ├── README ├── examples ├── client.js └── server.js ├── src └── jsonrpc.js └── test ├── jsonrpc-test.js └── test.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2009 Eric Florenzano and 2 | Ryan Tomayko 3 | 4 | Permission is hereby granted, free of charge, to any person ob- 5 | taining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without restric- 7 | tion, including without limitation the rights to use, copy, modi- 8 | fy, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is fur- 10 | nished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONIN- 18 | FRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 20 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NODE = env NODE_PATH=src node 2 | 3 | test: .PHONY 4 | ls -1 test/*-test.js | xargs -n 1 $(NODE) 5 | 6 | .PHONY: 7 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This is a JSON-RPC server and client library for node.js , 2 | the V8 based evented IO framework. 3 | 4 | Firing up an efficient JSON-RPC server becomes extremely simple: 5 | 6 | var rpc = require('jsonrpc'); 7 | 8 | function add(first, second) { 9 | return first + second; 10 | } 11 | rpc.expose('add', add); 12 | 13 | rpc.listen(8000, 'localhost'); 14 | 15 | 16 | And creating a client to speak to that server is easy too: 17 | 18 | var rpc = require('jsonrpc'); 19 | var sys = require('sys'); 20 | 21 | var client = rpc.getClient(8000, 'localhost'); 22 | 23 | client.call('add', [1, 2], function(result) { 24 | sys.puts('1 + 2 = ' + result); 25 | }); 26 | 27 | To learn more, see the examples directory, peruse test/jsonrpc-test.js, or 28 | simply "Use The Source, Luke". 29 | 30 | More documentation and development is on its way. -------------------------------------------------------------------------------- /examples/client.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var rpc = require('../src/jsonrpc'); 3 | 4 | var client = rpc.getClient(8000, 'localhost'); 5 | 6 | client.call('add', [1, 2], function(result) { 7 | sys.puts(' 1 + 2 = ' + result); 8 | }); 9 | 10 | client.call('multiply', [199, 2], function(result) { 11 | sys.puts('199 * 2 = ' + result); 12 | }); 13 | 14 | // Accessing modules is as simple as dot-prefixing. 15 | client.call('math.power', [3, 3], function(result) { 16 | sys.puts(' 3 ^ 3 = ' + result); 17 | }); 18 | 19 | // Call simply returns a promise, so we can add callbacks or errbacks at will. 20 | var promise = client.call('add', [1, 1]); 21 | promise.addCallback(function(result) { 22 | sys.puts(' 1 + 1 = ' + result + ', dummy!'); 23 | }); 24 | 25 | /* These calls should each take 1.5 seconds to complete. */ 26 | client.call('delayed.add', [1, 1, 1500], function(result) { 27 | sys.puts(result); 28 | }); 29 | 30 | client.call('delayed.echo', ['Echo.', 1500], function(result) { 31 | sys.puts(result); 32 | }); -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | var rpc = require('../src/jsonrpc'); 2 | 3 | /* Create two simple functions */ 4 | function add(first, second) { 5 | return first + second; 6 | } 7 | 8 | function multiply(first, second) { 9 | return first * second; 10 | } 11 | 12 | /* Expose those methods */ 13 | rpc.expose('add', add); 14 | rpc.expose('multiply', multiply); 15 | 16 | /* We can expose entire modules easily */ 17 | var math = { 18 | power: function(first, second) { return Math.pow(first, second); }, 19 | sqrt: function(num) { return Math.sqrt(num); } 20 | } 21 | rpc.exposeModule('math', math); 22 | 23 | /* Listen on port 8000 */ 24 | rpc.listen(8000, 'localhost'); 25 | 26 | /* By returning a promise, we can delay our response indefinitely, leaving the 27 | request hanging until the promise emits success. */ 28 | var delayed = { 29 | echo: function(data, delay) { 30 | var promise = new process.Promise(); 31 | setTimeout(function() { 32 | promise.emitSuccess(data); 33 | }, delay); 34 | return promise; 35 | }, 36 | 37 | add: function(first, second, delay) { 38 | var promise = new process.Promise(); 39 | setTimeout(function() { 40 | promise.emitSuccess(first + second); 41 | }, delay); 42 | return promise; 43 | } 44 | } 45 | 46 | rpc.exposeModule('delayed', delayed); -------------------------------------------------------------------------------- /src/jsonrpc.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var http = require('http'); 3 | 4 | var extend=function(a,b) 5 | { 6 | var prop; 7 | 8 | for(prop in b) 9 | { 10 | if(b.hasOwnProperty(prop)) 11 | { 12 | a[prop] = b[prop]; 13 | } 14 | } 15 | 16 | return a; 17 | }; 18 | 19 | var functions = {}; 20 | 21 | var METHOD_NOT_ALLOWED = "Method Not Allowed\n"; 22 | var INVALID_REQUEST = "Invalid Request\n"; 23 | 24 | var JSONRPCClient = function(port, host) { 25 | this.port = port; 26 | this.host = host; 27 | 28 | this.call = function(method, params, callback, errback, path) { 29 | // First we encode the request into JSON 30 | var requestJSON = JSON.stringify({ 31 | 'id': '' + (new Date()).getTime(), 32 | 'method': method, 33 | 'params': params 34 | }); 35 | // Then we build some basic headers. 36 | var headers = { 37 | 'host': host, 38 | 'Content-Length': requestJSON.length 39 | }; 40 | 41 | if(path===null) 42 | { 43 | path='/'; 44 | } 45 | 46 | var options={ 47 | host: host, 48 | port: port, 49 | path: path, 50 | headers: headers, 51 | method: 'POST' 52 | } 53 | 54 | var buffer = ''; 55 | 56 | var req = http.request(options, function(res) { 57 | res.on('data', function(chunk) { 58 | buffer = buffer + chunk; 59 | }); 60 | 61 | res.on('end', function() { 62 | var decoded = JSON.parse(buffer); 63 | if(decoded.hasOwnProperty('result')) 64 | { 65 | callback(null, decoded.result); 66 | } 67 | else 68 | { 69 | callback(decoded.error, null); 70 | } 71 | }); 72 | 73 | res.on('error', function(err) { 74 | callback(err, null); 75 | }); 76 | }); 77 | 78 | req.write(requestJSON); 79 | req.end(); 80 | }; 81 | } 82 | 83 | var JSONRPC = { 84 | 85 | functions: functions, 86 | 87 | exposeModule: function(mod, object) { 88 | var funcs = []; 89 | for(var funcName in object) { 90 | var funcObj = object[funcName]; 91 | if(typeof(funcObj) == 'function') { 92 | functions[mod + '.' + funcName] = funcObj; 93 | funcs.push(funcName); 94 | } 95 | } 96 | JSONRPC.trace('***', 'exposing module: ' + mod + ' [funs: ' + funcs.join(', ') + ']'); 97 | return object; 98 | }, 99 | 100 | expose: function(name, func) { 101 | JSONRPC.trace('***', 'exposing: ' + name); 102 | functions[name] = func; 103 | }, 104 | 105 | trace: function(direction, message) { 106 | sys.puts(' ' + direction + ' ' + message); 107 | }, 108 | 109 | listen: function(port, host) { 110 | JSONRPC.server.listen(port, host); 111 | JSONRPC.trace('***', 'Server listening on http://' + (host || '127.0.0.1') + ':' + port + '/'); 112 | }, 113 | 114 | handleInvalidRequest: function(req, res) { 115 | res.sendHeader(400, [['Content-Type', 'text/plain'], 116 | ['Content-Length', INVALID_REQUEST.length]]); 117 | res.sendBody(INVALID_REQUEST); 118 | res.finish(); 119 | }, 120 | 121 | handlePOST: function(req, res) { 122 | var buffer = ''; 123 | var promise = new process.Promise(); 124 | promise.addCallback(function(buf) { 125 | 126 | var decoded = JSON.parse(buf); 127 | 128 | // Check for the required fields, and if they aren't there, then 129 | // dispatch to the handleInvalidRequest function. 130 | if(!(decoded.method && decoded.params && decoded.id)) { 131 | return JSONRPC.handleInvalidRequest(req, res); 132 | } 133 | if(!JSONRPC.functions.hasOwnProperty(decoded.method)) { 134 | return JSONRPC.handleInvalidRequest(req, res); 135 | } 136 | 137 | // Build our success handler 138 | var onSuccess = function(funcResp) { 139 | JSONRPC.trace('-->', 'response (id ' + decoded.id + '): ' + JSON.stringify(funcResp)); 140 | var encoded = JSON.stringify({ 141 | 'result': funcResp, 142 | 'error': null, 143 | 'id': decoded.id 144 | }); 145 | res.sendHeader(200, [['Content-Type', 'application/json'], 146 | ['Content-Length', encoded.length]]); 147 | res.sendBody(encoded); 148 | res.finish(); 149 | }; 150 | 151 | // Build our failure handler (note that error must not be null) 152 | var onFailure = function(failure) { 153 | JSONRPC.trace('-->', 'failure: ' + JSON.stringify(failure)); 154 | var encoded = JSON.stringify({ 155 | 'result': null, 156 | 'error': failure || 'Unspecified Failure', 157 | 'id': decoded.id 158 | }); 159 | res.sendHeader(200, [['Content-Type', 'application/json'], 160 | ['Content-Length', encoded.length]]); 161 | res.sendBody(encoded); 162 | res.finish(); 163 | }; 164 | 165 | JSONRPC.trace('<--', 'request (id ' + decoded.id + '): ' + decoded.method + '(' + decoded.params.join(', ') + ')'); 166 | 167 | // Try to call the method, but intercept errors and call our 168 | // onFailure handler. 169 | var method = JSONRPC.functions[decoded.method]; 170 | var resp = null; 171 | try { 172 | resp = method.apply(null, decoded.params); 173 | } 174 | catch(err) { 175 | return onFailure(err); 176 | } 177 | 178 | // If it's a promise, we should add callbacks and errbacks, 179 | // but if it's not, we can just go ahead and call the callback. 180 | if(resp instanceof process.Promise) { 181 | resp.addCallback(onSuccess); 182 | resp.addErrback(onFailure); 183 | } 184 | else { 185 | onSuccess(resp); 186 | } 187 | }); 188 | req.addListener('body', function(chunk) { 189 | buffer = buffer + chunk; 190 | }); 191 | req.addListener('complete', function() { 192 | promise.emitSuccess(buffer); 193 | }); 194 | }, 195 | 196 | handleNonPOST: function(req, res) { 197 | res.sendHeader(405, [['Content-Type', 'text/plain'], 198 | ['Content-Length', METHOD_NOT_ALLOWED.length], 199 | ['Allow', 'POST']]); 200 | res.sendBody(METHOD_NOT_ALLOWED); 201 | res.finish(); 202 | }, 203 | 204 | handleRequest: function(req, res) { 205 | JSONRPC.trace('<--', 'accepted request'); 206 | if(req.method === 'POST') { 207 | JSONRPC.handlePOST(req, res); 208 | } 209 | else { 210 | JSONRPC.handleNonPOST(req, res); 211 | } 212 | }, 213 | 214 | server: http.createServer(function(req, res) { 215 | // TODO: Get rid of this extraneous extra function call. 216 | JSONRPC.handleRequest(req, res); 217 | }), 218 | 219 | getClient: function(port, host) { 220 | return new JSONRPCClient(port, host); 221 | } 222 | }; 223 | 224 | extend(exports, JSONRPC); 225 | -------------------------------------------------------------------------------- /test/jsonrpc-test.js: -------------------------------------------------------------------------------- 1 | process.mixin(GLOBAL, require('./test')); 2 | 3 | var sys = require('sys'); 4 | var jsonrpc = require('../src/jsonrpc'); 5 | 6 | // MOCK REQUEST/RESPONSE OBJECTS 7 | var MockRequest = function(method) { 8 | this.method = method; 9 | process.EventEmitter.call(this); 10 | }; 11 | sys.inherits(MockRequest, process.EventEmitter); 12 | 13 | var MockResponse = function() { 14 | process.EventEmitter.call(this); 15 | this.sendHeader = function(httpCode, httpHeaders) { 16 | this.httpCode = httpCode; 17 | this.httpHeaders = httpCode; 18 | }; 19 | this.sendBody = function(httpBody) { 20 | this.httpBody = httpBody; 21 | }; 22 | this.finish = function() {}; 23 | }; 24 | sys.inherits(MockResponse, process.EventEmitter); 25 | 26 | // A SIMPLE MODULE 27 | var TestModule = { 28 | foo: function (a, b) { 29 | return ['foo', 'bar', a, b]; 30 | }, 31 | 32 | other: 'hello' 33 | }; 34 | 35 | // EXPOSING FUNCTIONS 36 | 37 | test('jsonrpc.expose', function() { 38 | var echo = function(data) { 39 | return data; 40 | }; 41 | jsonrpc.expose('echo', echo); 42 | assert(jsonrpc.functions.echo === echo); 43 | }) 44 | 45 | test('jsonrpc.exposeModule', function() { 46 | jsonrpc.exposeModule('test', TestModule); 47 | sys.puts(jsonrpc.functions['test.foo']); 48 | sys.puts(TestModule.foo); 49 | assert(jsonrpc.functions['test.foo'] == TestModule.foo); 50 | }); 51 | 52 | // INVALID REQUEST 53 | 54 | test('GET jsonrpc.handleRequest', function() { 55 | var req = new MockRequest('GET'); 56 | var res = new MockResponse(); 57 | jsonrpc.handleRequest(req, res); 58 | assert(res.httpCode === 405); 59 | }); 60 | 61 | function testBadRequest(testJSON) { 62 | var req = new MockRequest('POST'); 63 | var res = new MockResponse(); 64 | jsonrpc.handleRequest(req, res); 65 | req.emit('body', testJSON); 66 | req.emit('complete'); 67 | sys.puts(res.httpCode); 68 | assert(res.httpCode === 400); 69 | } 70 | 71 | test('Missing object attribute (method)', function() { 72 | var testJSON = '{ "params": ["Hello, World!"], "id": 1 }'; 73 | testBadRequest(testJSON); 74 | }); 75 | 76 | test('Missing object attribute (params)', function() { 77 | var testJSON = '{ "method": "echo", "id": 1 }'; 78 | testBadRequest(testJSON); 79 | }); 80 | 81 | test('Missing object attribute (id)', function() { 82 | var testJSON = '{ "method": "echo", "params": ["Hello, World!"] }'; 83 | testBadRequest(testJSON); 84 | }); 85 | 86 | test('Unregistered method', function() { 87 | var testJSON = '{ "method": "notRegistered", "params": ["Hello, World!"], "id": 1 }'; 88 | testBadRequest(testJSON); 89 | }); 90 | 91 | // VALID REQUEST 92 | 93 | test('Simple synchronous echo', function() { 94 | var testJSON = '{ "method": "echo", "params": ["Hello, World!"], "id": 1 }'; 95 | var req = new MockRequest('POST'); 96 | var res = new MockResponse(); 97 | jsonrpc.handleRequest(req, res); 98 | req.emit('body', testJSON); 99 | req.emit('complete'); 100 | assert(res.httpCode === 200); 101 | var decoded = JSON.parse(res.httpBody); 102 | assert(decoded.id === 1); 103 | assert(decoded.error === null); 104 | assert(decoded.result == 'Hello, World!'); 105 | }); 106 | 107 | test('Using promise', function() { 108 | // Expose a function that just returns a promise that we can control. 109 | var promise = new process.Promise(); 110 | jsonrpc.expose('promiseEcho', function(data) { 111 | return promise; 112 | }); 113 | // Build a request to call that function 114 | var testJSON = '{ "method": "promiseEcho", "params": ["Hello, World!"], "id": 1 }'; 115 | var req = new MockRequest('POST'); 116 | var res = new MockResponse(); 117 | // Have the server handle that request 118 | jsonrpc.handleRequest(req, res); 119 | req.emit('body', testJSON); 120 | req.emit('complete'); 121 | // Now the request has completed, and in the above synchronous test, we 122 | // would be finished. However, this function is smarter and only completes 123 | // when the promise completes. Therefore, we should not have a response 124 | // yet. 125 | assert(res['httpCode'] == null); 126 | // We can force the promise to emit a success code, with a message. 127 | promise.emitSuccess('Hello, World!'); 128 | // Aha, now that the promise has finished, our request has finished as well. 129 | assert(res.httpCode === 200); 130 | var decoded = JSON.parse(res.httpBody); 131 | assert(decoded.id === 1); 132 | assert(decoded.error === null); 133 | assert(decoded.result == 'Hello, World!'); 134 | }); 135 | 136 | test('Triggering an errback', function() { 137 | var promise = new process.Promise(); 138 | jsonrpc.expose('errbackEcho', function(data) { 139 | return promise; 140 | }); 141 | var testJSON = '{ "method": "errbackEcho", "params": ["Hello, World!"], "id": 1 }'; 142 | var req = new MockRequest('POST'); 143 | var res = new MockResponse(); 144 | jsonrpc.handleRequest(req, res); 145 | req.emit('body', testJSON); 146 | req.emit('complete'); 147 | assert(res['httpCode'] == null); 148 | // This time, unlike the above test, we trigger an error and expect to see 149 | // it in the error attribute of the object returned. 150 | promise.emitError('This is an error'); 151 | assert(res.httpCode === 200); 152 | var decoded = JSON.parse(res.httpBody); 153 | assert(decoded.id === 1); 154 | assert(decoded.error == 'This is an error'); 155 | assert(decoded.result == null); 156 | }) -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | 3 | TEST = { 4 | passed: 0, 5 | failed: 0, 6 | assertions: 0, 7 | 8 | test: function (desc, block) { 9 | var _puts = sys.puts, 10 | output = "", 11 | result = '?', 12 | _boom = null; 13 | sys.puts = function (s) { output += s + "\n"; } 14 | try { 15 | sys.print(" " + desc + " ..."); 16 | block(); 17 | result = '.'; 18 | } catch(boom) { 19 | if ( boom == 'FAIL' ) { 20 | result = 'F'; 21 | } else { 22 | result = 'E'; 23 | _boom = boom; 24 | sys.puts(boom.toString()); 25 | } 26 | } 27 | sys.puts = _puts; 28 | if ( result == '.' ) { 29 | sys.print(" OK\n"); 30 | TEST.passed += 1; 31 | } else { 32 | sys.print(" FAIL\n"); 33 | sys.print(output.replace(/^/, " ") + "\n"); 34 | TEST.failed += 1; 35 | if ( _boom ) throw _boom; 36 | } 37 | }, 38 | 39 | assert: function (value, desc) { 40 | TEST.assertions += 1; 41 | if ( desc ) sys.puts("ASSERT: " + desc); 42 | if ( !value ) throw 'FAIL'; 43 | }, 44 | 45 | assert_equal: function (expect, is) { 46 | assert( 47 | expect == is, 48 | sys.inspect(expect) + " == " + sys.inspect(is) 49 | ); 50 | }, 51 | 52 | assert_boom: function (message, block) { 53 | var error = null; 54 | try { block() } 55 | catch (boom) { error = boom } 56 | 57 | if ( !error ) { 58 | sys.puts('NO BOOM'); 59 | throw 'FAIL' 60 | } 61 | if ( error != message ) { 62 | sys.puts('BOOM: ' + sys.inspect(error) + 63 | ' [' + sys.inspect(message) + ' expected]'); 64 | throw 'FAIL' 65 | } 66 | } 67 | }; 68 | 69 | process.mixin(exports, TEST); 70 | 71 | process.addListener('exit', function (code) { 72 | if ( !TEST.exit ) { 73 | TEST.exit = true; 74 | sys.puts("" + TEST.passed + " passed, " + TEST.failed + " failed"); 75 | if ( TEST.failed > 0 ) { process.exit(1) }; 76 | } 77 | }); 78 | --------------------------------------------------------------------------------