KurtPattyn/kimbu

View on GitHub
lib/utils/index.js

Summary

Maintainability
A
0 mins
Test Coverage
"use strict";

/**
 * The utils module provides some utility methods not provided by node.js.
 *
 * @module utils
*/

/**
 * Applies the given `func` to all elements of the array.
 * This is an optimized version of the default `forEach` method in JavaScript.
 * Code slightly adapted from {@link http://www.reddit.com/r/javascript/comments/2ecnw2/}
 *
 * @param {!Array} array - the array to apply the given `func` to.
 * @param {!Function} func - the function to call for each element of the given `array`
 *
 * @public
 * @alias module:utils.forEach
 */
function forEach(array, func) {
  let i = array.length;

  while (--i >= 0) {
    func(array[i], i, array);
  }
}

/**
 * Extends the given objects going from left to right. Objects to the left have priority over
 * objects to the right. None of the objects is overridden.
 * This method is non-recursive; i.e. it does not copy properties from parent objects.
 *
 * @param {...Object} objects - two or more objects to use for extension.
 *
 * @example
 * let target = utils.extend({ a: 1, b: 2 }, { a: 3 }, { c: 5 });
 * //now target = { a: 1, b: 2, c: 5 }
 *
 * @returns {{}}
 * @alias module:utils.extend
 * @public
 */
function extend(objects) {
  const sources = [].slice.call(arguments);
  const firstObject = sources.shift();
  let target = {};

  //first copy the first object into a new one
  forEach(Object.getOwnPropertyNames(firstObject), function(propName) {
    target[propName] = firstObject[propName];
  });

  //then copy over the properties of the remaining objects
  if (sources.length > 0) {
    forEach(sources, function (source) {
      forEach(Object.getOwnPropertyNames(source), function(propName) {
        if (!target.hasOwnProperty(propName)) {
          target[propName] = source[propName];
        }
      });
    });
  }
  return target;
}

/**
 * Defers the given `method` to be called on the next tick.
 *
 * @param {!Function} method - Function to be called later.
 *
 * @example
 * function toBeCalledLater(number) {
 *   //do something
 * }
 *
 * callLater(toBeCalledLater.bind(3));
 * //will call toBeCalledLater with the number 3 on the next process tick
 *
 * @public
 * @alias module:utils.callLater
 */
function callLater(method) {
  const parameters = [].slice.call(arguments, 1);

  process.nextTick(function() {
    method.apply(null, parameters);
  });
}

/**
 * Forwards the given `sourceEvent` from the `sourceObject` to the `targetEvent` on the `targetObject`.
 * Whenever the `sourceEvent` occurs on the `sourceObject`, the `targetEvent` will be emitted on the `targetObject`.
 *
 * Note: be careful to avoid circular forwarding as this will result in an endless loop (and exhaust the stack).
 *
 * @param {!Object} sourceObject - the object to redirect the sourceEvent from; must be an EventEmitter
 * @param {!String} sourceEvent - the name of the event to redirect
 * @param {!Object} targetObject - the object to send the redirected event to; must be an EventEmitter
 * @param {!String} targetEvent - the new name of the redirected event
 *
 * @public
 * @alias module:utils.forwardEvent
*/
function forwardEvent(sourceObject, sourceEvent, targetObject, targetEvent) {
  sourceObject.on(sourceEvent, targetObject.emit.bind(targetObject, targetEvent));
}

/**
 * Returns true if the given `string` ends with the given `searchString`. The search starts at
 * the given `position`. When no position is given or when position is greater the length of the
 * given 'string', search starts at the end of the `string`.
 * Code taken from {@link https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith}
 *
 * @param {!String} string - the string to search through
 * @param {!String} searchString - the string to search for
 * @param {Number=} position - Search within this string as if this string were only this long;
 * defaults to this string's actual length, clamped within the range established by this string's length.
 *
 * @public
 * @alias module:utils.endsWith
*/
function endsWith(string, searchString, position) {
  const subjectString = string;
  let pos = position;

  if (pos === undefined || pos > subjectString.length) {
    pos = subjectString.length;
  }
  pos -= searchString.length;
  const lastIndex = subjectString.indexOf(searchString, pos);

  return lastIndex !== -1 && lastIndex === pos;
}

/**
 * Filters the given JavaScript files for modules that implement the specified type.
 *
 * @param {!String} folder - the name of the folder containing the files
 * @param {!String[]} files - list of filenames to consider
 * @param {!Function} type - the interface that should be implemented
 * @param {!Function} callback - called when filtering has finished
 * @private
 */
function _filterModules(folder, files, type, callback) {
  const path = require("path");
  let modules = {};

  const jsFiles = files.filter(function (file) {
    return endsWith(file, ".js");
  });

  forEach(jsFiles, function (file) {
    const current = path.resolve(folder, file);
    const module = require(current);

    if (module.prototype instanceof type) {
      modules[module.prototype.constructor.name] = module;
    }
  });

  callback(modules);
}

/**
 * Finds node modules in the given `folder` that implement the specified `type` (aka interface).
 * This method is asynchronous and when finished will call the 'done' callback with an object hash
 * where the keys represent the name of the module and the values represent a handle to the module.
 *
 * @param {!String} folder - the folder to search for modules
 * @param {!Function} type - the constructor method of the type that the modules must implement
 * @param {!Function} done - called when search has finished.
 *
 * @example
 * function SomeInterface() {
 * }
 * utils.findModules(".", SomeInterface, function(modules) {
 *   for (let m in modules) {
 *     console.log("Found module: " + m + "; handle: " + modules[m];
 *   }
 * });
 *
 * @public
 * @alias module:utils.findModules
 *
 * @see module:utils.findModulesSync
 */
function findModules(folder, type, done) {
  const fs = require("fs");

  fs.readdir(folder, function(err, files) {
    if (err) {
      done({});
    } else {
      _filterModules(folder, files, type, done);
    }
  });
}

/**
 * Synchronous version of {@link module:utils.findModules }. This method blocks until it found all modules.
 *
 * @param {!String} folder - the folder to search for modules
 * @param {!Function} type - the constructor method of the type that the modules must implement
 * @returns {Object}
 *
 * @public
 * @alias module:utils.findModulesSync
 *
 * @see module:utils.findModules
 */
function findModulesSync(folder, type) {
  const fs = require("fs");

  try {
    const files = fs.readdirSync(folder);
    let modules = {};

    _filterModules(folder, files, type, function (mods) {
      modules = mods;
    });

    return modules;
  } catch(err) {
    /* istanbul ignore else */
    if (err) {
      //ignore error
    }
    return {};
  }
}

module.exports = {
  extend: extend,
  callLater: callLater,
  findModules: findModules,
  findModulesSync: findModulesSync,
  endsWith: endsWith,
  forwardEvent: forwardEvent,
  forEach: forEach
};