Mike Gerwitz

Activist for User Freedom

aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/client/Client.js1
-rw-r--r--src/client/dapi/DataApiMediator.js117
-rw-r--r--src/dapi/DataApiManager.js15
-rw-r--r--src/dapi/MissingDataError.js26
-rw-r--r--test/client/dapi/DataApiMediatorTest.js230
5 files changed, 342 insertions, 47 deletions
diff --git a/src/client/Client.js b/src/client/Client.js
index 75d5c56..04b84fb 100644
--- a/src/client/Client.js
+++ b/src/client/Client.js
@@ -347,6 +347,7 @@ module.exports = Class( 'Client' )
this._factory.createDataApiMediator(
this.ui,
this._dataValidator,
+ this.program.dapimap,
() => this.getQuote()
).monitor( this._dapiManager );
diff --git a/src/client/dapi/DataApiMediator.js b/src/client/dapi/DataApiMediator.js
index 2778f58..2a757d2 100644
--- a/src/client/dapi/DataApiMediator.js
+++ b/src/client/dapi/DataApiMediator.js
@@ -21,7 +21,8 @@
'use strict';
-const { Class } = require( 'easejs' );
+const { Class } = require( 'easejs' );
+const MissingDataError = require( '../../dapi/MissingDataError' );
/**
@@ -47,6 +48,12 @@ module.exports = Class( 'DataApiMediator',
'private _data_validator': null,
/**
+ * DataAPI source/destination field map
+ * @type {Object}
+ */
+ 'private _dapi_map': null,
+
+ /**
* Function returning active quote
* @type {function():Quote}
*/
@@ -60,17 +67,28 @@ module.exports = Class( 'DataApiMediator',
* used to produce errors on fields to ensure that its state can be
* appropriately cleared.
*
+ * DAPI_MAP stores destination:source field mappings, where source is
+ * the result of the DataAPI call and destination is the target field in
+ * which to store those data.
+ *
* Since the active quote changes at runtime, this constructor accepts a
* quote function QUOTEF to return the active quote.
*
* @param {Ui} ui UI
* @param {DataValidator} data_validator data validator
+ * @param {Object} dapi_map field source and destination map
* @param {function():Quote} quotef nullary function returning quote
*/
- constructor( ui, data_validator, quotef )
+ constructor( ui, data_validator, dapi_map, quotef )
{
+ if ( typeof dapi_map !== 'object' )
+ {
+ throw TypeError( "dapi_map must be a key/value object" );
+ }
+
this._ui = ui;
this._data_validator = data_validator;
+ this._dapi_map = dapi_map;
this._quotef = quotef;
},
@@ -93,7 +111,7 @@ module.exports = Class( 'DataApiMediator',
]
handlers.forEach( ( [ event, handler ] ) =>
- dapi_manager.on( event, handler.bind( this ) )
+ dapi_manager.on( event, handler.bind( this, dapi_manager ) )
);
return this;
@@ -108,14 +126,15 @@ module.exports = Class( 'DataApiMediator',
* in RESULTS will be selected, if any. If there are no results in
* RESULTS, the set value will be the empty string.
*
- * @param {string} name field name
- * @param {number} index field index
- * @param {Object<value,label>} val_label value and label
- * @param {Object} results DataAPI result set
+ * @param {DataApiManager} dapi_manager DataAPI manager
+ * @param {string} name field name
+ * @param {number} index field index
+ * @param {Object<value,label>} val_label value and label
+ * @param {Object} results DataAPI result set
*
* @return {undefined}
*/
- 'private _updateFieldData'( name, index, val_label, results )
+ 'private _updateFieldData'( dapi_manager, name, index, val_label, results )
{
const group = this._ui.getCurrentStep().getElementGroup( name );
@@ -141,7 +160,7 @@ module.exports = Class( 'DataApiMediator',
// keep existing value if it exists in the result set, otherwise
// use the first value of the set
- const update = indexes.map( ( _, i ) =>
+ const field_update = indexes.map( ( _, i ) =>
( results[ existing[ i ] ] )
? existing[ i ]
: this._getDefaultValue( val_label )
@@ -151,19 +170,88 @@ module.exports = Class( 'DataApiMediator',
group.setOptions( name, i, val_label, existing[ i ] )
);
- quote.setData( { [name]: update } );
+
+ const update = this._populateWithMap(
+ dapi_manager, name, indexes, quote
+ );
+
+ update[ name ] = field_update;
+
+ quote.setData( update );
+ },
+
+
+ /**
+ * Generate bucket update with field expansion data
+ *
+ * If multiple indexes are provided, updates will be merged. If
+ * expansion data are missing, then the field will be ignored.
+ *
+ * @param {DataApiManager} dapi_manager manager responsible for fields
+ * @param {string} name field name
+ * @param {Array<number>} indexes field indexes
+ * @param {Quote} quote source quote
+ *
+ * @return {undefined}
+ */
+ 'private _populateWithMap'( dapi_manager, name, indexes, quote )
+ {
+ const map = this._dapi_map[ name ];
+
+ // calculate field expansions for each index, which contains an
+ // object suitable as-is for use with Quote#setData
+ const expansions = indexes.map( ( _, i ) =>
+ {
+ try
+ {
+ return dapi_manager.getDataExpansion(
+ name, i, quote, map, false, {}
+ );
+ }
+ catch ( e )
+ {
+ if ( e instanceof MissingDataError )
+ {
+ // this value is ignored below
+ return undefined;
+ }
+
+ throw e;
+ }
+ } );
+
+ // produce a final update that merges each of the expansions
+ return expansions.reduce( ( update, expansion, i ) =>
+ {
+ // it's important that we check here instead of using #filter on
+ // the array so that we maintain index association
+ if ( expansion === undefined )
+ {
+ return update;
+ }
+
+ // merge each key individually
+ Object.keys( expansion ).forEach( key =>
+ {
+ update[ key ] = update[ key ] || [];
+ update[ key ][ i ] = expansion[ key ][ i ];
+ } );
+
+ return update;
+ }, {} );
},
/**
* Clear field options
*
- * @param {string} name field name
- * @param {number} index field index
+ * @param {DataApiManager} dapi_manager DataAPI manager
+ * @param {string} name field name
+ * @param {number} index field index
*
* @return {undefined}
*/
- 'private _clearFieldOptions'( name, index )
+ 'private _clearFieldOptions'( dapi_manager, name, index )
{
const group = this._ui.getCurrentStep().getElementGroup( name );
@@ -180,12 +268,13 @@ module.exports = Class( 'DataApiMediator',
/**
* Clear field failures
*
+ * @param {DataApiManager} dapi_manager DataAPI manager
* @param {string} name field name
* @param {number} index field index
*
* @return {undefined}
*/
- 'private _clearFieldFailures'( name, index )
+ 'private _clearFieldFailures'( dapi_manager, name, index )
{
this._data_validator.clearFailures( {
[name]: [ index ],
diff --git a/src/dapi/DataApiManager.js b/src/dapi/DataApiManager.js
index 9679058..73e8343 100644
--- a/src/dapi/DataApiManager.js
+++ b/src/dapi/DataApiManager.js
@@ -1,7 +1,7 @@
/**
* Manages DataAPI requests and return data
*
- * Copyright (C) 2016 R-T Specialty, LLC.
+ * Copyright (C) 2016, 2018 R-T Specialty, LLC.
*
* This file is part of the Liza Data Collection Framework
*
@@ -19,8 +19,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-var Class = require( 'easejs' ).Class,
- EventEmitter = require( 'events' ).EventEmitter;
+const { Class } = require( 'easejs' );
+const { EventEmitter } = require( 'events' );
+const MissingDataError = require( './MissingDataError' );
/**
@@ -613,14 +614,14 @@ module.exports = Class( 'DataApiManager' )
)
{
var field_data = ( this._fieldData[ name ] || {} )[ index ],
- data = {};
+ data = {},
field_value = ( diff[ name ] || bucket.getDataByName( name ) )[ index ];
// if it's undefined, then the change probably represents a delete
if ( field_value === undefined )
{
( this._fieldDataEmitted[ name ] || [] )[ index ] = false;
- return;
+ return {};
}
// if we have no field data, try the "combined" index
@@ -638,9 +639,9 @@ module.exports = Class( 'DataApiManager' )
if ( !predictive && !( data ) && ( field_value !== '' ) )
{
// hmm..that's peculiar.
- this.emit( 'error', Error(
+ throw MissingDataError(
'Data missing for field ' + name + '[' + index + ']!'
- ) );
+ );
}
else if ( !data )
{
diff --git a/src/dapi/MissingDataError.js b/src/dapi/MissingDataError.js
new file mode 100644
index 0000000..0c72611
--- /dev/null
+++ b/src/dapi/MissingDataError.js
@@ -0,0 +1,26 @@
+/**
+ * MissingDataError
+ *
+ * Copyright (C) 2018 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/>.
+ */
+
+const { Class } = require( 'easejs' );
+
+
+module.exports = Class( 'MissingDataError' )
+ .extend( Error, {} );
diff --git a/test/client/dapi/DataApiMediatorTest.js b/test/client/dapi/DataApiMediatorTest.js
index 8ad4d31..5272fe4 100644
--- a/test/client/dapi/DataApiMediatorTest.js
+++ b/test/client/dapi/DataApiMediatorTest.js
@@ -22,7 +22,11 @@
'use strict';
const { expect } = require( 'chai' );
-const Sut = require( '../../../' ).client.dapi.DataApiMediator;
+
+const {
+ client: { dapi: { DataApiMediator: Sut } },
+ dapi: { MissingDataError },
+} = require( '../../../' );
describe( "DataApiMediator", () =>
@@ -48,7 +52,7 @@ describe( "DataApiMediator", () =>
} );
const ui = createStubUi( {} ); // no field groups
- const sut = Sut( ui, {}, getQuote ).monitor( dapi_manager );
+ const sut = Sut( ui, {}, {}, getQuote ).monitor( dapi_manager );
dapi_manager.emit( 'updateFieldData', '', 0, {}, {} );
} );
@@ -59,41 +63,83 @@ describe( "DataApiMediator", () =>
name: 'foo',
index: 0,
value: [ "first", "second" ],
- expected: { foo: [ "first" ] },
+ expected: {
+ foo: [ "first" ],
+ dest1: [ "src1data" ],
+ dest2: [ "src2data" ],
+ },
val_label: [
{ value: "first result", label: "first" },
],
- results: { first: {}, second: {} },
+ results: {
+ first: { src1: "src1data", src2: "src2data" },
+ second: {},
+ },
+
+ expansion: [ {
+ dest1: [ "src1data" ],
+ dest2: [ "src2data" ],
+ } ],
},
{
label: "keeps existing value if in result set (second index)",
name: 'bar',
index: 1,
value: [ "first", "second" ],
- expected: { bar: [ , "second" ] },
+ expected: {
+ bar: [ , "second" ],
+ dest1: [ , "src1data_2" ],
+ dest2: [ , "src2data_2" ],
+ },
val_label: [
{ value: "first result", label: "first" },
{ value: "second result", label: "second" },
],
- results: { first: {}, second: {} },
+ results: {
+ first: {},
+ second: { src1: "src1data_2", src2: "src2data_2" },
+ },
+
+ expansion: [ , {
+ dest1: [ , "src1data_2" ],
+ dest2: [ , "src2data_2" ],
+ } ],
},
{
label: "keeps existing value if in result set (all indexes)",
name: 'bar',
index: -1,
value: [ "first", "second" ],
- expected: { bar: [ "first", "second" ] },
+ expected: {
+ bar: [ "first", "second" ],
+ dest1: [ "src1data", "src1data_2" ],
+ dest2: [ "src2data", "src2data_2" ],
+ },
val_label: [
{ value: "first result", label: "first" },
{ value: "second result", label: "second" },
],
- results: { first: {}, second: {} },
+ results: {
+ first: { src1: "src1data", src2: "src2data" },
+ second: { src1: "src1data_2", src2: "src2data_2" },
+ },
+
+ expansion: [
+ {
+ dest1: [ "src1data" ],
+ dest2: [ "src2data" ],
+ },
+ {
+ dest1: [ , "src1data_2" ],
+ dest2: [ , "src2data_2" ],
+ },
+ ],
},
{
@@ -101,42 +147,84 @@ describe( "DataApiMediator", () =>
name: 'foo',
index: 0,
value: [ "does not", "exist" ],
- expected: { foo: [ "first result" ] },
+ expected: {
+ foo: [ "first result" ],
+ desta: [ "src1data" ],
+ destb: [ "src2data" ],
+ },
val_label: [
{ value: "first result", label: "first" },
{ value: "second result", label: "second" },
],
- results: {},
+ results: {
+ first: { src1: "src1data", src2: "src2data" },
+ second: {},
+ },
+
+ expansion: [ {
+ desta: [ "src1data" ],
+ destb: [ "src2data" ],
+ } ],
},
{
label: "uses first value of result if existing not in result set (second index)",
name: 'foo',
index: 1,
value: [ "does not", "exist" ],
- expected: { foo: [ , "first result" ] },
+ expected: {
+ foo: [ , "first result" ],
+ desta: [ , "src1data" ],
+ destb: [ , "src2data" ],
+ },
val_label: [
{ value: "first result", label: "first" },
{ value: "second result", label: "second" },
],
- results: {},
+ results: {
+ first: { src1: "src1data", src2: "src2data" },
+ second: {},
+ },
+
+ expansion: [ , {
+ desta: [ , "src1data" ],
+ destb: [ , "src2data" ],
+ } ],
},
{
label: "uses first value of result if existing not in result set (all indexes)",
name: 'foo',
index: -1,
value: [ "does not", "exist" ],
- expected: { foo: [ "first result", "first result" ] },
+ expected: {
+ foo: [ "first result", "first result" ],
+ desta: [ "src1data", "src1data" ],
+ destb: [ "src1data", "src2data" ],
+ },
val_label: [
{ value: "first result", label: "first" },
{ value: "second result", label: "second" },
],
- results: {},
+ results: {
+ first: { src1: "src1data", src2: "src2data" },
+ second: {},
+ },
+
+ expansion: [
+ {
+ desta: [ "src1data" ],
+ destb: [ "src1data" ],
+ },
+ {
+ desta: [ , "src1data" ],
+ destb: [ , "src2data" ],
+ },
+ ],
},
{
@@ -144,38 +232,66 @@ describe( "DataApiMediator", () =>
name: 'foo',
index: 0,
value: [ "foo" ],
- expected: { foo: [ "" ] },
+ expected: {
+ foo: [ "" ],
+ dest1: [ "" ],
+ },
val_label: [],
results: {},
+ expansion: [ {
+ dest1: [ "" ],
+ } ],
},
{
label: "uses empty string if empty result set (second index)",
name: 'foo',
index: 1,
value: [ "foo", "bar" ],
- expected: { foo: [ , "" ] },
+ expected: {
+ foo: [ , "" ],
+ dest1: [ , "" ],
+ },
val_label: [],
results: {},
+ expansion: [ , {
+ dest1: [ , "" ],
+ } ],
},
{
label: "uses empty string if empty result set (all indexes)",
name: 'foo',
index: -1,
value: [ "foo", "bar" ],
- expected: { foo: [ "", "" ] },
+ expected: {
+ foo: [ "", "" ],
+ dest1: [ "", "" ],
+ dest2: [ "", "" ],
+ },
val_label: [],
results: {},
+ expansion: [
+ {
+ dest1: [ "" ],
+ dest2: [ "" ],
+ },
+ {
+ dest1: [ , "" ],
+ dest2: [ , "" ],
+ },
+ ],
},
- ].forEach( ( { label, name, index, value, expected, val_label, results }, i ) =>
+ ].forEach( ( {
+ label, name, index, value, expected, val_label, results, expansion
+ } ) =>
{
it( label, done =>
{
let set_options = false;
- const getQuote = () => ( {
+ const quote = {
getDataByName( given_name )
{
expect( given_name ).to.equal( name );
@@ -191,9 +307,29 @@ describe( "DataApiMediator", () =>
done();
},
- } );
+ };
+
+ const getQuote = () => quote;
- const dapi_manager = createStubDapiManager();
+ const dapi_manager = createStubDapiManager( expansion );
+
+ // this isn't a valid map, but comparing the objects will
+ // ensure that the map is actually used
+ const dapimap = { foo: {}, bar: {} };
+
+ dapi_manager.getDataExpansion = (
+ given_name, given_index, given_quote, given_map,
+ predictive, diff
+ ) =>
+ {
+ expect( given_name ).to.equal( name );
+ expect( given_quote ).to.equal( quote );
+ expect( given_map ).to.deep.equal( dapimap );
+ expect( predictive ).to.be.false;
+ expect( diff ).to.deep.equal( {} );
+
+ return expansion[ given_index ];
+ };
const field_groups = {
[name]: {
@@ -209,14 +345,56 @@ describe( "DataApiMediator", () =>
},
};
- const ui = createStubUi( field_groups );
- const sut = Sut( ui, {}, getQuote ).monitor( dapi_manager );
+ const ui = createStubUi( field_groups );
+
+ const sut = Sut( ui, {}, { [name]: dapimap }, getQuote )
+ .monitor( dapi_manager );
dapi_manager.emit(
'updateFieldData', name, index, val_label, results
);
} );
} );
+
+
+ it( 'does not perform expansion if data are not available', done =>
+ {
+ const dapi_manager = createStubDapiManager();
+
+ dapi_manager.getDataExpansion = () =>
+ {
+ throw MissingDataError(
+ 'this should happen, but should be caught'
+ );
+ };
+
+ const name = 'foo';
+ const value = 'bar';
+
+ const getQuote = () => ( {
+ getDataByName: () => [ value ],
+ setData( given_data )
+ {
+ // only the value should be set with no expansion data
+ expect( given_data ).to.deep.equal( {
+ [name]: [ value ],
+ } );
+
+ done();
+ },
+ } );
+
+ const field_groups = { [name]: { setOptions() {} } };
+
+ const ui = createStubUi( field_groups );
+ const sut = Sut( ui, {}, {}, getQuote ).monitor( dapi_manager );
+
+ const val_label = [
+ { value: value, label: "bar" },
+ ];
+
+ dapi_manager.emit( 'updateFieldData', name, 0, val_label, {} );
+ } );
} );
@@ -229,7 +407,7 @@ describe( "DataApiMediator", () =>
const field_groups = {}; // no groups
const ui = createStubUi( field_groups );
- const sut = Sut( ui ).monitor( dapi_manager );
+ const sut = Sut( ui, {}, {} ).monitor( dapi_manager );
dapi_manager.emit( 'clearFieldData', 'unknown', 0 );
} );
@@ -254,7 +432,7 @@ describe( "DataApiMediator", () =>
};
const ui = createStubUi( field_groups );
- const sut = Sut( ui ).monitor( dapi_manager );
+ const sut = Sut( ui, {}, {} ).monitor( dapi_manager );
dapi_manager.emit( 'clearFieldData', name, index );
} );
@@ -279,7 +457,7 @@ describe( "DataApiMediator", () =>
};
const ui = {}; // unused by this event
- const sut = Sut( ui, data_validator ).monitor( dapi_manager );
+ const sut = Sut( ui, data_validator, {} ).monitor( dapi_manager );
dapi_manager.emit( 'fieldLoaded', name, index );
} );