├── index.js ├── History.md ├── Makefile ├── .gitmodules ├── package.json ├── Readme.md ├── test └── jsonrpc.test.js └── lib └── connect-jsonrpc.js /index.js: -------------------------------------------------------------------------------- 1 | lib/connect-jsonrpc.js -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.0.1 / 2010-08-23 3 | ================== 4 | 5 | * Initial Release 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @./support/expresso/bin/expresso \ 4 | -I lib \ 5 | -I support/connect/lib/connect \ 6 | test/*.test.js 7 | 8 | .PHONY: test -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "support/expresso"] 2 | path = support/expresso 3 | url = git://github.com/visionmedia/expresso.git 4 | [submodule "support/connect"] 5 | path = support/connect 6 | url = git://github.com/senchalabs/connect.git 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-jsonrpc", 3 | "description": "JSON-RPC 2 middleware for connect", 4 | "version": "0.0.1", 5 | "main": "./lib/connect-jsonrpc.js", 6 | "contributors": [ 7 | { 8 | "name": "TJ Holowaychuk", 9 | "email": "tj@vision-media.ca" 10 | } 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/visionmedia/connect-jsonrpc.git" 15 | } 16 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Connect JSON-RPC 3 | 4 | ## Installation 5 | 6 | $ npm install connect-jsonrpc 7 | 8 | ## Examples 9 | 10 | The _jsonrpc_ middleware provides JSON-RPC 2.0 support. Below is an example exposing the _add_ and _sub_ methods: 11 | 12 | var math = { 13 | add: function(a, b, fn){ 14 | fn(null, a + b); 15 | }, 16 | sub: function(a, b, fn){ 17 | fn(null, a - b); 18 | } 19 | }; 20 | 21 | var date = { 22 | time: function(fn){ 23 | fn(null, new Date().toUTCString()); 24 | } 25 | }; 26 | 27 | connect.createServer( 28 | require('connect-jsonrpc')(math, date) 29 | ); 30 | 31 | When you wish to pass an exception simply invoke `fn(err)`, or pass the error code `fn(jsonrpc.INVALID_PARAMS)`. Otherwise `fn(null, result)` will respond with the given results. 32 | 33 | ## Example Requests 34 | 35 | Regular params: 36 | 37 | $ curl -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "method": "add", "params": [1,2], "id":2 }' http://localhost:3000 38 | 39 | Named params: 40 | 41 | $ curl -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "method": "add", "params": { "b": 1, "a": 2 }, "id":2 }' http://localhost:3000 42 | 43 | ## License 44 | 45 | (The MIT License) 46 | 47 | Copyright (c) 2010 Sencha Inc. 48 | 49 | Permission is hereby granted, free of charge, to any person obtaining 50 | a copy of this software and associated documentation files (the 51 | 'Software'), to deal in the Software without restriction, including 52 | without limitation the rights to use, copy, modify, merge, publish, 53 | distribute, sublicense, and/or sell copies of the Software, and to 54 | permit persons to whom the Software is furnished to do so, subject to 55 | the following conditions: 56 | 57 | The above copyright notice and this permission notice shall be 58 | included in all copies or substantial portions of the Software. 59 | 60 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 61 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 62 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 63 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 64 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 65 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 66 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/jsonrpc.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect'), 7 | assert = require('assert'), 8 | jsonrpc = require('connect-jsonrpc'); 9 | 10 | function run(procedures){ 11 | var server = connect.createServer( 12 | jsonrpc(procedures) 13 | ); 14 | server.call = function(obj, fn){ 15 | if (typeof obj !== 'string') obj = JSON.stringify(obj); 16 | assert.response(server, { 17 | url: '/', 18 | method: 'POST', 19 | data: obj, 20 | headers: { 'Content-Type': 'application/json' } 21 | }, function(res){ 22 | fn(res, JSON.parse(res.body)); 23 | }); 24 | }; 25 | return server; 26 | } 27 | 28 | module.exports = { 29 | 'test invalid version': function(){ 30 | var server = run({}); 31 | server.call({ 32 | jsonrpc: '1.0', 33 | method: 'add', 34 | params: [1,2], 35 | id: 1 36 | }, function(res, body){ 37 | assert.eql({ id: 1, error: { code: jsonrpc.INVALID_REQUEST, message: 'Invalid Request.' }, jsonrpc: '2.0'}, body); 38 | }); 39 | }, 40 | 41 | 'test invalid id': function(){ 42 | var server = run({}); 43 | server.call({ 44 | jsonrpc: '2.0', 45 | method: 'add', 46 | params: [1,2] 47 | }, function(res, body){ 48 | assert.eql({ id: null, error: { code: jsonrpc.INVALID_REQUEST, message: 'Invalid Request.' }, jsonrpc: '2.0' }, body); 49 | }); 50 | }, 51 | 52 | 'test parse error': function(){ 53 | var server = run({}); 54 | server.call('{ "invalid:', function(res, body){ 55 | assert.eql({ id: null, error: { code: jsonrpc.PARSE_ERROR, message: 'Parse Error.' }, jsonrpc: '2.0' }, body); 56 | }); 57 | }, 58 | 59 | 'test invalid method': function(){ 60 | var server = run({}); 61 | server.call({ 62 | jsonrpc: '2.0', 63 | method: 'add', 64 | id: 1 65 | }, function(res, body){ 66 | assert.eql({ id: 1, error: { code: jsonrpc.METHOD_NOT_FOUND, message: 'Method Not Found.' }, jsonrpc: '2.0' }, body); 67 | }); 68 | }, 69 | 70 | 'test passing method exceptions': function(){ 71 | var server = run({ 72 | add: function(a, b, fn){ 73 | if (arguments.length === 3) { 74 | if (typeof a === 'number' && typeof b === 'number') { 75 | fn(null, a + b); 76 | } else { 77 | var err = new Error('Arguments must be numeric.'); 78 | err.code = jsonrpc.INVALID_PARAMS; 79 | fn(err); 80 | } 81 | } else { 82 | fn = arguments[arguments.length - 1]; 83 | fn(jsonrpc.INVALID_PARAMS); 84 | } 85 | } 86 | }); 87 | 88 | // Invalid params 89 | 90 | server.call({ 91 | jsonrpc: '2.0', 92 | method: 'add', 93 | params: [1], 94 | id: 1 95 | }, function(res, body){ 96 | assert.eql({ id: 1, error: { code: jsonrpc.INVALID_PARAMS, message: 'Invalid Params.' }, jsonrpc: '2.0' }, body); 97 | }); 98 | 99 | // Valid 100 | 101 | server.call({ 102 | jsonrpc: '2.0', 103 | method: 'add', 104 | params: [1, 2], 105 | id: 2 106 | }, function(res, body){ 107 | assert.eql({ id: 2, result: 3, jsonrpc: '2.0' }, body); 108 | }); 109 | 110 | // Custom exception 111 | 112 | server.call({ 113 | jsonrpc: '2.0', 114 | method: 'add', 115 | params: [1, {}], 116 | id: 3 117 | }, function(res, body){ 118 | assert.eql({ id: 3, error: { code: jsonrpc.INVALID_PARAMS, message: 'Arguments must be numeric.' }, jsonrpc: '2.0' }, body); 119 | }); 120 | }, 121 | 122 | 'test methode call': function(){ 123 | var server = run({ 124 | add: function(a, b, fn){ 125 | fn(null, a + b); 126 | } 127 | }); 128 | server.call({ 129 | jsonrpc: '2.0', 130 | method: 'add', 131 | params: [1,2], 132 | id: 1 133 | }, function(res, body){ 134 | assert.eql({ id: 1, result: 3, jsonrpc: '2.0' }, body); 135 | }); 136 | }, 137 | 138 | 'test variable arguments': function(){ 139 | var server = run({ 140 | add: function(){ 141 | var sum = 0, 142 | fn = arguments[arguments.length - 1], 143 | len = arguments.length - 1; 144 | for (var i = 0; i < len; ++i) { 145 | sum += arguments[i]; 146 | } 147 | fn(null, sum); 148 | } 149 | }); 150 | server.call({ 151 | jsonrpc: '2.0', 152 | method: 'add', 153 | params: [1,2,3,4,5], 154 | id: 1 155 | }, function(res, body){ 156 | assert.eql({ id: 1, result: 15, jsonrpc: '2.0' }, body); 157 | }); 158 | }, 159 | 160 | 'test named params': function(){ 161 | var server = run({ 162 | delay: function(ms, msg, unused, fn){ 163 | setTimeout(function(){ 164 | fn(null, msg); 165 | }, ms); 166 | } 167 | }); 168 | 169 | server.call({ 170 | jsonrpc: '2.0', 171 | method: 'delay', 172 | params: { msg: 'Whoop!', ms: 50 }, 173 | id: 1 174 | }, function(res, body){ 175 | assert.eql({ id: 1, result: 'Whoop!', jsonrpc: '2.0' }, body); 176 | }); 177 | }, 178 | 179 | 'test batch calls': function(){ 180 | var server = run({ 181 | multiply: function(a, b, fn){ 182 | fn(null, a * b); 183 | }, 184 | sub: function(a, b, fn){ 185 | fn(null, a - b); 186 | } 187 | }); 188 | server.call([{ 189 | jsonrpc: '2.0', 190 | method: 'multiply', 191 | params: [2,2], 192 | id: 1 193 | }, { 194 | jsonrpc: '2.0', 195 | method: 'sub', 196 | params: [2, 1], 197 | id: 2 198 | }, 199 | {}, 200 | { jsonrpc: '2.0', id: 3 } 201 | ], function(res, body){ 202 | assert.eql({ id: 1, result: 4, jsonrpc: '2.0' }, body[0]); 203 | assert.eql({ id: 2, result: 1, jsonrpc: '2.0' }, body[1]); 204 | assert.eql({ id: null, error: { code: jsonrpc.INVALID_REQUEST, message: 'Invalid Request.' }, jsonrpc: '2.0' }, body[2]); 205 | assert.eql({ id: 3, error: { code: jsonrpc.INVALID_REQUEST, message: 'Invalid Request.' }, jsonrpc: '2.0' }, body[3]); 206 | }); 207 | } 208 | } -------------------------------------------------------------------------------- /lib/connect-jsonrpc.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Ext JS Connect 3 | * Copyright(c) 2010 Sencha Inc. 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var parse = require('url').parse, 12 | http = require('http'); 13 | 14 | /** 15 | * Export the `setup()` function. 16 | */ 17 | 18 | exports = module.exports = jsonrpc; 19 | 20 | /** 21 | * JSON-RPC version. 22 | */ 23 | 24 | var VERSION = exports.VERSION = '2.0'; 25 | 26 | /** 27 | * JSON parse error. 28 | */ 29 | 30 | var PARSE_ERROR = exports.PARSE_ERROR = -32700; 31 | 32 | /** 33 | * Invalid request due to invalid or missing properties. 34 | */ 35 | 36 | var INVALID_REQUEST = exports.INVALID_REQUEST = -32600; 37 | 38 | /** 39 | * Service method does not exist. 40 | */ 41 | 42 | var METHOD_NOT_FOUND = exports.METHOD_NOT_FOUND = -32601; 43 | 44 | /** 45 | * Invalid parameters. 46 | */ 47 | 48 | var INVALID_PARAMS = exports.INVALID_PARAMS = -32602; 49 | 50 | /** 51 | * Internal JSON-RPC error. 52 | */ 53 | 54 | var INTERNAL_ERROR = exports.INTERNAL_ERROR = -32603; 55 | 56 | /** 57 | * Default error messages. 58 | */ 59 | 60 | var errorMessages = exports.errorMessages = {}; 61 | errorMessages[PARSE_ERROR] = 'Parse Error.'; 62 | errorMessages[INVALID_REQUEST] = 'Invalid Request.'; 63 | errorMessages[METHOD_NOT_FOUND] = 'Method Not Found.'; 64 | errorMessages[INVALID_PARAMS] = 'Invalid Params.'; 65 | errorMessages[INTERNAL_ERROR] = 'Internal Error.'; 66 | 67 | /** 68 | * Accepts any number of objects, exposing their methods. 69 | * 70 | * @param {Object} ... 71 | * @return {Function} 72 | * @api public 73 | */ 74 | 75 | function jsonrpc(services) { 76 | services = services || {}; 77 | 78 | // Merge methods 79 | for (var i = 0, len = arguments.length; i < len; ++i) { 80 | var args = arguments[i]; 81 | Object.keys(args).forEach(function (key) { 82 | services[key] = args[key]; 83 | }); 84 | } 85 | 86 | /** 87 | * Handle JSON-RPC request. 88 | * 89 | * @param {Object} rpc 90 | * @param {Function} respond 91 | */ 92 | 93 | function handleRequest(rpc, respond){ 94 | if (validRequest(rpc)) { 95 | var method = services[rpc.method]; 96 | if (typeof method === 'function') { 97 | var params = []; 98 | // Unnamed params 99 | if (Array.isArray(rpc.params)) { 100 | params = rpc.params; 101 | // Named params 102 | } else if (typeof rpc.params === 'object') { 103 | var names = method.toString().match(/\((.*?)\)/)[1].match(/[\w]+/g); 104 | if (names) { 105 | for (var i = 0, len = names.length - 1; i < len; ++i) { 106 | params.push(rpc.params[names[i]]); 107 | } 108 | } else { 109 | // Function does not have named parameters 110 | return respond({ error: { code: INVALID_PARAMS, message: 'This service does not support named parameters.' }}); 111 | } 112 | } 113 | // Reply with the given err and result 114 | function reply(err, result){ 115 | if (err) { 116 | if (typeof err === 'number') { 117 | respond({ 118 | error: { 119 | code: err 120 | } 121 | }); 122 | } else { 123 | respond({ 124 | error: { 125 | code: err.code || INTERNAL_ERROR, 126 | message: err.message 127 | } 128 | }); 129 | } 130 | } else { 131 | respond({ 132 | result: result 133 | }); 134 | } 135 | } 136 | // Push reply function as the last argument 137 | params.push(reply); 138 | 139 | // Invoke the method 140 | try { 141 | method.apply(this, params); 142 | } catch (err) { 143 | reply(err); 144 | } 145 | } else { 146 | respond({ error: { code: METHOD_NOT_FOUND }}); 147 | } 148 | } else { 149 | respond({ error: { code: INVALID_REQUEST }}); 150 | } 151 | } 152 | 153 | return function jsonrpc(req, res, next) { 154 | var me = this, 155 | contentType = req.headers['content-type'] || ''; 156 | if (req.method === 'POST' && contentType.indexOf('application/json') >= 0) { 157 | var data = ''; 158 | req.setEncoding('utf8'); 159 | req.addListener('data', function(chunk) { data += chunk; }); 160 | req.addListener('end', function() { 161 | 162 | // Attempt to parse incoming JSON string 163 | 164 | try { 165 | var rpc = JSON.parse(data), 166 | batch = Array.isArray(rpc); 167 | } catch (err) { 168 | return respond(normalize(rpc, { error: { code: PARSE_ERROR }})); 169 | } 170 | 171 | /** 172 | * Normalize response object. 173 | */ 174 | 175 | function normalize(rpc, obj) { 176 | obj.id = rpc && typeof rpc.id === 'number' 177 | ? rpc.id 178 | : null; 179 | obj.jsonrpc = VERSION; 180 | if (obj.error && !obj.error.message) { 181 | obj.error.message = errorMessages[obj.error.code]; 182 | } 183 | return obj; 184 | } 185 | 186 | /** 187 | * Respond with the given response object. 188 | */ 189 | 190 | function respond(obj) { 191 | var body = JSON.stringify(obj); 192 | res.writeHead(200, { 193 | 'Content-Type': 'application/json', 194 | 'Content-Length': Buffer.byteLength(body) 195 | }); 196 | res.end(body); 197 | } 198 | 199 | // Handle requests 200 | 201 | if (batch) { 202 | var responses = [], 203 | len = rpc.length, 204 | pending = len; 205 | for (var i = 0; i < len; ++i) { 206 | (function(rpc){ 207 | handleRequest.call(me, rpc, function(obj){ 208 | responses.push(normalize(rpc, obj)); 209 | if (!--pending) { 210 | respond(responses); 211 | } 212 | }); 213 | })(rpc[i]); 214 | } 215 | } else { 216 | handleRequest.call(me, rpc, function(obj){ 217 | respond(normalize(rpc, obj)); 218 | }); 219 | } 220 | }); 221 | } else { 222 | next(); 223 | } 224 | }; 225 | }; 226 | 227 | /** 228 | * Check if the given request is a valid 229 | * JSON remote procedure call. 230 | * 231 | * - "jsonrpc" must match the supported version ('2.0') 232 | * - "id" must be numeric 233 | * - "method" must be a string 234 | * 235 | * @param {Object} rpc 236 | * @return {Boolean} 237 | * @api private 238 | */ 239 | 240 | function validRequest(rpc){ 241 | return rpc.jsonrpc === VERSION 242 | && (typeof rpc.id === 'number' || typeof rpc.id === 'string') 243 | && typeof rpc.method === 'string'; 244 | } 245 | --------------------------------------------------------------------------------