Adds _.merge() tests.
authordsc <dsc@wikimedia.org>
Fri, 13 Apr 2012 11:34:56 +0000 (04:34 -0700)
committerdsc <dsc@wikimedia.org>
Fri, 13 Apr 2012 11:34:56 +0000 (04:34 -0700)
lib/base/cascading-model.co
lib/util/cascade.co
lib/util/underscore/object.co
test/util/cascade-test.co
test/util/index.co
test/util/underscore/index.co [new file with mode: 0644]
test/util/underscore/underscore-object-test.co [new file with mode: 0644]

index e16f4d3..7582621 100644 (file)
@@ -25,15 +25,22 @@ CascadingModel = exports.CascadingModel = BaseModel.extend do # {{{
     initialize: ->
         BaseModel::initialize ...
     
+    
+    /**
+     * Recursively look up a (potenitally nested) attribute in the lookup chain.
+     * @param {String} key Attribute key (potenitally nested using dot-delimited subkeys).
+     * @returns {*}
+     */
     get: (key) ->
         @cascade.get key
     
+    
     toJSON: (opts={}) ->
         opts = {-collapseCascade, ...opts}
         if opts.collapseCascade
-            @cascade.toJSON()
+            @cascade.collapse()
         else
-            BaseModel::toJSON()
+            BaseModel::toJSON ...
     
 
 
index 9701a24..245e2bc 100644 (file)
@@ -2,17 +2,31 @@ _ = require 'kraken/util/underscore'
 
 hasOwn = ({}).hasOwnProperty
 
-# Sentinel for missing values.
+/**
+ * Sentinel for missing values.
+ */
 MISSING = void
 
+/**
+ * Tombstone for deleted, non-passthrough keys.
+ */
+TOMBSTONE = {}
+
 
 /**
  * @class A mapping of key-value pairs supporting lookup fallback across multiple objects.
  */
 class Cascade
+    /**
+     * Default tombstone for deleted, non-passthrough keys.
+     * @type TOMBSTONE
+     * @readonly
+     */
+    @TOMBSTONE = TOMBSTONE
+    
     
     /**
-     * Map holding the object's KV-pairs. It is also the first element of the
+     * Map holding the object's KV-pairs; always the second element of the
      * cascade lookup.
      * @type Object
      * @private
@@ -20,6 +34,14 @@ class Cascade
     _data : null
     
     /**
+     * Map of tombstones, marking intentionally unset keys in the object's
+     * KV-pairs; always the first element of the cascade lookup.
+     * @type Object<String, TOMBSTONE>
+     * @private
+     */
+    _tombstones : null
+    
+    /**
      * List of objects for lookups.
      * @type Array
      * @private
@@ -31,16 +53,20 @@ class Cascade
     /**
      * @constructor
      */
-    (data={}, lookups=[]) ->
-        @_data    = data
-        @_lookups = [@_data].concat lookups
+    (data={}, lookups=[], tombstones={}) ->
+        @_data       = data
+        @_tombstones = tombstones
+        @_lookups    = [@_data].concat lookups
     
     
     /**
      * @returns {Cascade} A copy of the data and lookup chain.
      */
     clone: ->
-        new Cascade {} import @_data, @_lookups.slice()
+        new Cascade do
+            {} import @_data
+            @_lookups.slice()
+            {} import @_tombstones
     
     
     
@@ -61,6 +87,12 @@ class Cascade
         @_lookups.length
     
     /**
+     * @returns {Array<Object>} The array of lookup dictionaries.
+     */
+    getLookups: ->
+        @_lookups
+    
+    /**
      * Adds a new lookup dictionary to the chain.
      * @returns {this}
      */
@@ -202,11 +234,31 @@ class Cascade
     
     
     /**
-     * Delete the given key from this object's concrete data. If missing,
-     * does not cascade.
+     * Delete the given key from this object's data dictionary and set a tombstone
+     * which ensures that future lookups do not cascade and thus see the key as
+     * `undefined`.
+     * 
+     * If the key is missing from the data dictionary the delete does not cascade,
+     * but the tombstone is still set.
+     * 
+     * @param {String} key Key to unset.
      * @returns {undefined|*} If found, returns the old value, and otherwise `undefined`.
      */
     unset: (key) ->
+        old = _.unsetNested @_data, key
+        _.setNested @_tombstones, key, TOMBSTONE, {+ensure}
+        old
+    
+    
+    /**
+     * Unsets the key in the data dictionary, but ensures future lookups also
+     * see the key as `undefined`, as opposed.
+     * 
+     * @param {String} key Key to unset.
+     * @returns {this} 
+     */
+    inherit: (key) ->
+        _.unsetNested @_tombstones, key
         _.unsetNested @_data, key
     
     
index 8642727..60e8465 100644 (file)
@@ -2,27 +2,30 @@ _ = require 'underscore'
 
 getProto    = Object.getPrototypeOf
 OBJ_PROTO   = Object.prototype
-hasOwn      = OBJ_PROTO.hasOwnProperty
-objToString = OBJ_PROTO.toString
-
+{
+    hasOwnProperty : hasOwn
+    toString       : objToString
+} = {}
 
 /**
  * Default options for delegate-accessor functions.
  */
-DEFAULT_DELEGATE_OPTIONS =
+DEFAULT_DELEGATE_OPTIONS = exports.DEFAULT_DELEGATE_OPTIONS =
     getter  : 'get'
     setter  : 'set'
     deleter : 'unset'
 
 /**
- * Default options for nested-accessor functions.
+ * Tombstone for deleted, non-passthrough keys.
  */
-DEFAULT_NESTED_OPTIONS = {-ensure} import DEFAULT_DELEGATE_OPTIONS
+TOMBSTONE = exports.TOMBSTONE = {}
 
 /**
- * Sentinel for missing values.
+ * Default options for nested-accessor functions.
  */
-MISSING = {}
+DEFAULT_NESTED_OPTIONS = exports.DEFAULT_NESTED_OPTIONS =
+    {-ensure, tombstone:TOMBSTONE} import DEFAULT_DELEGATE_OPTIONS
+
 
 
 /**
@@ -30,6 +33,7 @@ MISSING = {}
  */
 _obj = do
     
+    
     # isPlainObject : (o) ->
     #     !!( o and _.isObject(o) and OBJ_PROTO is getProto(o) )
     
@@ -96,7 +100,7 @@ _obj = do
     
     set: (obj, key, value, opts) ->
         return unless obj?
-        if _.isObject(key) and key?
+        if key? and _.isObject(key)
             [values, opts] = [key, value]
         else
             values = { "#key": value }
@@ -138,6 +142,8 @@ _obj = do
      * @param {String} [opts.getter="get"] Name of the sub-object getter method use if it exists.
      * @param {String} [opts.setter="set"] Name of the sub-object setter method use if it exists.
      * @param {String} [opts.deleter="unset"] Name of the sub-object deleter method use if it exists.
+     * @param {Object} [opts.tombstone=TOMBSTONE] Sentinel value to be interpreted as no-passthrough,
+     *  forcing the lookup to fail and return `undefined`.
      * @returns {undefined|Object} If found, the object is of the form 
      *  `{ key: Qualified key name, obj: Parent object of key, val: Value at obj[key], opts: Options }`. 
      *  Otherwise `undefined`.
@@ -169,7 +175,7 @@ _obj = do
      * @returns {null|Object} If found, returns the value, and otherwise `default`.
      */
     getNested : (obj, chain, def, opts) ->
-        {opts} = meta = _.getNestedMeta obj, chain, opts
+        meta = _.getNestedMeta obj, chain, opts
         return def if meta?.val is void
         meta.val
     
@@ -183,10 +189,10 @@ _obj = do
      * @returns {undefined|Any} If found, returns the old value, and otherwise `undefined`.
      */
     setNested : (obj, chain, value, opts) ->
-        {opts} = meta = _.getNestedMeta obj, chain, opts
-        return unless meta
-        _.set meta.obj, meta.key, value, opts
-        meta.val
+        return unless meta = _.getNestedMeta obj, chain, opts
+        {obj, key, val, opts} = meta
+        _.set obj, key, value, opts
+        val
     
     /**
      * Searches a heirarchical object for a potentially-nested key and removes it.
@@ -200,10 +206,10 @@ _obj = do
      * @returns {undefined|Any} The old value if found; otherwise `undefined`.
      */
     unsetNested : (obj, chain, opts) ->
-        {opts} = meta = _.getNestedMeta obj, chain, opts
-        return unless meta
-        _.unset meta.obj, meta.key, opts
-        meta.val
+        return unless meta = _.getNestedMeta obj, chain, opts
+        {obj, key, val, opts} = meta
+        _.unset obj, key, opts
+        val
     
     
     /**
@@ -216,33 +222,33 @@ _obj = do
      */
     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)
+        unless typeof target is "object" or _.isFunction(target)
+            target = if _.isArray donors[0] then [] else {}
         
         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]
+            _.each donor, (value, key) ->
+                current = target[key]
                 
                 # Prevent never-ending loop
-                continue if target is value
+                return 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 []
+                        current = [] unless _.isArray current
                     else
-                        clone = srcVal and if _.isPlainObject(srcVal) then srcVal else {}
+                        current = {} unless current and typeof current is 'object'
                     
                     # Never move original objects, clone them
-                    target[key] = _.deepcopy(clone, value)
+                    _.set target, key, _.merge(current, value)
                     
                 # Don't bring in undefined values
                 else if value is not void
-                    target[key] = value
+                    _.set target, key, value
         
         # Return the modified object
         target
index 253d3c2..7b2c30a 100644 (file)
@@ -3,9 +3,13 @@ assert  = require 'assert'
 _       = require 'kraken/util/underscore'
 Cascade = require 'kraken/util/cascade'
 
+assertArraysEqual = (actual, expected, name) ->
+    assert.deepEqual actual, expected
+    assert.ok _.isArray(actual), "_.isArray #name = #actual"
+    assert.equal actual?.length, expected.length, "#name.length"
 
-assertEqual = (got, expected, msg) ->
-    assert.equal got, expected, "#msg:\t Expected: #expected;\tGot: #got"
+assertEqual = (actual, expected, msg) ->
+    assert.equal actual, expected, "#msg:\t Expected: #expected;\tGot: #actual"
 
 exports.basicCascade = ->
     a = { a:1 }
index 1cbd9af..c6aecf5 100644 (file)
@@ -1,2 +1,3 @@
 cascade = require './cascade-test'
-exports import cascade
+underscore = require './underscore'
+exports import cascade import underscore
diff --git a/test/util/underscore/index.co b/test/util/underscore/index.co
new file mode 100644 (file)
index 0000000..32a70e8
--- /dev/null
@@ -0,0 +1,2 @@
+_object = require './underscore-object-test'
+exports import _object
diff --git a/test/util/underscore/underscore-object-test.co b/test/util/underscore/underscore-object-test.co
new file mode 100644 (file)
index 0000000..f2c635a
--- /dev/null
@@ -0,0 +1,82 @@
+_       = require 'underscore'
+assert  = require 'assert'
+
+_.mixin require 'kraken/util/underscore/object'
+
+
+assertArraysEqual = (actual, expected, name) ->
+    assert.deepEqual actual, expected
+    assert.ok _.isArray(actual), "_.isArray #name = #actual"
+    assert.equal actual?.length, expected.length, "#name.length"
+
+
+exports.testUnderscoreMerge = ->
+    a =
+        lol: 'cats'
+        hat: false
+        foo: bar:1
+        gatz: all:'your', base:'are', belong:false
+        unf: {o:'rly'}
+        nul: false
+    b =
+        lol: 'clowns'
+        hat: fez:true
+        baz: feh:2
+        gatz: belong:'to', us:true
+        unf: void
+        nul: null
+    
+    target = {}
+    res = _.merge target, a, b
+    
+    assert.strictEqual res, target
+    assert.equal res.lol, 'clowns'
+    assert.deepEqual res.foo, {bar:1}
+    assert.deepEqual res.baz, {feh:2}
+    assert.deepEqual res.hat, {fez:true}
+    assert.deepEqual res.gatz, {all:'your', base:'are', belong:'to', us:true}
+    assert.deepEqual res.unf, {o:'rly'}
+    assert.strictEqual res.nul, null
+    
+
+exports.testUnderscoreMergeArrays = ->
+    a =
+        yarr: true
+        
+        ray: [1, 2, 3]
+        gun: {lazer:1}
+        pew: ['a', 'b']
+        
+        empty: ['lies']
+        full: []
+        nul: null
+    
+    b =
+        arr: [1, 2]
+        yarr: [3, 4]
+        
+        ray: [4, void, 5, 6]
+        gun: ['pew']
+        pew: {3:'c', key:'val'}
+        
+        empty: []
+        full: ['truth']
+        nul: ['unf']
+    
+    target = {}
+    res = _.merge target, a, b
+    
+    assertArraysEqual res.arr, [1, 2], 'res.arr'
+    assertArraysEqual res.yarr, [3, 4], 'res.yarr'
+    assertArraysEqual res.ray, [4, 2, 5, 6], 'res.ray'
+    assertArraysEqual res.gun, ['pew'], 'res.gun'
+    
+    exp = ['a', 'b']
+    exp import { 3:'c', key:'val' }
+    assertArraysEqual res.pew, exp, 'res.pew'
+    assert.equal res.pew.key, 'val'
+    
+    assertArraysEqual res.empty, ['lies'], 'res.empty'
+    assertArraysEqual res.full, ['truth'], 'res.full'
+    assertArraysEqual res.nul, ['unf'], 'res.nul'
+