Implementation seems ok. Deeper tests to come

This commit is contained in:
Fabrice Marsaud 2015-04-02 15:32:08 +02:00
parent 35c64be3d7
commit cbd93f450e
6 changed files with 329 additions and 1234 deletions

View File

@ -0,0 +1,132 @@
/* eslint-env mocha */
import Collection, {DuplicateEntry, NoSuchEntry} from './collection'
import eventToPromise from 'event-to-promise'
import sinon from 'sinon'
import chai from 'chai'
const expect = chai.expect
import dirtyChai from 'dirty-chai'
chai.use(dirtyChai)
describe('Collection', function () {
beforeEach(function (done) {
this.col = new Collection()
this.col.add('bar', 0)
process.nextTick(done)
})
describe('#add()', function () {
it('adds item to the collection', function () {
const spy = sinon.spy()
this.col.on('add', spy)
this.col.add('foo', true)
expect(this.col.get('foo')).to.equal(true)
// No sync events.
sinon.assert.notCalled(spy)
// Async event.
return eventToPromise(this.col, 'add').then(function (added) {
expect(added).to.have.all.keys('foo')
expect(added.foo).to.equal(true)
})
})
it('throws an exception if the item already exists', function () {
expect(() => this.col.add('bar', true)).to.throw(DuplicateEntry)
})
})
describe('#update()', function () {
it('updates an item of the collection', function () {
const spy = sinon.spy()
this.col.on('update', spy)
this.col.update('bar', 1)
expect(this.col.get('bar')).to.equal(1) // Will be forgotten by de-duplication
this.col.update('bar', 2)
expect(this.col.get('bar')).to.equal(2)
// No sync events.
sinon.assert.notCalled(spy)
// Async event.
return eventToPromise(this.col, 'update').then(function (updated) {
expect(updated).to.have.all.keys('bar')
expect(updated.bar).to.equal(2)
})
})
it('throws an exception if the item does not exist', function () {
expect(() => this.col.update('baz', true)).to.throw(NoSuchEntry)
})
})
describe('#remove()', function () {
it('removes an item of the collection', function () {
const spy = sinon.spy()
this.col.on('remove', spy)
this.col.update('bar', 1)
expect(this.col.get('bar')).to.equal(1) // Will be forgotten by de-duplication
this.col.remove('bar')
// No sync events.
sinon.assert.notCalled(spy)
// Async event.
return eventToPromise(this.col, 'remove').then(function (removed) {
expect(removed).to.have.all.keys('bar')
expect(removed.bar).to.equal(null)
})
})
it('throws an exception if the item does not exist', function () {
expect(() => this.col.remove('baz', true)).to.throw(NoSuchEntry)
})
})
describe('#set()', function () {
it('adds item if collection has not key', function () {
const spy = sinon.spy()
this.col.on('add', spy)
this.col.set('foo', true)
expect(this.col.get('foo')).to.equal(true)
// No sync events.
sinon.assert.notCalled(spy)
// Async events.
return eventToPromise(this.col, 'add').then(function (added) {
expect(added).to.have.all.keys('foo')
expect(added.foo).to.equal(true)
})
})
it('updates item if collection has key', function () {
const spy = sinon.spy()
this.col.on('udpate', spy)
this.col.set('bar', 1)
expect(this.col.get('bar')).to.equal(1)
// No sync events.
sinon.assert.notCalled(spy)
// Async events.
return eventToPromise(this.col, 'update').then(function (updated) {
expect(updated).to.have.all.keys('bar')
expect(updated.bar).to.equal(1)
})
})
})
})

View File

@ -1,500 +0,0 @@
var chai = require('chai');
var expect = chai.expect;
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
var leche = require('leche');
var sinon = require('sinon');
var Collection = require('./collection');
describe('collection buffer', function () {
// ============================================================
var col;
var addSpy, addCount;
var updateSpy, updateCount;
var removeSpy, removeCount;
var flush;
before(function () {
col = new Collection.Collection();
addSpy = sinon.spy();
addCount = 0;
updateSpy = sinon.spy();
updateCount = 0;
removeSpy = sinon.spy();
removeCount = 0;
});
after(function () {
col.removeAllListeners();
});
beforeEach(/*'force flush', */function () {
col.removeAllListeners(); // flush may emit events
try { // In case test cases interrupt before flushing
if (flush) {
flush();
}
} catch(e) {
if (!e instanceof Collection.NotBuffering) {
throw e;
}
}
col.on('add', addSpy);
col.on('update', updateSpy);
col.on('remove', removeSpy);
});
// Collection is empty =======================================================
var data1 = {
foo: 1,
bar: [1, 2],
baz: {a:1, b:2}
};
describe('add', function () {
it('Emits no event when buffered, all data is emitted when flushed', function () {
flush = col.bufferChanges();
expect(addSpy.callCount).to.eq(addCount);
for (var prop in data1) {
expect(col.add(prop, data1[prop])).to.eq(col);
}
expect(addSpy.callCount).to.eq(addCount);
flush();
addCount++;
expect(addSpy.callCount).to.eq(addCount);
expect(addSpy.calledWith(data1)).to.be.true();
expect(flush).to.throw(Collection.NotBuffering);
});
});
// Collection contains data 1 ================================================
var data2 = {
foo: 3,
bar: [3, 4],
baz: {c:3, d:4}
};
var removedKeysData2 = {
foo: null,
bar: null,
baz: null
};
describe('update', function () {
it('Emits no event when buffered, all data is emitted when flushed', function () {
flush = col.bufferChanges();
expect(updateSpy.callCount).to.eq(updateCount);
for (var prop in data2) {
expect(col.update(prop, data2[prop])).to.eq(col);
}
expect(updateSpy.callCount).to.eq(updateCount);
flush();
updateCount++;
expect(updateSpy.callCount).to.eq(updateCount);
expect(updateSpy.calledWith(data2)).to.be.true();
expect(flush).to.throw(Collection.NotBuffering);
});
});
// Collection contains data 2 (update of data 1 keys) ========================
describe('touch', function () {
it('Marks a key as buffer-updated, and gives the value for object-property modification cases', function () {
flush = col.bufferChanges();
expect(updateSpy.callCount).to.eq(updateCount);
for (var prop in data2) {
expect(col.touch(prop)).to.eq(data2[prop]);
}
expect(updateSpy.callCount).to.eq(updateCount);
flush();
updateCount++;
expect(updateSpy.callCount).to.eq(updateCount);
expect(updateSpy.calledWith(data2)).to.be.true();
expect(flush).to.throw(Collection.NotBuffering);
});
});
describe('remove', function () {
it('Emits no event when buffered, removed keys are emitted when flushed', function () {
flush = col.bufferChanges();
expect(removeSpy.callCount).to.eq(removeCount);
for (var prop in data2) {
expect(col.remove(prop)).to.eq(col);
}
expect(removeSpy.callCount).to.eq(removeCount);
flush();
removeCount++;
expect(removeSpy.callCount).to.eq(removeCount);
expect(removeSpy.calledWith(removedKeysData2)).to.be.true();
expect(flush).to.throw(Collection.NotBuffering);
});
});
// Collection is empty =======================================================
var dataBefore = { // Will be removed if not re-added (-> udpate)
foo: 1,
bar: 2,
baz: 3
};
// Buffered from now
var dataToAdd = { // will be out of events if not post-added (-> add)
qux: 4,
hop: 6
};
var dataToUpdate = {
bar: 22,
};
var dataToRemove = {
hop: null,
};
// All above will be cleared
var dataToPostAdd = {
baz: 33,
hip: 5
};
// flush
var expectedRemovedData = {
foo: null,
bar: null,
};
var expectedUpdatedData = {
baz: 33
};
var expectedAddedData = {
hip:5
};
describe('clear', function () {
it('acts as a multi-remove', function () {
var prop;
for (prop in dataBefore) {
expect(col.add(prop, dataBefore[prop])).to.eq(col);
}
addCount = addSpy.callCount; // Not buffered, events have been emitted
flush = col.bufferChanges();
expect(addSpy.callCount).to.eq(addCount);
expect(updateSpy.callCount).to.eq(updateCount);
expect(removeSpy.callCount).to.eq(removeCount);
for (prop in dataToAdd) {
expect(col.add(prop, dataToAdd[prop])).to.eq(col);
}
for (prop in dataToUpdate) {
expect(col.update(prop, dataToUpdate[prop])).to.eq(col);
}
for (prop in dataToRemove) {
expect(col.remove(prop)).to.eq(col);
}
expect(col.clear()).to.eq(col);
for (prop in dataToPostAdd) {
expect(col.add(prop, dataToPostAdd[prop])).to.eq(col);
}
expect(addSpy.callCount).to.eq(addCount);
expect(updateSpy.callCount).to.eq(updateCount);
expect(removeSpy.callCount).to.eq(removeCount);
flush();
addCount++;
updateCount++;
removeCount++;
expect(addSpy.callCount).to.eq(addCount);
expect(updateSpy.callCount).to.eq(updateCount);
expect(removeSpy.callCount).to.eq(removeCount);
expect(addSpy.calledWith(expectedAddedData)).to.be.true();
expect(updateSpy.calledWith(expectedUpdatedData)).to.be.true();
expect(removeSpy.calledWith(expectedRemovedData)).to.be.true();
expect(flush).to.throw(Collection.NotBuffering);
});
});
describe('buffer', function() {
beforeEach(/*'Init collection before buffering', */function() {
col.removeAllListeners();
col.clear();
col.add('exist', 0);
col.add('disappear', 3);
col.on('add', addSpy);
col.on('update', updateSpy);
col.on('remove', removeSpy);
});
leche.withData(
{
'add && update(|set) => add': [
[
{action: 'add', key: 'new', value:1},
{action: 'set', key: 'new', value:1.5},
{action: 'update', key: 'new', value:2},
],
{
add: 1,
update: 0,
remove: 0
},
{
add: {'new': 2}
}
],
'set(1st == add) && update(|set) => add': [
[
{action: 'set', key: 'new', value:1},
{action: 'update', key: 'new', value:1.5},
{action: 'set', key: 'new', value:2},
],
{
add: 1,
update: 0,
remove: 0
},
{
add: {'new': 2}
}
],
'update && update => update': [
[
{action: 'update', key: 'exist', value:1},
{action: 'set', key: 'exist', value:1.5},
{action: 'update', key: 'exist', value:2},
],
{
add: 0,
update: 1,
remove: 0
},
{
update: {'exist': 2}
}
],
'update && remove => remove': [
[
{action: 'update', key: 'exist', value:1},
{action: 'set', key: 'exist', value:1},
{action: 'remove', key: 'exist'},
],
{
add: 0,
update: 0,
remove: 1
},
{
remove: {'exist': null}
}
],
'add && [update &&] remove => nothing': [
[
{action: 'add', key: 'new', value:1},
{action: 'update', key: 'new', value:2},
{action: 'set', key: 'new', value:3},
{action: 'remove', key: 'new'},
],
{
add: 0,
update: 0,
remove: 0
},
{}
],
'remove && add => update': [
[
{action: 'remove', key: 'exist'},
{action: 'add', key: 'exist', value:0}
],
{
add: 0,
update: 1,
remove: 0
},
{
update: {'exist': 0}
}
],
'remove && set => update': [
[
{action: 'remove', key: 'exist'},
{action: 'set', key: 'exist', value:0}
],
{
add: 0,
update: 1,
remove: 0
},
{
update: {'exist': 0}
}
],
'every entry is isolated': [
[
{action: 'update', key: 'disappear', value:22},
{action: 'remove', key: 'disappear'},
{action: 'add', key: 'new', value:1},
{action: 'update', key: 'new', value:222},
{action: 'update', key: 'exist', value:1},
{action: 'update', key: 'exist', value:2222},
{action: 'add', key: 'nothing', value:0},
{action: 'update', key: 'nothing', value:1},
{action: 'remove', key: 'nothing'},
],
{
add: 1,
update: 1,
remove: 1
},
{
remove: {'disappear': null},
add: {'new': 222},
update: {'exist': 2222}
}
]
},
function (actions, increments, eventArgs) {
it('Filters side effects forgotten by override', function () {
expect(addSpy.callCount).to.eq(addCount);
expect(updateSpy.callCount).to.eq(updateCount);
expect(removeSpy.callCount).to.eq(removeCount);
flush = col.bufferChanges();
actions.forEach(function (action) {
expect(
col[action.action](action.key, action.value)
).to.eq(col);
});
expect(addSpy.callCount).to.eq(addCount);
expect(updateSpy.callCount).to.eq(updateCount);
expect(removeSpy.callCount).to.eq(removeCount);
flush();
expect(flush).to.throw(Collection.NotBuffering);
addCount += increments.add;
updateCount += increments.update;
removeCount += increments.remove;
expect(addSpy.callCount).to.eq(addCount);
expect(updateSpy.callCount).to.eq(updateCount);
expect(removeSpy.callCount).to.eq(removeCount);
if (eventArgs.add) {
expect(addSpy.calledWith(eventArgs.add)).to.be.true();
}
if (eventArgs.update) {
expect(updateSpy.calledWith(eventArgs.update)).to.be.true();
}
if (eventArgs.remove) {
expect(removeSpy.calledWith(eventArgs.remove)).to.be.true();
}
});
}
);
});
});

View File

@ -1,210 +0,0 @@
var chai = require('chai');
var expect = chai.expect;
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
var leche = require('leche');
var sinon = require('sinon');
var Collection = require('./collection');
var col = new Collection.Collection();
describe('collection events', function () {
// ============================================================
var data1 = {
'primitive value': ['foo1', 1],
'array value': ['bar1', [1,2]],
'object value': ['baz1', {a:1, b:2}]
};
var addSpy;
var updateSpy;
var removeSpy;
beforeEach(function () {
addSpy = sinon.spy();
updateSpy = sinon.spy();
removeSpy = sinon.spy();
col.on('add', addSpy);
col.on('update', updateSpy);
col.on('remove', removeSpy);
});
afterEach(function () {
col.removeAllListeners();
});
// Collection is empty =======================================================
describe('add', function () {
leche.withData(data1, function (key, value) {
it('Emits an add event transporting the key and value', function () {
expect(addSpy.called).to.be.false();
expect(updateSpy.called).to.be.false();
expect(removeSpy.called).to.be.false();
expect(col.add(key, value)).to.eql(col);
expect(addSpy.called).to.be.true();
expect(addSpy.calledWith({key: value})).to.be.true();
expect(updateSpy.called).to.be.false();
expect(removeSpy.called).to.be.false();
});
});
});
var updateData1 = {
'primitive value': ['foo1', 3],
'array value': ['bar1', [3,4]],
'object value': ['baz1', {c:2, d:4}]
};
// Collection contains data1 =================================================
describe('update', function () {
leche.withData(updateData1, function (key, value) {
it('Emits an update event transporting the key and value', function () {
expect(addSpy.called).to.be.false();
expect(updateSpy.called).to.be.false();
expect(removeSpy.called).to.be.false();
expect(col.update(key, value)).to.eql(col);
expect(updateSpy.called).to.be.true();
expect(updateSpy.calledWith({key: value})).to.be.true();
expect(addSpy.called).to.be.false();
expect(removeSpy.called).to.be.false();
});
});
});
var data2 = {
'primitive value': ['foo2', 1],
'array value': ['bar2', [1,2]],
'object value': ['baz2', {a:1, b:2}]
};
// Collection contains data1 updated =========================================
describe('set', function () {
leche.withData(data1, function (key, value) {
it('Emits an update event for pre-existing keys', function () {
expect(addSpy.called).to.be.false();
expect(updateSpy.called).to.be.false();
expect(removeSpy.called).to.be.false();
expect(col.update(key, value)).to.eql(col);
expect(updateSpy.called).to.be.true();
expect(updateSpy.calledWith({key: value})).to.be.true();
expect(addSpy.called).to.be.false();
expect(removeSpy.called).to.be.false();
});
});
// Collection contains data1 =============================================
leche.withData(data2, function (key, value) {
it('Emits an add event for unexisting keys', function () {
expect(addSpy.called).to.be.false();
expect(updateSpy.called).to.be.false();
expect(removeSpy.called).to.be.false();
expect(col.add(key, value)).to.eql(col);
expect(addSpy.called).to.be.true();
expect(addSpy.calledWith({key: value})).to.be.true();
expect(updateSpy.called).to.be.false();
expect(removeSpy.called).to.be.false();
});
});
});
// Collection contains data1 & data 2 ========================================
describe('remove', function () {
leche.withData(data1, function (key, value) {
it('Emits an remove event transporting the key and removed value', function () {
expect(addSpy.called).to.be.false();
expect(updateSpy.called).to.be.false();
expect(removeSpy.called).to.be.false();
expect(col.remove(key)).to.eql(col);
expect(removeSpy.called).to.be.true();
expect(removeSpy.calledWith({key: value})).to.be.true();
expect(addSpy.called).to.be.false();
expect(updateSpy.called).to.be.false();
});
});
});
// Collection contains data 2 ================================================
describe('clear', function () {
var clearedData;
it('Emits a remove event', function() {
expect(addSpy.called).to.be.false();
expect(updateSpy.called).to.be.false();
expect(removeSpy.called).to.be.false();
expect(col.clear()).to.eq(col);
expect(removeSpy.calledOnce).to.be.true();
clearedData = removeSpy.lastCall.args[0];
expect(addSpy.called).to.be.false();
expect(updateSpy.called).to.be.false();
});
it('Emits no event if collection is empty', function() {
expect(addSpy.called).to.be.false();
expect(updateSpy.called).to.be.false();
expect(removeSpy.called).to.be.false();
expect(col.clear()).to.eq(col);
expect(addSpy.called).to.be.false();
expect(updateSpy.called).to.be.false();
expect(removeSpy.called).to.be.false();
});
leche.withData(data2, function (key, value) {
it('Emits a remove event with all cleared data', function () {
expect(clearedData[key]).to.eq(value);
});
});
});
});

View File

@ -1,273 +1,197 @@
import makeError from 'make-error';
import events from 'events';
import _forEach from 'lodash.foreach';
const AlreadyBuffering = makeError('AlreadyBuffering');
const NotBuffering = makeError('NotBuffering');
const IllegalAdd = makeError('IllegalAdd');
const DuplicateEntry = makeError('DuplicateEntry');
const NoSuchEntry = makeError('NoSuchEntry');
class Collection extends events.EventEmitter {
constructor () {
super();
this._map = {};
this._buffering = false;
this._size = 0;
}
bufferChanges () {
if (this._buffering) {
throw new AlreadyBuffering('Already buffering'); // FIXME Really ?...
}
this._buffering = true;
this._buffer = {};
return () => {
if (!this._buffering) {
throw new NotBuffering('Nothing to flush'); // FIXME Really ?
}
this._buffering = false; // FIXME Really ?
let data = {
add: {data: {}},
update: {data: {}},
remove: {data: {}}
};
for (let key in this._buffer) {
data[this._buffer[key]].data[key] = this.has(key) ?
this.get(key) :
null; // 'remove' case
data[this._buffer[key]].has = true;
}
['add', 'update', 'remove'].forEach(action => {
if (data[action].has) {
this.emit(action, data[action].data);
}
});
delete this._buffer;
};
}
_touch (action, key, value) {
if (this._buffering) {
switch(action) {
case 'add':
this._buffer[key] = this._buffer[key] ? 'update' : 'add';
break;
case 'update':
this._buffer[key] = this._buffer[key] || 'update';
break;
case 'remove':
switch(this._buffer[key]) {
case undefined:
case 'update':
this._buffer[key] = 'remove';
break;
case 'add':
delete this._buffer[key];
break;
}
break;
}
} else {
this.emit(action, {key: value});
}
}
getId (item) {
return item.id;
}
has (key) {
return Object.hasOwnProperty.call(this._map, 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 {
key = keyOrObjectWithId;
value = valueIfKey;
}
} else {
value = keyOrObjectWithId;
}
return [key, value];
}
_assertHas(key) {
if (!this.has(key)) {
throw new NoSuchEntry();
}
}
_assertHasNot(key) {
if (this.has(key)) {
throw new DuplicateEntry();
}
}
add (keyOrObjectWithId, valueIfKey = null) {
const [key, value] = this.resolveEntry.apply(this, arguments);
this._assertHasNot(key);
this._map[key] = value;
this._size++;
this._touch('add', key, value);
return this;
}
set (keyOrObjectWithId, valueIfKey = null) {
const [key, value] = this.resolveEntry.apply(this, arguments);
const action = this.has(key) ? 'update' : 'add';
this._map[key] = value;
if ('add' === action) {
this._size++;
}
this._touch(action, key, value);
return this;
}
get (key) {
this._assertHas(key);
return this._map[key];
}
update (keyOrObjectWithId, valueIfKey = null) {
const [key, value] = this.resolveEntry.apply(this, arguments);
this._assertHas(key);
this._map[key] = value;
this._touch('update', key, value);
return this;
}
touch (keyOrObjectWithId) {
const [key] = this.resolveEntry(keyOrObjectWithId, null);
this._assertHas(key);
if (!this._buffering) { // FIXME Really ?
// TODO throw something
}
this._touch('update', key);
return this.get(key);
}
remove (keyOrObjectWithId) {
const [key] = this.resolveEntry(keyOrObjectWithId, null);
this._assertHas(key);
const oldValue = this.get(key);
delete this._map[key];
this._size--;
// FIXME do we "emit" null in place of oldValue to harmonize with flush remove events ?
this._touch('remove', key, oldValue);
return this;
}
clear () {
if (this._size > 0) { // FIXME Really ?
if (!this._buffering) {
this.emit('remove', this._map);
} else {
for (let key in this._map) {
this._touch('remove', key);
}
}
}
this._map = {};
this._size = 0;
return this;
}
get size () {
return this._size;
}
get all () {
return this._map;
}
import events from 'events'
import makeError from 'make-error'
export const DuplicateEntry = makeError('DuplicateEntry')
export const BufferAlreadyFlushed = makeError('BufferAlreadyFlushed')
export const IllegalAdd = makeError('IllegalAdd')
export const NoSuchEntry = makeError('NoSuchEntry')
export default class Collection extends events.EventEmitter {
constructor () {
super()
this._buffering = 0
this._map = {}
this._size = 0
}
bufferChanges () {
if (this._buffering++ === 0) {
this._buffer = {}
}
let called = false
return () => {
if (called) {
throw new BufferAlreadyFlushed('Buffer flush already requested')
}
called = true
if (--this._buffering > 0) {
return
}
let data = {
add: {data: {}},
remove: {data: {}},
update: {data: {}}
}
for (let key in this._buffer) {
data[this._buffer[key]].data[key] = this.has(key) ?
this.get(key) :
null // "remove" case
data[this._buffer[key]].has = true
}
['add', 'update', 'remove'].forEach(action => {
if (data[action].has) {
this.emit(action, data[action].data)
}
})
delete this._buffer
}
}
_touch (action, key) {
if (this._buffering === 0) {
process.nextTick(this.bufferChanges())
}
switch (action) {
case 'add':
this._buffer[key] = this._buffer[key] ? 'update' : 'add'
break
case 'remove':
switch (this._buffer[key]) {
case undefined:
case 'update':
this._buffer[key] = 'remove'
break
case 'add':
delete this._buffer[key]
break
}
break
case 'update':
this._buffer[key] = this._buffer[key] || 'update'
break
}
}
getId (item) {
return item.id
}
has (key) {
return Object.hasOwnProperty.call(this._map, 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('Missing value, or object value does not provide id/key')
} else {
key = keyOrObjectWithId
value = valueIfKey
}
} else {
value = keyOrObjectWithId
}
return [key, value]
}
_assertHas (key) {
if (!this.has(key)) {
throw new NoSuchEntry('No ' + key + ' entry')
}
}
_assertHasNot (key) {
if (this.has(key)) {
throw new DuplicateEntry('Attempt to duplicate ' + key + ' entry')
}
}
add (keyOrObjectWithId, valueIfKey = null) {
const [key, value] = this.resolveEntry.apply(this, arguments)
this._assertHasNot(key)
this._map[key] = value
this._size++
this._touch('add', key)
return this
}
set (keyOrObjectWithId, valueIfKey = null) {
const [key, value] = this.resolveEntry.apply(this, arguments)
const action = this.has(key) ? 'update' : 'add'
this._map[key] = value
if (action === 'add') {
this._size++
}
this._touch(action, key)
return this
}
get (key) {
this._assertHas(key)
return this._map[key]
}
update (keyOrObjectWithId, valueIfKey = null) {
const [key, value] = this.resolveEntry.apply(this, arguments)
this._assertHas(key)
this._map[key] = value
this._touch('update', key)
return this
}
touch (keyOrObjectWithId) {
const [key] = this.resolveEntry(keyOrObjectWithId, null)
this._assertHas(key)
this._touch('update', key)
return this.get(key)
}
remove (keyOrObjectWithId) {
const [key] = this.resolveEntry(keyOrObjectWithId, null)
this._assertHas(key)
delete this._map[key]
this._size--
this._touch('remove', key)
return this
}
clear () {
for (let key in this._map) {
delete this._map[key]
this._size--
this._touch('remove', key)
}
return this
}
get size () {
return this._size
}
get all () {
return this._map
}
}
export default {
Collection,
AlreadyBuffering,
NotBuffering,
IllegalAdd,
DuplicateEntry,
NoSuchEntry
};

View File

@ -1,252 +0,0 @@
var chai = require('chai');
var expect = chai.expect;
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
var leche = require('leche');
var Collection = require('./collection');
var col = new Collection.Collection();
describe('collection', function () {
// Collection is empty =======================================================
var data1 = {
'primitive value': ['foo1', 1],
'array value': ['bar1', [1,2]],
'object value': ['baz1', {a:1, b:2}]
};
describe('add', function () {
leche.withData(data1, function (key, value) {
it('Adds a new entry to the collection', function () {
expect(col.add(key, value)).to.eql(col);
});
});
leche.withData(data1, function (key, value) {
it('We cannot add on a pre-existing key', function () {
expect(function () {
col.add(key, value);
}).to.throw(Collection.DuplicateEntry);
});
});
});
// Collection contains data 1 ================================================
var data2 = {
'primitive value': ['foo2', 1],
'array value': ['bar2', [1,2]],
'object value': ['baz2', {a:1, b:2}]
};
describe('set', function () {
leche.withData(data2, function (key, value) {
it('Sets an entry of the collection...', function () {
expect(col.set(key, value)).to.eql(col);
});
});
leche.withData(data2, function (key, value) {
it('...would it already exists or not', function () {
expect(col.set(key, value)).to.eql(col);
});
});
});
// Collection contains data 1 & 2 ============================================
var unexistingData = {
'Unexisting key/entry': ['wat', 'any']
};
describe('get', function () {
leche.withData(data1, function (key, value) {
it('Returns the value of an entry of the collection...', function () {
expect(col.get(key)).to.eql(value);
});
});
leche.withData(data2, function (key, value) {
it('Returns the value of an entry of the collection...', function () {
expect(col.get(key)).to.eql(value);
});
});
leche.withData(unexistingData, function (key) {
it('...or throws if it does not exist', function () {
expect(function () {
col.get(key);
}).to.throw(Collection.NoSuchEntry);
});
});
});
// Collection contains data 1 & 2 ============================================
var updateData2 = {
'primitive value': ['foo2', 3],
'array value': ['bar2', [3,4]],
'object value': ['baz2', {c:3, d:4}]
};
describe('update', function () {
leche.withData(updateData2, function (key, value) {
it('updates the given entries...', function () {
expect(col.update(key, value)).to.eql(col);
});
});
leche.withData(updateData2, 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(unexistingData, function (key, value) {
it('If the entry does not exist, updating throws', function () {
expect(function () {
col.update(key, value);
}).to.throw(Collection.NoSuchEntry);
});
});
});
// Collection contains data 1 & 2 updated ====================================
describe('remove', function () {
leche.withData(data2, function (key) {
it('removes the given entries...', function () {
expect(col.remove(key)).to.eql(col);
});
});
leche.withData(data2, function (key) {
it('...so trying to get them again throws...', function () {
expect(function () {
col.get(key);
}).to.throw(Collection.NoSuchEntry);
});
});
leche.withData(data2, function (key) {
it('...and trying to remove them again also throws', function () {
expect(function () {
col.remove(key);
}).to.throw(Collection.NoSuchEntry);
});
});
});
// Collection contains data 1 ================================================
describe('has', function () {
leche.withData(data1, function (key) {
it('Tells us if an entry exists...', function () {
expect(col.has(key)).to.be.true();
});
});
leche.withData(data2, function (key) {
it('...or not', function () {
expect(col.has(key)).to.be.false();
});
});
});
// Collection contains data 1 ================================================
describe('size', function () {
it('Reveals the number of existing entries', function () {
expect(col.size).to.eq(Object.keys(data1).length);
});
});
// Collection contains data 1 ================================================
describe('all', function () {
leche.withData(data1, 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(data2, 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);
});
});
// Collection contains data 1 ================================================
describe('clear', function () {
it('wipes out all the collection', function () {
expect(col.clear()).to.eq(col);
expect(col.size).to.eq(0);
expect(col.all).to.eql({});
});
});
});

View File

@ -11,6 +11,7 @@
"babel": "^4.7.16",
"chai": "^2.2.0",
"dirty-chai": "^1.2.0",
"event-to-promise": "^0.3.2",
"leche": "^2.1.1",
"mocha": "^2.2.1",
"sinon": "^1.14.1"