From 265d77d776eb4658e55d26407ad79719d1c685a4 Mon Sep 17 00:00:00 2001 From: Fabrice Marsaud Date: Tue, 31 Mar 2015 16:09:05 +0200 Subject: [PATCH] First main methods. No events, no buffer yet --- packages/xo-collection/collection.js | 552 +++++----------------- packages/xo-collection/collection.spec.js | 241 ++++++++++ packages/xo-collection/package.json | 10 +- packages/xo-collection/test.js | 104 ++-- 4 files changed, 433 insertions(+), 474 deletions(-) create mode 100644 packages/xo-collection/collection.spec.js diff --git a/packages/xo-collection/collection.js b/packages/xo-collection/collection.js index f33ad4f4e..bf87933d8 100644 --- a/packages/xo-collection/collection.js +++ b/packages/xo-collection/collection.js @@ -1,499 +1,195 @@ import makeError from 'make-error'; -const IllegalId = makeError('IllegalId'); -const OverrideViolation = makeError('OverrideViolation'); -const NotFound = makeError('NotFound'); -const TransactionAlreadyOpened = makeError('TransactionAlreadyOpened'); -const NoTransactionOpened = makeError('NoTransactionOpened'); -const FailedRollback = makeError('FailedRollback'); -const FailedReplay = makeError('FailedReplay'); -const UnrecognizedTransactionItem = makeError('UnrecognizedTransactionItem'); -const UnexpectedLogFormat = makeError('UnexpectedLogFormat'); -const UnexpectedLogItemData = makeError('UnexpectedLogItemData'); -const AlreadyPaused = makeError('AlreadyPaused'); -const NothingToFlush = makeError('NothingToFlush'); +const AlreadyBuffering = makeError('AlreadyBuffering'); +const NotBuffering = makeError('NotBuffering'); +const IllegalAdd = makeError('IllegalAdd'); +const DuplicateEntry = makeError('DuplicateEntry'); +const NoSuchEntry = makeError('NoSuchEntry'); class Collection { - constructor() { + constructor () { - this._collection = {}; - this._transaction = null; - this._track = []; - this._pause = false; + this._map = {}; + this._buffering = false; + this._size = 0; } - pause() { + _initBuffer () { - if (this._pause) { - throw new AlreadyPaused('Already paused'); + this._buffer = { + remove: [], + add: [], + update: [] + }; + + } + + bufferChanges (state = true) { + + if (state && this._buffer) { + throw new AlreadyBuffering('Already buffering'); // FIXME Really ?... } - this._pause = true; - - } - - flush(callback = undefined) { - - if (!this._pause) { - throw new NothingToFlush('NothingToFlush'); + if (!state && !this._buffer) { + throw new NotBuffering('Not buffering'); // FIXME Really ?... } + this._buffer = state; - this._pause = false; - let track = this._track; - this._track = []; - - return this.replay(track, callback); - - } - - begin(callback = undefined) { - - try { - - if (null !== this._transaction) { - throw new TransactionAlreadyOpened('A transaction is already opened'); - } - - this._transaction = []; - - if ('function' === typeof callback) { - callback(null, this); - } else { - return this; - } - - - } catch (err) { - - if ('function' === typeof callback) { - callback(err); - } else { - throw err; - } - + if (!this._buffer) { + this._initBuffer(); } } - commit(callback = undefined) { + flush () { - try { + if (!this._buffer) { + throw new NotBuffering('NothingToFlush'); + } - if (null === this._transaction) { - throw new NoTransactionOpened('No opened transaction to commit.'); - } + this._buffering = false; // FIXME Really ? - const transactionLog = this._transaction; - this._transaction = null; + // TODO Throws buffered events - if ('function' === typeof callback) { - callback(null, transactionLog); + this._initBuffer(); + + } + + _touch (key, action) { + + // TODO Buffers changes or throws an event + + } + + _set (key, value) { + + this._map[key] = value; + this._touch(key, 'update'); + return this; + + } + + _unset (key) { + + delete this._map[key]; + this._touch(key, 'remove'); + return this; + } + + getId (item) { + return item.id; + } + + has (key) { + + return this._map.hasOwnProperty(key); + + } + + resolveEntry (keyOrObjectWithId, valueIfKey = null) { + + let value; + let key = (undefined !== keyOrObjectWithId) ? + this.getId(keyOrObjectWithId) : + undefined; + + if (undefined === key) { + if (arguments.length < 2) { + throw new IllegalAdd(); } else { - return transactionLog; + key = keyOrObjectWithId; + value = valueIfKey; } + } else { + value = keyOrObjectWithId; + } + return [key, value]; + } - } catch (err) { - - if ('function' === typeof callback) { - callback(err); - } else { - throw err; - } + _assertHas(key) { + if (!this.has(key)) { + throw new NoSuchEntry(); } } - rollback(callback = undefined) { - - try { - - if (null === this._transaction) { - throw new NoTransactionOpened('No opened transaction to rollback.'); - } - - const log = this._transaction; - this._transaction = null; - - return this._rollback(log, callback); - - } catch(err) { - - if ('function' === typeof callback) { - callback(err); - } else { - throw err; - } + _assertNotHas(key) { + if (this.has(key)) { + throw new DuplicateEntry(); } } - _rollback(log, callback = undefined) { + add (keyOrObjectWithId, valueIfKey = null) { - try { + const [key, value] = this.resolveEntry.apply(this, arguments); - if (!Array.isArray(log)) { - throw new UnexpectedLogFormat('A transaction log must be an Array.'); - } + this._assertNotHas(key); + this._size++; - let item; - const done = []; + return this._set(key, value); - while(item = log.pop()) { - try { + } - this.checkLogItem(item); + set (keyOrObjectWithId, valueIfKey = null) { - switch (item.action) { - case 'insert': - this.delete(item.id); - break; - case 'update': - this.update(item.id, item.former); - break; - case 'delete': - this.insert(item.id, item.former); - break; - default: - throw new UnrecognizedTransactionItem( - 'Unrecognized item action : "' + item.action + - '" at index ' + log.lenght + '.' - ); - } - - done.unshift(item); - } catch(err) { - const exc = new FailedRollback( - 'Rollback failed on index ' + log.lenght + '.' - ); - exc.undone = log; - exc.done = done; - exc.internal = err; - exc.failedAction = item; - exc.index = log.lenght; - throw exc; - } - } - - if ('function' === typeof callback) { - callback(null, this); - } else { - return this; - } - - } catch(err) { - - if ('function' === typeof callback) { - callback(err); - } else { - throw err; - } + const [key, value] = this.resolveEntry.apply(this, arguments); + if (!this.has(key)) { + this._size++; } - } - - replay(log, callback = undefined) { - - try { - - if (!Array.isArray(log)) { - throw new UnexpectedLogFormat('A transaction log must be an Array.'); - } - - let item; - const done = []; - - while(item = log.shift()) { - try { - - this.checkLogItem(item); - - switch (item.action) { - case 'insert': - this.insert(item.id, item.item); - break; - case 'update': - this.update(item.id, item.item); - break; - case 'delete': - this.delete(item.id); - break; - default: - throw new UnrecognizedTransactionItem( - 'Unrecognized item action : "' + item.action + - '" at index ' + done.lenght + '.' - ); - } - - done.push(item); - } catch(err) { - const exc = new FailedReplay( - 'Replay failed on index ' + done.lenght + '.' - ); - exc.undone = log; - exc.done = done; - exc.internal = err; - exc.failedAction = item; - exc.index = done.lenght; - throw exc; - } - } - - if ('function' === typeof callback) { - callback(null, this); - } else { - return this; - } - - } catch(err) { - - if ('function' === typeof callback) { - callback(err); - } else { - throw err; - } - - } + return this._set(key, value); } - checkLogItem (item) { + get (key) { - if (!item.hasOwnProperty('id')) { - throw new UnexpectedLogItemData( - 'Missing id for ' + item.action + ' object.' - ); - } + this._assertHas(key); - const checkFormer = () => { - if (!item.hasOwnProperty('former')) { - throw new UnexpectedLogItemData( - 'Missing former item in ' + item.action + ' object.' - ); - } - }; + return this._map[key]; + } - const checkItem = () => { - if (!item.hasOwnProperty('item')) { - throw new UnexpectedLogItemData( - 'Missing item in ' + item.action + ' object.' - ); - } - }; + update (keyOrObjectWithId, valueIfKey = null) { - switch (item.action) { + const [key, value] = this.resolveEntry.apply(this, arguments); - case 'update': - checkItem(); - case 'delete': - checkFormer(); - break; - case 'insert': - checkItem(); - break; + this._assertHas(key); - default: - throw new UnrecognizedTransactionItem( - 'Unrecognizes item action : "' + item.action + '.' - ); - } + return this._set(key, value); } - insert(id, item, callback = undefined) { + remove (keyOrObjectWithId) { - try { + const [key] = this.resolveEntry(keyOrObjectWithId, null); - this.checkId(id); + this._assertHas(key); + this._size--; - if (this._has(id)) { - throw new OverrideViolation( - 'An insertion must not override the pre-existing id ' + id + '. ' + - 'Consider using update instead depending on your use case.' - ); - } - - if (!this._pause) { - this._collection[id] = item; - } - else { - this._track.push({ - action: 'insert', - id, - item - }); - } - - if ('function' === typeof callback) { - callback(null, this); - } else { - return this; - } - - } catch (err) { - - if ('function' === typeof callback) { - callback(err); - } else { - throw err; - } - - } + return this._unset(key); } - update(id, item, callback = undefined) { - - try { - - this.checkId(id); - - if (!this._has(id)) { - throw new NotFound( - 'No item to update at id ' + id + '. ' + - 'Consider using insert instead depending on your usecase.' - ); - } - - const former = this._collection[id]; - - if (!this._pause) { - this._collection[id] = item; - } else { - this._track.push({ - action: 'update', - id, - former, - item - }); - } - - if ('function' === typeof callback) { - callback(null, this); - } else { - return this; - } - - } catch (err) { - - if ('function' === typeof callback) { - callback(err); - } else { - throw err; - } - - } - + get size () { + return this._size; } - delete(id, callback = undefined) { - - try { - - this.checkId(id); - - if (!this._has(id)) { - throw new NotFound( - 'No item to remove at id' + id + '.' - ); - } - - const former = this._collection[id]; - - if (!this._pause) { - delete this._collection[id]; - } else { - this._track.push({ - action:'delete', - id, - former - }); - } - - if ('function' === typeof callback) { - callback(null, this); - } else { - return this; - } - - } catch (err) { - - if ('function' === typeof callback) { - callback(err); - } else { - throw err; - } - - } - - } - - get(id, callback = undefined) { - - try { - - this.checkId(id); - - if (!this._has(id)) { - throw new NotFound( - 'No item found at id ' + id - ); - } - - if ('function' === typeof callback) { - callback(null, this._collection[id]); - } else { - return this._collection[id]; - } - - } catch (err) { - - if ('function' === typeof callback) { - callback(err); - } else { - throw err; - } - - } - - } - - checkId (id) { - - if ('string' !== typeof id || id.length < 1) { - throw new IllegalId('id must be a non-empty string'); - } - - } - - has(id) { - - return this._has(this.checkId(id)); - - } - - _has(id) { - - return this._collection.hasOwnProperty(id); - + get all () { + return this._map; } } export default { Collection, - IllegalId, - OverrideViolation, - NotFound, - TransactionAlreadyOpened, - NoTransactionOpened, - FailedRollback, - FailedReplay, - UnrecognizedTransactionItem, - UnexpectedLogFormat, - UnexpectedLogItemData + AlreadyBuffering, + NotBuffering, + IllegalAdd, + DuplicateEntry, + NoSuchEntry }; \ No newline at end of file diff --git a/packages/xo-collection/collection.spec.js b/packages/xo-collection/collection.spec.js new file mode 100644 index 000000000..fa00aa2e0 --- /dev/null +++ b/packages/xo-collection/collection.spec.js @@ -0,0 +1,241 @@ +var chai = require('chai'); +var expect = chai.expect; +var dirtyChai = require('dirty-chai'); +chai.use(dirtyChai); +var leche = require('leche'); + +console.log(expect); + +var Collection = require('./collection'); + +var col = new Collection.Collection(); + +describe('collection', function () { + + // ============================================================ + + var fixtureValues1 = { + 'primitive value': ['foo1', 1], + 'array value': ['bar1', [1,2]], + 'object value': ['baz1', {a:1, b:2}] + }; + + describe('add', function () { + + leche.withData(fixtureValues1, function (key, value) { + + it('Adds a new entry to the collection', function () { + expect(col.add(key, value)).to.eql(col); + }); + + }); + + leche.withData(fixtureValues1, function (key, value) { + + it('We cannot add on a pre-existing key', function () { + expect(function () { + col.add(key, value); + }).to.throw(Collection.DuplicateEntry); + }); + + }); + + }); + + // ============================================================ + + var fixtureValues2 = { + 'primitive value': ['foo2', 1], + 'array value': ['bar2', [1,2]], + 'object value': ['baz2', {a:1, b:2}] + }; + + describe('set', function () { + + leche.withData(fixtureValues2, function (key, value) { + + it('Sets an entry of the collection...', function () { + expect(col.set(key, value)).to.eql(col); + }); + + }); + + leche.withData(fixtureValues2, function (key, value) { + + it('...would it already exists or not', function () { + expect(col.set(key, value)).to.eql(col); + }); + + }); + + }); + + // ============================================================ + + var fixtureUnexisting = { + 'Unexisting key/entry': ['wat', 'any'] + }; + + describe('get', function () { + + leche.withData(fixtureValues1, function (key, value) { + + it('Returns the value of an entry of the collection...', function () { + expect(col.get(key)).to.eql(value); + }); + + }); + + leche.withData(fixtureValues2, function (key, value) { + + it('Returns the value of an entry of the collection...', function () { + expect(col.get(key)).to.eql(value); + }); + + }); + + leche.withData(fixtureUnexisting, function (key) { + + it('...or throws if it does not exist', function () { + expect(function () { + col.get(key); + }).to.throw(Collection.NoSuchEntry); + }); + + }); + + }); + + // ============================================================ + + var fixtureUpdates = { + 'primitive value': ['foo2', 3], + 'array value': ['bar2', [3,4]], + 'object value': ['baz2', {c:3, d:4}] + }; + + describe('update', function () { + + leche.withData(fixtureUpdates, function (key, value) { + + it('updates the given entries...', function () { + expect(col.update(key, value)).to.eql(col); + }); + + }); + + leche.withData(fixtureUpdates, function (key, value) { + + it('...so we can see the values we get have changed accordingly', function () { + expect(col.get(key)).to.eql(value); + }); + + }); + + leche.withData(fixtureUnexisting, function (key, value) { + + it('If the entry does not exist, updating throws', function () { + expect(function () { + col.update(key, value); + }).to.throw(Collection.NoSuchEntry); + }); + + }); + + }); + + // ============================================================ + + describe('remove', function () { + + leche.withData(fixtureValues2, function (key) { + + it('removes the given entries...', function () { + expect(col.remove(key)).to.eql(col); + }); + + }); + + leche.withData(fixtureValues2, function (key) { + + it('...so trying to get them again throws...', function () { + expect(function () { + col.get(key); + }).to.throw(Collection.NoSuchEntry); + }); + + }); + + leche.withData(fixtureValues2, function (key) { + + it('...and trying to remove them again also throws', function () { + expect(function () { + col.remove(key); + }).to.throw(Collection.NoSuchEntry); + }); + + }); + + }); + + // ============================================================ + + describe('has', function () { + + leche.withData(fixtureValues1, function (key) { + + it('Tells us if an entry exists...', function () { + expect(col.has(key)).to.be.true(); + }); + + }); + + leche.withData(fixtureValues2, function (key) { + + it('...or not', function () { + expect(col.has(key)).to.be.false(); + }); + + }); + + }); + + // ============================================================ + + describe('size', function () { + + it('Reveals the number of existing entries', function () { + expect(col.size).to.eq(Object.keys(fixtureValues1).length); + }); + + }); + + // ============================================================ + + describe('all', function () { + + leche.withData(fixtureValues1, function (key, value) { + + it('Gives access to the internal collection...', function () { + expect(col.all).to.have.ownProperty(key); + expect(col.all[key]).to.eq(value); + }); + + }); + + leche.withData(fixtureValues2, function (key) { + + it('Gives access to the internal collection...', function () { + expect(col.all).to.not.have.ownProperty(key); + expect(col.all[key]).to.eq(undefined); + }); + + }); + + it('Gives access to the internal collection...', function () { + expect(col.size).to.eq(Object.keys(col.all).length); + }); + + }); + +}); \ No newline at end of file diff --git a/packages/xo-collection/package.json b/packages/xo-collection/package.json index f382fd471..fcf939331 100644 --- a/packages/xo-collection/package.json +++ b/packages/xo-collection/package.json @@ -6,9 +6,15 @@ "dependencies": { "make-error": "^0.3.0" }, - "devDependencies": {}, + "devDependencies": { + "babel": "^4.7.16", + "chai": "^2.2.0", + "dirty-chai": "^1.2.0", + "leche": "^2.1.1", + "mocha": "^2.2.1" + }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "mocha --require babel/register *.spec.js" }, "author": "Fabrice Marsaud ", "license": "aGPLv3" diff --git a/packages/xo-collection/test.js b/packages/xo-collection/test.js index 9d003cdff..2ac59da7f 100644 --- a/packages/xo-collection/test.js +++ b/packages/xo-collection/test.js @@ -1,65 +1,81 @@ import Collection from './collection'; -var co = new Collection.Collection(); +let col = new Collection.Collection(); -co.begin(); +col.add('foo', 1); + +// An object with id property + +// ==================== +// Jouer sur le passage par référence, et la convention d'objets avec une prop ID + +let obj = {id: 'bar', content: 2}; +col.add(obj); +console.log(obj.get('bar')); +// > {id: 'bar', content: 2} + +col.bufferChanges(true); +col.update('bar').content = 4; +// update accesses obj at bar key and marks bar as updated. No event emitted. + +col.get('bar').content = 5; +obj.content = 6; +// bar is already marked as updated, so ... + +col.flush(); +// ...Emits an update as bar has been "updated to 6" + +col.bufferChanges(true); +col.update(obj).content = 7; // Short writing without knowing ID +// WARNING, do not change ID after adding ... + +col.bufferChanges(false); +col.flush(); +// No event emitted ... exception thrown ?... +col.bufferChanges(true); +col.update(obj); +col.flush(); +// Emits an update event as bar has been "updated to 7" + +// ------------------------------------------------------------ +// Special cases : +let foo = {id: 'foo'}; +let bar = {id: 'bar'}; +col.add(foo); -co.insert('a', 1000); -console.log('a', co.get('a')); -co.update('a', 2000); -console.log('a', co.get('a')); -co.delete('a'); try { -console.log(co.get('a')); + col.update(foo, bar); } catch(e) { + // Throws an instant exception on ID violation console.error(e); } -console.log(co.commit()); - -console.log('====='); - -co.insert('b', 100); -console.log('b', co.get('b')); -co.begin(); -co.update('b', 200); -console.log('b', co.get('b')); -co.insert('c', 300); -co.update('b', 400); -console.log('b', co.get('b'), 'c', co.get('c')); -co.delete('b'); try { -console.log(co.get('b')); + col.udpate('foo', bar); } catch(e) { + // Same console.error(e); } -co.rollback(); - -console.log('b', co.get('b')); try { -console.log(co.get('c')); -} catch(e) { + col.update(foo).id = 'bar'; +} catch (e) { + // Throws an exception at Event emission (key !== content.id) console.error(e); } -console.log('====='); +col.bufferChanges(true); +col.remove(foo); +col.add(foo); +col.bufferChanges(false); +// Silent...(No events) -var coa = new Collection.Collection(); -coa.insert('x', 999); -coa.begin(); -coa.insert('a', 100); -coa.update('a', 150); -coa.insert('b', 200); -console.log('a', coa.get('a'), 'b', coa.get('b'), 'x', coa.get('x')); -var log = coa.commit(); -var cob = new Collection.Collection(); -cob.replay(log); -console.log('a', cob.get('a'), 'b', cob.get('b')); +col.bufferChanges(true); +col.update(foo).id = 'bar'; +// Nothing happens try { -console.log(cob.get('x')); -} catch(e) { - console.error(e); + col.flush(); +} catch (e) { + // Throws + console.log(e); } - -process.exit(0);