<commit_before>/*global  require:true, module:true */
/*jshint boss:true, browser:true */

(function() {
  var VERSION = "1.2.0";
  var Backbone = this.Backbone;
  var _ = this._;

  // Load Backbone and Underscore using require if we don't have the global for
  // some reason
  if (typeof require === "function") {
    if (!Backbone) Backbone = require("backbone");
    if (!_) _ = require("underscore");
  }

  function ensureArray(ob){
    return _.isArray(ob) ? ob : [ob];
  }

  function isConstructor(o) {
    return o.prototype && o === o.prototype.constructor;
  }

  /**
   * Few tested opinions on how to handle deeply nested views in Backbone.js
   * focusing on encapsulation and reusability.
   *
   * <https://github.com/epeli/backbone.viewmaster>
   *
   * @class Backbone.ViewMaster
   * @extends Backbone.View
   * */
  Backbone.ViewMaster = Backbone.View.extend({

    constructor: function() {
      Backbone.View.prototype.constructor.apply(this, arguments);

      /**
       * Boolean indicating whether this view has been rendered at least once.
       * Set by the `render` method and used by the parent view.
       *
       * @property rendered
       * @type Boolean
       **/
      this.rendered = false;

      /**
      * Reference to the parent view.
      *
      * @property parent
      * @type Object
      **/
      this.parent = null;

      /**
      * Object containing all child views. Key are CSS selectors and values
      * are arrays of views
      *
      * @private
      * @property _views
      * @type Object
      **/
      this._views = {};

      /**
      * Array of views to be removed on next `refreshViews` call.
      *
      * @private
      * @property _remove
      * @type Array
      **/
      this._remove = [];

      /**
      * Record of view container with changes
      *
      * @private
      * @type Object
      **/
      this._dirty = {};
    },

    /**
     * Template function. User must override this!
     *
     * @method template
     * @param {Object} context
     * @return {String / DOM object} rendered template
     **/
    template: function(){
      throw new Error("Template function not defined!");
    },

    /**
     * Returns the context object for the `template` method.
     *
     * Default: `this.model.toJSON()` or an empty object if view has no model.
     *
     * @method context
     * @return {Object} context
     **/
    context: function() {
      if (this.model) return this.model.toJSON();
      return {};
    },

    /**
     * Render this view with `this.template(this.context())` and refresh child
     * views with `this.refreshViews(options)`.
     *
     * @method render
     * @param {Object} options
     *   @param {Boolean} [options.force]
     *   force rerendering of child views
     * @return {Object} this
     **/
    render: function(opts) {
      opts = _.extend({}, opts);

      // Remove subviews with detach. This way they don't lose event handlers
      // and parent view can rerender itself
      this.eachView(function(sel, view) {
        view.$el.detach();
      });

      opts.detached = true;

      this.$el.html(this.template(this.context()));

      this.afterTemplate();

      // Mark this view as rendered. Parent view wont try to render this
      // anymore unless force:true is passed
      this.rendered = true;

      this.refreshViews(opts);

      return this;
    },

    /**
     * Overrideable by the view implementor.
     *
     * Called after the template has been rendered but before adding child
     * views.  This is perfect place to hook up any jQuery plugings using
     * `this.$` or `this.$el` or set access points to view DOM elements
     *
     *     this.$el("button").tooltipPlugin();
     *     this.$saveButton = this.$("button.save");
     *
     * because they cannot have any side affects to child views.
     *
     * @method aftertemplate
     **/
    afterTemplate: function() {},

    /**
     * Refresh any child view changes made with `setView`, `appendView`,
     * `prependView` or `insertView`. Tries to avoid doing any work unless it
     * is absolutely necessary or `{ force: true }` is passed.
     *
     * It calls `render` on child views only when a child has never been
     * rendered before.
     *
     * @method refreshViews
     * @param {Object} options
     *   @param {Boolean} [options.force]
     *   force rerendering of child views
     * @return {Object} this
     **/
    refreshViews: function(opts) {
      opts = _.extend({}, opts);
      var self = this;
      var dirty = this._dirty;

      var oldView;
      while (oldView = this._remove.shift()) oldView.remove();

      this.eachView(function(sel, view) {
        // Detach view from container if it is dirty to update possible view
        // order changes.
        var refresh = dirty[sel] || opts.force;

        if (refresh && !opts.detached) view.$el.detach();
        if (!view.rendered || opts.force) view.render(opts);
        if (refresh || opts.detached) self.$(sel).append(view.el);
      });

      this._dirty = {};
      return this;
    },

    /**
     * Detach view from its parent but keep it in DOM.
     *
     * @private
     * @method _removeParent
     * @return {Object} this
     **/
    _removeParent: function() {

      if (!this.parent) return;

      var key;
      for (key in this.parent._views) {
        this.parent._views[key] = _.without(
          this.parent._views[key], this
        );
      }

      this.parent = null;

      return this;
    },

    /**
     * Prepare views to be nested in this view. Set up event bubbling and
     * remove possible previous parent.
     *
     * @private
     * @method _prepareViews
     * @param {Object/Constructor/Array} view(s) One or array of view objects
     * or Constructors
     **/
    _prepareViews: function(views) {
      var self = this;
      views = _.map(ensureArray(views), _.bind(this._createInstance, this));

      _.each(views, function(view) {
        // when parent is changing remove it from previous
        if (view.parent && view.parent !== self) view._removeParent();
        view.parent = self;
      });

      return views;
    },

    _createInstance: function(view) {
      return isConstructor(view) ? new view({
        model: this.model,
        collection: this.collection
      }) : view;
    },

    /**
    * Like `trigger` but the events are bubbled all the way up to all
    * parent views too.
    *
    * @method bubble
    * @param {String} event
    * @param {any} [args*] Zero or more objects to be passed along with the
    * event
    * @return {Object} this
    **/
    bubble: function() {
      var args = _.toArray(arguments);
      this.trigger.apply(this, args);
      if (this.parent) this.parent.bubble.apply(this.parent, args);
      return this;
    },

    /**
    * Opposite of `bubble`. The events are broadcasted all the way down to all
    * child views too.
    *
    * @method broadcast
    * @param {String} event
    * @param {any} [args*] Zero or more objects to be passed along with the
    * event
    * @return {Object} this
    **/
    broadcast: function() {
      var args = _.toArray(arguments);
      this.trigger.apply(this, args);
      this.eachView(function(sel, view) {
        view.broadcast.apply(view, args);
      });
      return this;
    },

    /**
     * Iterate each child view.
     *
     * @method eachView
     * @param {Function} iterator
     *   @param {String} iterator.selector CSS selector for the view container
     *   @param {Object} iterator.view The view object
     * @return {Object} this
     **/
    eachView: function(fn) {
      _.each(this._views, function(views, sel) {
        _.each(views, function(view) {
          fn(sel, view);
        });
      });
    },

    /**
     * Set a view or an array of views to a given view container.
     *
     * If previous set of views is replaced by a new set — the views not
     * present in the new set will be discarded with `remove` on the next
     * `refreshViews` call unless detached with `detach` method.
     *
     * If `views` parameter is a constructor function it will be instantiated
     * with `model` and `collection` of the parent. This applies to
     * `appendView`, `prependview` and `insertView` too.
     *
     * @protected
     * @method setView
     * @param {String} selector CSS selector for the view container
     * @param {Object/Constructor/Array} view(s) One or array of view objects
     * or Constructors
     **/
    setView: function(sel, currentViews) {
      var previousViews;
      currentViews = this._prepareViews(currentViews);

      if (previousViews = this._views[sel]) {
        this._remove = this._remove.concat(
          _.difference(previousViews, currentViews)
        );
      }

      this._views[sel] = currentViews;
      this._dirty[sel] = true;
      return this;
    },

    /**
     * Insert views to specific index in the view array.
     *
     * @protected
     * @method insertView
     * @param {String} selector CSS selector for the view container
     * @param {Number} index Index to insert view(s)
     * @param {Object/Constructor/Array} view(s) Views to insert
     **/
    insertView: function(sel, index, views) {
      var current;
      views = this._prepareViews(views);
      if (current = this._views[sel]) {
        current.slice().splice.apply(current, [index, 0].concat(views));
      }
      else {
        this._views[sel] = views;
      }

      this._dirty[sel] = true;
      return this;
    },

    /**
     * Append a views to a given view container.
     *
     * @protected
     * @method appendView
     * @param {String} selector CSS selector for the view container
     * @param {Object/Constructor/Array} view(s) Views to append
     **/
    appendView: function(sel, views) {
      this._views[sel] = (this._views[sel] || []).concat(
        this._prepareViews(views)
      );

      this._dirty[sel] = true;
      return this;
    },

    /**
     * Prepend a views to a given view container.
     *
     * @protected
     * @method prependView
     * @param {String} selector CSS selector for the view container
     * @param {Object/Constructor/Array} view(s) Views to prepend
     **/
    prependView: function(sel, views) {
      this._views[sel] = this._prepareViews(views).concat(
        this._views[sel] || []
      );

      this._dirty[sel] = true;
      return this;
    },

    /**
     * Return array of views for given container or null if the container have
     * not been used.
     *
     * @protected
     * @method getViews
     * @param {String} selector container CSS selector
     * @return {Array/null} views
     **/
    getViews: function(sel) {
      var views;
      if (views = this._views[sel]) return views.slice();
      return null;
    },

    /**
     * Detaches view from its parent and DOM.
     *
     * @method detach
     * @return {Object} this
     **/
    detach: function() {
      this._removeParent();
      this.$el.detach();
      return this;
    },

    /**
     * Discard this view and all its children for good. Clears all events bound
     * for the view. Use `detach` instead if you need to use the view again
     * some time later.
     *
     * @method remove
     * @return {Object} this
     **/
    remove: function() {
      Backbone.View.prototype.remove.apply(this, arguments);
      this._removeParent();
      this.eachView(function(sel, view) {
        view.remove();
      });
      return this;
    }

  });

  Backbone.ViewMaster.VERSION = VERSION;
  if (typeof module !== 'undefined') module.exports = Backbone.ViewMaster;

})();
<commit_msg>typo in docs<commit_after>/*global  require:true, module:true */
/*jshint boss:true, browser:true */

(function() {
  var VERSION = "1.2.0";
  var Backbone = this.Backbone;
  var _ = this._;

  // Load Backbone and Underscore using require if we don't have the global for
  // some reason
  if (typeof require === "function") {
    if (!Backbone) Backbone = require("backbone");
    if (!_) _ = require("underscore");
  }

  function ensureArray(ob){
    return _.isArray(ob) ? ob : [ob];
  }

  function isConstructor(o) {
    return o.prototype && o === o.prototype.constructor;
  }

  /**
   * Few tested opinions on how to handle deeply nested views in Backbone.js
   * focusing on encapsulation and reusability.
   *
   * <https://github.com/epeli/backbone.viewmaster>
   *
   * @class Backbone.ViewMaster
   * @extends Backbone.View
   * */
  Backbone.ViewMaster = Backbone.View.extend({

    constructor: function() {
      Backbone.View.prototype.constructor.apply(this, arguments);

      /**
       * Boolean indicating whether this view has been rendered at least once.
       * Set by the `render` method and used by the parent view.
       *
       * @property rendered
       * @type Boolean
       **/
      this.rendered = false;

      /**
      * Reference to the parent view.
      *
      * @property parent
      * @type Object
      **/
      this.parent = null;

      /**
      * Object containing all child views. Key are CSS selectors and values
      * are arrays of views
      *
      * @private
      * @property _views
      * @type Object
      **/
      this._views = {};

      /**
      * Array of views to be removed on next `refreshViews` call.
      *
      * @private
      * @property _remove
      * @type Array
      **/
      this._remove = [];

      /**
      * Record of view container with changes
      *
      * @private
      * @type Object
      **/
      this._dirty = {};
    },

    /**
     * Template function. User must override this!
     *
     * @method template
     * @param {Object} context
     * @return {String / DOM object} rendered template
     **/
    template: function(){
      throw new Error("Template function not defined!");
    },

    /**
     * Returns the context object for the `template` method.
     *
     * Default: `this.model.toJSON()` or an empty object if view has no model.
     *
     * @method context
     * @return {Object} context
     **/
    context: function() {
      if (this.model) return this.model.toJSON();
      return {};
    },

    /**
     * Render this view with `this.template(this.context())` and refresh child
     * views with `this.refreshViews(options)`.
     *
     * @method render
     * @param {Object} options
     *   @param {Boolean} [options.force]
     *   force rerendering of child views
     * @return {Object} this
     **/
    render: function(opts) {
      opts = _.extend({}, opts);

      // Remove subviews with detach. This way they don't lose event handlers
      // and parent view can rerender itself
      this.eachView(function(sel, view) {
        view.$el.detach();
      });

      opts.detached = true;

      this.$el.html(this.template(this.context()));

      this.afterTemplate();

      // Mark this view as rendered. Parent view wont try to render this
      // anymore unless force:true is passed
      this.rendered = true;

      this.refreshViews(opts);

      return this;
    },

    /**
     * Overrideable by the view implementor.
     *
     * Called after the template has been rendered but before adding child
     * views.  This is perfect place to hook up any jQuery plugings using
     * `this.$` or `this.$el` or set access points to view DOM elements
     *
     *     this.$el("button").tooltipPlugin();
     *     this.$saveButton = this.$("button.save");
     *
     * because they cannot have any side affects to child views.
     *
     * @method afterTemplate
     **/
    afterTemplate: function() {},

    /**
     * Refresh any child view changes made with `setView`, `appendView`,
     * `prependView` or `insertView`. Tries to avoid doing any work unless it
     * is absolutely necessary or `{ force: true }` is passed.
     *
     * It calls `render` on child views only when a child has never been
     * rendered before.
     *
     * @method refreshViews
     * @param {Object} options
     *   @param {Boolean} [options.force]
     *   force rerendering of child views
     * @return {Object} this
     **/
    refreshViews: function(opts) {
      opts = _.extend({}, opts);
      var self = this;
      var dirty = this._dirty;

      var oldView;
      while (oldView = this._remove.shift()) oldView.remove();

      this.eachView(function(sel, view) {
        // Detach view from container if it is dirty to update possible view
        // order changes.
        var refresh = dirty[sel] || opts.force;

        if (refresh && !opts.detached) view.$el.detach();
        if (!view.rendered || opts.force) view.render(opts);
        if (refresh || opts.detached) self.$(sel).append(view.el);
      });

      this._dirty = {};
      return this;
    },

    /**
     * Detach view from its parent but keep it in DOM.
     *
     * @private
     * @method _removeParent
     * @return {Object} this
     **/
    _removeParent: function() {

      if (!this.parent) return;

      var key;
      for (key in this.parent._views) {
        this.parent._views[key] = _.without(
          this.parent._views[key], this
        );
      }

      this.parent = null;

      return this;
    },

    /**
     * Prepare views to be nested in this view. Set up event bubbling and
     * remove possible previous parent.
     *
     * @private
     * @method _prepareViews
     * @param {Object/Constructor/Array} view(s) One or array of view objects
     * or Constructors
     **/
    _prepareViews: function(views) {
      var self = this;
      views = _.map(ensureArray(views), _.bind(this._createInstance, this));

      _.each(views, function(view) {
        // when parent is changing remove it from previous
        if (view.parent && view.parent !== self) view._removeParent();
        view.parent = self;
      });

      return views;
    },

    _createInstance: function(view) {
      return isConstructor(view) ? new view({
        model: this.model,
        collection: this.collection
      }) : view;
    },

    /**
    * Like `trigger` but the events are bubbled all the way up to all
    * parent views too.
    *
    * @method bubble
    * @param {String} event
    * @param {any} [args*] Zero or more objects to be passed along with the
    * event
    * @return {Object} this
    **/
    bubble: function() {
      var args = _.toArray(arguments);
      this.trigger.apply(this, args);
      if (this.parent) this.parent.bubble.apply(this.parent, args);
      return this;
    },

    /**
    * Opposite of `bubble`. The events are broadcasted all the way down to all
    * child views too.
    *
    * @method broadcast
    * @param {String} event
    * @param {any} [args*] Zero or more objects to be passed along with the
    * event
    * @return {Object} this
    **/
    broadcast: function() {
      var args = _.toArray(arguments);
      this.trigger.apply(this, args);
      this.eachView(function(sel, view) {
        view.broadcast.apply(view, args);
      });
      return this;
    },

    /**
     * Iterate each child view.
     *
     * @method eachView
     * @param {Function} iterator
     *   @param {String} iterator.selector CSS selector for the view container
     *   @param {Object} iterator.view The view object
     * @return {Object} this
     **/
    eachView: function(fn) {
      _.each(this._views, function(views, sel) {
        _.each(views, function(view) {
          fn(sel, view);
        });
      });
    },

    /**
     * Set a view or an array of views to a given view container.
     *
     * If previous set of views is replaced by a new set — the views not
     * present in the new set will be discarded with `remove` on the next
     * `refreshViews` call unless detached with `detach` method.
     *
     * If `views` parameter is a constructor function it will be instantiated
     * with `model` and `collection` of the parent. This applies to
     * `appendView`, `prependview` and `insertView` too.
     *
     * @protected
     * @method setView
     * @param {String} selector CSS selector for the view container
     * @param {Object/Constructor/Array} view(s) One or array of view objects
     * or Constructors
     **/
    setView: function(sel, currentViews) {
      var previousViews;
      currentViews = this._prepareViews(currentViews);

      if (previousViews = this._views[sel]) {
        this._remove = this._remove.concat(
          _.difference(previousViews, currentViews)
        );
      }

      this._views[sel] = currentViews;
      this._dirty[sel] = true;
      return this;
    },

    /**
     * Insert views to specific index in the view array.
     *
     * @protected
     * @method insertView
     * @param {String} selector CSS selector for the view container
     * @param {Number} index Index to insert view(s)
     * @param {Object/Constructor/Array} view(s) Views to insert
     **/
    insertView: function(sel, index, views) {
      var current;
      views = this._prepareViews(views);
      if (current = this._views[sel]) {
        current.slice().splice.apply(current, [index, 0].concat(views));
      }
      else {
        this._views[sel] = views;
      }

      this._dirty[sel] = true;
      return this;
    },

    /**
     * Append a views to a given view container.
     *
     * @protected
     * @method appendView
     * @param {String} selector CSS selector for the view container
     * @param {Object/Constructor/Array} view(s) Views to append
     **/
    appendView: function(sel, views) {
      this._views[sel] = (this._views[sel] || []).concat(
        this._prepareViews(views)
      );

      this._dirty[sel] = true;
      return this;
    },

    /**
     * Prepend a views to a given view container.
     *
     * @protected
     * @method prependView
     * @param {String} selector CSS selector for the view container
     * @param {Object/Constructor/Array} view(s) Views to prepend
     **/
    prependView: function(sel, views) {
      this._views[sel] = this._prepareViews(views).concat(
        this._views[sel] || []
      );

      this._dirty[sel] = true;
      return this;
    },

    /**
     * Return array of views for given container or null if the container have
     * not been used.
     *
     * @protected
     * @method getViews
     * @param {String} selector container CSS selector
     * @return {Array/null} views
     **/
    getViews: function(sel) {
      var views;
      if (views = this._views[sel]) return views.slice();
      return null;
    },

    /**
     * Detaches view from its parent and DOM.
     *
     * @method detach
     * @return {Object} this
     **/
    detach: function() {
      this._removeParent();
      this.$el.detach();
      return this;
    },

    /**
     * Discard this view and all its children for good. Clears all events bound
     * for the view. Use `detach` instead if you need to use the view again
     * some time later.
     *
     * @method remove
     * @return {Object} this
     **/
    remove: function() {
      Backbone.View.prototype.remove.apply(this, arguments);
      this._removeParent();
      this.eachView(function(sel, view) {
        view.remove();
      });
      return this;
    }

  });

  Backbone.ViewMaster.VERSION = VERSION;
  if (typeof module !== 'undefined') module.exports = Backbone.ViewMaster;

})();
<|endoftext|>


<commit_before>"use strict";

// Modules
let fs = require('fs');
let XRegExp = require('xregexp').XRegExp;
let Context = require('./context');

/**
 * Router designed to route
 * to controller/action
 */
class Router {

  static routeMiddleware(req, res) {
    // Lets find the route
    let routes = Router.getRoutes(req.method);
    let context = new Context(req, res);

    try {
      if (!routes) {
        // No Routes to check, Probably unsupported method
        context.notFound();
      }

      // Lets loop through the routes and match them
      for (let route of routes) {

        let matches = route.regex.xexec(req.path);
        if (matches) {
          if (Router.callController(context, route)) {
            return;
          }
        }
      }

      // Lets check for a default Controller/Action math
      var regex = XRegExp('^/(?<controller>(?:[^\\WA-Z][\\w]*/)*[A-Z][\\w\\\\]*)/?(?<action>\\w+)?/?(?<params>[^\\?]+)?\\??.*$');
      let matches = regex.xexec(req.path);
      if (matches && matches.controller && matches.action) {
        let route = {
          regex: regex,
          ctrlAction: `${matches.controller}.${matches.action}`
        };

        if (Router.callController(context, route)) {
          return;
        }
      }

      context.notFound();
    } catch(e) {
      context.onError(e);
    }
  }

  /**
   * Find the Controller/Action then call
   * the before, then the action
   *
   * @param {Context} context - the context object
   * @param {object} route - object with regex and ctrlAction
   *
   * @returns {boolean} - TRUE on successful call, FALSE otherwise
   */
  static callController(context, route) {
    // Parse out the real controller action
    let ctrlAction = XRegExp.replace(context.req.path, route.regex, route.ctrlAction);
    let split = ctrlAction.split('.');

    // If we have the right number, controller/action
    if (2 === split.length) {
      let controller = Router.getController(split[0]);
      if (!controller) {
        return false;
      }

      // Make sure the action exists
      if ('function' !== typeof controller[split[1]]) {
        return false;
      }

      context.controllerName = split[0];
      context.actionName = split[1];

      try {
        controller.before.call(context, function () {
          controller[split[1]].call(context);
        });
      } catch(e) {
        context.onError(e);
      }

      return true;
    }

    return false;
  }

  /**
   * Load controllers from disk and store
   * in memory for later use
   *
   * @param http
   */
  static loadControllers(http) {
    let config = http.config;
    let controllerPath = require('path').normalize(sand.appPath + config.controllerPath);

    if (!fs.existsSync(controllerPath)) {
      http.log.warn("WARN: Missing controllers directory. Skipping loading of controllers...".yellow);
      return;
    }

    let controllers = require('require-all')({
      dirname: controllerPath,
      filter: /(\w+)\.js/
    });

    Router.controllers = flattenObject(controllers);
  }

  /**
   * Load the routes and split them up into
   * sections separated by method
   *
   * @param http
   */
  static loadRoutes(http) {
    let config = http.config;
    let routesPath = require('path').normalize(sand.appPath + config.routesPath);

    if (!routesPath.endsWith('.js')) {
      routesPath += '.js';
    }

    if (!fs.existsSync(routesPath)) {
      http.log.warn(`WARN: Missing routes file: ${routesPath}. Skipping the loading of routes...`);
      return;
    }

    // Load Routes Config
    var routes = require(routesPath);

    // Loop through routes
    for (var key in routes) {
      if (routes.hasOwnProperty(key)) {
        // Get Method and Route
        let split = Router.getMethodAndRegex(key, http);
        if (!split) {
          http.warn(`Invalid route key: ${key}`);
          continue;
        }

        // If they passed and object we need to treat it differently
        if ('object' === typeof routes[key]) {
          // For each method add new route
          for(var meth in routes[key]) {
            if (routes[key].hasOwnProperty(meth)) {
              Router.addRoute(meth, split.regex, routes[key][meth]);
            }
          }
        } else if ('string' === typeof routes[key]) {
          // Lets add the simple route
          Router.addRoute(split.method, split.regex, routes[key]);
        } else {
          http.warn('Invalid Controller/Action', routes[key]);
        }
      }
    }
  }

  /**
   * Add the route to the global object
   *
   * @param {string} method - the HTTP method
   * @param {string} regex - the route regex
   * @param {string} action - the controller/action
   */
  static addRoute(method, regex, action) {
    method = method.toUpperCase();

    // Create the method routes holder if it doesn't exist
    if ('undefined' === typeof Router.routes[method]) {
      Router.routes[method] = [];
    }

    // Add the route
    Router.routes[method].push({
      regex: XRegExp(regex),
      ctrlAction: action
    });
  }

  static getMethodAndRegex(str) {
    let split = str.split(' ');
    var meth = 'GET';
    var regex = '';

    // Lets split out the method and regex
    if (2 === split.length) {
      meth = split[0];
      regex = split[1];
    } else if (1 === split.length) {
      regex = split[0];
    } else {
      return null;
    }

    // If this string is not a regex, add `^` and `$`
    if (!regex.startsWith('^') && !regex.endsWith('$')) {
      regex = `^${regex}$`;
    }

    return {
      method: meth,
      regex: regex
    };
  }

  /**
   * Get the routes attached to a HTTP method
   *
   * @param {string} method - the HTTP method
   *
   * @returns {*}
   */
  static getRoutes(method) {
    return Router.routes[method.toUpperCase()];
  }

  /**
   * Get the controller instance if it exists
   * null otherwise
   *
   * @param {string} name - controller name
   *
   * @returns {*}
   */
  static getController(name) {
    name = name.replace('/', '.');
    if ('function' === typeof Router.controllers[name]) {
      return Router.controllers[name];
    }

    return null;
  }
}

Router.controllers = null;
Router.routes = {};

module.exports = function(http) {
  Router.loadControllers(http);
  Router.loadRoutes(http);

  return Router.routeMiddleware;
};

function flattenObject(obj) {
  var toReturn = {};

  for (var i in obj) {
    if (!obj.hasOwnProperty(i)) {
      continue;
    }

    if ('object' === typeof obj[i] && obj !== null) {
      var flatObject = flattenObject(obj[i]);
      for (var x in flatObject) {
        if (!flatObject.hasOwnProperty(x)) continue;

        toReturn[i + '.' + x] = flatObject[x];
      }
    } else {
      toReturn[i] = obj[i];
    }
  }
  return toReturn;
}<commit_msg>added warning for using instance action instead of static<commit_after>"use strict";

// Modules
let fs = require('fs');
let XRegExp = require('xregexp').XRegExp;
let Context = require('./context');

/**
 * Router designed to route
 * to controller/action
 */
class Router {

  static routeMiddleware(req, res) {
    // Lets find the route
    let routes = Router.getRoutes(req.method);
    let context = new Context(req, res);

    try {
      if (!routes) {
        // No Routes to check, Probably unsupported method
        context.notFound();
      }

      // Lets loop through the routes and match them
      for (let route of routes) {

        let matches = route.regex.xexec(req.path);
        if (matches) {
          if (Router.callController(context, route)) {
            return;
          }
        }
      }

      // Lets check for a default Controller/Action math
      var regex = XRegExp('^/(?<controller>(?:[^\\WA-Z][\\w]*/)*[A-Z][\\w\\\\]*)/?(?<action>\\w+)?/?(?<params>[^\\?]+)?\\??.*$');
      let matches = regex.xexec(req.path);
      if (matches && matches.controller && matches.action) {
        let route = {
          regex: regex,
          ctrlAction: `${matches.controller}.${matches.action}`
        };

        if (Router.callController(context, route)) {
          return;
        }
      }

      context.notFound();
    } catch(e) {
      context.onError(e);
    }
  }

  /**
   * Find the Controller/Action then call
   * the before, then the action
   *
   * @param {Context} context - the context object
   * @param {object} route - object with regex and ctrlAction
   *
   * @returns {boolean} - TRUE on successful call, FALSE otherwise
   */
  static callController(context, route) {
    // Parse out the real controller action
    let ctrlAction = XRegExp.replace(context.req.path, route.regex, route.ctrlAction);
    let split = ctrlAction.split('.');

    // If we have the right number, controller/action
    if (2 === split.length) {
      let controller = Router.getController(split[0]);
      if (!controller) {
        return false;
      }

      // Make sure the action exists
      if ('function' !== typeof controller[split[1]]) {
        if ('function' === typeof controller.prototype[split[1]]) {
          sand.http.warn(`${split[0] + '.' + split[1]} was found as an instance method, you probably meant to mark it as static`);
        }
        return false;
      }

      context.controllerName = split[0];
      context.actionName = split[1];

      try {
        controller.before.call(context, function () {
          controller[split[1]].call(context);
        });
      } catch(e) {
        context.onError(e);
      }

      return true;
    }

    return false;
  }

  /**
   * Load controllers from disk and store
   * in memory for later use
   *
   * @param http
   */
  static loadControllers(http) {
    let config = http.config;
    let controllerPath = require('path').normalize(sand.appPath + config.controllerPath);

    if (!fs.existsSync(controllerPath)) {
      http.log.warn("WARN: Missing controllers directory. Skipping loading of controllers...".yellow);
      return;
    }

    let controllers = require('require-all')({
      dirname: controllerPath,
      filter: /(\w+)\.js/
    });

    Router.controllers = flattenObject(controllers);
  }

  /**
   * Load the routes and split them up into
   * sections separated by method
   *
   * @param http
   */
  static loadRoutes(http) {
    let config = http.config;
    let routesPath = require('path').normalize(sand.appPath + config.routesPath);

    if (!routesPath.endsWith('.js')) {
      routesPath += '.js';
    }

    if (!fs.existsSync(routesPath)) {
      http.log.warn(`WARN: Missing routes file: ${routesPath}. Skipping the loading of routes...`);
      return;
    }

    // Load Routes Config
    var routes = require(routesPath);

    // Loop through routes
    for (var key in routes) {
      if (routes.hasOwnProperty(key)) {
        // Get Method and Route
        let split = Router.getMethodAndRegex(key, http);
        if (!split) {
          http.warn(`Invalid route key: ${key}`);
          continue;
        }

        // If they passed and object we need to treat it differently
        if ('object' === typeof routes[key]) {
          // For each method add new route
          for(var meth in routes[key]) {
            if (routes[key].hasOwnProperty(meth)) {
              Router.addRoute(meth, split.regex, routes[key][meth]);
            }
          }
        } else if ('string' === typeof routes[key]) {
          // Lets add the simple route
          Router.addRoute(split.method, split.regex, routes[key]);
        } else {
          http.warn('Invalid Controller/Action', routes[key]);
        }
      }
    }
  }

  /**
   * Add the route to the global object
   *
   * @param {string} method - the HTTP method
   * @param {string} regex - the route regex
   * @param {string} action - the controller/action
   */
  static addRoute(method, regex, action) {
    method = method.toUpperCase();

    // Create the method routes holder if it doesn't exist
    if ('undefined' === typeof Router.routes[method]) {
      Router.routes[method] = [];
    }

    // Add the route
    Router.routes[method].push({
      regex: XRegExp(regex),
      ctrlAction: action
    });
  }

  static getMethodAndRegex(str) {
    let split = str.split(' ');
    var meth = 'GET';
    var regex = '';

    // Lets split out the method and regex
    if (2 === split.length) {
      meth = split[0];
      regex = split[1];
    } else if (1 === split.length) {
      regex = split[0];
    } else {
      return null;
    }

    // If this string is not a regex, add `^` and `$`
    if (!regex.startsWith('^') && !regex.endsWith('$')) {
      regex = `^${regex}$`;
    }

    return {
      method: meth,
      regex: regex
    };
  }

  /**
   * Get the routes attached to a HTTP method
   *
   * @param {string} method - the HTTP method
   *
   * @returns {*}
   */
  static getRoutes(method) {
    return Router.routes[method.toUpperCase()];
  }

  /**
   * Get the controller instance if it exists
   * null otherwise
   *
   * @param {string} name - controller name
   *
   * @returns {*}
   */
  static getController(name) {
    name = name.replace('/', '.');
    if ('function' === typeof Router.controllers[name]) {
      return Router.controllers[name];
    }

    return null;
  }
}

Router.controllers = null;
Router.routes = {};

module.exports = function(http) {
  Router.loadControllers(http);
  Router.loadRoutes(http);

  return Router.routeMiddleware;
};

function flattenObject(obj) {
  var toReturn = {};

  for (var i in obj) {
    if (!obj.hasOwnProperty(i)) {
      continue;
    }

    if ('object' === typeof obj[i] && obj !== null) {
      var flatObject = flattenObject(obj[i]);
      for (var x in flatObject) {
        if (!flatObject.hasOwnProperty(x)) continue;

        toReturn[i + '.' + x] = flatObject[x];
      }
    } else {
      toReturn[i] = obj[i];
    }
  }
  return toReturn;
}<|endoftext|>


<commit_before>const { Boards }    = require('boards');
const fileExists    = require('file-exists');
const procurator    = require('procurator');
const { Homefront } = require('homefront');
const path          = require('path');
const glob          = require('glob');

// @todo * CHECK DEPENDENCIES
class Runner {
  constructor(config) {
    this.config = Homefront.merge({ tasks: {}, appRoot: '.', templateRoot: '.' }, config);
    this.boards = new Boards({ discoveryConfig: { prefix: 'boards-preset' } });
  }

  composeTask(task, parameters) {
    if (this.config.tasks[task]) {
      return this.config.tasks[task];
    }

    // Preset. Check if exists.
    const parts      = task.split(':');
    const presetName = parts.shift();
    const presets    = this.boards.getPlugins();
    const preset     = presets[presetName];
    task             = parts[0];

    if (!preset) {
      throw new Error(`Preset "${presetName}" not found.`);
    }

    parameters.fallbackSourceDirectory = preset.templateRoot;

    return preset.tasks[task];
  }

  run(task, parameters) {
    parameters = Homefront.merge({}, this.config.parameters || {}, parameters);

    return this.apply('default:prepare', parameters).then(() => {
      return this.apply(task, parameters)
    });
  }

  apply(task, parameters) {
    let instructions = task;

    if (typeof task === 'string') {
      instructions = this.composeTask(task, parameters);

      if (!instructions) {
        return Promise.reject(new Error(`Instructions for task "${task}" not found.`));
      }
    }

    if (!instructions) {
      return Promise.reject(new Error(`Invalid instructions provided.`));
    }

    if (!Array.isArray(instructions)) {
      instructions = [instructions];
    }

    let previousTask = null;

    return Promise.all(instructions.map(instruction => {
      let runner;

      if (previousTask) {
        runner       = previousTask.then(() => this.runTask(instruction, parameters));
        previousTask = null;
      } else {
        runner = this.runTask(instruction, parameters);
      }

      if (instruction.sync) {
        previousTask = runner;
      }

      return runner;
    }));
  }

  prepareParams(method, params) {
    const preparedParams = method(params);

    if (typeof preparedParams === 'object') {
      return preparedParams;
    }

    return params;
  }

  runTask(instruction, parameters) {
    instruction.from = instruction.from || '';

    // Prepare for step. Copy parameters to not affect other tasks.
    if (typeof instruction.prepare === 'function') {
      parameters = this.prepareParams(instruction.prepare, Homefront.merge({}, parameters));
    }

    // Allow dynamic tasks to be supplied.
    if (typeof instruction.dynamicTask === 'function') {
      return Promise.resolve(instruction.dynamicTask(parameters)).then(tasks => {
        this.apply(tasks, parameters);
      });
    }

    if (typeof instruction.definedTask !== 'undefined') {
      if (typeof instruction.definedTask !== 'string') {
        throw new Error(`definedTask must be a string. Got ${typeof instruction.definedTask}.`);
      }

      if (instruction.isolated && typeof instruction.prepare !== 'function') {
        parameters = Homefront.merge({}, parameters);
      }

      return this.apply(instruction.definedTask, parameters);
    }

    if (typeof instruction.task === 'function') {
      return instruction.task(parameters, this.boards);
    }

    if (typeof this[instruction.task] !== 'function') {
      throw new Error(`Invalid task "${instruction.task}" supplied`);
    }

    return this[instruction.task](instruction, parameters);
  }

  modify(instruction, parameters) {
    return this.boards.generate('ModificationGenerator', Object.assign({}, parameters, {
      sourceDirectory: this.config.appRoot,
      targetDirectory: this.config.appRoot,
      sourceFile     : this.getTarget(instruction.target, parameters),
      modify         : { patch: instruction.patch }
    }));
  }

  applyGenerate(template, target, sourceDirectory, instruction, parameters) {
    const parsed = path.parse(this.getTarget(target, parameters));

    return this.boards.generate('TemplateGenerator', Object.assign({}, parameters, {
      targetFile     : parsed.base,
      targetDirectory: path.join(this.config.appRoot, parsed.dir || '.'),
      sourceFile     : template,
      sourceDirectory
    }));
  }

  generate(instruction, parameters) {
    if (!instruction.glob) {
      const template = this.getTarget(instruction.template, parameters);

      return this.applyGenerate(template, instruction.target, this.getTemplateDirectory(instruction, parameters), instruction, parameters);
    }

    const glob         = this.getTarget(instruction.glob, parameters);
    const globPromises = [];

    if (this.config.templateRoot) {
      globPromises.push(this.glob(glob, path.join(this.config.templateRoot, instruction.from)));
    } else {
      // Push empty array so we can use array position further down
      globPromises.push(Promise.resolve([]));
    }

    if (parameters.fallbackSourceDirectory) {
      globPromises.push(this.glob(glob, path.join(parameters.fallbackSourceDirectory, instruction.from)));
    }

    return Promise.all(globPromises)
      .then(globbed => {
        const projectMatches = globbed[0];
        const generates      = [];
        const makeApply      = (match, dir) => {
          const target = path.join(instruction.target, match);

          generates.push(this.applyGenerate(target, target, path.join(dir, instruction.from), instruction, parameters));
        };

        projectMatches.forEach(match => makeApply(match, this.config.templateRoot));

        if (globbed[1]) {
          globbed[1]
            .filter(match => !projectMatches.includes(match))
            .forEach(match => makeApply(match, parameters.fallbackSourceDirectory));
        }

        return Promise.all(generates);
      });
  }

  getTemplateDirectory(instruction, parameters) {
    if (fileExists.sync(path.resolve(this.config.templateRoot, instruction.template))) {
      return this.config.templateRoot;
    }

    const fallback = parameters.fallbackSourceDirectory;

    if (!fallback) {
      throw new Error(`Template "${instruction.template}" not found.`);
    }

    if (!fileExists.sync(path.resolve(fallback, instruction.template))) {
      throw new Error(`Preset template "${instruction.template}" not found.`);
    }

    return fallback;
  }

  glob(pattern, cwd) {
    return new Promise((resolve, reject) => {
      glob(pattern, { nodir: true, cwd }, (error, results) => {
        if (error) {
          return reject(error);
        }

        return resolve(results);
      });
    });
  }

  getTarget(target, parameters) {
    if (typeof target === 'function') {
      return target(parameters);
    }

    return procurator.sync(target, parameters);
  }
}

module.exports.Runner = Runner;
<commit_msg>style(Runner): remove todo<commit_after>const { Boards }    = require('boards');
const fileExists    = require('file-exists');
const procurator    = require('procurator');
const { Homefront } = require('homefront');
const path          = require('path');
const glob          = require('glob');

class Runner {
  constructor(config) {
    this.config = Homefront.merge({ tasks: {}, appRoot: '.', templateRoot: '.' }, config);
    this.boards = new Boards({ discoveryConfig: { prefix: 'boards-preset' } });
  }

  composeTask(task, parameters) {
    if (this.config.tasks[task]) {
      return this.config.tasks[task];
    }

    // Preset. Check if exists.
    const parts      = task.split(':');
    const presetName = parts.shift();
    const presets    = this.boards.getPlugins();
    const preset     = presets[presetName];
    task             = parts[0];

    if (!preset) {
      throw new Error(`Preset "${presetName}" not found.`);
    }

    parameters.fallbackSourceDirectory = preset.templateRoot;

    return preset.tasks[task];
  }

  run(task, parameters) {
    parameters = Homefront.merge({}, this.config.parameters || {}, parameters);

    return this.apply('default:prepare', parameters).then(() => {
      return this.apply(task, parameters)
    });
  }

  apply(task, parameters) {
    let instructions = task;

    if (typeof task === 'string') {
      instructions = this.composeTask(task, parameters);

      if (!instructions) {
        return Promise.reject(new Error(`Instructions for task "${task}" not found.`));
      }
    }

    if (!instructions) {
      return Promise.reject(new Error(`Invalid instructions provided.`));
    }

    if (!Array.isArray(instructions)) {
      instructions = [instructions];
    }

    let previousTask = null;

    return Promise.all(instructions.map(instruction => {
      let runner;

      if (previousTask) {
        runner       = previousTask.then(() => this.runTask(instruction, parameters));
        previousTask = null;
      } else {
        runner = this.runTask(instruction, parameters);
      }

      if (instruction.sync) {
        previousTask = runner;
      }

      return runner;
    }));
  }

  prepareParams(method, params) {
    const preparedParams = method(params);

    if (typeof preparedParams === 'object') {
      return preparedParams;
    }

    return params;
  }

  runTask(instruction, parameters) {
    instruction.from = instruction.from || '';

    // Prepare for step. Copy parameters to not affect other tasks.
    if (typeof instruction.prepare === 'function') {
      parameters = this.prepareParams(instruction.prepare, Homefront.merge({}, parameters));
    }

    // Allow dynamic tasks to be supplied.
    if (typeof instruction.dynamicTask === 'function') {
      return Promise.resolve(instruction.dynamicTask(parameters)).then(tasks => {
        this.apply(tasks, parameters);
      });
    }

    if (typeof instruction.definedTask !== 'undefined') {
      if (typeof instruction.definedTask !== 'string') {
        throw new Error(`definedTask must be a string. Got ${typeof instruction.definedTask}.`);
      }

      if (instruction.isolated && typeof instruction.prepare !== 'function') {
        parameters = Homefront.merge({}, parameters);
      }

      return this.apply(instruction.definedTask, parameters);
    }

    if (typeof instruction.task === 'function') {
      return instruction.task(parameters, this.boards);
    }

    if (typeof this[instruction.task] !== 'function') {
      throw new Error(`Invalid task "${instruction.task}" supplied`);
    }

    return this[instruction.task](instruction, parameters);
  }

  modify(instruction, parameters) {
    return this.boards.generate('ModificationGenerator', Object.assign({}, parameters, {
      sourceDirectory: this.config.appRoot,
      targetDirectory: this.config.appRoot,
      sourceFile     : this.getTarget(instruction.target, parameters),
      modify         : { patch: instruction.patch }
    }));
  }

  applyGenerate(template, target, sourceDirectory, instruction, parameters) {
    const parsed = path.parse(this.getTarget(target, parameters));

    return this.boards.generate('TemplateGenerator', Object.assign({}, parameters, {
      targetFile     : parsed.base,
      targetDirectory: path.join(this.config.appRoot, parsed.dir || '.'),
      sourceFile     : template,
      sourceDirectory
    }));
  }

  generate(instruction, parameters) {
    if (!instruction.glob) {
      const template = this.getTarget(instruction.template, parameters);

      return this.applyGenerate(template, instruction.target, this.getTemplateDirectory(instruction, parameters), instruction, parameters);
    }

    const glob         = this.getTarget(instruction.glob, parameters);
    const globPromises = [];

    if (this.config.templateRoot) {
      globPromises.push(this.glob(glob, path.join(this.config.templateRoot, instruction.from)));
    } else {
      // Push empty array so we can use array position further down
      globPromises.push(Promise.resolve([]));
    }

    if (parameters.fallbackSourceDirectory) {
      globPromises.push(this.glob(glob, path.join(parameters.fallbackSourceDirectory, instruction.from)));
    }

    return Promise.all(globPromises)
      .then(globbed => {
        const projectMatches = globbed[0];
        const generates      = [];
        const makeApply      = (match, dir) => {
          const target = path.join(instruction.target, match);

          generates.push(this.applyGenerate(target, target, path.join(dir, instruction.from), instruction, parameters));
        };

        projectMatches.forEach(match => makeApply(match, this.config.templateRoot));

        if (globbed[1]) {
          globbed[1]
            .filter(match => !projectMatches.includes(match))
            .forEach(match => makeApply(match, parameters.fallbackSourceDirectory));
        }

        return Promise.all(generates);
      });
  }

  getTemplateDirectory(instruction, parameters) {
    if (fileExists.sync(path.resolve(this.config.templateRoot, instruction.template))) {
      return this.config.templateRoot;
    }

    const fallback = parameters.fallbackSourceDirectory;

    if (!fallback) {
      throw new Error(`Template "${instruction.template}" not found.`);
    }

    if (!fileExists.sync(path.resolve(fallback, instruction.template))) {
      throw new Error(`Preset template "${instruction.template}" not found.`);
    }

    return fallback;
  }

  glob(pattern, cwd) {
    return new Promise((resolve, reject) => {
      glob(pattern, { nodir: true, cwd }, (error, results) => {
        if (error) {
          return reject(error);
        }

        return resolve(results);
      });
    });
  }

  getTarget(target, parameters) {
    if (typeof target === 'function') {
      return target(parameters);
    }

    return procurator.sync(target, parameters);
  }
}

module.exports.Runner = Runner;
<|endoftext|>


<commit_before>const EventEmitter = require('events').EventEmitter;
const Ws = require('ws');
const DeepExtend = require('deep-extend');

const ClientConnection = require('./ClientConnection.js');

class Server extends EventEmitter {

    constructor(wsOptions, guacdOptions, clientOptions, callbacks) {
        super();

        if (wsOptions.hasOwnProperty('server')) {
            this.wsOptions = wsOptions;
        } else {
            this.wsOptions = Object.assign({
                port: 8080
            }, wsOptions);
        }

        this.guacdOptions = Object.assign({
            host: '127.0.0.1',
            port: 4822
        }, guacdOptions);

        this.clientOptions = {};
        DeepExtend(this.clientOptions, {
            log: {
                verbose: true
            },

            crypt: {
                cypher: 'AES-256-CBC',
            },

            connectionDefaultSettings: {
                rdp: {
                    'args': 'connect',
                    'port': '3389',
                    'width': 1024,
                    'height': 768,
                    'dpi': 96,
                },
                vnc: {
                    'args': 'connect',
                    'port': '5900',
                    'width': 1024,
                    'height': 768,
                    'dpi': 96,
                }
            },

            allowedUnencryptedConnectionSettings: {
                rdp: [
                    'width',
                    'height',
                    'dpi'
                ],
                vnc: [
                    'width',
                    'height',
                    'dpi'
                ]
            }

        }, clientOptions);

        this.callbacks = Object.assign({
            processConnectionSettings: (settings, callback) => callback(undefined, settings)
        }, callbacks);

        this.connectionsCount = 0;
        this.activeConnections = new Map();

        console.log('Starting guacamole-lite websocket server');

        this.webSocketServer = new Ws.Server(this.wsOptions);
        this.webSocketServer.on('connection', this.newConnection.bind(this));

        process.on('SIGTERM', this.close.bind(this));
        process.on('SIGINT', this.close.bind(this));

    }

    close() {
        console.log('Closing all connections and exiting...');
        this.webSocketServer.close(() => {
            this.activeConnections.forEach((activeConnection) => {
                activeConnection.close();
            });
        });
    }

    newConnection(webSocketConnection) {
        this.connectionsCount++;
        this.activeConnections.set(this.connectionsCount, new ClientConnection(this, this.connectionsCount, webSocketConnection));
    }
}

module.exports = Server;
<commit_msg>Added default options for ssh and telnet<commit_after>const EventEmitter = require('events').EventEmitter;
const Ws = require('ws');
const DeepExtend = require('deep-extend');

const ClientConnection = require('./ClientConnection.js');

class Server extends EventEmitter {

    constructor(wsOptions, guacdOptions, clientOptions, callbacks) {
        super();

        if (wsOptions.hasOwnProperty('server')) {
            this.wsOptions = wsOptions;
        } else {
            this.wsOptions = Object.assign({
                port: 8080
            }, wsOptions);
        }

        this.guacdOptions = Object.assign({
            host: '127.0.0.1',
            port: 4822
        }, guacdOptions);

        this.clientOptions = {};
        DeepExtend(this.clientOptions, {
            log: {
                verbose: true
            },

            crypt: {
                cypher: 'AES-256-CBC',
            },

            connectionDefaultSettings: {
                rdp: {
                    'args': 'connect',
                    'port': '3389',
                    'width': 1024,
                    'height': 768,
                    'dpi': 96,
                },
                vnc: {
                    'args': 'connect',
                    'port': '5900',
                    'width': 1024,
                    'height': 768,
                    'dpi': 96,
                },
                ssh: {
                    'args': 'connect',
                    'port': 22,
                },
                telnet: {
                    'args': 'connect',
                    'port': 23,
                }
            },

            allowedUnencryptedConnectionSettings: {
                rdp: [
                    'width',
                    'height',
                    'dpi'
                ],
                vnc: [
                    'width',
                    'height',
                    'dpi'
                ],
                ssh: [
                    'color-scheme',
                    'font-name',
                    'font-size'
                ],
                telnet: [
                    'color-scheme',
                    'font-name',
                    'font-size'
                ]
            }

        }, clientOptions);

        this.callbacks = Object.assign({
            processConnectionSettings: (settings, callback) => callback(undefined, settings)
        }, callbacks);

        this.connectionsCount = 0;
        this.activeConnections = new Map();

        console.log('Starting guacamole-lite websocket server');

        this.webSocketServer = new Ws.Server(this.wsOptions);
        this.webSocketServer.on('connection', this.newConnection.bind(this));

        process.on('SIGTERM', this.close.bind(this));
        process.on('SIGINT', this.close.bind(this));

    }

    close() {
        console.log('Closing all connections and exiting...');
        this.webSocketServer.close(() => {
            this.activeConnections.forEach((activeConnection) => {
                activeConnection.close();
            });
        });
    }

    newConnection(webSocketConnection) {
        this.connectionsCount++;
        this.activeConnections.set(this.connectionsCount, new ClientConnection(this, this.connectionsCount, webSocketConnection));
    }
}

module.exports = Server;
<|endoftext|>


<commit_before>'use strict';

var path = require('path'),
    _ = require('lodash-node'),
    crypto = require('crypto'),
    File = require('vinyl'),
    should = require('should'),
    through = require('through2'),
    sinon = require('sinon');

var cache = require('../');

require('mocha');

describe('gulp-cache', function () {
    var sandbox,
        fakeFileHandler,
        fakeTask;

    beforeEach(function (done) {
        sandbox = sinon.sandbox.create();

        // Spy on the fakeFileHandler to check if it gets called later
        fakeFileHandler = sandbox.spy(function (file, enc, cb) {
            file.ran = true;

            if (Buffer.isBuffer(file.contents)) {
                file.contents = new Buffer(String(file.contents) + '-modified');
            }

            cb(null, file);
        });
        fakeTask = through.obj(fakeFileHandler);

        cache.fileCache.clear('default', done);
    });

    afterEach(function () {
        sandbox.restore();
    });

    it('throws an error if no task is passed', function () {
        var shouldThrow = function () {
            cache();
        };

        shouldThrow.should.throw();
    });

    it('exposes the Cache object for creating custom Caches', function () {
        should.exist(cache.Cache);
    });

    describe('in streaming mode', function () {
        it('does not work', function (done) {
            // Create a proxied plugin stream
            var proxied = cache(fakeTask, {
                key: function (file, cb) {
                    // For testing async key generation
                    setTimeout(function () {
                        cb(null, '123');
                    }, 1);
                },
                value: function (file, cb) {
                    // For testing async value generation
                    setTimeout(function () {
                        cb(null, {
                            ran: file.ran,
                            cached: true
                        });
                    }, 1);
                }
            });

            proxied
            .on('error', function(err) {
              err.message.should.equal('Cannot operate on stream sources');
              done();
            })
            .end(new File({ contents: through() }));
        });
    });

    describe('in buffered mode', function () {
        it('can clear all the cache', function (done) {
            cache.clearAll(function (err) {
                done(err);
            });
        });

        it('can clear specific cache', function (done) {
            var fakeFileCache = {
                    removeCached: sandbox.spy(function (category, hash, cb) {
                        return cb();
                    })
                },
                someKeyHash = crypto.createHash('md5').update('somekey').digest('hex');

            cache.clear({
                name: 'somename',
                fileCache: fakeFileCache,
                key: function () {
                    return 'somekey';
                }
            })
            .on('data', function () {
                fakeFileCache.removeCached.calledWith('somename', someKeyHash).should.equal(true);
                done();
            })
            .end(new File({
                contents: new Buffer('something')
            }));
        });

        it('only caches successful tasks', function (done) {
            // Create a proxied plugin stream
            var valStub = sandbox.stub().returns({
                    ran: true,
                    cached: true
                });

            cache(fakeTask, {
              success: function () {
                return false;
              },
              value: valStub
            })
            .on('data', function () {
                valStub.called.should.equal(false);
                done();
            })
            .end(new File({
                contents: new Buffer('abufferwiththiscontent')
            }));
        });

        it('sets the content correctly on subsequently ran cached tasks', function (done) {
            // Create a proxied plugin stream
            var proxied = cache(fakeTask, {
                success: function () {
                    return true;
                }
            });

            proxied.once('data', function (file) {
                String(file.contents).should.equal('abufferwiththiscontent-modified');

                proxied.once('data', function (file2) {
                    should.exist(file2.isBuffer());
                    String(file2.contents).should.equal('abufferwiththiscontent-modified');

                    done();
                });

                proxied.end(new File({
                    contents: new Buffer('abufferwiththiscontent')
                }));
            });

            proxied.write(new File({
                contents: new Buffer('abufferwiththiscontent')
            }));
        });

        it('can proxy a task with specific options', function (done) {
            // create the fake file
            var fakeFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                }),
                otherFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                });

            // Create a proxied plugin stream
            var proxied = cache(fakeTask, {
                value: function (file) {
                    return {
                        ran: file.ran,
                        cached: true,
                        contents: (file.contents || file._contents)
                    };
                }
            });

            // write the fake file to it
            proxied.write(fakeFile);

            // wait for the file to come back out
            proxied.once('data', function (file) {
                // make sure it came out the same way it went in
                file.isBuffer().should.equal(true);

                // check the contents are same
                file.contents.toString('utf8').should.equal('abufferwiththiscontent-modified');

                // Check it assigned the proxied task result
                file.ran.should.equal(true);
                should.not.exist(file.cached);

                // Check the original task was called
                fakeFileHandler.called.should.equal(true);

                // Reset for the second run through
                fakeFileHandler.reset();

                // Write the same file again, should be cached result
                proxied.write(otherFile);

                proxied.once('data', function (secondFile) {
                    secondFile.isBuffer().should.equal(true);

                    String(secondFile.contents).should.equal('abufferwiththiscontent-modified');

                    // Cached value should have been applied
                    secondFile.ran.should.equal(true);
                    secondFile.cached.should.equal(true);

                    // Should not have called the original task
                    fakeFileHandler.called.should.equal(false);

                    done();
                });
            });
        });

        it('can proxy a task using task.cacheable', function (done) {
            // create the fake file
            var fakeFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                }),
                otherFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                });

            // Let the task define the cacheable aspects.
            fakeTask.cacheable = {
                key: sandbox.spy(function (file) {
                    return file.contents.toString('utf8');
                }),
                success: sandbox.stub().returns(true),
                value: sandbox.spy(function (file) {
                    return {
                        ran: true,
                        cached: true,
                        contents: file.contents || file._contents
                    };
                })
            };

            var proxied = cache(fakeTask);

            // write the fake file to it
            proxied.write(fakeFile);

            // wait for the file to come back out
            proxied.once('data', function (file) {
                // make sure it came out the same way it went in
                file.isBuffer().should.equal(true);

                // check the contents are same
                String(file.contents).should.equal('abufferwiththiscontent-modified');

                // Verify the cacheable options were used.
                fakeTask.cacheable.key.called.should.equal(true);
                fakeTask.cacheable.success.called.should.equal(true);
                fakeTask.cacheable.value.called.should.equal(true);

                _.invoke([
                    fakeTask.cacheable.key,
                    fakeTask.cacheable.success,
                    fakeTask.cacheable.value,
                    fakeFileHandler
                ], 'reset');

                // Write the same file again, should be cached result
                proxied.write(otherFile);

                proxied.once('data', function (secondFile) {
                    fakeTask.cacheable.key.called.should.equal(true);
                    fakeTask.cacheable.success.called.should.equal(false);
                    fakeTask.cacheable.value.called.should.equal(false);

                    // Should not have called the original task
                    fakeFileHandler.called.should.equal(false);

                    // Cached value should have been applied
                    secondFile.cached.should.equal(true);

                    done();
                });
            });
        });

        it('can proxy a task using task.cacheable with user overrides', function (done) {
            // create the fake file
            var fakeFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                }),
                otherFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                });

            // Let the task define the cacheable aspects.
            fakeTask.cacheable = {
                key: sandbox.spy(function (file) {
                    return String(file.contents);
                }),
                success: sandbox.stub().returns(true),
                value: sandbox.stub().returns({
                    ran: true,
                    cached: true
                })
            };

            var overriddenValue = sandbox.stub().returns({
                    ran: true,
                    cached: true,
                    overridden: true
                });

            // write the fake file to it
            cache(fakeTask, { value: overriddenValue })
            // wait for the file to come back out
            .once('data', function (file) {
                // make sure it came out the same way it went in
                file.isBuffer().should.equal(true);

                // check the contents are same
                String(file.contents).should.equal('abufferwiththiscontent-modified');

                // Verify the cacheable options were used.
                fakeTask.cacheable.key.called.should.equal(true);
                fakeTask.cacheable.success.called.should.equal(true);
                fakeTask.cacheable.value.called.should.equal(false);
                overriddenValue.called.should.equal(true);

                _.invoke([
                        fakeTask.cacheable.key,
                        fakeTask.cacheable.success,
                        fakeTask.cacheable.value,
                        overriddenValue,
                        fakeFileHandler
                    ], 'reset');

                this.once('data', function (secondFile) {
                    // Cached value should have been applied
                    secondFile.cached.should.equal(true);
                    secondFile.overridden.should.equal(true);

                    fakeTask.cacheable.key.called.should.equal(true);
                    fakeTask.cacheable.success.called.should.equal(false);
                    fakeTask.cacheable.value.called.should.equal(false);
                    overriddenValue.called.should.equal(false);

                    // Should not have called the original task
                    fakeFileHandler.called.should.equal(false);

                    done();
                })
                // Write the same file again, should be cached result
                .end(otherFile);
            })
            .write(fakeFile);
        });

        it('can be passed just a string for the value', function (done) {
            // create the fake file
            var fakeFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                }),
                otherFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                });

            // Create a proxied plugin stream
            cache(fakeTask, { value: 'ran' })
            .once('data', function (file) {
                // Check it assigned the proxied task result
                file.ran.should.equal(true);

                this.once('data', function (secondFile) {
                    // Cached value should have been applied
                    secondFile.ran.should.equal(true);
                    done();
                });

                // Write the same file again, should be cached result
                this.end(otherFile);
            })
            .write(fakeFile);
        });

        it('can store changed contents of files', function (done) {
            // create the fake file
            var fakeFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                }),
                updatedFileHandler = sandbox.spy(function (file, enc, cb) {
                    file.contents = new Buffer('updatedcontent');
                    cb(null, file);
                });

            fakeTask = through.obj(updatedFileHandler);

            // Create a proxied plugin stream
            var proxied = cache(fakeTask);

            // write the fake file to it
            proxied.write(fakeFile);

            // wait for the file to come back out
            proxied.once('data', function (file) {
                // Check for updated content
                String(file.contents).should.equal('updatedcontent');

                // Check original handler was called
                updatedFileHandler.called.should.equal(true);

                updatedFileHandler.reset();

                this.once('data', function () {
                    String(file.contents).should.equal('updatedcontent');

                    // Check original handler was not called.
                    updatedFileHandler.called.should.equal(false);

                    done();
                });

                // Write the same file again, should be cached result
                this.end(new File({
                    contents: new Buffer('abufferwiththiscontent')
                }));
            });
        });

        it('does not throw memory leak warning when proxying tasks', function (done) {
            fakeTask = through.obj(function (file, enc, cb) {
                setTimeout(function () {
                    file.contents = new Buffer(file.contents.toString() + ' updated');

                    cb(null, file);
                }, 10);
            });

            var files = _.times(30, function (val, i) {
                    return new File({
                        contents: new Buffer('Test File ' + i)
                    });
                }),
                proxied = cache(fakeTask);

            var origMaxListeners = fakeTask._maxListeners;
            var errSpy = sandbox.spy(console, 'error');

            var processedCount = 0;
            proxied
            .on('data', function () {
                processedCount += 1;
            })
            .on('finish', function() {
              processedCount.should.equal(30);
              errSpy.called.should.equal(false, 'Called console.error');
              fakeTask._maxListeners.should.equal(origMaxListeners || 0);

              done();
            });

            files.forEach(function (file) {
                proxied.write(file);
            });

            proxied.end();
        });

        it('sets the path on cached results', function (done) {
            // create the fake file
            var filePath = path.join(process.cwd(), 'test', 'fixtures', 'in', 'file1.txt'),
                fakeFile = new File({
                    path: filePath,
                    contents: new Buffer('abufferwiththiscontent')
                }),
                updatedFileHandler = sandbox.spy(function (file, enc, cb) {
                    file.contents = new Buffer('updatedcontent');

                    cb(null, file);
                });

            fakeTask = through.obj(updatedFileHandler);

            // Create a proxied plugin stream
            var proxied = cache(fakeTask);

            // write the fake file to it
            proxied.write(fakeFile);

            // wait for the file to come back out
            proxied.once('data', function (file) {
                // Check original handler was called
                updatedFileHandler.called.should.equal(true);

                // Check the path is on there
                file.path.should.equal(filePath);

                updatedFileHandler.reset();

                // Write the same file again, should be cached result
                proxied.write(new File({
                    contents: new Buffer('abufferwiththiscontent')
                }));

                proxied.once('data', function (secondFile) {
                    // Check for same file path
                    should.exist(secondFile.path);
                    secondFile.path.should.equal(filePath);

                    // Check original handler was not called.
                    updatedFileHandler.called.should.equal(false);

                    done();
                });
            });
        });
    });
});
<commit_msg>Add more tests for `null` file objects<commit_after>'use strict';

var path = require('path'),
    _ = require('lodash-node'),
    crypto = require('crypto'),
    File = require('vinyl'),
    should = require('should'),
    through = require('through2'),
    sinon = require('sinon');

var cache = require('../');

require('mocha');

describe('gulp-cache', function () {
    var sandbox,
        fakeFileHandler,
        fakeTask;

    beforeEach(function (done) {
        sandbox = sinon.sandbox.create();

        // Spy on the fakeFileHandler to check if it gets called later
        fakeFileHandler = sandbox.spy(function (file, enc, cb) {
            file.ran = true;

            if (Buffer.isBuffer(file.contents)) {
                file.contents = new Buffer(String(file.contents) + '-modified');
            }

            cb(null, file);
        });
        fakeTask = through.obj(fakeFileHandler);

        cache.fileCache.clear('default', done);
    });

    afterEach(function () {
        sandbox.restore();
    });

    it('throws an error if no task is passed', function () {
        var shouldThrow = function () {
            cache();
        };

        shouldThrow.should.throw();
    });

    it('exposes the Cache object for creating custom Caches', function () {
        should.exist(cache.Cache);
    });

    it('pass through the directories', function (done) {
      var directory = new File();
      var proxied = cache(fakeTask);

        proxied
        .on('data', function (file) {
          file.should.eql(directory);
          file.isNull().should.equal(true);
          done();
        })
        .end(new File());
    });

    describe('in streaming mode', function () {
        it('does not work', function (done) {
            // Create a proxied plugin stream
            var proxied = cache(fakeTask, {
                key: function (file, cb) {
                    // For testing async key generation
                    setTimeout(function () {
                        cb(null, '123');
                    }, 1);
                },
                value: function (file, cb) {
                    // For testing async value generation
                    setTimeout(function () {
                        cb(null, {
                            ran: file.ran,
                            cached: true
                        });
                    }, 1);
                }
            });

            proxied
            .on('error', function(err) {
                err.message.should.equal('Cannot operate on stream sources');
                done();
            })
            .end(new File({ contents: through() }));
        });
    });

    describe('in buffered mode', function () {
        it('only caches successful tasks', function (done) {
            // Create a proxied plugin stream
            var valStub = sandbox.stub().returns({
                    ran: true,
                    cached: true
                });

            cache(fakeTask, {
              success: function () {
                return false;
              },
              value: valStub
            })
            .on('data', function () {
                valStub.called.should.equal(false);
                done();
            })
            .end(new File({
                contents: new Buffer('abufferwiththiscontent')
            }));
        });

        it('sets the content correctly on subsequently ran cached tasks', function (done) {
            // Create a proxied plugin stream
            var proxied = cache(fakeTask, {
                success: function () {
                    return true;
                }
            });

            proxied.once('data', function (file) {
                String(file.contents).should.equal('abufferwiththiscontent-modified');

                proxied.once('data', function (file2) {
                    should.exist(file2.isBuffer());
                    String(file2.contents).should.equal('abufferwiththiscontent-modified');

                    done();
                });

                proxied.end(new File({
                    contents: new Buffer('abufferwiththiscontent')
                }));
            });

            proxied.write(new File({
                contents: new Buffer('abufferwiththiscontent')
            }));
        });

        it('can proxy a task with specific options', function (done) {
            // create the fake file
            var fakeFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                }),
                otherFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                });

            // Create a proxied plugin stream
            var proxied = cache(fakeTask, {
                value: function (file) {
                    return {
                        ran: file.ran,
                        cached: true,
                        contents: (file.contents || file._contents)
                    };
                }
            });

            // write the fake file to it
            proxied.write(fakeFile);

            // wait for the file to come back out
            proxied.once('data', function (file) {
                // make sure it came out the same way it went in
                file.isBuffer().should.equal(true);

                // check the contents are same
                file.contents.toString('utf8').should.equal('abufferwiththiscontent-modified');

                // Check it assigned the proxied task result
                file.ran.should.equal(true);
                should.not.exist(file.cached);

                // Check the original task was called
                fakeFileHandler.called.should.equal(true);

                // Reset for the second run through
                fakeFileHandler.reset();

                // Write the same file again, should be cached result
                proxied.write(otherFile);

                proxied.once('data', function (secondFile) {
                    secondFile.isBuffer().should.equal(true);

                    String(secondFile.contents).should.equal('abufferwiththiscontent-modified');

                    // Cached value should have been applied
                    secondFile.ran.should.equal(true);
                    secondFile.cached.should.equal(true);

                    // Should not have called the original task
                    fakeFileHandler.called.should.equal(false);

                    done();
                });
            });
        });

        it('can proxy a task using task.cacheable', function (done) {
            // create the fake file
            var fakeFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                }),
                otherFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                });

            // Let the task define the cacheable aspects.
            fakeTask.cacheable = {
                key: sandbox.spy(function (file) {
                    return file.contents.toString('utf8');
                }),
                success: sandbox.stub().returns(true),
                value: sandbox.spy(function (file) {
                    return {
                        ran: true,
                        cached: true,
                        contents: file.contents || file._contents
                    };
                })
            };

            var proxied = cache(fakeTask);

            // write the fake file to it
            proxied.write(fakeFile);

            // wait for the file to come back out
            proxied.once('data', function (file) {
                // make sure it came out the same way it went in
                file.isBuffer().should.equal(true);

                // check the contents are same
                String(file.contents).should.equal('abufferwiththiscontent-modified');

                // Verify the cacheable options were used.
                fakeTask.cacheable.key.called.should.equal(true);
                fakeTask.cacheable.success.called.should.equal(true);
                fakeTask.cacheable.value.called.should.equal(true);

                _.invoke([
                    fakeTask.cacheable.key,
                    fakeTask.cacheable.success,
                    fakeTask.cacheable.value,
                    fakeFileHandler
                ], 'reset');

                // Write the same file again, should be cached result
                proxied.write(otherFile);

                proxied.once('data', function (secondFile) {
                    fakeTask.cacheable.key.called.should.equal(true);
                    fakeTask.cacheable.success.called.should.equal(false);
                    fakeTask.cacheable.value.called.should.equal(false);

                    // Should not have called the original task
                    fakeFileHandler.called.should.equal(false);

                    // Cached value should have been applied
                    secondFile.cached.should.equal(true);

                    done();
                });
            });
        });

        it('can proxy a task using task.cacheable with user overrides', function (done) {
            // create the fake file
            var fakeFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                }),
                otherFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                });

            // Let the task define the cacheable aspects.
            fakeTask.cacheable = {
                key: sandbox.spy(function (file) {
                    return String(file.contents);
                }),
                success: sandbox.stub().returns(true),
                value: sandbox.stub().returns({
                    ran: true,
                    cached: true
                })
            };

            var overriddenValue = sandbox.stub().returns({
                    ran: true,
                    cached: true,
                    overridden: true
                });

            // write the fake file to it
            cache(fakeTask, { value: overriddenValue })
            // wait for the file to come back out
            .once('data', function (file) {
                // make sure it came out the same way it went in
                file.isBuffer().should.equal(true);

                // check the contents are same
                String(file.contents).should.equal('abufferwiththiscontent-modified');

                // Verify the cacheable options were used.
                fakeTask.cacheable.key.called.should.equal(true);
                fakeTask.cacheable.success.called.should.equal(true);
                fakeTask.cacheable.value.called.should.equal(false);
                overriddenValue.called.should.equal(true);

                _.invoke([
                        fakeTask.cacheable.key,
                        fakeTask.cacheable.success,
                        fakeTask.cacheable.value,
                        overriddenValue,
                        fakeFileHandler
                    ], 'reset');

                this.once('data', function (secondFile) {
                    // Cached value should have been applied
                    secondFile.cached.should.equal(true);
                    secondFile.overridden.should.equal(true);

                    fakeTask.cacheable.key.called.should.equal(true);
                    fakeTask.cacheable.success.called.should.equal(false);
                    fakeTask.cacheable.value.called.should.equal(false);
                    overriddenValue.called.should.equal(false);

                    // Should not have called the original task
                    fakeFileHandler.called.should.equal(false);

                    done();
                })
                // Write the same file again, should be cached result
                .end(otherFile);
            })
            .write(fakeFile);
        });

        it('can be passed just a string for the value', function (done) {
            // create the fake file
            var fakeFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                }),
                otherFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                });

            // Create a proxied plugin stream
            cache(fakeTask, { value: 'ran' })
            .once('data', function (file) {
                // Check it assigned the proxied task result
                file.ran.should.equal(true);

                this.once('data', function (secondFile) {
                    // Cached value should have been applied
                    secondFile.ran.should.equal(true);
                    done();
                });

                // Write the same file again, should be cached result
                this.end(otherFile);
            })
            .write(fakeFile);
        });

        it('can store changed contents of files', function (done) {
            // create the fake file
            var fakeFile = new File({
                    contents: new Buffer('abufferwiththiscontent')
                }),
                updatedFileHandler = sandbox.spy(function (file, enc, cb) {
                    file.contents = new Buffer('updatedcontent');
                    cb(null, file);
                });

            fakeTask = through.obj(updatedFileHandler);

            // Create a proxied plugin stream
            var proxied = cache(fakeTask);

            // write the fake file to it
            proxied.write(fakeFile);

            // wait for the file to come back out
            proxied.once('data', function (file) {
                // Check for updated content
                String(file.contents).should.equal('updatedcontent');

                // Check original handler was called
                updatedFileHandler.called.should.equal(true);

                updatedFileHandler.reset();

                this.once('data', function () {
                    String(file.contents).should.equal('updatedcontent');

                    // Check original handler was not called.
                    updatedFileHandler.called.should.equal(false);

                    done();
                });

                // Write the same file again, should be cached result
                this.end(new File({
                    contents: new Buffer('abufferwiththiscontent')
                }));
            });
        });

        it('does not throw memory leak warning when proxying tasks', function (done) {
            fakeTask = through.obj(function (file, enc, cb) {
                setTimeout(function () {
                    file.contents = new Buffer(file.contents.toString() + ' updated');

                    cb(null, file);
                }, 10);
            });

            var files = _.times(30, function (val, i) {
                    return new File({
                        contents: new Buffer('Test File ' + i)
                    });
                }),
                proxied = cache(fakeTask);

            var origMaxListeners = fakeTask._maxListeners;
            var errSpy = sandbox.spy(console, 'error');

            var processedCount = 0;
            proxied
            .on('data', function () {
                processedCount += 1;
            })
            .on('finish', function() {
              processedCount.should.equal(30);
              errSpy.called.should.equal(false, 'Called console.error');
              fakeTask._maxListeners.should.equal(origMaxListeners || 0);

              done();
            });

            files.forEach(function (file) {
                proxied.write(file);
            });

            proxied.end();
        });

        it('sets the path on cached results', function (done) {
            // create the fake file
            var filePath = path.join(process.cwd(), 'test', 'fixtures', 'in', 'file1.txt'),
                fakeFile = new File({
                    path: filePath,
                    contents: new Buffer('abufferwiththiscontent')
                }),
                updatedFileHandler = sandbox.spy(function (file, enc, cb) {
                    file.contents = new Buffer('updatedcontent');

                    cb(null, file);
                });

            fakeTask = through.obj(updatedFileHandler);

            // Create a proxied plugin stream
            var proxied = cache(fakeTask);

            // write the fake file to it
            proxied.write(fakeFile);

            // wait for the file to come back out
            proxied.once('data', function (file) {
                // Check original handler was called
                updatedFileHandler.called.should.equal(true);

                // Check the path is on there
                file.path.should.equal(filePath);

                updatedFileHandler.reset();

                // Write the same file again, should be cached result
                proxied.write(new File({
                    contents: new Buffer('abufferwiththiscontent')
                }));

                proxied.once('data', function (secondFile) {
                    // Check for same file path
                    should.exist(secondFile.path);
                    secondFile.path.should.equal(filePath);

                    // Check original handler was not called.
                    updatedFileHandler.called.should.equal(false);

                    done();
                });
            });
        });
    });

    it('does nothing when it tries to clear a directory', function (done) {
        cache.clear()
        .on('data', function (file) {
            file.isNull().should.equal(true);
            done();
        })
        .end(new File());
    });

    it('cannot clear specific stream cache', function (done) {
        cache.clear()
        .on('error', function (err) {
            err.message.should.equal('Cannot operate on stream sources');
            done();
        })
        .end(new File({contents: through()}));
    });

    it('can clear specific buffer cache', function (done) {
        var fakeFileCache = {
                removeCached: sandbox.spy(function (category, hash, cb) {
                    return cb();
                })
            },
            someKeyHash = crypto.createHash('md5').update('somekey').digest('hex');

        cache.clear({
            name: 'somename',
            fileCache: fakeFileCache,
            key: function () {
                return 'somekey';
            }
        })
        .on('data', function () {
            fakeFileCache.removeCached.calledWith('somename', someKeyHash).should.equal(true);
            done();
        })
        .end(new File({contents: new Buffer('something')}));
    });

    it('can clear all the cache', function () {
        cache.clearAll();
    });

    it('can clear all the cache with callback', function (done) {
        cache.clearAll(done);
    });
});
<|endoftext|>