From 185c5f226c13c0c4b13ae380aa6b6b1be24f507d Mon Sep 17 00:00:00 2001 From: dsc Date: Fri, 13 Apr 2012 04:34:56 -0700 Subject: [PATCH] Adds _.merge() tests. Conflicts: lib/base/cascading-model.co --- lib/base/cascading-model.co | 57 ++++++++++++++++ lib/util/cascade.co | 68 +++++++++++++++++-- lib/util/underscore/object.co | 60 ++++++++++-------- test/util/cascade-test.co | 8 ++- test/util/index.co | 3 +- test/util/underscore/index.co | 2 + test/util/underscore/underscore-object-test.co | 82 ++++++++++++++++++++++++ 7 files changed, 242 insertions(+), 38 deletions(-) create mode 100644 lib/base/cascading-model.co create mode 100644 test/util/underscore/index.co create mode 100644 test/util/underscore/underscore-object-test.co diff --git a/lib/base/cascading-model.co b/lib/base/cascading-model.co new file mode 100644 index 0000000..7582621 --- /dev/null +++ b/lib/base/cascading-model.co @@ -0,0 +1,57 @@ +{ _, op, +} = require 'kraken/util' +{ BaseModel, BaseList, +} = require 'kraken/base/base-model' + +Cascade = require 'kraken/util/cascade' + + + +/** + * @class A model that implements cascading lookups for its attributes. + */ +CascadingModel = exports.CascadingModel = BaseModel.extend do # {{{ + /** + * The lookup cascade. + * @type Cascade + */ + cascade : null + + + constructor: function CascadingModel (attributes={}, opts) + @cascade = new Cascade attributes + BaseModel.call this, attributes, opts + + 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.collapse() + else + BaseModel::toJSON ... + + + +# Proxy Cascade methods +<[ + addLookup removeLookup popLookup shiftLookup unshiftLookup + isOwnProperty isOwnValue isInheritedValue isModifiedValue +]>.forEach (methodname) -> + CascadingModel::[methodname] = -> @cascade[methodname].apply @cascade, arguments + +# }}} + + + diff --git a/lib/util/cascade.co b/lib/util/cascade.co index c551440..cbeedb8 100644 --- a/lib/util/cascade.co +++ b/lib/util/cascade.co @@ -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 + * @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 @@ -54,6 +80,12 @@ class Cascade this /** + * @returns {Array} The array of lookup dictionaries. + */ + getLookups: -> + @_lookups + + /** * Adds a new lookup dictionary to the chain. * @returns {this} */ @@ -173,11 +205,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 diff --git a/lib/util/underscore/object.co b/lib/util/underscore/object.co index 8642727..60e8465 100644 --- a/lib/util/underscore/object.co +++ b/lib/util/underscore/object.co @@ -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 diff --git a/test/util/cascade-test.co b/test/util/cascade-test.co index 253d3c2..7b2c30a 100644 --- a/test/util/cascade-test.co +++ b/test/util/cascade-test.co @@ -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 } diff --git a/test/util/index.co b/test/util/index.co index 1cbd9af..c6aecf5 100644 --- a/test/util/index.co +++ b/test/util/index.co @@ -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 index 0000000..32a70e8 --- /dev/null +++ b/test/util/underscore/index.co @@ -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 index 0000000..f2c635a --- /dev/null +++ b/test/util/underscore/underscore-object-test.co @@ -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' + -- 1.7.0.4