Adds Cascade class, tests.
authordsc <dsc@wikimedia.org>
Mon, 26 Mar 2012 22:50:43 +0000 (15:50 -0700)
committerdsc <dsc@wikimedia.org>
Mon, 26 Mar 2012 22:50:43 +0000 (15:50 -0700)
Cokefile
lib/util/cascade.co
lib/util/index.co
test/index.co [new file with mode: 0644]
test/util/cascade-test.co [new file with mode: 0644]
test/util/index.co [new file with mode: 0644]
www/modules.yaml

index 1e04534..3064b0e 100644 (file)
--- a/Cokefile
+++ b/Cokefile
@@ -5,6 +5,9 @@ require 'buildtools/tasks'
 MODULE_LINK      = 'node_modules/kraken'
 EXPRESS_DEP_MIME = 'node_modules/express/node_modules/mime'
 
+
+
+
 task \link 'Link package source to node_modules so the name resolves correctly' ->
     # Browser-based require doens't support relative requires, but things
     # like `require 'kraken/utils'` rarely work in node without this hack.
@@ -23,36 +26,65 @@ task \install 'Install project dependencies.' ->
 task \setup 'Ensure project is set up for development.' ->
     invoke \install
     invoke \link
-    invoke \update_version
+
 
 
 task \server 'Start dev server' ->
     invoke \setup
+    invoke \update_version
     say ''
     run 'lib/server/server.co'
 
+
 task \build 'Build coco sources' ->
     invoke \setup
     coco <[ -bjc package.co ]>
 
+
 task \test 'Rebuild test files and run tests' ->
     invoke \setup
-    tests = glob.globSync 'test/*.co'
-    tests.forEach (file) ->
-        js = file.replace '.co', '.js'
-        fs.unlinkSync js if exists js
-        coco [ '-bc', file ]
-    sh 'expresso'
+    invoke \cleanup_tests
+    
+    # Compile tests to JS so Expresso finds them
+    say "Compiling tests...".white.bold
+    glob 'test/**/*.co', {+sync} .forEach -> coco [ '-bc', it ]
+    say 'ok.\n'
+    
+    say "Running tests...".white.bold
+    err <- sh 'expresso', {-die}
+    say "#{err and 'yep' or 'ok'}.\n"
+    
+    # Clean up JS turds
+    invoke \cleanup_tests
+
+
+task \cleanup_tests 'Removes compiled tests' ->
+    # XXX: erp. only works if no .js files by default :P
+    # say "Cleaning up old test files...".white.bold
+    # js_files = glob 'test/**/*.js', {+sync}
+    # for js of js_files.filter( -> exists it )
+    #     say "unlink #js"
+    #     fs.unlinkSync js
+    # say 'ok.\n'
+    
+    glob 'test/**/*.co', {+sync}
+        .map -> it.replace('.co', '.js')
+        .filter exists
+        .forEach fs.unlinkSync
 
 task \clean 'Clean up environment and artifacts' ->
-    remove [MODULE_LINK, EXPRESS_DEP_MIME, 'var', 'tmp/dest'], true
+    invoke \cleanup_tests
+    remove [MODULE_LINK, EXPRESS_DEP_MIME, 'var', 'tmp/dist'], true
+
+
 
 task \source_list 'Print a list of the source file paths.' ->
-    invoke \link
+    invoke \setup
     {sources} = require 'kraken/server/view-helpers'
     say sources("www/modules.yaml", 'dev').join '\n'
 
 # task \dist 'Assemble a distribution package for deploy' ->
+#     invoke \cleanup_tests
 #     ...
 
 
index 6b3ade0..3fa01a2 100644 (file)
@@ -27,25 +27,31 @@ class Cascade
      * @constructor
      */
     (data={}, lookups=[]) ->
-        @_data = {} import data
-        @_lookups = [@_data].concat lookups.slice()
+        @_data    = data
+        @_lookups = [@_data].concat lookups
     
     
     /**
-     * @returns {Cascade} A copy of the lookup chain.
+     * @returns {Cascade} A copy of the data and lookup chain.
      */
     clone: ->
-        new Cascade @_data, @_lookups
+        new Cascade {} import @_data, @_lookups.slice()
     
     
     
-    ### Lookups ###
+    ### Data & Lookups ###
+    
+    setData: (data) ->
+        @_data = @_lookups[0] = data
+        this
     
     /**
      * Adds a new lookup dictionary to the chain.
      * @returns {this}
      */
     addLookup: (dict) ->
+        return this unless dict?
+        throw new Error "Lookup dictionary must be an object! dict=#dict" unless dict
         @_lookups.push dict
         this
     
@@ -54,9 +60,20 @@ class Cascade
      * @returns {this}
      */
     removeLookup: (dict) ->
-        _.remove @_lookups, dict
+        _.remove @_lookups, dict if dict
         this
     
+    /**
+     * @returns {Boolean} Whether the value at `key` belongs to this object or exists in the cascade.
+     */
+    isOwnValue: (key) ->
+        @_data[key] is not void
+    
+    /**
+     * @returns {Number} Number of lookup dictionaries.
+     */
+    size: ->
+        @_lookups.length
     
     
     
@@ -66,8 +83,7 @@ class Cascade
      * @returns {Boolean} Whether there is a value at the given key.
      */
     has : (key) ->
-        (@get key, undefined) is not undefined
-    
+        (@get key, void) is not void
     
     /**
      * @returns {*} First value for the given key found in the lookup chain,
@@ -75,18 +91,41 @@ class Cascade
      */
     get : (key, def) ->
         for data of @_lookups
-            val = _.getNested data, key, undefined
-            return val if val is not undefined
+            if typeof data.get is 'function'
+                val = data.get key, void
+            else
+                val = _.getNested data, key, void
+            return val if val is not void
         def
     
     /**
+     * Sets a key to a value, accepting nested keys and creating intermediary objects as necessary.
+     * @public
+     * @name set
      * @param {String} key Key to set.
      * @param {*} val Non-`undefined` value to set.
      * @returns {this}
      */
-    set : (key, val) ->
-        throw new Error("Value and key cannot be undefined!") unless key and val is not undefined
-        _.setNested @_data, key, val
+    /**
+     * @public
+     * @name set
+     * @param {Object} values Map of KV pairs to set. No value may be `undefined`.
+     * @returns {this}
+     */
+    set : (values) ->
+        # Handle @set(k, val)
+        if arguments.length > 1 and typeof values is 'string'
+            [key, val] = arguments
+            throw new Error("Value and key cannot be undefined!") if not key or val is void
+            values = { "#key": val }
+        
+        # Trailing `true` in call to `set()` and `setNested()` is to ensure the
+        # creation of missing intermediate objects.
+        if typeof @_data.set is 'function'
+            for key, val in values then @_data.set key, val, true
+        else
+            for key, val in values then _.setNested @_data, key, val, true
+        
         this
     
     
@@ -95,8 +134,8 @@ class Cascade
      * does not cascade.
      * @returns {undefined|*} If found, returns the old value, and otherwise `undefined`.
      */
-    del: (key) ->
-        _.unsetNested @_data, key, false
+    unset: (key) ->
+        _.unsetNested @_data, key
     
     
     
@@ -104,7 +143,7 @@ class Cascade
     
     extend : (...args) ->
         for o of args
-            for k,v in o then @set k, v
+            for k, v in o then @set k, v
         this
     
     toObject: ->
index 285a8f2..2cc39e1 100644 (file)
@@ -16,17 +16,5 @@ backbone  = require 'kraken/util/backbone'
 parser    = require 'kraken/util/parser'
 
 
-## Debug
-_.dump = (o, label='dump') ->
-    if not _.isArray(o) and _.isObject(o)
-        console.group label
-        for k, v in o
-            console.log "#k:", v
-        console.groupEnd()
-    else
-        console.log label, o
-    o
-
-
 exports import { root, _, op, backbone, parser, }
 # exports import { root, _, op, HashSet, BitString, crc32, parser, }
diff --git a/test/index.co b/test/index.co
new file mode 100644 (file)
index 0000000..33541de
--- /dev/null
@@ -0,0 +1,4 @@
+coco = require 'coco'
+util = require './util'
+
+exports import util
diff --git a/test/util/cascade-test.co b/test/util/cascade-test.co
new file mode 100644 (file)
index 0000000..4524bf2
--- /dev/null
@@ -0,0 +1,94 @@
+assert  = require 'assert'
+
+_       = require 'kraken/underscore'
+Cascade = require 'kraken/util/cascade'
+
+
+assertEqual = (got, expected, msg) ->
+    assert.equal got, expected, "#msg:\t Expected: #expected;\tGot: #got"
+
+exports.basicCascade = ->
+    a = { a:1 }
+    b = { b:2 }
+    c = { c:3 }
+    
+    c1 = new Cascade
+    assertEqual c1.get('a'), void,  "[c1] Primary data lookup before set"
+    c1.set 'a', 1
+    assertEqual c1.get('a'), 1,     "[c1] Primary data lookup (after set)"
+    assertEqual c1.get('b'), void,  "[c1] Cascade lookup (depth=1, before set)"
+    c1.addLookup b
+    assertEqual c1.get('a'), 1,     "[c1] Primary data lookup (after lookup added)"
+    assertEqual c1.get('b'), 2,     "[c1] Cascade lookup (depth=1, first)"
+    assertEqual c1.get('c'), void,  "[c1] Cascade lookup (depth=2, unset)"
+    c1.addLookup c
+    assert.ok c1.has('a'),          "[c1] Cascade has 'a' key (depth=0)"
+    assert.ok c1.has('b'),          "[c1] Cascade has 'b' key (depth=1)"
+    assert.ok c1.has('c'),          "[c1] Cascade has 'c' key (depth=2)"
+    assertEqual c1.get('a'), 1,     "[c1] Primary data lookup (after lookup #2 added)"
+    assertEqual c1.get('b'), 2,     "[c1] Cascade lookup (depth=1, second)"
+    assertEqual c1.get('c'), 3,     "[c1] Cascade lookup (depth=2, first)"
+    c1.removeLookup b
+    assertEqual c1.get('a'), 1,     "[c1] Primary data lookup (after lookup b removed)"
+    assertEqual c1.get('b'), void,  "[c1] Cascade lookup (depth=1, third)"
+    assertEqual c1.get('c'), 3,     "[c1] Cascade lookup (depth=2, removed)"
+    c1.removeLookup c
+    assertEqual c1.get('a'), 1,     "[c1] Primary data lookup (after lookup c removed)"
+    assertEqual c1.get('b'), void,  "[c1] Cascade lookup (depth=1, removed)"
+    c1.unset 'a'
+    assertEqual c1.get('a'), void,  "[c1] Primary data lookup (after removed)"
+    
+    c2 = new Cascade a, [b, c]
+    assert.notStrictEqual c1, c2,   "[c2] Different Cascades should differ"
+    
+    assert.ok c2.has('a'),          "[c2] Cascade has 'a' key (depth=0)"
+    assert.ok c2.has('b'),          "[c2] Cascade has 'b' key (depth=1)"
+    assert.ok c2.has('c'),          "[c2] Cascade has 'c' key (depth=2)"
+    assertEqual c2.get('a'), 1,     "[c2] Primary data lookup"
+    assertEqual c2.get('b'), 2,     "[c2] Cascade lookup (depth=1, first)"
+    assertEqual c2.get('c'), 3,     "[c2] Cascade lookup (depth=2, first)"
+    c2.removeLookup b
+    assertEqual c2.get('a'), 1,     "[c2] Primary data lookup (after lookup b removed)"
+    assertEqual c2.get('b'), void,  "[c2] Cascade lookup (depth=1, second)"
+    assertEqual c2.get('c'), 3,     "[c2] Cascade lookup (depth=2, removed)"
+    c2.removeLookup c
+    assertEqual c2.get('a'), 1,     "[c2] Primary data lookup (after lookup c removed)"
+    assertEqual c2.get('b'), void,  "[c2] Cascade lookup (depth=1, removed)"
+    c2.unset 'a'
+    assertEqual c2.get('a'), void,  "[c2] Primary data lookup (after removed)"
+
+
+exports.nestedKeys = ->
+    o = {}
+    a =
+        lol: 'cats'
+        hat: false
+        foo: bar:1
+    b =
+        lol: 'clowns'
+        hat: fez:true
+        baz: feh:2
+        bats: 13
+    
+    c = new Cascade o, [a, b]
+    
+    assertEqual c.get('lol'),       'cats',     "Shadow  c.get('lol')"
+    assertEqual c.get('bats'),      13,         "Simple cascade  c.get('bats')"
+    assertEqual c.get('foo.bar'),   1,          "Nested  c.get('foo.bar')"
+    assertEqual c.get('baz.feh'),   2,          "Nested cascade  c.get('bar.feh')"
+    
+    assertEqual c.get('hat'),       false,      "Shadow non-cascade  c.get('hat')"
+    assertEqual c.get('hat.fez'),   true,       "Unshadow due to cascade  c.get('hat.fez')"
+    
+    c.set 'hat.fez', 'red'
+    assertEqual c.get('hat.fez'),   'red',      "After nested set  c.get('hat.fez')"
+    assertEqual o.hat?.fez,         'red',      "After nested set  o.hat?.fez"
+    assertEqual b.hat?.fez,         true,       "After nested set  b.hat?.fez"
+    
+    c.unset 'hat.fez'
+    assertEqual c.get('hat.fez'),   true,       "After unset  c.get('hat.fez')"
+    assertEqual o.hat?.fez,         void,       "After unset  o.hat?.fez"
+
+
+
+
diff --git a/test/util/index.co b/test/util/index.co
new file mode 100644 (file)
index 0000000..1cbd9af
--- /dev/null
@@ -0,0 +1,2 @@
+cascade = require './cascade-test'
+exports import cascade
index 3ade6ac..f3c8187 100644 (file)
@@ -13,23 +13,33 @@ dev:
         - es5-shim.min
         - modernizr.min
         - json2.min
-        - jquery.min
+        
+        - jquery
         - jquery.history.min
         - jquery.hotkeys.min
         - jquery.isotope.min
-        # - jquery.tipsy.min # handled by bootstrap now?
-        - spin.min
-        - jquery.spin.min
+        
+        # handled by bootstrap now?
+        # - jquery.tipsy.min
+        
+        # - spin.min
+        # - jquery.spin.min
         - bootstrap.min
+        
+        # Browserify must come before any .mod files
         - browserify
-        # - require
-        - underscore.mod.min
-        - underscore.string.mod.min
-        - backbone.mod.min
-        - backbone.nested.mod.min
+        
+        - underscore.mod
+        - underscore.string.mod
+        
+        - backbone.mod
+        - backbone.nested.mod
+        - synapse.mod
+        
         - showdown.mod.min
         - jade.runtime.min
-        - dygraph.min
+        
+        - dygraph
 
 -   suffix: .mod.js
     paths: