From dfcca807dee46682552735b956935cf123de27f7 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 24 Aug 2017 14:30:58 -0400 Subject: [PATCH] Add AutoObjectStore * src/store/AutoObjectStore.js: Add class. * test/store/AutoObjectStoreTest.js: Add test case. --- src/store/AutoObjectStore.js | 177 ++++++++++++++++++++++++++++++ test/store/AutoObjectStoreTest.js | 114 +++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 src/store/AutoObjectStore.js create mode 100644 test/store/AutoObjectStoreTest.js diff --git a/src/store/AutoObjectStore.js b/src/store/AutoObjectStore.js new file mode 100644 index 0000000..165dbb7 --- /dev/null +++ b/src/store/AutoObjectStore.js @@ -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 . + */ + +'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.} + */ + '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.} 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; + }, +} ); diff --git a/test/store/AutoObjectStoreTest.js b/test/store/AutoObjectStoreTest.js new file mode 100644 index 0000000..859521b --- /dev/null +++ b/test/store/AutoObjectStoreTest.js @@ -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 . + */ + +'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 ); + } ); +} );