From 55f0af3d14297aca693e03a51d72e651971063f9 Mon Sep 17 00:00:00 2001 From: Jake Dempsey Date: Wed, 28 Sep 2011 13:49:10 -0500 Subject: [PATCH] added additional tests to prove the index stays in sync with the storage. Also made fixes to the memory adapter because it would not pass the existing tests. The dom adapter was missing the exists api method and its remove implementation was not adhering to the api of passing a key or array to remove. --- lawnchair.tmproj | 234 +++++++++++++++++++++++++++++++++++++++++ makefile | 2 +- src/adapters/dom.js | 220 ++++++++++++++++++++------------------ src/adapters/memory.js | 16 +-- test/lawnchair-spec.js | 139 +++++++++++++++++------- 5 files changed, 465 insertions(+), 146 deletions(-) create mode 100644 lawnchair.tmproj diff --git a/lawnchair.tmproj b/lawnchair.tmproj new file mode 100644 index 00000000..8c8c208d --- /dev/null +++ b/lawnchair.tmproj @@ -0,0 +1,234 @@ + + + + + currentDocument + src/adapters/dom.js + documents + + + expanded + + name + lawnchair + regexFolderFilter + !.*/(\.[^/]*|CVS|_darcs|_MTN|\{arch\}|blib|.*~\.nib|.*\.(framework|app|pbproj|pbxproj|xcode(proj)?|bundle))$ + sourceDirectory + + + + fileHierarchyDrawerWidth + 368 + metaData + + makefile + + caret + + column + 56 + line + 20 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + src/adapters/dom.js + + caret + + column + 30 + line + 76 + + firstVisibleColumn + 0 + firstVisibleLine + 49 + + src/adapters/memory.js + + caret + + column + 25 + line + 2 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 0 + selectFrom + + column + 22 + line + 2 + + selectTo + + column + 27 + line + 2 + + + test/index.html + + caret + + column + 0 + line + 0 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + test/lawnchair-spec.js + + caret + + column + 66 + line + 251 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 230 + selectFrom + + column + 0 + line + 251 + + selectTo + + column + 66 + line + 251 + + + test/lib/json2.js + + caret + + column + 32 + line + 1 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + test/lib/lawnchair.js + + caret + + column + 0 + line + 0 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + + openDocuments + + test/lawnchair-spec.js + makefile + test/lib/json2.js + test/lib/lawnchair.js + src/adapters/memory.js + src/adapters/dom.js + test/index.html + + showFileHierarchyDrawer + + showFileHierarchyPanel + + treeState + + lawnchair + + isExpanded + + subItems + + lib + + isExpanded + + subItems + + + src + + isExpanded + + subItems + + adapters + + isExpanded + + subItems + + + + + test + + isExpanded + + subItems + + lib + + isExpanded + + subItems + + + plugin + + isExpanded + + subItems + + + + + util + + isExpanded + + subItems + + + + + + windowFrame + {{0, 54}, {1440, 824}} + + diff --git a/makefile b/makefile index cff1a2b8..76e3e8fb 100644 --- a/makefile +++ b/makefile @@ -1,4 +1,4 @@ -VERSION = "0.6.1" +VERSION = "0.6.3" PRIMARY_ADAPTER = "dom" SECONDARY_ADAPTER = "window-name" diff --git a/src/adapters/dom.js b/src/adapters/dom.js index c7ddedee..94b3f58d 100644 --- a/src/adapters/dom.js +++ b/src/adapters/dom.js @@ -3,14 +3,15 @@ * === * - originally authored by Joseph Pecoraro * - */ + */ // // TODO does it make sense to be chainable all over the place? -// chainable: nuke, remove, all, get, save, all +// chainable: nuke, remove, all, get, save, all // not chainable: valid, keys // Lawnchair.adapter('dom', (function() { - var storage = window.localStorage + var storage = window.localStorage; + // the indexer is an encapsulation of the helpers needed to keep an ordered index of the keys var indexer = function(name) { return { @@ -18,137 +19,152 @@ Lawnchair.adapter('dom', (function() { key: name + '._index_', // returns the index all: function() { - var a = JSON.parse(storage.getItem(this.key)) - if (a === null) storage.setItem(this.key, JSON.stringify([])) // lazy init - return JSON.parse(storage.getItem(this.key)) + var a = JSON.parse(storage.getItem(this.key)); + if (a === null) storage.setItem(this.key, JSON.stringify([])); + // lazy init + return JSON.parse(storage.getItem(this.key)); }, // adds a key to the index - add: function (key) { - var a = this.all() - a.push(key) - storage.setItem(this.key, JSON.stringify(a)) + add: function(key) { + var a = this.all(); + a.push(key); + this.reIndex(a); }, // deletes a key from the index - del: function (key) { - var a = this.all(), r = [] - // FIXME this is crazy inefficient but I'm in a strata meeting and half concentrating - for (var i = 0, l = a.length; i < l; i++) { - if (a[i] != key) r.push(a[i]) - } - storage.setItem(this.key, JSON.stringify(r)) + del: function(key) { + var idx = this.find(key); + if (idx < 0) return + + var a = this.all(); + a.splice(idx, 1); + this.reIndex(a); }, // returns index for a key - find: function (key) { - var a = this.all() - for (var i = 0, l = a.length; i < l; i++) { - if (key === a[i]) return i - } - return false + find: function(key) { + return this.all().indexOf(key); + }, + // reIndex based on new keys + reIndex: function(a) { + storage.setItem(this.key, JSON.stringify(a)); } - } - } - - // adapter api + }; + }; + + var keyWithPrefix = function(key) { + return this.name + '.' + key; + }; + + // adapter api return { - - // ensure we are in an env with localStorage - valid: function () { - return !!storage + + // ensure we are in an env with localStorage + valid: function() { + return !! storage; }, - init: function (options, callback) { - this.indexer = indexer(this.name) - if (callback) this.fn(this.name, callback).call(this, this) + init: function(options, callback) { + this.indexer = indexer(this.name); + this.keyWithPrefix = keyWithPrefix; + if (callback) this.fn(this.name, callback).call(this, this); }, - - save: function (obj, callback) { - var key = obj.key ? this.name + '.' + obj.key : this.name + '.' + this.uuid() + + save: function(obj, callback) { + var origKey = obj.key ? obj.key: this.uuid(); + var key = this.keyWithPrefix(origKey); // if the key is not in the index push it on - if (this.indexer.find(key) === false) this.indexer.add(key) - // now we kil the key and use it in the store colleciton + if (this.indexer.find(key) < 0) this.indexer.add(key); + // now we kill the key and use it in the store colleciton delete obj.key; - storage.setItem(key, JSON.stringify(obj)) - obj.key = key.slice(this.name.length + 1) + storage.setItem(key, JSON.stringify(obj)); + obj.key = origKey; if (callback) { - this.lambda(callback).call(this, obj) + this.lambda(callback).call(this, obj); } - return this + return this; }, - batch: function (ary, callback) { - var saved = [] + batch: function(ary, callback) { + var saved = []; // not particularily efficient but this is more for sqlite situations for (var i = 0, l = ary.length; i < l; i++) { - this.save(ary[i], function(r){ - saved.push(r) - }) + this.save(ary[i], + function(r) { + saved.push(r); + }); } - if (callback) this.lambda(callback).call(this, saved) - return this + if (callback) this.lambda(callback).call(this, saved); + return this; }, - + // accepts [options], callback keys: function(callback) { - if (callback) { - var name = this.name - , keys = this.indexer.all().map(function(r){ return r.replace(name + '.', '') }) - this.fn('keys', callback).call(this, keys) + if (callback) { + var name = this.name; + var keys = this.indexer.all().map(function(r) { + return r.replace(name + '.', '') + }); + this.fn('keys', callback).call(this, keys); } - return this // TODO options for limit/offset, return promise + return this; + // TODO options for limit/offset, return promise }, - - get: function (key, callback) { - if (this.isArray(key)) { - var r = [] - for (var i = 0, l = key.length; i < l; i++) { - var k = this.name + '.' + key[i] - , obj = JSON.parse(storage.getItem(k)) - if (obj) { - obj.key = key[i] - r.push(obj) - } + + get: function(keyOrArray, callback) { + var keys = this.isArray(keyOrArray) ? keyOrArray: [keyOrArray]; + var r = [] + for (var i = 0, l = keys.length; i < l; i++) { + var k = this.keyWithPrefix(keys[i]); + var obj = JSON.parse(storage.getItem(k)); + if (obj){ + obj.key = keys[i]; + r.push(obj); } - if (callback) this.lambda(callback).call(this, r) - } else { - var k = this.name + '.' + key - , obj = JSON.parse(storage.getItem(k)) - if (obj) obj.key = key - if (callback) this.lambda(callback).call(this, obj) } - return this + var callbackParam = (r.length == 0) ? null : (r.length == 1 ? r[0] : r); + if (callback) this.lambda(callback).call(this, callbackParam); + return this; }, // NOTE adapters cannot set this.__results but plugins do // this probably should be reviewed - all: function (callback) { - var idx = this.indexer.all() - , r = [] - , o - , k + all: function(callback) { + var idx = this.indexer.all(); + var r = []; + var o; + var k; for (var i = 0, l = idx.length; i < l; i++) { - k = idx[i] //v - o = JSON.parse(storage.getItem(k)) - o.key = k.replace(this.name + '.', '') - r.push(o) + k = idx[i]; + o = JSON.parse(storage.getItem(k)); + o.key = k.replace(this.name + '.', ''); + r.push(o); + } + if (callback) this.fn(this.name, callback).call(this, r); + return this; + }, + + remove: function(keyOrArray, callback) { + var keys = this.isArray(keyOrArray) ? keyOrArray: [keyOrArray]; + var k; + for (var i = 0, l = keys.length; i < l; i++) { + k = this.keyWithPrefix(keys[i]); + this.indexer.del(k); + storage.removeItem(k); } - if (callback) this.fn(this.name, callback).call(this, r) - return this + if (callback) this.lambda(callback).call(this); + return this; }, - - remove: function (keyOrObj, callback) { - var key = this.name + '.' + (typeof keyOrObj === 'string' ? keyOrObj : keyOrObj.key) - this.indexer.del(key) - storage.removeItem(key) - if (callback) this.lambda(callback).call(this) - return this + + exists: function(key, callback) { + var exists = !!storage.getItem(this.keyWithPrefix(key)); + this.lambda(callback).call(this, exists); + return this; }, - - nuke: function (callback) { - this.all(function(r) { - for (var i = 0, l = r.length; i < l; i++) { - this.remove(r[i]); - } - if (callback) this.lambda(callback).call(this) - }) - return this + + nuke: function(callback) { + this.keys(function(theKeys) { + this.remove(theKeys); + }); + if (callback) this.lambda(callback).call(this); + return this; } -}})()); + } +})()); diff --git a/src/adapters/memory.js b/src/adapters/memory.js index 35417c83..bad20e94 100644 --- a/src/adapters/memory.js +++ b/src/adapters/memory.js @@ -10,16 +10,20 @@ Lawnchair.adapter('memory', (function(){ return this }, - keys: function() { return index }, + keys: function(cb) { + this.lambda(cb).call(this, index) + return this + }, save: function(obj, cb) { var key = obj.key || this.uuid() - - if (obj.key) delete obj.key - + this.exists(key, function(exists) { - if (!exists) index.push(key) - + if (!exists){ + if (obj.key) delete obj.key + index.push(key) + } + storage[key] = obj if (cb) { diff --git a/test/lawnchair-spec.js b/test/lawnchair-spec.js index 502963db..32ef7452 100755 --- a/test/lawnchair-spec.js +++ b/test/lawnchair-spec.js @@ -67,21 +67,59 @@ test('full callback syntax', function() { }); }) -test('adding, nuking and size tests', function() { - QUnit.stop(); - QUnit.expect(2); +test('index and store are in sync when adding', function() { + QUnit.stop(); + QUnit.expect(2); + + store.save(me, function() { + store.all(function(r) { + equals(r.length, 1, 'parameter should have length 1 after saving a single record'); + + }); + + store.keys(function(keys){ + equals(keys.length, 1, 'parameter should have length of 1 after saving a single record'); + }); + QUnit.start(); + }); +}) - store.save(me, function() { - store.all(function(r) { - equals(r.length, 1, 'parameter should have length 1 after saving a single record'); - store.nuke(function() { - store.all(function(r) { - equals(r.length, 0, 'parameter should have length 0 after nuking'); - QUnit.start(); - }); - }); - }); - }); +test('index and store are in sync when nuking', function() { + QUnit.stop(); + QUnit.expect(2); + + store.save(me, function() { + store.nuke(function(){ + store.all(function(r) { + equals(r.length, 0, 'store should have length 0 after nuking'); + + }); + + store.keys(function(keys){ + equals(keys.length, 0, 'index should have length of 0 after nuking'); + }); + }) + QUnit.start(); + }); +}) + +test('index and store are in sync when removing', function() { + QUnit.stop(); + QUnit.expect(2); + + store.save({key: 'somekey', value:'somevalue'}, function() { + store.remove('somekey', function(){ + store.all(function(r) { + equals(r.length, 0, 'store should have length 0 after removing'); + + }); + + store.keys(function(keys){ + equals(keys.length, 0, 'index should have length of 0 after removing'); + }); + }) + QUnit.start(); + }); }) test( 'shorthand callback syntax', function() { @@ -321,7 +359,7 @@ test( 'full callback syntax', function() { store.get('somekey', function(r){ ok(true, 'callback got called'); - same(this, store, '"this" should be teh Lawnchair instance'); + same(this, store, '"this" should be the Lawnchair instance'); QUnit.start(); }); }); @@ -346,7 +384,6 @@ module('remove()', { } }); - test( 'chainable', function() { QUnit.expect(1); QUnit.stop(); @@ -366,7 +403,7 @@ test( 'full callback syntax', function() { store.save({key:'somekey', name:'something'}, function() { store.remove('somekey', function(r){ ok(true, 'callback got called'); - same(this, store, '"this" should be teh Lawnchair instance'); + same(this, store, '"this" should be the Lawnchair instance'); QUnit.start(); }); }); @@ -381,26 +418,54 @@ test('short callback syntax', function() { }); }); -// FIXME need to add tests for batch deletion -test( 'remove functionality', function() { +test('remove functionality', function() { QUnit.stop(); - QUnit.expect(2); + QUnit.expect(1); - store.save({name:'joni'}, function(r) { - //store.find("r.name == 'joni'", function(r){ - store.remove(r, function(r) { - store.all(function(all) { - equals(all.length, 0, "should have length 0 after saving, finding, and removing a record using entire object"); - store.save({key:'die', name:'dudeman'}, function(r) { - store.remove('die', function(r){ - store.all(function(rec) { - equals(rec.length, 0, "should have length 0 after saving and removing by string key"); - QUnit.start(); - }); - }); - }); - }); - }); - //}); - }); + store.save({key:'somekey', name:'something'}, function(r) { + store.remove('somekey', function(){ + store.all(function(rec) { + equals(rec.length, 0, "should have length 0 after saving and removing by string key"); + QUnit.start(); + }); + }); + }); +}); + +test('remove batch functionality', function() { + QUnit.stop(); + QUnit.expect(3); + + var t = [{key:'test-get'},{key:'test-get-1'}]; + store.batch(t, function() { + this.remove(['test-get','test-get-1'], function() { + ok(true, 'callback got called'); + same(this, store, '"this" should be the Lawnchair instance'); + store.all(function(rec) { + equals(rec.length, 0, "should have length 0 after saving and removing by batch"); + }); + QUnit.start() + }) + }) }); + + +module('exists()', { + setup:function() { + QUnit.stop(); + store.nuke(function() { QUnit.start(); }); + } +}); + +test('exists functionality', function() { + QUnit.stop(); + QUnit.expect(1); + + store.save({key:'somekey', name:'something'}, function() { + store.all(function(rec) { + equals(rec.length, 1, "should have length 1 after saving"); + }); + QUnit.start(); + }); + +}); \ No newline at end of file