Files
elasticsearch-js/src/lib/utils.js
2013-10-24 11:43:09 -07:00

456 lines
13 KiB
JavaScript

var path = require('path'),
_ = require('lodash'),
fs = require('fs'),
requireDir = require('require-directory'),
qs = require('querystring'),
url = require('url'),
nodeUtils = require('util');
/**
* Custom utils library, basically a modified version of [lodash](http://lodash.com/docs) +
* [node.utils](http://nodejs.org/api/util.html#util_util) that doesn't use mixins to prevent
* confusion when requiring lodash itself.
*
* @class utils
* @static
*/
var utils = _.extend({}, _, nodeUtils);
_ = utils;
utils.inspect = function (thing, opts) {
return nodeUtils.inspect(thing, _.defaults(opts || {}, {
showHidden: true,
depth: null,
color: true
}));
};
/**
* Link to [path.join](http://nodejs.org/api/path.html#path_path_join_path1_path2)
*
* @method utils.joinPath
* @type {function}
*/
utils.joinPath = path.join;
/**
* Require all of the modules in a directory
*
* @method requireDir
* @param {Function} module - The module object which will own the required modules.
* @param {String} path - Path to the directory which will be traversed (can be relative to module)
* @return {Object} - An object with each required files
*/
utils.requireDir = function (module, dirPath) {
if (dirPath && dirPath[0] === '.') {
dirPath = path.join(path.dirname(module.filename), dirPath);
}
return requireDir(module, dirPath);
};
/**
* Requires all of the files in a directory, then transforms the filenames into
* StudlyCase -- one level deep for now.
* @param {Function} module - The module object which will own the required modules.
* @param {String} dirPath - Path to the directory which will be traversed (can be relative to module)
* @return {Object} - An object with each required files, keys will be the StudlyCase version of the filenames.
*/
utils.requireClasses = function (module, dirPath) {
return utils.reKey(utils.requireDir(module, dirPath), utils.studlyCase, false);
};
/**
* Recursively re-key an object, applying "transform" to each key
* @param {Object} obj - The object to re-key
* @param {Function} transform - The transformation function to apply to each key
* @param {Boolean} [recursive=false] - Should this act recursively?
* @param {Object} out - used primarily for recursion, allows you to specify the object which new keys will be written to
* @return {Object}
*/
utils.reKey = function (obj, transform, recursive) {
// defaults
if (typeof recursive === 'undefined') { recursive = true; }
if (typeof transform !== 'function') { throw new TypeError('invalid transform function'); }
var out = {};
_.each(obj, function (prop, name) {
if (recursive && typeof prop === 'object') {
out[transform(name)] = utils.reKey(prop, transform, recursive);
} else {
out[transform(name)] = prop;
}
});
return out;
};
/**
* Recursively merge two objects, walking into each object and concating arrays. If both to and from have a value at a
* key, but the values' types don't match to's value is left unmodified. Only Array and Object values are merged - that
* it to say values with a typeof "object"
*
* @param {Object} to - Object to merge into (no cloning, the original object
* is modified)
* @param {Object} from - Object to pull changed from
* @return {Object} - returns the modified to value
*/
utils.deepMerge = function (to, from) {
Object.keys(from).forEach(function (key) {
switch (typeof to[key]) {
case 'undefined':
to[key] = from[key];
break;
case 'object':
if (_.isArray(to[key]) && _.isArray(from[key])) {
to[key] = to[key].concat(from[key]);
}
else if (_.isPlainObject(to[key]) && _.isPlainObject(from[key])) {
utils.deepMerge(to[key], from[key]);
}
}
});
return to;
};
/**
* Test if a value is an array and it's contents are of a specific type
*
* @method isArrayOf<Strings|Object|Array|Finite|Function|RegExp>s
* @param {Array} arr - An array to check
* @return {Boolean}
*/
'String Object PlainObject Array Finite Function RegExp'.split(' ').forEach(function (type) {
var check = _.bindKey(_, 'is' + type);
utils['isArrayOf' + type + 's'] = function (arr) {
// quick shallow check of arrays
return _.isArray(arr) && _.every(arr.slice(0, 10), check);
};
});
/**
* Capitalize the first letter of a word
*
* @todo Tests
* @method ucfirst
* @param {string} word - The word to transform
* @return {string}
*/
utils.ucfirst = function (word) {
return word[0].toUpperCase() + word.substring(1).toLowerCase();
};
/**
* Transform a string into StudlyCase
*
* @todo Tests
* @method studlyCase
* @param {String} string
* @return {String}
*/
utils.studlyCase = function (string) {
return _.map(string.split(/\b|_/), function (word, i) {
return word.match(/^[a-z]+$/i) ? utils.ucfirst(word) : '';
}).join('');
};
/**
* Transform a string into camelCase
*
* @todo Tests
* @method cameCase
* @param {String} string
* @return {String}
*/
utils.camelCase = function (string) {
return _.map(string.split(/\b|_/), function (word, i) {
if (word.match(/^[a-z]+$/i)) {
return i === 0 ? word.toLowerCase() : utils.ucfirst(word);
} else {
return '';
}
}).join('');
};
/**
* Lower-case a string, and return an empty string if any is not a string
*
* @todo Tests
* @param any {*} - Something or nothing
* @returns {string}
*/
utils.toLowerString = function (any) {
if (any) {
if (typeof any !== 'string') {
any = any.toString();
}
} else {
any = '';
}
return any.toLowerCase();
};
/**
* Upper-case the string, return an empty string if any is not a string
*
* @todo Tests
* @param any {*} - Something or nothing
* @returns {string}
*/
utils.toUpperString = function (any) {
if (any) {
if (typeof any !== 'string') {
any = any.toString();
}
} else {
any = '';
}
return any.toUpperCase();
};
/**
* Test if a value is "numeric" meaning that it can be transformed into something besides NaN
*
* @todo Tests
* @method isNumeric
* @param {*} val
* @return {Boolean}
*/
utils.isNumeric = function (val) {
return !isNaN(val === null ? NaN : val * 1);
};
// regexp to test for intervals
var intervalRE = /^(\d+(?:\.\d+)?)([Mwdhmsy])$/;
/**
* Test if a string represents an interval (eg. 1m, 2Y)
*
* @todo Test
* @method isInterval
* @param {String} val
* @return {Boolean}
*/
utils.isInterval = function (val) {
return !!(val.match && val.match(intervalRE));
};
/**
* Repeat a string n times
*
* @todo Test
* @todo TestPerformance
* @method repeat
* @param {String} what - The string to repeat
* @param {Number} times - Times the string should be repeated
* @return {String}
*/
utils.repeat = function (what, times) {
return (new Array(times + 1)).join(what);
};
/**
* Convert an object into a query string
*
* @method makeQueryString
* @param {Object} obj - The object to convert
* @param {Boolean} [start=true] - Should the query string start with a '?'
* @return {String}
*/
utils.makeQueryString = function (obj, start) {
var str = qs.stringify(obj);
return (start === false || str === '') ? str : '?' + str;
};
/**
* Override node's util.inherits function to also supply a callSuper function on the child class that can be called
* with the instance and the arguments passed to the child's constructor. This should only be called from within the
* constructor of the child class and should be removed from the code once the constructor is "done".
*
* @param constructor {Function} - the constructor that should subClass superConstructor
* @param superConstructor {Function} - The parent constructor
*/
utils.inherits = function (constructor, superConstructor) {
nodeUtils.inherits(constructor, superConstructor);
constructor.callSuper = function (inst, args) {
if (args) {
if (_.isArguments(args)) {
utils.applyArgs(superConstructor, inst, args);
} else {
utils.applyArgs(superConstructor, inst, arguments, 1);
}
} else {
superConstructor.call(inst);
}
};
};
/**
* Remove leading/trailing spaces from a string
*
* @param str {String} - Any string
* @returns {String}
*/
utils.trim = function (str) {
return typeof str === 'string' ? str.replace(/^\s+|\s+$/g, '') : '';
};
utils.collectMatches = function (text, regExp) {
var matches = [], match;
while (match = regExp.exec(text)) {
matches.push(match);
if (regExp.global !== true) {
// would loop forever if not true
break;
}
}
return matches;
};
var startsWithProtocolRE = /^([a-z]+:)?\/\//;
/**
* Runs a string through node's url.parse, removing the return value's host property and insuring the text has a
* protocol first
*
* @todo Tests
* @param urlString {String} - a url of some sort
* @returns {Object} - an object containing 'hostname', 'port', 'protocol', 'path', and a few other keys
*/
utils.parseUrl = function (urlString) {
if (!startsWithProtocolRE.text(urlString)) {
urlString = 'http://' + urlString;
}
var info = url.parse(urlString);
return info;
};
/**
* Formats a urlinfo object, sort of juggling the 'host' and 'hostname' keys based on the presense of the port and
* including http: as the default protocol.
*
* @todo Tests,
* @todo add checking for ':' at the end of the protocol
* @param urlInfo {Object} - An object, similar to that returned from _.parseUrl
* @returns {String}
*/
utils.formatUrl = function (urlInfo) {
var info = _.pick(urlInfo, ['protocol', 'hostname', 'port']);
if (info.port && urlInfo.host && !info.hostname) {
info.hostname = urlInfo.host;
delete info.host;
}
if (!info.protocol) {
info.protocol = 'http:';
}
return url.format(info);
};
/**
* Call a function, applying the arguments object to it in an optimized way, rather than always turning it into an array
*
* @param func {Function} - The function to execute
* @param context {*} - The context the function will be executed with
* @param args {Arguments} - The arguments to send to func
* @param [sliceIndex=0] {Integer} - The index that args should be sliced at, before feeding args to func
* @returns {*} - the return value of func
*/
utils.applyArgs = function (func, context, args, sliceIndex) {
sliceIndex = sliceIndex || 0;
switch (args.length - sliceIndex) {
case 0:
return func.call(context);
case 1:
return func.call(context, args[0 + sliceIndex]);
case 2:
return func.call(context, args[0 + sliceIndex], args[1 + sliceIndex]);
case 3:
return func.call(context, args[0 + sliceIndex], args[1 + sliceIndex], args[2 + sliceIndex]);
case 4:
return func.call(context, args[0 + sliceIndex], args[1 + sliceIndex], args[2 + sliceIndex], args[3 + sliceIndex]);
case 5:
return func.call(context, args[0 + sliceIndex], args[1 + sliceIndex],
args[2 + sliceIndex], args[3 + sliceIndex], args[4 + sliceIndex]);
default:
return func.apply(context, Array.prototype.slice.call(args, sliceIndex));
}
};
/**
* Schedule a function to be called on the next tick, and supply it with these arguments
* when it is called.
* @return {[type]} [description]
*/
_.nextTick = function (cb) {
// bind the function and schedule it
process.nextTick(_.bindKey(_, 'applyArgs', cb, null, arguments, 1));
};
/**
* Marks a method as a handler. Currently this just makes a property on the method
* flagging it to be bound to the object at object creation when "makeBoundMethods" is called
*
* ```
* ClassName.prototype.methodName = _.handler(function () {
* // this will always be bound when called via classInstance.bound.methodName
* this === classInstance
* });
* ```
*
* @alias _.scheduled
* @param {Function} func - The method that is being defined
* @return {Function}
*/
_.handler = function (func) {
func._provideBound = true;
return func;
};
_.scheduled = _.handler;
/**
* Creates an "bound" property on an object, which all or a subset of methods from
* the object which are bound to the original object.
*
* ```
* var obj = {
* onEvent: function () {}
* };
*
* _.makeBoundMethods(obj);
*
* obj.bound.onEvent() // is bound to obj, and can safely be used as an event handler.
* ```
*
* @param {Object} obj - The object to bind the methods to
* @param {Array} [methods] - The methods to bind, false values === bind them all
*/
_.makeBoundMethods = function (obj, methods) {
obj.bound = {};
if (!methods) {
methods = [];
for (var prop in obj) {
// dearest maintainer, we want to look through the prototype
if (typeof obj[prop] === 'function' && obj[prop]._provideBound === true) {
obj.bound[prop] = _.bind(obj[prop], obj);
}
}
} else {
_.each(methods, function (method) {
obj.bound[method] = _.bindKey(obj, method);
});
}
};
_.noop = function () {};
_.getStackTrace = function (callee) {
var e = {};
Error.captureStackTrace(e, callee || _.getStackTrace);
return '\n' + e.stack.split('\n').slice(1).join('\n');
};
module.exports = utils;