Code coverage report for backbone-proxy/backbone-proxy.js

Statements: 91.74% (100 / 109)      Branches: 90% (36 / 40)      Functions: 91.67% (33 / 36)      Lines: 92.45% (98 / 106)      Ignored: none     

All files » backbone-proxy/ » backbone-proxy.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337                      1         1               1 1                                                   1                                       1 5107     1   2313 1267 1349         1904                       771   791   771 771 771   771 791     771   771       1                         1 5107 5107 5107 5107 5107     1       2675 1976 1976     699           2675 2675 2222 1587 3388     635   2222                         1904         1904 1169 1139 1139             735 31 20                 704 1302 1280   1302         1904 1904 1904 1904       771 771 717   771       1                     1 3478           3478 24346 2188 2188                                         3478 6956 110           3478 2180                 3478 10434 278 278 278 278 278   278 70   278 16     278 278       3478 3478 3478   3478     3478 3478                   1     3478   3478 5107   3478 3478 3478            
//     Backbone Proxy v0.1.1
//
//     https://github.com/biril/backbone-proxy
//     Licensed and freely distributed under the MIT License
//     Copyright (c) 2014 Alex Lambiris
 
/*globals exports, define, require, _, Backbone */
 
// Detect env & export module
// --------------------------
 
(function (root, createModule) {
  'use strict';
 
  // A global `define` method with an `amd` property signifies the presence of an AMD loader
  //  (e.g. require.js, curl.js)
  Iif (typeof define === 'function' && define.amd) {
    return define(['underscore', 'backbone', 'exports'], function (_, Backbone, exports) {
      return createModule(exports, _, Backbone);
    });
  }
 
  // A global `exports` object signifies CommonJS-like enviroments that support `module.exports`,
  //  e.g. Node
  Eif (typeof exports === 'object') {
    return createModule(exports, require('underscore'), require('backbone'));
  }
 
  // Otherwise we assume running in a browser - no AMD loader:
 
  // Save a reference to previous value of `BackboneProxy` before overwriting it - so that it can
  //  be restored on `noConflict`
  var previousBackboneProxy = root.BackboneProxy;
 
  // Create the BackboneProxy module, attaching `BackboneProxy` to global scope
  createModule(root.BackboneProxy = {}, _, Backbone);
 
  // And add the `noConflict` method. This sets the `BackboneProxy` _global_ to to its previous
  //  value (_once_), returning a reference to `BackboneProxy` (_always_)
  root.BackboneProxy.noConflict = function () {
    var BackboneProxy = root.BackboneProxy;
    root.BackboneProxy = previousBackboneProxy;
    return (BackboneProxy.noConflict = function () { return BackboneProxy; }).call();
  };
 
// Create module
// -------------
 
}(this, function (BackboneProxy, _, Backbone) {
  'use strict';
 
  var
 
    // Model built-in events. Note that 'all' is not considered to be one of them
    builtInEventNames = [
      'add', 'remove', 'reset', 'change', 'destroy', 'request', 'sync', 'error', 'invalid'
    ],
 
    // Names of 'event API' (`Backbone.Event`) methods
    eventApiMethodNames = [
      'on', 'off', 'trigger', 'once', 'listenTo', 'stopListening', 'listenToOnce'
    ],
 
    // ### SubscriptionCollection
    // A collection of subscriptions, where each is a `<event, callback, context, proxyCallback>`
    //  4-tuple. Every `EventEngine` instance maintains a `SubscriptionCollection` instance to
    //  keep track of registered callbacks and their mapping to _proxy_-callbacks
 
    //
    SubscriptionCollection = (function () {
 
      function SubscriptionCollection () {
        this._items = [];
      }
 
      SubscriptionCollection.prototype = {
        _markToKeep: function (propName, propValue) {
          if (!propValue) { return; }
          for (var i = this._items.length - 1; i >= 0; --i) {
            this._items[i].keep = this._items[i][propName] !== propValue;
          }
        },
        // Store subscription of given attributes
        store: function (event, callback, context, proxyCallback) {
          this._items.push({
            event: event,
            callback: callback,
            context: context,
            proxyCallback: proxyCallback
          });
        },
        // Unstore subscriptions that match given `event` / `callback` / `context`. None of the
        //  params denote mandatory arguments and those given will be used to _filter_ the
        //  subscriptions to be removed. Invoking the method without any arguments will remove
        //  _all_ subscriptions. This behaviour is in line with that of `Backbone.Model#off`
        unstore: function (event, callback, context) {
          var itemsUnstored = [], itemsKept = [];
 
          _(this._items).each(function (item) { item.keep = false; });
 
          this._markToKeep('event', event);
          this._markToKeep('callback', callback);
          this._markToKeep('context', context);
 
          for (var i = this._items.length - 1; i >= 0; --i) {
            (this._items[i].keep ? itemsKept : itemsUnstored).push(this._items[i]);
          }
 
          this._items = itemsKept;
 
          return itemsUnstored;
        }
      };
 
      return SubscriptionCollection;
    }()),
 
 
    // ### Event Engine
    // A module that builds on top of `Backbone.Events` to support invoking registered callbacks in
    //  response to events triggered on a given `proxied`, on behalf of a given `proxy`. Every
    //  `ModelProxyProto` instance (i.e. every `Proxy.prototype`) contains an Event Engine to
    //  facilitate forwarding events from proxied to proxy
 
    //
    EventEngine = (function () {
 
      function EventEngine(proxy, proxied) {
        this._proxy = proxy;
        this._proxied = proxied;
        this._subscriptions = new SubscriptionCollection();
        this._isListeningToProxied = false;
        this._markerContext = {};
      }
 
      EventEngine.prototype = _({}).extend(Backbone.Events, {
 
        // Get a value indicating whether there's currently _any_ listeners registered on the engine
        _hasEvents: function () {
          for (var key in this._events) {
            Eif (this._events.hasOwnProperty(key)) {
              return true;
            }
          }
          return false;
        },
 
        // Listen or stop listening for the 'all' event on `proxied` depending on whether there's
        //  currently _any_ listeners registered on the event engine (equivalently, on the proxy)
        _manageSubscriptionToProxied: function () {
          var hasEvents = this._hasEvents();
          if (this._isListeningToProxied !== hasEvents) {
            if (hasEvents) {
              this._proxied.on('all', _.bind(function () {
                Backbone.Events.trigger.apply(this, arguments);
              }, this), this._markerContext);
            } else {
              this._proxied.off(null, null, this._markerContext);
            }
            this._isListeningToProxied = hasEvents;
          }
        },
 
        // For any given subscription (defined by `event` / `callback` / `context`), create a
        //  _proxy-callback_ - a callback that will be invoked by by Backbone's Event module in
        //  place of the original caller-provided callback. Depending on the type of the event the
        //  given subscription refers to, the proxy-callback will take care of appropriately
        //  setting `model` arguments that may be present (when dealing with model built-in events)
        //  as well as the context (if it's not explicitly set by the caller)
        _createProxyCallback: function (event, callback, context) {
 
          // The context is the one that's already specified _or_ set to the proxy
          context || (context = this._proxy);
 
          // If the subscription is for a model built-in event, the proxy-callback will have to
          //  replace the model argument with the proxy (as Backbone's Event module will set it to
          //  the proxied when invoking the callback). Same goes for the context
          if (_(builtInEventNames).contains(event) || !event.indexOf('change:')) {
            return function () {
              arguments[0] = this._proxy;
              callback.apply(context, arguments);
            };
          }
 
          // So the subscription doesn't concern a built-in event. If additionally it's not for the
          //  'all' event, then we're dealing with a user-defined event. In this case we only have
          //  to make sure that the callback is invoked the appropriate context
          if (event !== 'all') {
            return function () {
              callback.apply(context, arguments);
            };
          }
 
          // So The subscription is for the 'all' event: The given callback will run for built-in
          //  events as well as arbitrary, user-defined events. We'll have to check the type of the
          //  event at callback-invocation-time and treat it as one of the two cases. (And yes, if
          //  client-code decides to `trigger()` an event which is named like a built-in but
          //  doesn't carry the expected parameters, things will go sideways)
          return function (event) {
            if (_(builtInEventNames).contains(event) || !event.indexOf('change:')) {
              arguments[1] = this._proxy;
            }
            callback.apply(context, arguments);
          };
        },
 
        on: function (event, callback, context) {
          var proxyCallback = this._createProxyCallback(event, callback, context);
          this._subscriptions.store(event, callback, context, proxyCallback);
          Backbone.Events.on.call(this, event, proxyCallback, context);
          this._manageSubscriptionToProxied();
        },
 
        off: function (event, callback, context) {
          var matchingSubscriptions = this._subscriptions.unstore(event, callback, context);
          _(matchingSubscriptions).each(function (subscription) {
            Backbone.Events.off.call(this, subscription.event, subscription.proxyCallback, subscription.context);
          }, this);
          this._manageSubscriptionToProxied();
        }
      });
 
      return EventEngine;
    }()),
 
 
    // ### Model Proxy Prototype
    // The prototype of Proxy, _per given `proxied` model_. Overrides certain `Backbone.Model`
    //  methods ultimately delegating to the implementations of the given `proxied`
 
    // Create a prototype for Proxy, given a `proxied` model
    createModelProxyProtoForProxied = function (proxied) {
 
      function ModelProxyProto() {
        var createPersistenceMethod;
 
        // #### Prototype's event API methods - they all delegate to the internal Event Engine
        // e.g. `on`, `off`
 
        //
        _(eventApiMethodNames).each(function (methodName) {
          this[methodName] = function () {
            this._eventEngine[methodName].apply(this._eventEngine, arguments);
            return this;
          };
        }, this);
 
 
        // #### Prototype's methods that should always be invoked with `this` set to `proxied`
        // e.g. `set`
 
        // This ensures that
        //
        //  * all other model methods in the call-graph of the invoked method (e.g. `proxied.set`)
        //     are also invoked with `this` set to `proxied`.
        //  * all properties which may be set on the model as part of the invoked method's code
        //     path are set on `proxied`
        //
        // For example, this ensures that the `model` parameter made available to event listeners
        //  attached to `proxied` will be `proxied` - not `proxy` (which would otherwise be the
        //  case). In terms of set properties, consider `validationError` which also needs to be
        //  set on `proxied` - not `proxy`.
 
        // Generally, we just need to forward to `proxied`
        _(['isNew', 'url']).each(function (methodName) {
          this[methodName] = function () {
            return proxied[methodName]();
          };
        }, this);
 
        // Specifically, for the case of `set`, we need to replace the returned reference with
        //  `this`. To get proper chaining
        this.set = function () {
          return proxied.set.apply(proxied, arguments) ? this : false;
        };
 
 
        // #### Prototype's persistence methods
        // i.e. `fetch`, `save` & `destroy`
 
        // Create persistence method of `methodName`, where `isWithAttributes` indicates whether
        //  the method excepts attributes (`save`) or just options (`fetch`, `destroy`)
        createPersistenceMethod = function (methodName, isWithAttributes) {
          return function () {
            var opts, success, error, args;
            opts = arguments[isWithAttributes ? 1 : 0];
            opts = opts ? _.clone(opts) : {};
            success = opts.success;
            error = opts.error;
 
            success && (opts.success = _.bind(function (model, response, opts) {
              success(this, response, opts);
            }, this));
            error && (opts.error = _.bind(function (model, response, opts) {
              error(this, response, opts);
            }, this));
 
            args = isWithAttributes ? [arguments[0], opts] : [opts];
            return proxied[methodName].apply(proxied, args);
          };
        };
 
        this.fetch   = createPersistenceMethod('fetch',   false);
        this.destroy = createPersistenceMethod('destroy', false);
        this.save    = createPersistenceMethod('save',    true);
 
        this._proxied = proxied;
      }
 
      ModelProxyProto.prototype = proxied;
      return ModelProxyProto;
    };
 
 
  // ### BackboneProxy
  // The BackboneProxy module. Features a single `extend` method by means of which a proxy 'class'
  //  `Proxy` may be created. E.g `UserProxy = BackboneProxy.extend(user)`. Proxy classes may be
  //  instantiated into proxies, e.g. `user = new UserProxy()`
 
  // Return the BackboneProxy module
  return _(BackboneProxy).extend({
 
    extend: function (proxied) {
      var ctor, ModelProxyProto;
 
      ctor = function ModelProxy() {
        this._eventEngine = new EventEngine(this, proxied);
      };
      ModelProxyProto = createModelProxyProtoForProxied(proxied);
      ctor.prototype = new ModelProxyProto();
      return ctor;
    }
 
  });
 
}));