├── 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 |
--------------------------------------------------------------------------------