--- /dev/null
+{ _, 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
+
+# }}}
+
+
+
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
_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
/**
* @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
this
/**
+ * @returns {Array<Object>} The array of lookup dictionaries.
+ */
+ getLookups: ->
+ @_lookups
+
+ /**
* Adds a new lookup dictionary to the chain.
* @returns {this}
*/
/**
- * 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
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
+
/**
*/
_obj = do
+
# isPlainObject : (o) ->
# !!( o and _.isObject(o) and OBJ_PROTO is getProto(o) )
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 }
* @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`.
* @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
* @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.
* @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
/**
*/
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
_ = 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 }
cascade = require './cascade-test'
-exports import cascade
+underscore = require './underscore'
+exports import cascade import underscore
--- /dev/null
+_object = require './underscore-object-test'
+exports import _object
--- /dev/null
+_ = 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'
+