Source: lib/utils.js

var _ = require('lodash');
var JSONStream = require('JSONStream');
var JSONstringify = require('json-stringify-safe');

/** * @namespace */
var Utils = module.exports;

// same reference as other files use, for tidyness
var utils = Utils;

/**
 *  Generates a JSON-RPC 1.0 or 2.0 request
 *  @param {String} method Name of method to call
 *  @param {Array|Object} params Array of parameters passed to the method as specified, or an object of parameter names and corresponding value
 *  @param {String|Number|null} [id] Request ID can be a string, number, null for explicit notification or left out for automatic generation
 *  @param {Object} [options]
 *  @param {Number} [options.version=2] - JSON-RPC version to use (1 or 2)
 *  @param {Function} [options.generator] - Function that is passed the request, and the options object and is expected to return a request ID
 *  @throws {TypeError} If any of the parameters are invalid
 *  @return {Object} A JSON-RPC 1.0 or 2.0 request
 */
Utils.request = function(method, params, id, options) {
  if(typeof(method) !== 'string') {
    throw new TypeError(method + ' must be a string');
  }

  options = options || {};

  var request = {
    method: method
  };

  // assume that we are doing a 2.0 request unless specified differently
  if(typeof(options.version) === 'undefined' || options.version !== 1) {
    request.jsonrpc = '2.0';
  }

  if(params) {

    // params given, but invalid?
    if(typeof(params) !== 'object' && !Array.isArray(params)) {
      throw new TypeError(params + ' must be an object, array or omitted');
    }

    request.params = params;

  }

  // if id was left out, generate one (null means explicit notification)
  if(typeof(id) === 'undefined') {
    var generator = typeof(options.generator) === 'function' ? options.generator : Utils.generateId;
    request.id = generator(request, options);
  } else {
    request.id = id;
  }

  return request;
};

/**
 *  Generates a JSON-RPC 1.0 or 2.0 response
 *  @param {Object} error Error member
 *  @param {Object} result Result member
 *  @param {String|Number|null} id Id of request
 *  @param {Number} version JSON-RPC version to use
 *  @return {Object} A JSON-RPC 1.0 or 2.0 response
 */
Utils.response = function(error, result, id, version) {
  id = typeof(id) === 'undefined' || id === null ? null : id;
  error = typeof(error) === 'undefined' || error === null ? null : error;
  version = typeof(version) === 'undefined' || version === null ? 2 : version;
  var response = (version === 2) ? { jsonrpc: "2.0", id: id } : { id: id };

  // errors are always included in version 1
  if(version === 1) {
    response.error = error;
  }

  // one or the other with precedence for errors
  if(error) {
    response.error = error;
  } else {
    response.result = result;
  }
  return response;
};

/**
 *  Generates a random UUID
 *  @return {String}
 */
Utils.generateId = function() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8);
    return v.toString(16);
  });
};

/**
 *  Merges properties of object b into object a
 *  @param {...Object} Objects to be merged
 *  @return {Object}
 *  @private
 */
Utils.merge = function() {
  return _.extend.apply(null, arguments);
};

/**
 * Parses an incoming stream for requests using JSONStream
 * @param {Stream} stream
 * @param {Object} options
 * @param {Function} onRequest - Called once for stream errors and an unlimited amount of times for valid requests
 */
Utils.parseStream = function(stream, options, onRequest) {

  var onError = _.once(onRequest);
  var onSuccess = _.partial(onRequest, null);

  var result = JSONStream.parse();

  result.on('data', function(data) {

    // apply reviver walk function to prevent stringify/parse again
    if(_.isFunction(options.reviver)) {
      data = Utils.walk({'': data}, '', options.reviver);
    }

    onSuccess(data);
  });

  result.on('error', onError);
  stream.on('error', onError);

  stream.pipe(result);

};

/**
 *  Helper to parse a stream and interpret it as JSON
 *  @param {Stream} stream Stream instance
 *  @param {Function} [reviver] Optional reviver for JSON.parse
 *  @param {Function} callback
 */
Utils.parseBody = function(stream, options, callback) {

  callback = _.once(callback);
  var data = '';

  stream.setEncoding('utf8');

  stream.on('data', function(str) {
    data += str;
  });

  stream.on('error', function(err) {
    callback(err);
  });

  stream.on('end', function() {
    utils.JSON.parse(data, options, function(err, request) {
      if(err) {
        return callback(err);
      }
      callback(null, request);
    });
  });

};

/**
 *  Returns a HTTP request listener bound to the server in the argument.
 *  @param {http.Server} self Instance of a HTTP server
 *  @param {JaysonServer} server Instance of JaysonServer (typically jayson.Server)
 *  @return {Function}
 *  @private
 */
Utils.getHttpListener = function(self, server) {
  return function(req, res) {
    var options = self.options || {};

    server.emit('http request', req);

    //  405 method not allowed if not POST
    if(!Utils.isMethod(req, 'POST')) {
      return respond('Method Not Allowed', 405, {'allow': 'POST'});
    }

    // 415 unsupported media type if Content-Type is not correct
    if(!Utils.isContentType(req, 'application/json')) {
      return respond('Unsupported Media Type', 415);
    }

    Utils.parseBody(req, options, function(err, request) {
      if(err) {
        return respond(err, 400);
      }

      server.call(request, function(error, success) {
        var response = error || success;
        if(!response) {
          // no response received at all, must be a notification
          return respond('', 204);
        }

        utils.JSON.stringify(response, options, function(err, body) {
          if(err) {
            return respond(err, 500);
          }

          var headers = {
            'content-length': Buffer.byteLength(body, options.encoding),
            'content-type': 'application/json; charset=utf-8'
          };

          respond(body, 200, headers);
        });

      });

    });

    function respond(response, code, headers) {
      var body = response instanceof Error ? response.toString() : response;
      server.emit('http response', res, req);
      res.writeHead(code, headers || {});
      res.end(body);
    }

  };
};

/**
 *  Determines if a HTTP Request comes with a specific Content-Type
 *  @param {ServerRequest} request
 *  @param {String} type
 *  @return {Boolean}
 *  @private
 */
Utils.isContentType = function(request, type) {
  request = request || {headers: {}};
  var contentType = request.headers['content-type'] || '';
  return RegExp(type, 'i').test(contentType);
};

/**
 *  Determines if a HTTP Request is of a specific method
 *  @param {ServerRequest} request
 *  @param {String} method
 *  @return {Boolean}
 *  @private
 */
Utils.isMethod = function(request, method) {
  method = (method || '').toUpperCase();
  return (request.method || '') === method;
};

/**
 *  Determines the parameter names of a function
 *  @param {Function} func
 *  @return {Array}
 *  @private
 */
Utils.getParameterNames = function(func) {
  if(typeof(func) !== 'function') {
    return [];
  }

  var body = func.toString().replace(/[\n\r]/g, " ");
  var args = /^(?:function )*.*?\((.+?)\)/.exec(body);
  if(!args) {
    return [];
  }

  var list = (args.pop() || '').split(',');
  return list.map(function(arg) { return arg.trim(); });
};

/** * @namespace */
Utils.JSON = {};

/**
 * Parses a JSON string and then invokes the given callback
 * @param {String} str The string to parse
 * @param {Object} options Object with options, possibly holding a "reviver" function
 */
Utils.JSON.parse = function(str, options, callback) {
  var reviver = null;
  var obj = null;
  options = options || {};

  if(_.isFunction(options.reviver)) {
    reviver = options.reviver;
  }

  try {
    obj = JSON.parse.apply(JSON, _.compact([str, reviver]));
  } catch(err) {
    return callback(err);
  }

  callback(null, obj);
};

/**
 * Stringifies JSON and then invokes the given callback
 * @param {Object} obj The object to stringify
 * @param {Object} options Object with options, possibly holding a "replacer" function
 */
Utils.JSON.stringify = function(obj, options, callback) {
  var replacer = null;
  var str = null;
  options = options || {};

  if(_.isFunction(options.replacer)) {
    replacer = options.replacer;
  }

  try {
    str = JSONstringify.apply(JSON, _.compact([obj, replacer]));
  } catch(err) {
    return callback(err);
  }

  callback(null, str);
};

/**
 * Recursively walk an object and apply a function on its members
 * @param {Object} holder The object to walk
 * @param {String} key The key to look at
 * @param {Function} fn The function to apply to members
 * @return {Object}
 */
Utils.walk = function(holder, key, fn) {
  var k, v, value = holder[key];
  if (value && typeof value === 'object') {
    for (k in value) {
      if (Object.prototype.hasOwnProperty.call(value, k)) {
        v = Utils.walk(value, k, fn);
        if (v !== undefined) {
          value[k] = v;
        } else {
          delete value[k];
        }
      }
    }
  }
  return fn.call(holder, key, value);
};

/** * @namespace */
Utils.Request = {};

/**
 * Determines if the passed request is a batch request
 * @param {Object} request The request
 * @return {Boolean}
 */
Utils.Request.isBatch = function(request) {
  return Array.isArray(request);
};

/**
 * Determines if the passed request is a notification request
 * @param {Object} request The request
 * @return {Boolean}
 */
Utils.Request.isNotification = function(request) {
  return Boolean(
    request
    && !Utils.Request.isBatch(request)
    && (typeof(request.id) === 'undefined'
         || request.id === null)
  );
};

/**
 * Determines if the passed request is a valid JSON-RPC 2.0 Request
 * @param {Object} request The request
 * @return {Boolean}
 */
Utils.Request.isValidVersionTwoRequest = function(request) {
  return Boolean(
    request
    && typeof(request) === 'object'
    && request.jsonrpc === '2.0'
    && typeof(request.method) === 'string'
    && (
      typeof(request.params) === 'undefined'
      || Array.isArray(request.params)
      || (request.params && typeof(request.params) === 'object')
    )
    && (
      typeof(request.id) === 'undefined'
      || typeof(request.id) === 'string'
      || typeof(request.id) === 'number'
      || request.id === null
    )
  );
};

/**
 * Determines if the passed request is a valid JSON-RPC 1.0 Request
 * @param {Object} request The request
 * @return {Boolean}
 */
Utils.Request.isValidVersionOneRequest = function(request) {
  return Boolean(
    request
    && typeof(request) === 'object'
    && typeof(request.method) === 'string'
    && Array.isArray(request.params)
    && typeof(request.id) !== 'undefined'
  );
};

/**
 * Determines if the passed request is a valid JSON-RPC Request
 * @param {Object} request The request
 * @param {Number} version JSON-RPC version 1 or 2
 * @return {Boolean}
 */
Utils.Request.isValidRequest = function(request, version) {
  version = version === 1 ? 1 : 2;
  return Boolean(
    request
    && (
      (version === 1 && Utils.Request.isValidVersionOneRequest(request)) ||
      (version === 2 && Utils.Request.isValidVersionTwoRequest(request))
    )
  );
};

/** * @namespace */
Utils.Response = {};

/**
 * Determines if the passed error is a valid JSON-RPC error response
 * @param {Object} error The error
 * @param {Number} version JSON-RPC version 1 or 2
 * @return {Boolean}
 */
Utils.Response.isValidError = function(error, version) {
  version = version === 1 ? 1 : 2;
  return Boolean(
    version === 1 && (
      typeof(error) !== 'undefined'
      && error !== null
    )
    || version === 2 && (
      error
      && typeof(error.code) === 'number'
      && parseInt(error.code, 10) === error.code
      && typeof(error.message) === 'string'
    )
  );
};