Skip to content

Instantly share code, notes, and snippets.

@fresc81
Created September 28, 2012 19:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fresc81/3801652 to your computer and use it in GitHub Desktop.
Save fresc81/3801652 to your computer and use it in GitHub Desktop.
/*!
* Ext JS Connect
* Copyright(c) 2010 Sencha Inc.
* MIT Licensed
*/
/**
* Module dependencies.
*/
var parse = require('url').parse,
http = require('http');
/**
* Export the `setup()` function.
*/
exports = module.exports = jsonrpc;
/**
* JSON-RPC version.
*/
var VERSION = exports.VERSION = '2.0';
/**
* JSON parse error.
*/
var PARSE_ERROR = exports.PARSE_ERROR = -32700;
/**
* Invalid request due to invalid or missing properties.
*/
var INVALID_REQUEST = exports.INVALID_REQUEST = -32600;
/**
* Service method does not exist.
*/
var METHOD_NOT_FOUND = exports.METHOD_NOT_FOUND = -32601;
/**
* Invalid parameters.
*/
var INVALID_PARAMS = exports.INVALID_PARAMS = -32602;
/**
* Internal JSON-RPC error.
*/
var INTERNAL_ERROR = exports.INTERNAL_ERROR = -32603;
/**
* Default error messages.
*/
var errorMessages = exports.errorMessages = {};
errorMessages[PARSE_ERROR] = 'Parse Error.';
errorMessages[INVALID_REQUEST] = 'Invalid Request.';
errorMessages[METHOD_NOT_FOUND] = 'Method Not Found.';
errorMessages[INVALID_PARAMS] = 'Invalid Params.';
errorMessages[INTERNAL_ERROR] = 'Internal Error.';
/**
* Accepts any number of objects, exposing their methods.
*
* @param {Object} ...
* @return {Function}
* @api public
*/
function jsonrpc(services) {
services = services || {};
// Merge methods
for (var i = 0, len = arguments.length; i < len; ++i) {
var args = arguments[i];
Object.keys(args).forEach(function (key) {
services[key] = args[key];
});
}
/**
* Handle JSON-RPC request.
*
* @param {Object} rpc
* @param {Function} respond
*/
function handleRequest(rpc, req, res, respond){
if (validRequest(rpc)) {
var method = services[rpc.method];
if (typeof method === 'function') {
var params = [];
// Unnamed params
if (Array.isArray(rpc.params)) {
params = rpc.params;
// Named params
} else if (typeof rpc.params === 'object') {
var names = method.toString().match(/\((.*?)\)/)[1].match(/[\w]+/g);
if (names) {
for (var i = 0, len = names.length - 1; i < len; ++i) {
params.push(rpc.params[names[i]]);
}
} else {
// Function does not have named parameters
return respond({ error: { code: INVALID_PARAMS, message: 'This service does not support named parameters.' }});
}
}
// Reply with the given err and result
function reply(err, result){
if (err) {
if (typeof err === 'number') {
respond({
error: {
code: err
}
});
} else {
respond({
error: {
code: err.code || INTERNAL_ERROR,
message: err.message
}
});
}
} else {
respond({
result: result
});
}
}
// store references to request response
reply.req = req;
reply.res = res;
// Push reply function as the last argument
params.push(reply);
// Invoke the method
try {
method.apply(this, params);
} catch (err) {
reply(err);
}
} else {
respond({ error: { code: METHOD_NOT_FOUND }});
}
} else {
respond({ error: { code: INVALID_REQUEST }});
}
}
return function jsonrpc(req, res, next) {
var me = this,
contentType = req.headers['content-type'] || '';
if (req.method === 'POST' && contentType.indexOf('application/json') >= 0) {
var data = '';
req.setEncoding('utf8');
req.addListener('data', function(chunk) { data += chunk; });
req.addListener('end', function() {
// Attempt to parse incoming JSON string
try {
var rpc = JSON.parse(data),
batch = Array.isArray(rpc);
} catch (err) {
return respond(normalize(rpc, { error: { code: PARSE_ERROR }}));
}
/**
* Normalize response object.
*/
function normalize(rpc, obj) {
obj.id = rpc && typeof rpc.id === 'number'
? rpc.id
: null;
obj.jsonrpc = VERSION;
if (obj.error && !obj.error.message) {
obj.error.message = errorMessages[obj.error.code];
}
return obj;
}
/**
* Respond with the given response object.
*/
function respond(obj) {
var body = JSON.stringify(obj);
res.writeHead(200, {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body)
});
res.end(body);
}
// Handle requests
if (batch) {
var responses = [],
len = rpc.length,
pending = len;
for (var i = 0; i < len; ++i) {
(function(rpc){
handleRequest.call(me, rpc, req, res, function(obj){
responses.push(normalize(rpc, obj));
if (!--pending) {
respond(responses);
}
});
})(rpc[i]);
}
} else {
handleRequest.call(me, rpc, req, res, function(obj){
respond(normalize(rpc, obj));
});
}
});
} else {
next();
}
};
};
/**
* Check if the given request is a valid
* JSON remote procedure call.
*
* - "jsonrpc" must match the supported version ('2.0')
* - "id" must be numeric
* - "method" must be a string
*
* @param {Object} rpc
* @return {Boolean}
* @api private
*/
function validRequest(rpc){
return rpc.jsonrpc === VERSION
&& typeof rpc.id === 'number'
&& typeof rpc.method === 'string';
}
@fresc81
Copy link
Author

fresc81 commented Sep 28, 2012

var connect=require('connect'),
jsonrpc=require('connect-jsonrpc'),
service={ sayHello: function (name, num, fn) { if (arguments.length!=3) throw new Error("parameters do not match"); fn(null, 'hello '+name+'('+num+') -> '+fn.req.url); } },
server=connect.createServer(jsonrpc(service)).listen(80);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment