API Docs for: 0.8.0
Show:

File: include/util.js

/*
    Copyright (C) 2016  PencilBlue, LLC

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";

//dependencies
var os     = require('os');
var util   = require('util');
var async  = require('async');
var extend = require('node.extend');
var fs     = require('fs');
var path   = require('path');
var uuid   = require('uuid');
var RegExpUtils = require('./utils/reg_exp_utils');

/**
 * Provides a set of utility functions used throughout the code base
 *
 * @module Services
 * @class Util
 * @constructor
 */
function Util(){}

/**
 * Clones an object by serializing it and then re-parsing it.
 * WARNING: Objects with circular dependencies will cause an error to be thrown.
 * @static
 * @method clone
 * @param {Object} object The object to clone
 * @return {Object|Array} Cloned object
 */
Util.clone = function(object){
    return JSON.parse(JSON.stringify(object));
};

/**
 * Performs a deep merge and returns the result.  <b>NOTE:</b> DO NOT ATTEMPT
 * TO MERGE PROPERTIES THAT REFERENCE OTHER PROPERTIES.  This could have
 * unintended side-effects as well as cause errors due to circular dependencies.
 * @static
 * @method deepMerge
 * @param {Object} from
 * @param {Object} to
 * @return {Object}
 */
Util.deepMerge = function(from, to) {
    return extend(true, to, from);
};

/**
 * Checks if the supplied object is an errof. If the object is an error the
 * function will throw the error.
 * @static
 * @method ane
 * @param {Object} obj The object to check
 */
Util.ane = function(obj){
	if (util.isError(obj)) {
		throw obj;
	}
};

/**
 * Initializes an array with the specified number of values.  The value at each
 * index can be static or a function may be provided.  In the event that a
 * function is provided the function will be called for each item to be placed
 * into the array.  The return value of the function will be placed into the
 * array.
 * @static
 * @method initArray
 * @param {Integer} cnt The length of the array to create
 * @param {Function|String|Number} val The value to initialize each index of
 * the array
 * @return {Array} The initialized array
 */
Util.initArray = function(cnt, val) {
    var v = [];
    var isFunc = Util.isFunction(val);
    for(var i = 0; i < cnt; i++) {
        v.push(isFunc ? val(i) : val);
    }
    return v;
};

/**
 * Escapes a regular expression.
 * @deprecated since 0.7.1 Will be removed in 1.0.  Use RegExpUtils
 * @static
 * @method escapeRegExp
 * @param {String} The expression to escape
 * @return {String} Escaped regular expression.
 */
Util.escapeRegExp = function(str) {
	return RegExpUtils.escape(str);
};

/**
 * Merges the properties from the first parameter into the second. This modifies
 * the second parameter instead of creating a new object.
 *
 * @method merge
 * @param {Object} from
 * @param {Object} to
 * @return {Object} The 'to' variable
 */
Util.merge = function(from, to) {
    Util.forEach(from, function(val, propName) {
        to[propName] = val;
    });
    return to;
};

/**
 * Creates an object that has both the properties of "a" and "b".  When both
 * objects have a property with the same name, "b"'s value will be preserved.
 * @static
 * @method union
 * @return {Object} The union of properties from both a and b.
 */
Util.union = function(a, b) {
	var union = {};
	Util.merge(a, union);
	Util.merge(b, union);
	return union;
};

/**
 * Creates a set of tasks that can be executed by the "async" module.
 * @static
 * @method getTasks
 * @param {Array} iterable The array of items to build tasks for
 * @param {Function} getTaskFunction The function that creates and returns the
 * task to be executed.
 * @example
 * <code>
 * var items = ['apple', 'orange'];
 * var tasks = util.getTasks(items, function(items, i) {
 *     return function(callback) {
 *         console.log(items[i]);
 *         callback(null, null);
 *     };
 * });
 * async.series(tasks, util.cb);
 * <code>
 */
Util.getTasks = function (iterable, getTaskFunction) {
	var tasks = [];
	for (var i = 0; i < iterable.length; i++) {
		tasks.push(getTaskFunction(iterable, i));
	}
	return tasks;
};

/**
 * Wraps a function in an anonymous function.  The wrapper function will call
 * the wrapped function with the provided context.  This comes in handy when
 * creating your own task arrays in conjunction with the async function when a
 * prototype function needs to be called with a specific context.
 * @static
 * @method wrapTask
 * @param {*} context The value of "this" for the function to be called
 * @param {Function} func The function to be executed
 * @param {Array} [argArray] The arguments to be supplied to the func parameter
 * when executed.
 * @return {Function}
 */
Util.wrapTask = function(context, func, argArray) {
    if (!util.isArray(argArray)) {
        argArray = [];
    }
    return function(callback) {
        argArray.push(callback);
        func.apply(context, argArray);
    };
};

/**
 * Wraps a task in a context as well as a function to mark the start and end time.  The result of the task will be
 * provided in the callback as the "result" property of the result object.  The time of execution can be found as the
 * "time" property.
 * @static
 * @method wrapTimedTask
 * @param {*} context The value of "this" for the function to be called
 * @param {Function} func The function to be executed
 * @param {string} [name] The task's name
 * @param {Array} [argArray] The arguments to be supplied to the func parameter
 * when executed.
 * @return {Function}
 */
Util.wrapTimedTask = function(context, func, name, argArray) {
    if (Util.isString(argArray)) {
        name = argArray;
        argArray = [];
    }
    var task = Util.wrapTask(context, func, argArray);
    return function(callback) {
        var start = Date.now();
        task(function(err, result) {
            callback(err, {
                result: result,
                time: Date.now() - start,
                start: start,
                name: name
            });
        });
    };
};

/**
 * Provides an implementation of for each that accepts an array or object.
 * @static
 * @method forEach
 * @param {Object|Array} iterable
 * @param {Function} handler A function that accepts 4 parameters.  The value
 * of the current property or index.  The current index (property name if object).  The iterable.
 * Finally, the numerical index if the iterable is an object.
 */
Util.forEach = function(iterable, handler) {

    var internalHandler;
    var internalIterable;
    if (util.isArray(iterable)) {
        internalHandler = handler;
        internalIterable = iterable;
    }
    else if (Util.isObject(iterable)) {

        internalIterable = Object.getOwnPropertyNames(iterable);
        internalHandler = function(propName, i) {
            handler(iterable[propName], propName, iterable, i);
        };
    }
    else {
        return false;
    }

    //execute native foreach on interable
    internalIterable.forEach(internalHandler);
};

/**
 * Hashes an array
 * @static
 * @method arrayToHash
 * @param {Array} array      The array to hash
 * @param {*} [defaultVal=true] Default value if the hashing fails
 * @return {Object} Hash
 */
Util.arrayToHash = function(array, defaultVal) {
	if (!util.isArray(array)) {
		return null;
	}

    //set the default value
    if (Util.isNullOrUndefined(defaultVal)) {
        defaultVal = true;
    }
	var hash = {};
	for(var i = 0; i < array.length; i++) {
		if (Util.isFunction(defaultVal)) {
			hash[defaultVal(array, i)] = array[i];
		}
		else {
			hash[array[i]] = defaultVal;
		}
	}
	return hash;
};

/**
 * Converts an array to an object.
 * @static
 * @method arrayToObj
 * @param {Array} array The array of items to transform from an array to an
 * object
 * @param {String|Function} keyFieldOrTransform When this field is a string it
 * is expected that the array contains objects and that the objects contain a
 * property that the string represents.  The value of that field will be used
 * as the property name in the new object.  When this parameter is a function
 * it is passed two parameters: the array being operated on and the index of
 * the current item.  It is expected that the function will return a value
 * representing the key in the new object.
 * @param {String|Function} [valFieldOrTransform] When this value is a string
 * it is expected that the array contains objects and that the objects contain
 * a property that the string represents.  The value of that field will be used
 * as the property value in the new object.  When this parameter is a function
 * it is passed two parameters: the array being operated on and the index of
 * the current item.  It is expected that the function return a value
 * representing the value of the derived property for that item.
 * @return {Object} The converted array.
 */
Util.arrayToObj = function(array, keyFieldOrTransform, valFieldOrTransform) {
    if (!util.isArray(array)) {
		return null;
	}

    var keyIsString = Util.isString(keyFieldOrTransform);
    var keyIsFunc   = Util.isFunction(keyFieldOrTransform);
    if (!keyIsString && !keyIsFunc) {
        return null;
    }

    var valIsString = Util.isString(valFieldOrTransform);
    var valIsFunc   = Util.isFunction(valFieldOrTransform);
    if (!valIsString && !valIsFunc) {
        valFieldOrTransform = null;
    }

    var obj = {};
    for (var i = 0; i < array.length; i++) {

        var item = array[i];
        var key  = keyIsString ? item[keyFieldOrTransform] : keyFieldOrTransform(array, i);

        if (valIsString) {
            obj[key] = item[valFieldOrTransform];
        }
        else if (valIsFunc) {
            obj[key] = valFieldOrTransform(array, i);
        }
        else {
            obj[key] = item;
        }
    }
    return obj;
};

/**
 * Converts an array of objects into a hash where the key the value of the
 * specified property. If multiple objects in the array have the same value for
 * the specified value then the last one found will be kept.
 * @static
 * @method objArrayToHash
 * @param {Array} array The array to convert
 * @param {String} hashProp The property who's value will be used as the key
 * for each object in the array.
 * @return {Object} A hash of the values in the array
 */
Util.objArrayToHash = function(array, hashProp) {
    if (!util.isArray(array)) {
		return null;
	}

    var hash = {};
	for(var i = 0; i < array.length; i++) {
        hash[array[i][hashProp]] = array[i];
	}
	return hash;
};

/**
 * Converts a hash to an array. When provided, the hashKeyProp will be the
 * property name of each object in the array that holds the hash key.
 * @static
 * @method hashToArray
 * @param {Object} obj The object to convert
 * @param {String} [hashKeyProp] The property name that will hold the hash key.
 * @return {Array} An array of each property value in the hash.
 */
Util.hashToArray = function(obj, hashKeyProp) {
	if (!Util.isObject(obj)) {
		return null;
	}

    var doHashKeyTransform = Util.isString(hashKeyProp);
    return Object.keys(obj).reduce(function(prev, prop) {
        prev.push(obj[prop]);
        if (doHashKeyTransform) {
            obj[prop][hashKeyProp] = prop;
        }
        return prev;
    }, []);
};

/**
 * Inverts a hash
 * @static
 * @method invertHash
 * @param {Object} obj Hash object
 * @return {Object} Inverted hash
 */
Util.invertHash = function(obj) {
	if (!Util.isObject(obj)) {
		return null;
	}

	var new_obj = {};
	for (var prop in obj) {
		if (obj.hasOwnProperty(prop)) {
			new_obj[obj[prop]] = prop;
		}
	}
	return new_obj;
};

/**
 * Clones an array
 * @static
 * @method copyArray
 * @param {Array} array
 * @return {Array} Cloned array
 */
Util.copyArray = function(array) {
	if (!util.isArray(array)) {
		return null;
	}

	var clone = [];
	for (var i = 0; i < array.length; i++) {
		clone.push(array[i]);
	}
	return clone;
};

Util.dedupeArray = function(array) {
    var hash = Util.arrayToHash(array);
    return Object.keys(hash);
};

/**
 * Pushes all of one array's values into another
 * @static
 * @method arrayPushAll
 * @param {Array} from
 * @param {Array} to
 * @return {Boolean} FALSE when the parameters are not Arrays
 */
Util.arrayPushAll = function(from, to) {
	if (!util.isArray(from) || !util.isArray(to)) {
		return false;
	}

	for (var i = 0; i < from.length; i++) {
		to.push(from[i]);
	}
};

/**
 * Empty callback function just used as a place holder if a callback is required
 * and the result is not needed.
 * @static
 * @method cb
 */
Util.cb = function(/*err, result*/){
	//do nothing
};

/**
 * Creates a unique Id
 * @static
 * @method uniqueId
 * @return {String} Unique Id Object
 */
Util.uniqueId = function(){
	return uuid.v4();
};

/**
 * Tests if a value is an object
 * @static
 * @method isObject
 * @param {*} value
 * @return {Boolean}
 */
Util.isObject = function(value) {
	return !Util.isNullOrUndefined(value) && typeof value === 'object';
};

/**
 * Tests if a value is an string
 * @static
 * @method isString
 * @param {*} value
 * @return {Boolean}
 */
Util.isString = function(value) {
	return !Util.isNullOrUndefined(value) && typeof value === 'string';
};

/**
 * Tests if a value is a function
 * @static
 * @method isFunction
 * @param {*} value
 * @return {Boolean}
 */
Util.isFunction = function(value) {
	return !Util.isNullOrUndefined(value) && typeof value === 'function';
};

/**
 * Tests if a value is NULL or undefined
 * @static
 * @method isNullOrUndefined
 * @param {*} value
 * @return {Boolean}
 */
Util.isNullOrUndefined = function(value) {
    return value === null || typeof value === 'undefined';
};

/**
 * Tests if a value is a boolean
 * @static
 * @method isBoolean
 * @param {*} value
 * @return {Boolean}
 */
Util.isBoolean = function(value) {
    return value === true || value === false;
};

/**
 * Retrieves the subdirectories of a path
 * @static
 * @method getDirectories
 * @param {String}   dirPath The starting path
 * @param {Function} cb      Callback function
 */
Util.getDirectories = function(dirPath, cb) {

	var dirs = [];
	fs.readdir(dirPath, function(err, files) {
		if (util.isError(err)) {
			return cb(err);
		}

		var tasks = Util.getTasks(files, function(files, index) {
			return function(callback) {

				var fullPath = path.join(dirPath, files[index]);
				fs.stat(fullPath, function(err, stat) {
                    if (util.isError(err)) {
                        return cb(err);
                    }
                    if (Util.isNullOrUndefined(stat)) {
                        console.log('WARN: Util: unstatable file encountered: %s', fullPath);
                    }
					else if (stat.isDirectory()) {
						dirs.push(fullPath);
					}
					callback(err);
				});
			};
		});
		async.parallel(tasks, function(err/*, results*/) {
			cb(err, dirs);
		});
	});
};

/**
 * Retrieves file and/or directorie absolute paths under a given directory path.
 * @static
 * @method getFiles
 * @param {String} dirPath The path to the directory to be examined
 * @param {Object} [options] Options that customize the results
 * @param {Boolean} [options.recursive=false] A flag that indicates if
 * directories should be recursively searched.
 * @param {Function} [options.filter] A function that returns a boolean
 * indicating if the file should be included in the result set.  The function
 * should take two parameters.  The first is a string value representing the
 * absolute path of the file.  The second is the stat object for the file.
 * @param {Function} cb A callback that takes two parameters. The first is an
 * Error, if occurred. The second is an array of strings representing the
 * absolute paths for files that met the criteria specified by the filter
 * function.
 */
Util.getFiles = function(dirPath, options, cb) {
    if (Util.isFunction(options)) {
        cb      = options;
        options = {
            recursive: false,
            filter: function(/*fullPath, stat*/) { return true; }
        };
    }

    //read files from dir
    fs.readdir(dirPath, function(err, q) {
        if (util.isError(err)) {
			return cb(err);
		}

        //seed the queue with the absolute paths not just the file names
        for (var i = 0; i < q.length; i++) {
            q[i] = path.join(dirPath, q[i]);
        }

        //process the q
        var filePaths = [];
        async.whilst(
            function() { return q.length > 0; },
            function(callback) {

                var fullPath = q.shift();
				fs.stat(fullPath, function(err, stat) {
					if (util.isError(err)) {
                        return callback(err);
					}

                    //apply filter
                    var meetsCriteria = true;
                    if (Util.isFunction(options.filter)) {
                        meetsCriteria = options.filter(fullPath, stat);
                    }

                    //examine result and add it when criteria is met
                    if (meetsCriteria) {
                        filePaths.push(fullPath);
                    }

                    //when recursive queue up directory's for processing
                    if (!options.recursive || !stat.isDirectory()) {
                        return callback(null);
                    }

                    //read the directory contents and append it to the queue
                    fs.readdir(fullPath, function(err, childFiles) {
                        if (util.isError(err)) {
                            return callback(err);
                        }

                        childFiles.forEach(function(item) {
                            q.push(path.join(fullPath, item));
                        });
                        callback(null);
                    });
				});
            },
            function(err) {
                cb(err, filePaths);
            }
        );
    });
};

/* Asynchronously makes the specified directory structure.
 * @static
 * @method mkdirsSync
 * @param {String} absoluteDirPath The absolute path of the directory structure
 * to be created
 * @param {Boolean} isFileName When true the value after the last file
 * separator is treated as a file.  This means that a directory with that value
 * will not be created.
 * @param {Function} cb A callback that provides an error, if occurred
 */
Util.mkdirs = function(absoluteDirPath, isFileName, cb) {
    if (Util.isFunction(isFileName)) {
        cb = isFileName;
        isFileName = false;
    }

    if (!Util.isString(absoluteDirPath)) {
        return cb(new Error('absoluteDirPath must be a valid file path'));
    }

    var pieces = absoluteDirPath.split(path.sep);

    var curr      = '';
    var isWindows = os.type().toLowerCase().indexOf('windows') !== -1;
    var tasks     = Util.getTasks(pieces, function(pieces, i) {
        return function(callback) {

            //we need to skip the first one bc it will probably be empty and we
            //want to skip the last one because it will probably be the file
            //name not a directory.
            var p = pieces[i];
            if (p.length === 0 || (isFileName && i >= pieces.length - 1)) {
                return callback();
            }

            curr += (isWindows && i === 0 ? '' : path.sep) + p;
            fs.exists(curr, function(exists) {
                if (exists) {
                    return callback();
                }
                fs.mkdir(curr, callback);
            });
        };
    });
    async.series(tasks, function(err/*, results*/){
        cb(err);
    });
};

/**
 * Synchronously makes the specified directory structure.
 * @static
 * @method mkdirsSync
 * @param {String} absoluteDirPath The absolute path of the directory structure
 * to be created
 * @param {Boolean} isFileName When true the value after the last file
 * separator is treated as a file.  This means that a directory with that value
 * will not be created.
 */
Util.mkdirsSync = function(absoluteDirPath, isFileName) {
    if (!Util.isString(absoluteDirPath)) {
        throw new Error('absoluteDirPath must be a valid file path');
    }

    var pieces    = absoluteDirPath.split(path.sep);
    var curr      = '';
    var isWindows = os.type().toLowerCase().indexOf('windows') !== -1;
    pieces.forEach(function(p, i) {

        //we need to skip the first one bc it will probably be empty and we
        //want to skip the last one because it will probably be the file
        //name not a directory.
        if (p.length === 0 || (isFileName && i >= pieces.length - 1)) {
            return;
        }

        curr += (isWindows && i === 0 ? '' : path.sep) + p;
        if (!fs.existsSync(curr)) {
            fs.mkdirSync(curr);
        }
    });
};

/**
 * Retrieves the extension off of the end of a string that represents a URI to
 * a resource
 * @static
 * @method getExtension
 * @param {String} filePath URI to the resource
 * @param {Object} [options]
 * @param {Boolean} [options.lower=false] When TRUE the extension will be returned as lower case
 * @param {String} [options.sep] The file path separator used in the path.  Defaults to the OS default.
 * @return {String} The value after the last '.' character
 */
Util.getExtension = function(filePath, options) {
    if (!Util.isString(filePath) || filePath.length <= 0) {
        return null;
    }
    if (!Util.isObject(options)) {
        options = {};
    }

    //do to the end of the path
    var pathPartIndex = filePath.lastIndexOf(options.sep || path.sep) || 0;
    if (pathPartIndex > -1) {
			filePath = filePath.substr(pathPartIndex);
		}

    var ext = null;
    var index = filePath.lastIndexOf('.');
    if (index >= 0) {
        ext = filePath.substring(index + 1);

        //apply options
        if (options.lower) {
            ext = ext.toLowerCase();
        }
    }
    return ext;
};

/**
 * Creates a filter function to be used with the getFiles function to skip files that are not of the specified type
 * @static
 * @method getFileExtensionFilter
 * @param extension
 * @return {Function}
 */
Util.getFileExtensionFilter = function(extension) {
    var ext = '.' + extension;
    return function(fullPath) {
        return fullPath.lastIndexOf(ext) === (fullPath.length - ext.length);
    };
};

//inherit from node's version of 'util'.  We can't use node's "util.inherits"
//function because util is an object and not a prototype.
Util.merge(util, Util);

/**
 * Overrides the basic inherit functionality to include static functions and
 * properties of prototypes
 * @static
 * @method inherits
 * @param {Function} Type1
 * @param {Function} Type2
 */
Util.inherits = function(Type1, Type2) {
    if (Util.isNullOrUndefined(Type1) || Util.isNullOrUndefined(Type2)) {
        throw new Error('The type parameters must be objects or prototypes');
    }

    util.inherits(Type1, Type2);
    Util.merge(Type2, Type1);
};

/**
 * Provides typical conversions for time
 * @static
 * @readonly
 * @property TIME
 * @type {Object}
 */
Util.TIME = Object.freeze({

	MILLIS_PER_SEC: 1000,
	MILLIS_PER_MIN: 60000,
	MILLIS_PER_HOUR: 3600000,
	MILLIS_PER_DAY: 86400000
});

//exports
module.exports = Util;