UNPKG

remjson

Version:

JSON-RPC 1.0/2.0 compliant server and client

418 lines (351 loc) 12.4 kB
var remjson = require('./'); var events = require('events'); var _ = require('lodash'); var utils = require('./utils'); /** * Constructor for a RemJson Server * @class Server * @extends require('events').EventEmitter * @param {Object} [methods] Methods to add * @param {Object} [options] * @param {Boolean} [options.collect=false] Passed to RemJson.Method as an option when created * @param {Boolean} [options.params] Passed to RemJson.Method as an option when created * @param {Function} [options.reviver] Reviver function for JSON * @param {Function} [options.replacer] Replacer function for JSON * @param {String} [options.encoding="utf8"] Encoding to use * @param {Number} [options.version=2] JSON-RPC version to use (1|2) * @param {Function} [options.router] Function to use for routing methods * @param {Function} [options.scope] The scope of server's methods, can be override by method scope options * @property {Object} options A reference to the internal options object that can be modified directly * @property {Object} errorMessages Map of error code to error message pairs that will be used in server responses * @property {ServerHttp} http HTTP interface constructor * @property {ServerHttps} https HTTPS interface constructor * @property {ServerTcp} tcp TCP interface constructor * @property {ServerTls} tls TLS interface constructor * @property {Middleware} middleware Middleware generator function * @return {Server} * @api public */ var Server = function (methods, options) { if (!(this instanceof Server)) { return new Server(methods, options); } var defaults = { reviver: null, replacer: null, encoding: 'utf8', version: 2, collect: false, router: function (method) { return this.getMethod(method); } }; this.options = utils.merge(defaults, options || {}); // bind router to the server this.options.router = this.options.router.bind(this); this._methods = {}; // adds methods passed to constructor this.methods(methods || {}); // assigns interfaces to this instance var interfaces = Server.interfaces; for (var name in interfaces) { this[name] = interfaces[name].bind(interfaces[name], this); } // copies error messages for defined codes into this instance this.errorMessages = {}; for (var handle in Server.errors) { var code = Server.errors[handle]; this.errorMessages[code] = Server.errorMessages[code]; } }; require('util').inherits(Server, events.EventEmitter); module.exports = Server; /** * Interfaces that will be automatically bound as properties of a Server instance * @enum {Function} * @static */ Server.interfaces = { http: require(__dirname + '/server/http'), https: require(__dirname + '/server/https'), tcp: require(__dirname + '/server/tcp'), tls: require(__dirname + '/server/tls'), mqtt: require(__dirname + '/server/mqtt'), middleware: require(__dirname + '/server/middleware') }; /** * JSON-RPC specification errors that map to an integer code * @enum {Number} * @static */ Server.errors = { PARSE_ERROR: -32700, INVALID_REQUEST: -32600, METHOD_NOT_FOUND: -32601, INVALID_PARAMS: -32602, INTERNAL_ERROR: -32603 }; /* * Error codes that map to an error message * @enum {String} * @static */ Server.errorMessages = {}; Server.errorMessages[Server.errors.PARSE_ERROR] = 'Parse Error'; Server.errorMessages[Server.errors.INVALID_REQUEST] = 'Invalid request'; Server.errorMessages[Server.errors.METHOD_NOT_FOUND] = 'Method not found'; Server.errorMessages[Server.errors.INVALID_PARAMS] = 'Invalid method parameter(s)'; Server.errorMessages[Server.errors.INTERNAL_ERROR] = 'Internal error'; /** * Adds a single method to the server * @param {String} name Name of method to add * @param {Function|Client} definition Function or Client for a relayed method * @param {Object} [scope] The scope of method * @throws {TypeError} Invalid parameters * @api public */ Server.prototype.method = function (name, definition, scope) { var isRelay = definition instanceof remjson.Client; var isMethod = definition instanceof remjson.Method; var isFunction = _.isFunction(definition); // a valid method is either a function or a client (relayed method) if (!isRelay && !isMethod && !isFunction) { throw new TypeError('method definition must be either a function, an instance of remjson.Client or an instance of remjson.Method'); } if (!name || typeof(name) !== 'string') { throw new TypeError('"' + name + '" must be a non-zero length string'); } if (/^rpc\./.test(name)) { throw new TypeError('"' + name + '" is a reserved method name'); } // make instance of remjson.Method if (!isRelay && !isMethod) { definition = new remjson.Method(definition, { collect: this.options.collect, params: this.options.params, scope: scope || this.options.scope }); } this._methods[name] = definition; }; /** * Adds a batch of methods to the server * @param {Object} methods Methods to add * @param {Object} [scope] The scope of methods * @api public */ Server.prototype.methods = function (methods, scope) { methods = methods || {}; for (var name in methods) { this.method(name, methods[name], scope); } }; /** * Checks if a method is registered with the server * @param {String} name Name of method * @return {Boolean} * @api public */ Server.prototype.hasMethod = function (name) { return name in this._methods; }; /** * Removes a method from the server * @param {String} name * @api public */ Server.prototype.removeMethod = function (name) { if (this.hasMethod(name)) { delete this._methods[name]; } }; /** * Gets a method from the server * @param {String} name * @return {Method} * @api public */ Server.prototype.getMethod = function (name) { return this._methods[name]; }; /** * Returns a JSON-RPC compatible error property * @param {Number} [code=-32603] Error code * @param {String} [message="Internal error"] Error message * @param {Object} [data] Additional data that should be provided * @return {Object} * @api public */ Server.prototype.error = function (code, message, data) { if (typeof(code) !== 'number') { code = Server.errors.INTERNAL_ERROR; } if (typeof(message) !== 'string') { message = this.errorMessages[code] || ''; } var error = {code: code, message: message}; if (typeof(data) !== 'undefined') error.data = data; return error; }; /** * Calls a method on the server * @param {Object|Array|String} request A JSON-RPC request object. Object for single request, Array for batches and String for automatic parsing (using the reviver option) * @param {Function} [originalCallback] Callback that receives one of two arguments: first is an error and the second a response * @api public */ Server.prototype.call = function (request, originalCallback) { var self = this; if (typeof(originalCallback) !== 'function') { originalCallback = function () { }; } // compose the callback so that we may emit an event on every response var callback = function (error, response) { var emit = self.emit.bind(self, 'response'); self.emit('response', request, response || error); originalCallback.apply(null, arguments); }; maybeParse(request, this.options, function (err, request) { if (err) { var error = self.error(Server.errors.PARSE_ERROR, null, err); return callback(utils.response(error, undefined, undefined, self.options.version)); } // is this a batch request? if (utils.Request.isBatch(request)) { // batch requests not allowed for version 1 if (self.options.version === 1) { var error = self.error(Server.errors.INVALID_REQUEST); return callback(utils.response(error, undefined, undefined, self.options.version)); } // special case if empty batch request if (!request.length) { var error = self.error(Server.errors.INVALID_REQUEST); return callback(utils.response(error, undefined, undefined, self.options.version)); } return self._batch(request, callback); } self.emit('request', request); // is the request valid? if (!utils.Request.isValidRequest(request, self.options.version)) { var error = self.error(Server.errors.INVALID_REQUEST); return callback(utils.response(error, undefined, undefined, self.options.version)); } // from now on we are "notification-aware" and can deliberately ignore errors for such requests var respond = function (error, result) { if (utils.Request.isNotification(request)) return callback(); var response = utils.response(error, result, request.id, self.options.version); if (response.error) callback(response); else callback(null, response); }; var method = self._resolveRouter(request.method, request.params); // are we attempting to invoke a relayed method? if (method instanceof remjson.Client) { return method.request(request.method, request.params, request.id, function (error, response) { if (utils.Request.isNotification(request)) return callback(); callback(error, response); }); } // does the method exist? if (!(method instanceof remjson.Method)) { return respond(self.error(Server.errors.METHOD_NOT_FOUND)); } // execute remjson.Method instance method.execute(self, request.params, function (error, result) { if (utils.Response.isValidError(error, self.options.version)) { return respond(error); } // got an invalid error if (error) { return respond(self.error(Server.errors.INTERNAL_ERROR)); } respond(null, result); }); }); }; /** * Invoke the router * @param {String} method Method to resolve * @param {Array|Object} params Request params * @return {Method} */ Server.prototype._resolveRouter = function (method, params) { var router = this.options.router; if (!_.isFunction(router)) { router = function (method) { return this.getMethod(method); } } var resolved = router.call(this, method, params); // got a remjson.Method or a remjson.Client, return it if ((resolved instanceof remjson.Method) || (resolved instanceof remjson.Client)) { return resolved; } // got a regular function, make it an instance of remjson.Method if (_.isFunction(resolved)) { return new remjson.Method(resolved); } }; /** * Evaluates a batch request * @api private */ Server.prototype._batch = function (requests, callback) { var self = this; var responses = []; this.emit('batch', requests); /** * @ignore */ var maybeRespond = function () { // done when we have filled up all the responses with a truthy value var isDone = responses.every(function (response) { return response !== null; }) if (isDone) { // filters away notifications var filtered = responses.filter(function (res) { return res !== true; }); // only notifications in request means empty response if (!filtered.length) return callback(); callback(null, filtered); } }; /** * @ignore */ var wrapper = function (request, index) { responses[index] = null; return function () { if (utils.Request.isValidRequest(request, self.options.version)) { self.call(request, function (error, response) { responses[index] = error || response || true; maybeRespond(); }); } else { var error = self.error(Server.errors.INVALID_REQUEST); responses[index] = utils.response(error, undefined, undefined, self.options.version); maybeRespond(); } }; }; var stack = requests.map(function (request, index) { // ignore possibly nested requests if (utils.Request.isBatch(request)) return null; return wrapper(request, index); }); stack.forEach(function (method) { if (typeof(method) === 'function') method(); }); }; /** * Parse "request" if it is a string, else just invoke callback * @ignore */ function maybeParse(request, options, callback) { if (typeof(request) === 'string') { utils.JSON.parse(request, options, callback); } else { callback(null, request); } }