Add AutoObjectStore
* src/store/AutoObjectStore.js: Add class. * test/store/AutoObjectStoreTest.js: Add test case.master
parent
edce23f14b
commit
dfcca807de
|
@ -0,0 +1,177 @@
|
||||||
|
/**
|
||||||
|
* Convert objects to Stores upon retrieval
|
||||||
|
*
|
||||||
|
* Copyright (C) 2017 R-T Specialty, LLC.
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Trait, Class } = require( 'easejs' );
|
||||||
|
const Store = require( './Store' );
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert objects into sub-stores containing its key/value pairs
|
||||||
|
*
|
||||||
|
* When retrieving a value that is an object, it will first be converted
|
||||||
|
* into a Store and populated with the key/value pairs of that
|
||||||
|
* object. Non-object values will remain untouched.
|
||||||
|
*
|
||||||
|
* This trait expects a constructor function to instantiate a new
|
||||||
|
* Store. Providing the same constructor as was used to instantiate the
|
||||||
|
* current object will allow for an object to be recursively converted into
|
||||||
|
* nested Stores.
|
||||||
|
*
|
||||||
|
* Sub-stores are cached until the value of the key is references
|
||||||
|
* changes, after which point another request to `#get` will instantiate a
|
||||||
|
* _new_ store. The previous store will not be modified to reflect the new
|
||||||
|
* value.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* store.get( 'foo' ); // new Store (1)
|
||||||
|
* store.get( 'foo' ); // existing Store (1)
|
||||||
|
* store.add( 'foo', {} );
|
||||||
|
* store.get( 'foo' ); // new Store (2)
|
||||||
|
* store.add( 'foo', "bar" );
|
||||||
|
* store.get( 'foo' ); // "bar"
|
||||||
|
*/
|
||||||
|
module.exports = Trait( 'AutoObjectStore' )
|
||||||
|
.implement( Store )
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructor for object sub-stores
|
||||||
|
* @type {function(Object):Store}
|
||||||
|
*/
|
||||||
|
'private _ctor': null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store cache
|
||||||
|
* @type {Object.<Store>}
|
||||||
|
*/
|
||||||
|
'private _stores': {},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize with Store constructor
|
||||||
|
*
|
||||||
|
* `ctor` will be used to instantiate Stores as needed.
|
||||||
|
*
|
||||||
|
* @param {function():Store} ctor Store constructor
|
||||||
|
*/
|
||||||
|
__mixin( ctor )
|
||||||
|
{
|
||||||
|
this._ctor = ctor;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add item to store under `key` with value `value`
|
||||||
|
*
|
||||||
|
* Any cached store for `key` will be cleared so that future `#get`
|
||||||
|
* requests return up-to-date data.
|
||||||
|
*
|
||||||
|
* @param {string} key store key
|
||||||
|
* @param {*} value value for key
|
||||||
|
*
|
||||||
|
* @return {Promise.<Store>} promise to add item to store, resolving to
|
||||||
|
* self (for chaining)
|
||||||
|
*/
|
||||||
|
'virtual abstract override public add'( key, value )
|
||||||
|
{
|
||||||
|
return this.__super( key, value )
|
||||||
|
.then( ret =>
|
||||||
|
{
|
||||||
|
delete this._stores[ key ];
|
||||||
|
return ret;
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve item from store under `key`
|
||||||
|
*
|
||||||
|
* If the returned value is an object, it will automatically be
|
||||||
|
* converted into a store and populated with the object's
|
||||||
|
* values; otherwise, the value will be returned unaltered.
|
||||||
|
*
|
||||||
|
* Only vanilla objects (that is---not instances of anything but
|
||||||
|
* `Object`) will be converted into a Store.
|
||||||
|
*
|
||||||
|
* @param {string} key store key
|
||||||
|
*
|
||||||
|
* @return {Promise} promise for the key value
|
||||||
|
*/
|
||||||
|
'virtual abstract override public get'( key )
|
||||||
|
{
|
||||||
|
if ( this._stores[ key ] !== undefined )
|
||||||
|
{
|
||||||
|
return Promise.resolve( this._stores[ key ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.__super( key )
|
||||||
|
.then( value =>
|
||||||
|
{
|
||||||
|
if ( !this._isConvertable( value ) )
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create and cache store (we cache _before_ populating,
|
||||||
|
// otherwise another request might come in and create yet
|
||||||
|
// another store before we have a chance to complete
|
||||||
|
// populating)
|
||||||
|
const substore = this._ctor();
|
||||||
|
|
||||||
|
this._stores[ key ] = substore;
|
||||||
|
|
||||||
|
return substore.populate( value )
|
||||||
|
.then( () => substore );
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether given value should be converted into a Store
|
||||||
|
*
|
||||||
|
* Only vanilla objects (that is---not instances of anything but
|
||||||
|
* `Object`) will be converted into a Store.
|
||||||
|
*
|
||||||
|
* @param {*} value value under consideration
|
||||||
|
*
|
||||||
|
* @return {boolean} whether to convert `value`
|
||||||
|
*/
|
||||||
|
'private _isConvertable'( value )
|
||||||
|
{
|
||||||
|
if ( typeof value !== 'object' )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctor = value.constructor || {};
|
||||||
|
|
||||||
|
// instances of prototypes should be left alone, so we should ignore
|
||||||
|
// everything that's not a vanilla object
|
||||||
|
if ( ctor !== Object )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
} );
|
|
@ -0,0 +1,114 @@
|
||||||
|
/**
|
||||||
|
* Tests AutoObjectStore
|
||||||
|
*
|
||||||
|
* Copyright (C) 2017 R-T Specialty, LLC.
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const chai = require( 'chai' );
|
||||||
|
const expect = chai.expect;
|
||||||
|
|
||||||
|
chai.use( require( 'chai-as-promised' ) );
|
||||||
|
|
||||||
|
const {
|
||||||
|
AutoObjectStore: Sut,
|
||||||
|
MemoryStore: Store,
|
||||||
|
} = require( '../../' ).store;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe( 'AutoObjectStore', () =>
|
||||||
|
{
|
||||||
|
describe( "given an object value", () =>
|
||||||
|
{
|
||||||
|
it( "applies given ctor to objects", () =>
|
||||||
|
{
|
||||||
|
const obj = Store();
|
||||||
|
const dummy_ctor = () => obj;
|
||||||
|
const sut = Store.use( Sut( dummy_ctor ) )();
|
||||||
|
|
||||||
|
const foo = sut
|
||||||
|
.add( 'foo', {} )
|
||||||
|
.then( _ => sut.get( 'foo' ) );
|
||||||
|
|
||||||
|
return expect( foo )
|
||||||
|
.to.eventually.deep.equal( obj );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( "adds object values to new store", () =>
|
||||||
|
{
|
||||||
|
const obj = { bar: "baz" };
|
||||||
|
const sut = Store.use( Sut( Store ) )();
|
||||||
|
|
||||||
|
const bar = sut
|
||||||
|
.add( 'foo', obj )
|
||||||
|
.then( _ => sut.get( 'foo' ) )
|
||||||
|
.then( substore => substore.get( 'bar' ) );
|
||||||
|
|
||||||
|
return expect( bar ).to.eventually.equal( obj.bar );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( "caches sub-store until key changes", () =>
|
||||||
|
{
|
||||||
|
const obj = {};
|
||||||
|
const sut = Store.use( Sut( Store ) )();
|
||||||
|
|
||||||
|
return sut
|
||||||
|
.add( 'foo', {} )
|
||||||
|
.then( _ => sut.get( 'foo' ) )
|
||||||
|
.then( store1 =>
|
||||||
|
expect( sut.get( 'foo' ) ).to.eventually.equal( store1 )
|
||||||
|
.then( _ => sut.add( 'foo', "new" ) )
|
||||||
|
.then( _ => sut.get( 'foo' ) )
|
||||||
|
.then( store2 =>
|
||||||
|
expect( store2 ).to.not.equal( store1 )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( "leaves non-objects untouched", () =>
|
||||||
|
{
|
||||||
|
const expected = "bar";
|
||||||
|
const sut = Store.use( Sut( () => null ) )();
|
||||||
|
|
||||||
|
const foo = sut
|
||||||
|
.add( 'foo', expected )
|
||||||
|
.then( _ => sut.get( 'foo' ) );
|
||||||
|
|
||||||
|
return expect( foo ).to.eventually.equal( expected );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
// includes class instances, since easejs generates prototypes
|
||||||
|
it( "leaves prototype instances untouched", () =>
|
||||||
|
{
|
||||||
|
const expected = ( new function() {} );
|
||||||
|
const sut = Store.use( Sut( () => null ) )();
|
||||||
|
|
||||||
|
const foo = sut
|
||||||
|
.add( 'foo', expected )
|
||||||
|
.then( _ => sut.get( 'foo' ) );
|
||||||
|
|
||||||
|
return expect( foo ).to.eventually.equal( expected );
|
||||||
|
} );
|
||||||
|
} );
|
Loading…
Reference in New Issue