Mike Gerwitz

Activist for User Freedom

aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Gerwitz <gerwitzm@lovullo.com>2017-01-27 11:07:01 -0500
committerMike Gerwitz <gerwitzm@lovullo.com>2017-01-30 00:29:15 -0500
commitb62673791b176f2f06b902f1c576b78cea3fdfa5 (patch)
tree0755163172bced0ed6641ad0eb1ae8f5a0c90b3c
parent29fb75d1a386ec6b48eda0cda4b72113783d58f7 (diff)
downloadliza-b62673791b176f2f06b902f1c576b78cea3fdfa5.tar.gz
liza-b62673791b176f2f06b902f1c576b78cea3fdfa5.tar.bz2
liza-b62673791b176f2f06b902f1c576b78cea3fdfa5.zip
Add PatternProxy Store trait
Life is so much less miserable now that the project is supporting ES6. * src/store/PatternProxy.js: Add trait. * src/store/StorePatternError.js: Add Error. * test/store/PatternProxyTest.js: Add test case. DEV-2296
-rw-r--r--src/store/PatternProxy.js222
-rw-r--r--src/store/StorePatternError.js31
-rw-r--r--test/store/PatternProxyTest.js164
3 files changed, 417 insertions, 0 deletions
diff --git a/src/store/PatternProxy.js b/src/store/PatternProxy.js
new file mode 100644
index 0000000..e541106
--- /dev/null
+++ b/src/store/PatternProxy.js
@@ -0,0 +1,222 @@
+/**
+ * Store proxy to sub-stores based on key patterns
+ *
+ * Copyright (C) 2017 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 <http://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+const Trait = require( 'easejs' ).Trait;
+const Class = require( 'easejs' ).Class;
+const Store = require( './Store' );
+const StorePatternError = require( './StorePatternError' );
+
+
+/**
+ * Proxy to sub-stores based on key patterns
+ *
+ * Patterns are an array of the form `[pattern, store]`. If a key matches
+ * `pattern`, then the request is proxied to `store`. If the pattern
+ * contains a match group, then group 1 will be used as the key for `store`.
+ *
+ * @example
+ * const store1 = Store();
+ * const store2 = Store();
+ *
+ * const patterns = [
+ * [ /^foo:/, store1 ],
+ * [ /^bar:(.*)$/, store2 ],
+ * ];
+ *
+ * const proxy = Store.use( PatternProxy( patterns ) )();
+ *
+ * // Promise resolving to "baz"
+ * proxy.add( 'foo:bar', 'baz' ).then( () => store1.get( 'foo:bar' );
+ *
+ * // Promise resolving to "quux"
+ * proxy.add( 'bar:baz', 'quux' ).then( () => store1.get( 'baz' );
+ *
+ * // Promise rejecting with StorePatternError
+ * proxy.add( 'unknown', 'nope' );
+ *
+ * // Promise resolving to "quuux"
+ * store2.add( 'quux', 'quuux' )
+ * .then( () => proxy.get( 'bar:quux' );)
+ *
+ * Note that this will perform a linear search on each of the patterns. You
+ * can optimize this by putting the patterns in order of most frequently
+ * encountered, descending.
+ *
+ * If a key fails to match any pattern, a `StorePatternError` is thrown. To
+ * provide a default pattern, create a regular expression that matches on
+ * any input (e.g. `/./`).)
+ */
+module.exports = Trait( 'PatternProxy' )
+ .implement( Store )
+ .extend(
+{
+ /**
+ * Pattern mapping to internal store
+ * @type {Array.<Array.<RegExp,Store>>}
+ */
+ 'private _patterns': [],
+
+
+ /**
+ * Define pattern map
+ *
+ * `patterns` should be an array of arrays, of this form:
+ *
+ * @example
+ * [ [ /a/, storea ], [ /^b:(.*)$/, storeb ] ]
+ *
+ * That is: a regular expression that, when matched, maps to the
+ * associated store. If the regular expression contains a match group,
+ * group 1 will be used as the key name in the destination store.
+ *
+ * @param {Array.<Array.<RegExp,Store>>} patterns pattern map
+ */
+ __mixin( patterns )
+ {
+ this._patterns = this._validatePatternMap( patterns );
+ },
+
+
+ /**
+ * Verify that pattern map contains valid mappings
+ *
+ * @param {Array.<Array.<RegExp,Store>>} patterns pattern map
+ *
+ * @return {Array} `patterns` argument
+ */
+ 'private _validatePatternMap'( patterns )
+ {
+ if ( !Array.isArray( patterns ) )
+ {
+ throw TypeError( "Pattern map must be an array" );
+ }
+
+ patterns.forEach( ( [ pattern, store ], i ) =>
+ {
+ if ( !( pattern instanceof RegExp ) )
+ {
+ throw TypeError(
+ `Pattern must be a RegExp at index ${i}`
+ );
+ }
+
+ if ( !Class.isA( Store, store ) )
+ {
+ throw TypeError(
+ `Pattern must map to Store at index ${i}`
+ );
+ }
+ } );
+
+ return patterns;
+ },
+
+
+ /**
+ * Proxy item with value `value` to internal store matching against `key`
+ *
+ * Note that the key stored may be different than `key`. This
+ * information is important only if the internal stores are not
+ * encapsulated.
+ *
+ * @param {string} key store key to match against
+ * @param {*} value value for key
+ *
+ * @return {Promise.<Store>} promise to add item to store, resolving to
+ * self (for chaining)
+ */
+ 'virtual public abstract override add'( key, value )
+ {
+ return this.matchKeyToStore( key )
+ .then( ( { store, key:skey } ) => store.add( skey, value ) );
+ },
+
+
+ /**
+ * Retrieve item from an internal store matching against `key`
+ *
+ * Note that the key stored may be different than `key`. This
+ * information is important only if the internal stores are not
+ * encapsulated.
+ *
+ * The promise will be rejected if the key is unavailable.
+ *
+ * @param {string} key store key to pattern match
+ *
+ * @return {Promise} promise for the key value
+ */
+ 'virtual public abstract override get'( key )
+ {
+ // XXX
+ return this.matchKeyToStore( key )
+ .then( ( { store, key:skey } ) => store.get( skey ) );
+ },
+
+
+ /**
+ * Attempt to map `key` to a Store
+ *
+ * If no patterns match against `key`, the Promise will be rejected.
+ *
+ * @param {string} key key to match against
+ *
+ * @return {Promise.<Object>} {store,key} on success,
+ * StorePatternError on failure
+ */
+ 'protected matchKeyToStore'( key )
+ {
+ for ( let [ pattern, store ] of this._patterns )
+ {
+ const [ match, skey=key ] = key.match( pattern ) || [];
+
+ if ( match !== undefined )
+ {
+ return Promise.resolve( {
+ store: store,
+ key: skey
+ } );
+ }
+ }
+
+ return Promise.reject( StorePatternError(
+ `Key '${key}' does not match any pattern`
+ ) );
+ },
+
+
+ /**
+ * Clear all pattern stores
+ *
+ * This simply calls `#clear` on all stores associated with all
+ * patterns.
+ *
+ * @return {Promise.<Store>} promise to add item to store, resolving to
+ * self (for chaining)
+ */
+ 'virtual public abstract override clear'()
+ {
+ return Promise.all(
+ this._patterns.map( ( [ , store ] ) => store.clear() )
+ );
+ },
+} );
diff --git a/src/store/StorePatternError.js b/src/store/StorePatternError.js
new file mode 100644
index 0000000..cc51687
--- /dev/null
+++ b/src/store/StorePatternError.js
@@ -0,0 +1,31 @@
+/**
+ * Error when Store pattern matching fails
+ *
+ * Copyright (C) 2017 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 <http://www.gnu.org/licenses/>.
+ */
+
+var Class = require( 'easejs' ).Class;
+
+
+/**
+ * Store pattern matching failure
+ *
+ * A key request did not match any patterns known to the Store.
+ */
+module.exports = Class( 'StorePatternError' )
+ .extend( ReferenceError, {} );
diff --git a/test/store/PatternProxyTest.js b/test/store/PatternProxyTest.js
new file mode 100644
index 0000000..cfddddd
--- /dev/null
+++ b/test/store/PatternProxyTest.js
@@ -0,0 +1,164 @@
+/**
+ * Test case for PatternProxy trait
+ *
+ * Copyright (C) 2017 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 <http://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+const store = require( '../../' ).store;
+const chai = require( 'chai' );
+const expect = chai.expect;
+const Store = store.MemoryStore;
+const Sut = store.PatternProxy;
+const sinon = require( 'sinon' );
+
+chai.use( require( 'chai-as-promised' ) );
+
+
+describe( 'store.PatternProxy', () =>
+{
+ describe( 'fails on invalid pattern map', () =>
+ {
+ [
+ // not a pattern
+ [ {}, Store() ],
+
+ // not a Store
+ [ /^./, {} ],
+
+ // missing Store
+ [ /^./ ],
+
+ // missing all
+ [],
+ ].forEach( ( patterns, i ) =>
+ it( `(${i})`, () =>
+ {
+ expect( () => Store.use( Sut( [ patterns ] ) )() )
+ .to.throw( TypeError );
+ } )
+ );
+ } );
+
+
+ it( 'proxies #add by pattern', () =>
+ {
+ const store1 = Store();
+ const store2 = Store();
+
+ // second strips
+ const patterns = [
+ [ /^foo:/, store1 ],
+ [ /^bar:(.*)$/, store2 ],
+ ];
+
+ return Promise.all( [
+ expect(
+ Store.use( Sut( patterns ) )()
+ .add( 'foo:moo', 'moo' )
+ .then( store => store1.get( 'foo:moo' ) )
+ ).to.eventually.equal( 'moo' ),
+
+ expect(
+ Store.use( Sut( patterns ) )()
+ .add( 'bar:quux', 'quuxval' )
+ .then( store => store2.get( 'quux' ) )
+ ).to.eventually.equal( 'quuxval' ),
+ ] );
+ } );
+
+
+ it( 'proxies #get by pattern', () =>
+ {
+ const store1 = Store();
+ const store2 = Store();
+
+ // second strips
+ const patterns = [
+ [ /^foo:/, store1 ],
+ [ /^bar:(.*)$/, store2 ],
+ ];
+
+ const sut = Store.use( Sut( patterns ) )();
+
+ return Promise.all( [
+ expect(
+ store1.add( 'foo:bar', 'moo' )
+ .then( () => sut.get( 'foo:bar' ) )
+ ).to.eventually.equal( 'moo' ),
+
+ expect(
+ store2.add( 'quux', 'quuxval' )
+ .then( () => sut.get( 'bar:quux' ) )
+ ).to.eventually.equal( 'quuxval' ),
+ ] );
+ } );
+
+
+ // if no matches, error (like traditional functional pattern matching)
+ it( 'fails on #add or #get when match fails', () =>
+ {
+ const patterns = [ [ /moo/, Store() ] ];
+
+ return Promise.all( [
+ expect(
+ Store.use( Sut( patterns ) )()
+ .add( 'uh', 'no' )
+ ).to.eventually.be.rejectedWith( store.StorePatternError ),
+
+ expect(
+ Store.use( Sut( patterns ) )()
+ .get( 'sorry', 'sir' )
+ ).to.eventually.be.rejectedWith( store.StorePatternError ),
+ ] );
+ } );
+
+
+ describe( '#clear', () =>
+ {
+ it( 'invokes #clear on all contained stores', () =>
+ {
+ const store1 = Store();
+ const store2 = Store();
+
+ const mocks = [ store1, store2 ].map( store =>
+ {
+ const mock = sinon.mock( store );
+
+ mock.expects( 'clear' ).once();
+ return mock;
+ } );
+
+ const patterns = [
+ [ /^a/, store1 ],
+ [ /^b/, store2 ],
+ ];
+
+ const sut = Store.use( Sut( patterns ) )();
+
+ return sut.clear()
+ .then( given_sut => {
+ // TODO: uncomment once `this.__inst' in Traits is fixed
+ // in GNU ease.js
+ // expect( given_sut ).to.equal( sut );
+ mocks.forEach( mock => mock.verify() );
+ } );
+ } );
+ } );
+} );