Implements Cascade tombstones.
authordsc <dsc@wikimedia.org>
Fri, 13 Apr 2012 13:35:52 +0000 (06:35 -0700)
committerdsc <dsc@wikimedia.org>
Fri, 13 Apr 2012 13:35:52 +0000 (06:35 -0700)
lib/util/cascade.co
lib/util/underscore/object.co
test/util/cascade-test.co

index 245e2bc..15b7532 100644 (file)
@@ -18,7 +18,7 @@ TOMBSTONE = {}
  */
 class Cascade
     /**
-     * Default tombstone for deleted, non-passthrough keys.
+     * Sentinel tombstone for deleted, non-passthrough keys.
      * @type TOMBSTONE
      * @readonly
      */
@@ -79,12 +79,23 @@ class Cascade
         @_data = @_lookups[0] = data
         this
     
+    getTombstones: ->
+        @_tombstones
+    
+    # setTombstones: (tombstones) ->
+    #     @_tombstones = tombstones
+    #     for k, v in _.collapseObject @_tombstones
+    #         if v
+    #             _.setNested @_tombstones, k, TOMBSTONE, {+ensure}
+    #         else
+    #             _.unsetNested @_tombstones, k
+    #     this
     
     /**
      * @returns {Number} Number of lookup dictionaries.
      */
     size: ->
-        @_lookups.length
+        @_lookups.length - 1
     
     /**
      * @returns {Array<Object>} The array of lookup dictionaries.
@@ -139,10 +150,22 @@ class Cascade
     
     
     /**
+     * @returns {Boolean} Whether there is a tombstone set for `key`.
+     */
+    hasTombstone: (key) ->
+        o = @_tombstones
+        for part of key.split('.')
+            o = o[part]
+            return true if o is TOMBSTONE
+            return false unless o
+        false
+    
+    /**
      * @returns {Boolean} Whether `key` belongs to this object (not inherited
      *  from the cascade).
      */
     isOwnProperty: (key) ->
+        return true if @hasTombstone key
         meta = _.getNestedMeta(@_data, key)
         meta?.obj and hasOwn.call meta.obj, key
     
@@ -151,27 +174,30 @@ class Cascade
      *  from the cascade) and is defined.
      */
     isOwnValue: (key) ->
-        @isOwnProperty(key) and _.getNested(@_data, key, MISSING) is not MISSING
-    
-    /**
-     * @returns {Boolean} Whether the value at `key` is different from that
-     *  inherited by from the cascade.
-     */
-    isModifiedValue: (key, strict=false) ->
-        not @isInheritedValue key, strict
+        not @hasTombstone key
+        and @isOwnProperty key
+        and _.getNested(@_data, key, MISSING) is not MISSING
     
     /**
      * @returns {Boolean} Whether the value at `key` is the same as that
      *  inherited by from the cascade.
      */
     isInheritedValue: (key, strict=false) ->
+        return false if @hasTombstone key
         val = @get key
-        cVal = @_getInCascade key, MISSING, 1
+        cVal = @_getInCascade key, MISSING, 2
         if strict
             val is cVal
         else
             _.isEqual val, cVal
     
+    /**
+     * @returns {Boolean} Whether the value at `key` is different from that
+     *  inherited by from the cascade.
+     */
+    isModifiedValue: (key, strict=false) ->
+        not @isInheritedValue key, strict
+    
     
     
     ### Value Accessors ###
@@ -185,8 +211,12 @@ class Cascade
      *  and `def` otherwise.
      */
     _getInCascade : (key, def, idx=0) ->
-        for data of @_lookups.slice(idx)
-            val = _.getNested data, key, MISSING
+        return def if @hasTombstone key
+        
+        lookups = if idx then @_lookups.slice(idx) else @_lookups
+        for data of lookups
+            val = _.getNested data, key, MISSING, {tombstone:TOMBSTONE}
+            return def if val is TOMBSTONE
             return val unless val is MISSING
         def
     
@@ -228,6 +258,7 @@ class Cascade
         
         # Set and ensure the creation of missing intermediate objects.
         for key, val in values
+            _.unsetNested @_tombstones, key
             _.setNested @_data, key, val, {+ensure}
         
         this
@@ -245,7 +276,8 @@ class Cascade
      * @returns {undefined|*} If found, returns the old value, and otherwise `undefined`.
      */
     unset: (key) ->
-        old = _.unsetNested @_data, key
+        old = @get key
+        _.unsetNested @_data, key
         _.setNested @_tombstones, key, TOMBSTONE, {+ensure}
         old
     
@@ -275,7 +307,10 @@ class Cascade
      * @returns {Object}
      */
     collapse: ->
-        _.merge {}, ...@_lookups.slice().reverse()
+        o = _.merge {}, ...@_lookups.slice(1).reverse()
+        for k in @_tombstones
+            delete o[k]
+        _.merge o, @_data
     
     /**
      * Returns a plain object for JSON serialization via {@link Cascade#collapse()}.
@@ -325,11 +360,14 @@ class Cascade
         "#{Cls.displayName or Cls.name}()"
 
 
+# Alias methods to alternate names
 ALIASES =
-    collapse : 'toObject'
-    each     : 'forEach'
+    setTombstone : 'unset'
+    toObject     : 'collapse'
+    forEach      : 'each'
 
-for src, dest in ALIASES
+for dest, src in ALIASES
     Cascade::[dest] = Cascade::[src]
 
+
 module.exports = exports = Cascade
index 60e8465..4e49c46 100644 (file)
@@ -143,7 +143,7 @@ _obj = do
      * @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`.
+     *  forcing the lookup to fail and return `undefined`. TODO: opts.returnTombstone
      * @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`.
@@ -155,9 +155,14 @@ _obj = do
         
         _.reduce do
             chain
-            (obj, key, idx, chain) ->
-                return void unless obj?
+            (obj, key, idx) ->
+                return unless obj?
                 val = _.get obj, key, void, opts
+                
+                if val is opts.tombstone
+                    return unless ops.ensure
+                    val = void
+                
                 if idx is len
                     return { key, val, obj, opts }
                 if not val? and opts.ensure
index 7b2c30a..afaebe8 100644 (file)
@@ -71,6 +71,7 @@ exports.nestedKeys = ->
     b =
         lol: 'clowns'
         hat: fez:true
+        rat: drat:2, spat:2
         baz: feh:2
         bats: 13
     
@@ -89,9 +90,28 @@ exports.nestedKeys = ->
     assertEqual o.hat?.fez,         'red',      "After nested set  o.hat?.fez"
     assertEqual b.hat?.fez,         true,       "After nested set  b.hat?.fez"
     
+    c.inherit 'hat.fez'
+    assertEqual c.get('hat.fez'),   true,       "After inherit  c.get('hat.fez')"
+    assertEqual o.hat?.fez,         void,       "After inherit  o.hat?.fez"
+    
     c.unset 'hat.fez'
-    assertEqual c.get('hat.fez'),   true,       "After unset  c.get('hat.fez')"
+    assertEqual c.get('hat.fez'),   void,       "After unset  c.get('hat.fez')"
     assertEqual o.hat?.fez,         void,       "After unset  o.hat?.fez"
+    assertEqual c.hasTombstone('hat.fez'), true, "After unset  c.hasTombstone('hat.fez')"
+    assertEqual b.hat?.fez,         true,       "After unset  b.hat?.fez"
+    
+    c.set 'rat.drat', 1
+    assertEqual c.get('rat.drat'),  1,          "Shadow nested  c.get('rat.drat')"
+    assertEqual c.get('rat.spat'),  2,          "Cascade nested  c.get('rat.spat')"
+    c.inherit 'rat'
+    assertEqual c.get('rat.drat'),  2,          "After inherit  c.get('rat.drat')"
+    assertEqual c.get('rat.spat'),  2,          "After inherit  c.get('rat.spat')"
+    c.unset 'rat'
+    assertEqual c.get('rat.drat'),  void,       "After unset  c.get('rat.drat')"
+    assertEqual c.get('rat.spat'),  void,       "After unset  c.get('rat.spat')"
+    assertEqual c.hasTombstone('rat.drat'), true, "After unset  c.hasTombstone('rat.drat')"
+    assertEqual c.hasTombstone('rat.spat'), true, "After unset  c.hasTombstone('rat.spat')"
+    assertEqual c.hasTombstone('rat'),      true, "After unset  c.hasTombstone('rat')"