diff --git a/src/store/MemoryStore.js b/src/store/MemoryStore.js new file mode 100644 index 0000000..74bfbdf --- /dev/null +++ b/src/store/MemoryStore.js @@ -0,0 +1,134 @@ +/** + * Generic key/value store in local memory + * + * Copyright (C) 2016 LoVullo Associates, Inc. + * + * This file is part of the Liza Data Collection Framework + * + * Liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var Class = require( 'easejs' ).Class, + Store = require( './Store' ); + + +/** + * Generic key/value store with bulk clear + * + * @todo There's a lot of overlap between this concept and that of the + * Bucket. Maybe have the Bucket layer atop of simple Store + * interface as a step toward a new, simpler Bucket + * implementation. This was not implemented atop of the Bucket + * interface because its haphazard implementation would + * overcomplicate this. + * + * @example + * let s = MemoryStore(); + * + * s.add( 'foo', 'bar' ); + * s.add( 'baz', 'quux' ); + * s.get( 'foo' ); // bar + * s.get( 'baz' ); // quux + * + * s.clear(); + * s.get( 'foo' ); // undefined + * s.get( 'baz' ); // undefined + */ +module.exports = Class( 'MemoryStore' ) + .implement( Store ) + .extend( +{ + /** + * Key/value store + * + * @type {Object} + */ + 'private _store': {}, + + + /** + * Add item to cache under `key` with value `value` + * + * @param {string} key cache key + * @param {*} value value for key + * + * @return {Store} self + */ + 'virtual public add': function( key, value ) + { + this._store[ key ] = value; + + return this; + }, + + + /** + * Retrieve item from cache under `key` + * + * @param {string} key cache key + * + * @return {*} `key` value + */ + 'virtual public get': function( key ) + { + return this._store[ key ]; + }, + + + /** + * Clear all items in cache + * + * @return {Store} self + */ + 'virtual public clear': function() + { + this._store = {}; + + return this; + }, + + + /** + * Fold (reduce) all cached values + * + * This provides a way to iterate through all cached values and + * their keys while providing a useful functional result (folding). + * + * The order of folding is undefined. + * + * The ternary function `callback` is of the same form as + * {@link Array#fold}: the first argument is the value of the + * accumulator (initialized to the value of `initial`; the second + * is the cached item; and the third is the key of that item. + * + * @param {function(*,*,string=)} callback folding function + * @param {*} initial initial value for accumulator + * + * @return {*} folded value (final accumulator value) + */ + 'public reduce': function( callback, initial ) + { + var store = this._store; + + return Object.keys( store ) + .map( function( key ) + { + return [ key, store[ key ] ]; + } ) + .reduce( function( accum, values ) + { + return callback( accum, values[ 1 ], values[ 0 ] ); + }, initial ); + } +} ); diff --git a/src/store/Store.js b/src/store/Store.js new file mode 100644 index 0000000..3daf69b --- /dev/null +++ b/src/store/Store.js @@ -0,0 +1,91 @@ +/** + * Generic key/value store + * + * Copyright (C) 2016 LoVullo Associates, Inc. + * + * This file is part of the Liza Data Collection Framework + * + * Liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var Interface = require( 'easejs' ).Interface; + + +/** + * Generic key/value store with bulk clear + * + * @todo There's a lot of overlap between this concept and that of the + * Bucket. Maybe have the Bucket layer atop of simple Store + * interface as a step toward a new, simpler Bucket + * implementation. This was not implemented atop of the Bucket + * interface because its haphazard implementation would + * overcomplicate this. + */ +module.exports = Interface( 'Store', +{ + /** + * Add item to store under `key` with value `value` + * + * The promise will be fulfilled with an object containing the + * `key` and `value` added to the store; this is convenient for + * promises. + * + * @param {string} key store key + * @param {*} value value for key + * + * @return {Promise} promise to add item to store + */ + 'public add': [ 'key', 'value' ], + + + /** + * Retrieve item from store under `key` + * + * The promise will be rejected if the key is unavailable. + * + * @param {string} key store key + * + * @return {Promise} promise for the key value + */ + 'public get': [ 'key' ], + + + /** + * Clear all items in store + * + * @return {Promise} promise to clear store + */ + 'public clear': [], + + + /** + * Fold (reduce) all stored values + * + * This provides a way to iterate through all stored values and + * their keys while providing a useful functional result (folding). + * + * The order of folding is undefined. + * + * The ternary function `callback` is of the same form as + * {@link Array#fold}: the first argument is the value of the + * accumulator (initialized to the value of `initial`; the second + * is the stored item; and the third is the key of that item. + * + * @param {function(*,*,string=)} callback folding function + * @param {*} initial initial value for accumulator + * + * @return {Promise} promise of a folded value (final accumulator value) + */ + 'public reduce': [ 'callback', 'initial' ], +} ); diff --git a/test/store/MemoryStoreTest.js b/test/store/MemoryStoreTest.js new file mode 100644 index 0000000..a152911 --- /dev/null +++ b/test/store/MemoryStoreTest.js @@ -0,0 +1,185 @@ +/** + * Test case for MemoryStore + * + * Copyright (C) 2016 LoVullo Associates, Inc. + * + * This file is part of the Liza Data Collection Framework + * + * Liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +"use strict"; + +var store = require( '../../' ).store, + expect = require( 'chai' ).expect, + Class = require( 'easejs' ).Class, + Trait = require( 'easejs' ).Trait, + Sut = store.MemoryStore; + + +describe( 'store.MemoryStore', () => +{ + describe( '#add', () => + { + it( 'adds item to store when missing', () => + { + const sut = Sut(); + const item = {}; + + expect( + sut.add( 'foo', item ) + .get( 'foo' ) + ).to.equal( item ); + } ); + + + it( 'replaces item in store if existing', () => + { + const sut = Sut(); + const item = {}; + + expect( + sut.add( 'foo', [] ) + .add( 'foo', item ) + .get( 'foo' ) + ).to.equal( item ); + } ); + } ); + + + // most things implicitly tested above + describe( '#get', () => + { + it( 'returns undefined if store item does not exist', () => + { + expect( Sut().get( 'unknown' ) ).to.be.undefined; + } ); + } ); + + + describe( '#clear', () => + { + it( 'removes all items from store', () => + { + const sut = Sut(); + const keys = [ 'foo', 'bar', 'baz' ]; + + keys.forEach( key => sut.add( key ) ); + + // should remove all items + sut.clear(); + + keys.forEach( key => expect( sut.get( key ) ).to.be.undefined ); + } ); + + + it( 'returns self', () => + { + const sut = Sut(); + + expect( sut.clear() ).to.equal( sut ); + } ); + } ); + + + describe( 'with mixin', () => + { + it( 'allows overriding add', done => + { + const expected_key = 'foo'; + const expected_value = {}; + + Sut.use( + Trait.extend( Sut, + { + 'override add'( key, value ) + { + expect( key ).to.equal( expected_key ); + expect( value ).to.equal( expected_value ); + done(); + } + } ) + )().add( expected_key, expected_value ); + } ); + + + it( 'allows overriding get', done => + { + const expected_key = 'bar'; + + Sut.use( + Trait.extend( Sut, + { + 'override get'( key ) + { + expect( key ).to.equal( expected_key ); + done(); + } + } ) + )().get( expected_key ); + } ); + + + it( 'allows overriding clear', done => + { + Sut.use( + Trait.extend( Sut, + { + 'override clear'( key ) + { + done(); + } + } ) + )().clear(); + } ); + } ); + + + describe( '#reduce', () => + { + it( 'folds each stored item', () => + { + const StubSut = Sut.extend( + { + sum() + { + return this.reduce( + ( accum, item, key ) => + { + // correct key for item? + expect( item ).to.equal( vals[ key ] ); + + return accum + item; + }, + 5 + ); + } + } ); + + const sut = StubSut(); + const vals = { + one: 1, + two: 2, + three: 3, + }; + + Object.keys( vals ).forEach( + ( key, i ) => sut.add( key, vals[ key ] ) + ); + + // implicitly tests initial + expect( sut.sum() ).to.equal( 11 ); + } ); + } ); +} );