Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

first commit

  • Loading branch information...
commit 693d5ee45cee04e97498100c213a5acc9dc24a2b 0 parents
@ericflo ericflo authored
22 LICENSE
@@ -0,0 +1,22 @@
+ Copyright (C) 2009 Eric Florenzano <eflorenzano.com/aboutme/> and
+ Ryan Tomayko <tomayko.com/about>
+
+Permission is hereby granted, free of charge, to any person ob-
+taining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without restric-
+tion, including without limitation the rights to use, copy, modi-
+fy, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is fur-
+nished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONIN-
+FRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
6 Makefile
@@ -0,0 +1,6 @@
+NODE = env NODE_PATH=src node
+
+test: .PHONY
+ ls -1 test/*-test.js | xargs -n 1 $(NODE)
+
+.PHONY:
29 README
@@ -0,0 +1,29 @@
+This is a JSON-RPC server and client library for node.js <http://nodejs.org/>,
+the V8 based evented IO framework.
+
+Firing up an efficient JSON-RPC server becomes extremely simple:
+
+ var rpc = require('jsonrpc');
+
+ function add(first, second) {
+ return first + second;
+ }
+ rpc.expose('add', add);
+
+ rpc.listen(8000, 'localhost');
+
+
+And creating a client to speak to that server is easy too:
+
+ var rpc = require('jsonrpc');
+
+ var client = rpc.getClient(8000, 'localhost');
+
+ client.call('add', [1, 2], function(result) {
+ sys.puts('1 + 2 = ' + result);
+ });
+
+To learn more, see the examples directory, peruse test/jsonrpc-test.js, or
+simply "Use The Source, Luke".
+
+More documentation and development is on its way.
33 examples/client.js
@@ -0,0 +1,33 @@
+var sys = require('sys');
+var rpc = require('../src/jsonrpc');
+
+var client = rpc.getClient(8000, 'localhost');
+
+client.call('add', [1, 2], function(result) {
+ sys.puts(' 1 + 2 = ' + result);
+});
+
+client.call('multiply', [199, 2], function(result) {
+ sys.puts('199 * 2 = ' + result);
+});
+
+// Accessing modules is as simple as dot-prefixing.
+client.call('math.power', [3, 3], function(result) {
+ sys.puts(' 3 ^ 3 = ' + result);
+});
+
+// Call simply returns a promise, so we can add callbacks or errbacks at will.
+var promise = client.call('add', [1, 1]);
+promise.addCallback(function(result) {
+ sys.puts(' 1 + 1 = ' + result + ', dummy!');
+});
+
+/* THESE DON'T WORK YET, SEE NOTES IN <examples/server.js>
+
+client.call('delayed.add', [1, 1, 1000], function(result) {
+ sys.puts(result);
+});
+
+client.call('delayed.multiply', [5, 5, 500], function(result) {
+ sys.puts(result);
+});*/
62 examples/server.js
@@ -0,0 +1,62 @@
+var rpc = require('../src/jsonrpc');
+
+/* Create two simple functions */
+function add(first, second) {
+ return first + second;
+}
+
+function multiply(first, second) {
+ return first * second;
+}
+
+/* Expose those methods */
+rpc.expose('add', add);
+rpc.expose('multiply', multiply);
+
+/* We can expose entire modules easily */
+var math = {
+ power: function(first, second) { return Math.pow(first, second); },
+ sqrt: function(num) { return Math.sqrt(num); }
+}
+rpc.exposeModule('math', math);
+
+/* Listen on port 8000 */
+rpc.listen(8000, 'localhost');
+
+/*
+=====================================================================================
+NOTE
+=====================================================================================
+Right now creating process.Promise objects inside of these callback functions
+somehow causes node.js to bail with this error:
+
+"V8 FATAL ERROR. v8::Object::SetInternalField() Writing internal field out of bounds"
+
+Once this is fixed, or I figure out where I went wrong, I'll be able to
+uncomment this code.
+=====================================================================================
+
+
+var delayed = {
+ echo: function(data, delay) {
+ var promise = process.Promise();
+ setTimeout(function() {
+ promise.emitSuccess(data);
+ }, delay);
+ return promise;
+ },
+
+ add: function(first, second, delay) {
+ var promise = process.Promise();
+ if(1) {
+ return first + second;
+ }
+ setTimeout(function() {
+ promise.emitSuccess(first + second);
+ }, delay);
+ return promise;
+ }
+}
+
+rpc.exposeModule('delayed', delayed);
+*/
204 src/jsonrpc.js
@@ -0,0 +1,204 @@
+var sys = require('sys');
+var http = require('http');
+
+var functions = {};
+
+var METHOD_NOT_ALLOWED = "Method Not Allowed\n";
+var INVALID_REQUEST = "Invalid Request\n";
+
+var JSONRPCClient = function(port, host) {
+ this.port = port;
+ this.host = host;
+
+ var client = http.createClient(port, host);
+
+ this.call = function(method, params, callback, errback, path) {
+ // First we encode the request into JSON
+ var requestJSON = JSON.stringify({
+ 'id': '' + (new Date()).getTime(),
+ 'method': method,
+ 'params': params
+ });
+ // Then we build some basic headers.
+ var headers = {
+ 'host': host,
+ 'Content-Length': requestJSON.length
+ }
+ // We will be returning a Promise for when this result completes, so
+ // we first need to instantiate it.
+ var promise = new process.Promise();
+ // Now we'll make a request to the server
+ var request = client.post(path || '/', headers);
+ request.sendBody(requestJSON);
+ request.finish(function(response) {
+ // We need to buffer the response chunks in a nonblocking way.
+ var buffer = '';
+ response.addListener('body', function(chunk) {
+ buffer = buffer + chunk;
+ });
+ // When all the responses are finished, we decode the JSON and
+ // depending on whether it's got a result or an error, we call
+ // emitSuccess or emitError on the promise.
+ response.addListener('complete', function() {
+ var decoded = JSON.parse(buffer);
+ if(decoded.hasOwnProperty('result')) {
+ promise.emitSuccess(decoded.result);
+ }
+ else {
+ promise.emitError(decoded.error);
+ }
+ });
+ });
+ // If a callback was passed, we politely attach it to the promise.
+ if(callback) {
+ promise.addCallback(callback);
+ }
+ if(errback) {
+ promise.addErrback(errback);
+ }
+ return promise;
+ };
+}
+
+var JSONRPC = {
+
+ functions: functions,
+
+ exposeModule: function(mod, object) {
+ var funcs = [];
+ for(var funcName in object) {
+ var funcObj = object[funcName];
+ if(typeof(funcObj) == 'function') {
+ functions[mod + '.' + funcName] = funcObj;
+ funcs.push(funcName);
+ }
+ }
+ JSONRPC.trace('***', 'exposing module: ' + mod + ' [funs: ' + funcs.join(', ') + ']');
+ return object;
+ },
+
+ expose: function(name, func) {
+ JSONRPC.trace('***', 'exposing: ' + name);
+ functions[name] = func;
+ },
+
+ trace: function(direction, message) {
+ sys.puts(' ' + direction + ' ' + message);
+ },
+
+ listen: function(port, host) {
+ JSONRPC.server.listen(port, host);
+ JSONRPC.trace('***', 'Server listening on http://' + (host || '127.0.0.1') + ':' + port + '/');
+ },
+
+ handleInvalidRequest: function(req, res) {
+ res.sendHeader(400, [['Content-Type', 'text/plain'],
+ ['Content-Length', INVALID_REQUEST.length]]);
+ res.sendBody(INVALID_REQUEST);
+ res.finish();
+ },
+
+ handlePOST: function(req, res) {
+ var buffer = '';
+ var promise = new process.Promise();
+ promise.addCallback(function(buf) {
+
+ var decoded = JSON.parse(buf);
+
+ // Check for the required fields, and if they aren't there, then
+ // dispatch to the handleInvalidRequest function.
+ if(!(decoded.method && decoded.params && decoded.id)) {
+ return JSONRPC.handleInvalidRequest(req, res);
+ }
+ if(!JSONRPC.functions.hasOwnProperty(decoded.method)) {
+ return JSONRPC.handleInvalidRequest(req, res);
+ }
+
+ // Build our success handler
+ var onSuccess = function(funcResp) {
+ JSONRPC.trace('-->', 'response (id ' + decoded.id + '): ' + JSON.stringify(funcResp));
+ var encoded = JSON.stringify({
+ 'result': funcResp,
+ 'error': null,
+ 'id': decoded.id
+ });
+ res.sendHeader(200, [['Content-Type', 'application/json'],
+ ['Content-Length', encoded.length]]);
+ res.sendBody(encoded);
+ res.finish();
+ };
+
+ // Build our failure handler (note that error must not be null)
+ var onFailure = function(failure) {
+ JSONRPC.trace('-->', 'failure: ' + JSON.stringify(failure));
+ var encoded = JSON.stringify({
+ 'result': null,
+ 'error': failure || 'Unspecified Failure',
+ 'id': decoded.id
+ });
+ res.sendHeader(200, [['Content-Type', 'application/json'],
+ ['Content-Length', encoded.length]]);
+ res.sendBody(encoded);
+ res.finish();
+ };
+
+ JSONRPC.trace('<--', 'request (id ' + decoded.id + '): ' + decoded.method + '(' + decoded.params.join(', ') + ')');
+
+ // Try to call the method, but intercept errors and call our
+ // onFailure handler.
+ var method = JSONRPC.functions[decoded.method];
+ var resp = null;
+ try {
+ resp = method.apply(null, decoded.params);
+ }
+ catch(err) {
+ return onFailure(err);
+ }
+
+ // If it's a promise, we should add callbacks and errbacks,
+ // but if it's not, we can just go ahead and call the callback.
+ if(resp instanceof process.Promise) {
+ resp.addCallback(onSuccess);
+ resp.addErrback(onFailure);
+ }
+ else {
+ onSuccess(resp);
+ }
+ });
+ req.addListener('body', function(chunk) {
+ buffer = buffer + chunk;
+ });
+ req.addListener('complete', function() {
+ promise.emitSuccess(buffer);
+ });
+ },
+
+ handleNonPOST: function(req, res) {
+ res.sendHeader(405, [['Content-Type', 'text/plain'],
+ ['Content-Length', METHOD_NOT_ALLOWED.length],
+ ['Allow', 'POST']]);
+ res.sendBody(METHOD_NOT_ALLOWED);
+ res.finish();
+ },
+
+ handleRequest: function(req, res) {
+ JSONRPC.trace('<--', 'accepted request');
+ if(req.method === 'POST') {
+ JSONRPC.handlePOST(req, res);
+ }
+ else {
+ JSONRPC.handleNonPOST(req, res);
+ }
+ },
+
+ server: http.createServer(function(req, res) {
+ // TODO: Get rid of this extraneous extra function call.
+ JSONRPC.handleRequest(req, res);
+ }),
+
+ getClient: function(port, host) {
+ return new JSONRPCClient(port, host);
+ }
+};
+
+process.mixin(exports, JSONRPC);
156 test/jsonrpc-test.js
@@ -0,0 +1,156 @@
+process.mixin(GLOBAL, require('./test'));
+
+var sys = require('sys');
+var jsonrpc = require('../src/jsonrpc');
+
+// MOCK REQUEST/RESPONSE OBJECTS
+var MockRequest = function(method) {
+ this.method = method;
+ process.EventEmitter.call(this);
+};
+sys.inherits(MockRequest, process.EventEmitter);
+
+var MockResponse = function() {
+ process.EventEmitter.call(this);
+ this.sendHeader = function(httpCode, httpHeaders) {
+ this.httpCode = httpCode;
+ this.httpHeaders = httpCode;
+ };
+ this.sendBody = function(httpBody) {
+ this.httpBody = httpBody;
+ };
+ this.finish = function() {};
+};
+sys.inherits(MockResponse, process.EventEmitter);
+
+// A SIMPLE MODULE
+var TestModule = {
+ foo: function (a, b) {
+ return ['foo', 'bar', a, b];
+ },
+
+ other: 'hello'
+};
+
+// EXPOSING FUNCTIONS
+
+test('jsonrpc.expose', function() {
+ var echo = function(data) {
+ return data;
+ };
+ jsonrpc.expose('echo', echo);
+ assert(jsonrpc.functions.echo === echo);
+})
+
+test('jsonrpc.exposeModule', function() {
+ jsonrpc.exposeModule('test', TestModule);
+ sys.puts(jsonrpc.functions['test.foo']);
+ sys.puts(TestModule.foo);
+ assert(jsonrpc.functions['test.foo'] == TestModule.foo);
+});
+
+// INVALID REQUEST
+
+test('GET jsonrpc.handleRequest', function() {
+ var req = new MockRequest('GET');
+ var res = new MockResponse();
+ jsonrpc.handleRequest(req, res);
+ assert(res.httpCode === 405);
+});
+
+function testBadRequest(testJSON) {
+ var req = new MockRequest('POST');
+ var res = new MockResponse();
+ jsonrpc.handleRequest(req, res);
+ req.emit('body', testJSON);
+ req.emit('complete');
+ sys.puts(res.httpCode);
+ assert(res.httpCode === 400);
+}
+
+test('Missing object attribute (method)', function() {
+ var testJSON = '{ "params": ["Hello, World!"], "id": 1 }';
+ testBadRequest(testJSON);
+});
+
+test('Missing object attribute (params)', function() {
+ var testJSON = '{ "method": "echo", "id": 1 }';
+ testBadRequest(testJSON);
+});
+
+test('Missing object attribute (id)', function() {
+ var testJSON = '{ "method": "echo", "params": ["Hello, World!"] }';
+ testBadRequest(testJSON);
+});
+
+test('Unregistered method', function() {
+ var testJSON = '{ "method": "notRegistered", "params": ["Hello, World!"], "id": 1 }';
+ testBadRequest(testJSON);
+});
+
+// VALID REQUEST
+
+test('Simple synchronous echo', function() {
+ var testJSON = '{ "method": "echo", "params": ["Hello, World!"], "id": 1 }';
+ var req = new MockRequest('POST');
+ var res = new MockResponse();
+ jsonrpc.handleRequest(req, res);
+ req.emit('body', testJSON);
+ req.emit('complete');
+ assert(res.httpCode === 200);
+ var decoded = JSON.parse(res.httpBody);
+ assert(decoded.id === 1);
+ assert(decoded.error === null);
+ assert(decoded.result == 'Hello, World!');
+});
+
+test('Using promise', function() {
+ // Expose a function that just returns a promise that we can control.
+ var promise = new process.Promise();
+ jsonrpc.expose('promiseEcho', function(data) {
+ return promise;
+ });
+ // Build a request to call that function
+ var testJSON = '{ "method": "promiseEcho", "params": ["Hello, World!"], "id": 1 }';
+ var req = new MockRequest('POST');
+ var res = new MockResponse();
+ // Have the server handle that request
+ jsonrpc.handleRequest(req, res);
+ req.emit('body', testJSON);
+ req.emit('complete');
+ // Now the request has completed, and in the above synchronous test, we
+ // would be finished. However, this function is smarter and only completes
+ // when the promise completes. Therefore, we should not have a response
+ // yet.
+ assert(res['httpCode'] == null);
+ // We can force the promise to emit a success code, with a message.
+ promise.emitSuccess('Hello, World!');
+ // Aha, now that the promise has finished, our request has finished as well.
+ assert(res.httpCode === 200);
+ var decoded = JSON.parse(res.httpBody);
+ assert(decoded.id === 1);
+ assert(decoded.error === null);
+ assert(decoded.result == 'Hello, World!');
+});
+
+test('Triggering an errback', function() {
+ var promise = new process.Promise();
+ jsonrpc.expose('errbackEcho', function(data) {
+ return promise;
+ });
+ var testJSON = '{ "method": "errbackEcho", "params": ["Hello, World!"], "id": 1 }';
+ var req = new MockRequest('POST');
+ var res = new MockResponse();
+ jsonrpc.handleRequest(req, res);
+ req.emit('body', testJSON);
+ req.emit('complete');
+ assert(res['httpCode'] == null);
+ // This time, unlike the above test, we trigger an error and expect to see
+ // it in the error attribute of the object returned.
+ promise.emitError('This is an error');
+ assert(res.httpCode === 200);
+ var decoded = JSON.parse(res.httpBody);
+ assert(decoded.id === 1);
+ assert(decoded.error == 'This is an error');
+ assert(decoded.result == null);
+})
77 test/test.js
@@ -0,0 +1,77 @@
+var sys = require('sys');
+
+TEST = {
+ passed: 0,
+ failed: 0,
+ assertions: 0,
+
+ test: function (desc, block) {
+ var _puts = sys.puts,
+ output = "",
+ result = '?',
+ _boom = null;
+ sys.puts = function (s) { output += s + "\n"; }
+ try {
+ sys.print(" " + desc + " ...");
+ block();
+ result = '.';
+ } catch(boom) {
+ if ( boom == 'FAIL' ) {
+ result = 'F';
+ } else {
+ result = 'E';
+ _boom = boom;
+ sys.puts(boom.toString());
+ }
+ }
+ sys.puts = _puts;
+ if ( result == '.' ) {
+ sys.print(" OK\n");
+ TEST.passed += 1;
+ } else {
+ sys.print(" FAIL\n");
+ sys.print(output.replace(/^/, " ") + "\n");
+ TEST.failed += 1;
+ if ( _boom ) throw _boom;
+ }
+ },
+
+ assert: function (value, desc) {
+ TEST.assertions += 1;
+ if ( desc ) sys.puts("ASSERT: " + desc);
+ if ( !value ) throw 'FAIL';
+ },
+
+ assert_equal: function (expect, is) {
+ assert(
+ expect == is,
+ sys.inspect(expect) + " == " + sys.inspect(is)
+ );
+ },
+
+ assert_boom: function (message, block) {
+ var error = null;
+ try { block() }
+ catch (boom) { error = boom }
+
+ if ( !error ) {
+ sys.puts('NO BOOM');
+ throw 'FAIL'
+ }
+ if ( error != message ) {
+ sys.puts('BOOM: ' + sys.inspect(error) +
+ ' [' + sys.inspect(message) + ' expected]');
+ throw 'FAIL'
+ }
+ }
+};
+
+process.mixin(exports, TEST);
+
+process.addListener('exit', function (code) {
+ if ( !TEST.exit ) {
+ TEST.exit = true;
+ sys.puts("" + TEST.passed + " passed, " + TEST.failed + " failed");
+ if ( TEST.failed > 0 ) { process.exit(1) };
+ }
+});

0 comments on commit 693d5ee

Please sign in to comment.
Something went wrong with that request. Please try again.