Cascade.toJSON() now recursively merges objects using _.merge().
authordsc <dsc@wikimedia.org>
Thu, 12 Apr 2012 22:27:15 +0000 (15:27 -0700)
committerdsc <dsc@wikimedia.org>
Thu, 12 Apr 2012 22:27:15 +0000 (15:27 -0700)
lib/util/cascade.co
lib/util/underscore/object.co

index fb1cdb7..c551440 100644 (file)
@@ -188,8 +188,25 @@ class Cascade
         for o of arguments then @set o
         this
     
+    /**
+     * Recursively collapses the Cascade to a plain object by recursively merging the
+     * lookups (in reverse order) into the data.
+     * @returns {Object}
+     */
+    collapse: ->
+        _.merge {}, ...@_lookups.slice().reverse()
+    
+    /**
+     * Returns a plain object for JSON serialization via {@link Cascade#collapse()}.
+     * The name of this method is a bit confusing, as it doesn't actually return a 
+     * JSON string -- but I'm afraid that it's the way that the JavaScript API for 
+     * `JSON.stringify()` works.
+     * 
+     * @see https://developer.mozilla.org/en/JSON#toJSON()_method
+     * @return {Object} Plain object for JSON serialization.
+     */
     toJSON: ->
-        _.extend {}, ...@_lookups.slice().reverse()
+        @collapse()
     
     # XXX: should unique? but then won't map 1:1 to @values()...
     keys: ->
@@ -228,8 +245,8 @@ class Cascade
 
 
 ALIASES =
-    toJSON : 'toObject'
-    each   : 'forEach'
+    collapse : 'toObject'
+    each     : 'forEach'
 
 for src, dest in ALIASES
     Cascade::[dest] = Cascade::[src]
index eb2ea2e..8642727 100644 (file)
@@ -1,7 +1,9 @@
 _ = require 'underscore'
 
-OBJ_PROTO = Object.prototype
-getProto = Object.getPrototypeOf
+getProto    = Object.getPrototypeOf
+OBJ_PROTO   = Object.prototype
+hasOwn      = OBJ_PROTO.hasOwnProperty
+objToString = OBJ_PROTO.toString
 
 
 /**
@@ -28,11 +30,28 @@ MISSING = {}
  */
 _obj = do
     
+    # isPlainObject : (o) ->
+    #     !!( o and _.isObject(o) and OBJ_PROTO is getProto(o) )
+    
     /**
      * @returns {Boolean} Whether value is a plain object or not.
      */
-    isPlainObject : (o) ->
-        !! o and _.isObject(o) and OBJ_PROTO is getProto(o)
+    isPlainObject: (obj) ->
+        # Must be an Object.
+        # Because of IE, we also have to check the presence of the constructor property.
+        # Make sure that DOM nodes and window objects don't pass through, as well.
+        if not obj or objToString.call(obj) !== "[object Object]" or obj.nodeType or obj.setInterval
+            return false
+        
+        # Not own constructor property must be Object
+        return false if obj.constructor
+            and not hasOwn.call(obj, "constructor")
+            and not hasOwn.call(obj.constructor.prototype, "isPrototypeOf")
+        
+        # Own properties are enumerated firstly, so to speed up,
+        # if last one is own, then all properties are own.
+        for key in obj then ;
+        return key is void or hasOwn.call obj, key
     
     
     /**
@@ -186,6 +205,49 @@ _obj = do
         _.unset meta.obj, meta.key, opts
         meta.val
     
+    
+    /**
+     * Recursively merges together any number of donor objects into the target object.
+     * Modified from `jQuery.extend()`.
+     * 
+     * @param {Object} target Target object of the merge.
+     * @param {Object} ...donors Donor objects.
+     * @returns {Object} 
+     */
+    merge: (target={}, ...donors) ->
+        # Handle case when target is a string or something (possible in deep copy)
+        target = {} unless typeof target is "object" or _.isFunction(target)
+        
+        for donor of donors
+            # Only deal with non-null/undefined values
+            continue unless donor?
+            
+            # Extend the base object
+            for key, value in donor
+                srcVal = target[key]
+                
+                # Prevent never-ending loop
+                continue if target is value
+                
+                # Recurse if we're merging plain objects or arrays
+                if value and (_.isPlainObject(value) or (valueIsArray = _.isArray(value)))
+                    if valueIsArray
+                        valueIsArray = false
+                        clone = srcVal and if _.isArray(srcVal) then srcVal else []
+                    else
+                        clone = srcVal and if _.isPlainObject(srcVal) then srcVal else {}
+                    
+                    # Never move original objects, clone them
+                    target[key] = _.deepcopy(clone, value)
+                    
+                # Don't bring in undefined values
+                else if value is not void
+                    target[key] = value
+        
+        # Return the modified object
+        target
+    
+