diff --git a/src/bucket/StagingBucket.js b/src/bucket/StagingBucket.js
index 665d5c7..e69aec6 100644
--- a/src/bucket/StagingBucket.js
+++ b/src/bucket/StagingBucket.js
@@ -80,6 +80,12 @@ module.exports = Class( 'StagingBucket' )
*/
'private _dirty': false,
+ /**
+ * Prevent setCommittedValues from bypassing staging
+ * @type {boolean}
+ */
+ 'private _noStagingBypass': false,
+
/**
* Initializes staging bucket with the provided data bucket
@@ -155,6 +161,11 @@ module.exports = Class( 'StagingBucket' )
*/
'public setCommittedValues': function( data /*, ...*/ )
{
+ if ( this._noStagingBypass )
+ {
+ return this.setValues.apply( this, arguments );
+ }
+
this._bucket.setValues.apply( this._bucket, arguments );
// no use in triggering a pre-update, since these values are
@@ -165,6 +176,20 @@ module.exports = Class( 'StagingBucket' )
},
+ /**
+ * Prevent #setCommittedValues from bypassing staging
+ *
+ * When set, #setCommittedValues will act as an alias of #setValues.
+ *
+ * @return {StagingBucket} self
+ */
+ 'public forbidBypass'()
+ {
+ this._noStagingBypass = true;
+ return this;
+ },
+
+
/**
* Determine whether values have changed
*
@@ -179,31 +204,108 @@ module.exports = Class( 'StagingBucket' )
*/
'private _hasChanged': function( data, merge_index )
{
+ let changed = false;
+
for ( let name in data )
{
- let values = data[ name ];
- let cur = this._curdata[ name ] || [];
+ let values = data[ name ];
+ let cur = this._curdata[ name ] || [];
+ let len = this._length( values );
+ let has_null = ( len !== values.length );
- if ( !merge_index && ( values.length !== cur.length ) )
+ let merge_len_change = (
+ merge_index && has_null && ( len < cur.length )
+ );
+
+ let replace_len_change = (
+ !merge_index && ( len !== cur.length )
+ );
+
+ // quick change check (index removal if merge_index, or index
+ // count change if not merge_index)
+ if ( merge_len_change || replace_len_change )
{
- return true;
+ changed = true;
+ continue;
}
- for ( let index in values )
+ for ( let index = 0; index < len; index++ )
{
if ( merge_index && ( values[ index ] === undefined ) )
{
continue;
}
- if ( values[ index ] !== cur[ index ] )
+ if ( !this._deepEqual( values[ index ], cur[ index ] ) )
{
- return true;
+ changed = true;
+ continue;
}
+
+ // unchanged
+ values[ index ] = undefined;
+ }
+
+ // if nothing is left, remove entirely
+ if ( !values.some( x => x !== undefined ) )
+ {
+ delete data[ name ];
}
}
- return false;
+ return changed;
+ },
+
+
+ /**
+ * Get actual length of vector
+ *
+ * This considers when the last element of the vector is a null value,
+ * which is a truncation indicator.
+ *
+ * @param {Array} values value vector
+ *
+ * @return {number} length of vector considering truncation
+ */
+ 'private _length'( values )
+ {
+ if ( values[ values.length - 1 ] === null )
+ {
+ return values.length - 1;
+ }
+
+ return values.length;
+ },
+
+
+ /**
+ * Recursively check for equality of two vavlues
+ *
+ * This only recognizes nested arrays (vectors). They are not
+ * traditionally encountered in the bucket, but may exist.
+ *
+ * The final comparison is by string equality, since bucket values are
+ * traditionally strings.
+ *
+ * @param {*} a first vector or scalar
+ * @param {*} b second vector or scalar
+ *
+ * @return {boolean} whether `a` and `b` are equal
+ */
+ 'private _deepEqual'( a, b )
+ {
+ if ( Array.isArray( a ) )
+ {
+ if ( !Array.isArray( b ) || ( a.length !== b.length ) )
+ {
+ return false;
+ }
+
+ return a.map( ( item, i ) => this._deepEqual( item, b[ i ] ) )
+ .every( res => res === true );
+ }
+
+ return ''+a === ''+b;
},
diff --git a/src/calc/Calc.js b/src/calc/Calc.js
index e0d7383..5410ba1 100644
--- a/src/calc/Calc.js
+++ b/src/calc/Calc.js
@@ -27,7 +27,14 @@ function _each( data, value, callback )
for ( var i = 0; i < data_len; i++ )
{
+ // index removals are null
+ if ( data[ i ] === null )
+ {
+ continue;
+ }
+
cur_val = ( value[ i ] !== undefined ) ? value[ i ] : cur_val;
+
result.push( callback( data[ i ], cur_val, i ) );
}
@@ -51,6 +58,11 @@ exports.join = function( data, value )
{
return _each( data, value, function( arr, delimiter )
{
+ if ( !Array.isArray( arr ) )
+ {
+ arr = [];
+ }
+
return arr.join( delimiter );
});
};
@@ -121,7 +133,11 @@ exports.length = function( data )
break;
}
- result.push( item.length );
+ var len = ( item[ item.length - 1 ] === null )
+ ? item.length - 1
+ : item.length;
+
+ result.push( len );
}
return result;
@@ -214,8 +230,8 @@ exports.date = function()
// formatted as
return [
now.getFullYear() + '-'
- + ( now.getMonth() + 1 ) + '-'
- + now.getDate()
+ + ( '0' + ( now.getMonth() + 1 ) ).substr( -2 ) + '-'
+ + ( '0' + now.getDate() ).substr( -2 )
];
};
@@ -277,8 +293,8 @@ exports.relativeDate = function( data, value )
// return in the YYYY-MM-DD format, since that's what our fields are
// formatted as
return date_new.getFullYear() + '-'
- + ( date_new.getMonth() + 1 ) + '-'
- + date_new.getDate();
+ + ( '0' + ( date_new.getMonth() + 1 ) ).substr( -2 ) + '-'
+ + ( '0' + date_new.getDate() ).substr( -2 );
} );
};
@@ -647,8 +663,43 @@ exports.value = function( data, indexes )
};
+exports.repeat = function( data, value )
+{
+ var times = value[ 0 ] || 0;
+ var result = [];
+
+ while ( times-- > 0 )
+ {
+ result.push( data );
+ }
+
+ return result;
+};
+
+
+exports.repeatConcat = function( data, value )
+{
+ var times = value[ 0 ] || 0;
+ var result = [];
+
+ while ( times-- > 0 )
+ {
+ result = result.concat( data );
+ }
+
+ return result;
+};
+
+
+exports.index = function( data, value )
+{
+ var index = value[ 0 ] || 0;
+
+ return data[ index ] || [];
+};
+
+
exports[ 'void' ] = function()
{
return [];
};
-
diff --git a/src/dapi/DataApi.js b/src/dapi/DataApi.js
index 5ae6c41..fc6bf3f 100644
--- a/src/dapi/DataApi.js
+++ b/src/dapi/DataApi.js
@@ -19,7 +19,9 @@
* along with this program. If not, see .
*/
-var Interface = require( 'easejs' ).Interface;
+'use strict';
+
+const { Interface } = require( 'easejs' );
/**
@@ -42,8 +44,8 @@ module.exports = Interface( 'DataApi',
* The first parameter of the callback shall contain an Error in the event
* of a failure; otherwise, it shall be null.
*
- * @param {Object=} data request params
- * @param {function(?Error,*)} callback continuation upon reply
+ * @param {?Object|string} data params or post data
+ * @param {function(?Error,*):string} callback continuation upon reply
*
* @return {DataApi} self
*/
diff --git a/src/dapi/DataApiFactory.js b/src/dapi/DataApiFactory.js
index 446df54..9c7dfb4 100644
--- a/src/dapi/DataApiFactory.js
+++ b/src/dapi/DataApiFactory.js
@@ -19,13 +19,17 @@
* along with this program. If not, see .
*/
+'use strict';
+
const Class = require( 'easejs' ).Class;
const HttpDataApi = require( './http/HttpDataApi' );
const XhrHttpImpl = require( './http/XhrHttpImpl' );
const JsonResponse = require( './format/JsonResponse' );
+const ResponseApply = require( './format/ResponseApply' );
const RestrictedDataApi = require( './RestrictedDataApi' );
const StaticAdditionDataApi = require( './StaticAdditionDataApi' );
const BucketDataApi = require( './BucketDataApi' );
+const QuoteDataApi = require( './QuoteDataApi' );
/**
@@ -36,8 +40,8 @@ module.exports = Class( 'DataApiFactory',
/**
* Return a DataApi instance for the requested service type
*
- * The source and method have type-specific meaning; that is, "source" may
- * be a URL and "method" may be get/post for a RESTful service.
+ * The source and method have type-specific meaning; that is, "source"
+ * may be a URL and "method" may be get/post for a RESTful service.
*
* @param {string} type service type (e.g. "rest")
* @param {Object} desc API description
@@ -46,39 +50,11 @@ module.exports = Class( 'DataApiFactory',
*/
'public fromType': function( type, desc, bucket )
{
- var api = null,
- source = ( desc.source || '' ),
- method = ( desc.method || '' ),
+ const static_data = ( desc['static'] || [] );
+ const nonempty = !!desc.static_nonempty;
+ const multiple = !!desc.static_multiple;
- static_data = ( desc['static'] || [] ),
- nonempty = !!desc.static_nonempty,
- multiple = !!desc.static_multiple;
-
- switch ( type )
- {
- case 'rest':
- const impl = this.createHttpImpl();
-
- api = HttpDataApi.use( JsonResponse )(
- source,
- method.toUpperCase(),
- impl
- );
- break;
-
- case 'local':
- // currently, only local bucket data sources are supported
- if ( source !== 'bucket' )
- {
- throw Error( "Unknown local data API source: " + source );
- }
-
- api = BucketDataApi( bucket, desc.retvals );
- break;
-
- default:
- throw Error( 'Unknown data API type: ' + type );
- };
+ const api = this._createDataApi( type, desc, bucket );
return RestrictedDataApi(
StaticAdditionDataApi( api, nonempty, multiple, static_data ),
@@ -87,6 +63,90 @@ module.exports = Class( 'DataApiFactory',
},
+ /**
+ * Create DataApi instance
+ *
+ * @param {string} type API type
+ * @param {Object} desc API descriptor
+ * @param {Bucket} bucket data bucket
+ *
+ * @return {DataApi}
+ */
+ 'private _createDataApi'( type, desc, bucket )
+ {
+ const source = ( desc.source || '' );
+ const method = ( desc.method || '' );
+ const enctype = ( desc.enctype || '' );
+
+ switch ( type )
+ {
+ case 'rest':
+ return this._createHttp(
+ HttpDataApi.use( JsonResponse ),
+ source,
+ method,
+ enctype
+ );
+
+ case 'local':
+ // currently, only local bucket data sources are supported
+ if ( source !== 'bucket' )
+ {
+ throw Error( "Unknown local data API source: " + source );
+ }
+
+ return BucketDataApi( bucket, desc.retvals );
+
+ case 'quote':
+ return QuoteDataApi(
+ this._createHttp(
+ HttpDataApi
+ .use( JsonResponse )
+ .use( ResponseApply( data => [ data ] ) ),
+ source,
+ method,
+ enctype
+ )
+ );
+
+ default:
+ throw Error( 'Unknown data API type: ' + type );
+ };
+ },
+
+
+ /**
+ * Create HttpDataApi instance
+ *
+ * The `Base` is intended to allow for the caller to mix traits in.
+ *
+ * @param {HttpDataApi} Base HttpDataApi type
+ * @param {string} source URL
+ * @param {string} method HTTP method
+ * @param {string} enctype MIME media type (for POST)
+ *
+ * @return {HttpDataApi}
+ */
+ 'private _createHttp'( Base, source, method, enctype )
+ {
+ const impl = this.createHttpImpl();
+
+ return Base(
+ source,
+ method.toUpperCase(),
+ impl,
+ enctype
+ );
+ },
+
+
+ /**
+ * Create HttpImpl
+ *
+ * This is simply intended to allow subtypes to override the type.
+ *
+ * @return {XhrHttpImpl}
+ */
'virtual protected createHttpImpl'()
{
return XhrHttpImpl( XMLHttpRequest );
diff --git a/src/dapi/QuoteDataApi.js b/src/dapi/QuoteDataApi.js
new file mode 100644
index 0000000..16e35b7
--- /dev/null
+++ b/src/dapi/QuoteDataApi.js
@@ -0,0 +1,143 @@
+/**
+ * Transform key/value data into standard quote request
+ *
+ * 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 .
+ *
+ * This is insurance-specific using a standardized request format for
+ * producing insurance quote data.
+ */
+
+
+const { Class } = require( 'easejs' );
+const DataApi = require( './DataApi' );
+const EventEmitter = require( 'events' ).EventEmitter;
+
+
+/**
+ * Structure flat key/value data for quote request
+ *
+ * The request structure can be seen in #mapData. No fields are required to
+ * be present---they all have defaults; the philosophy is to allow the
+ * server to fail if necessary. Basic validations (e.g. ensuring correct
+ * data type and format) may be added in the future.
+ *
+ * This DataApi is responsible only for data transformation---it is expected
+ * to decorate a DataApi capable of performing an actual data transfer.
+ */
+module.exports = Class( 'QuoteDataApi' )
+ .implement( DataApi )
+ .extend(
+{
+ /**
+ * Decorated DataApi
+ *
+ * @type {DataApi}
+ */
+ 'private _dapi': null,
+
+
+ /**
+ * Initialize with DataApi to decorate
+ *
+ * @param {DataApi} dapi subject to decorate
+ */
+ constructor( dapi )
+ {
+ if ( !( Class.isA( DataApi, dapi ) ) )
+ {
+ throw TypeError(
+ 'Expected object of type DataApi; given: ' + data_api
+ );
+ }
+
+ this._dapi = dapi;
+ },
+
+
+ /**
+ * Request data from the service
+ *
+ * @param {Object=} data request params
+ * @param {function(?Error,Object)=} callback server response callback
+ *
+ * @return {DataApi} self
+ */
+ 'public request'( data, callback )
+ {
+ this._dapi.request( this.mapData( data ), callback );
+ },
+
+
+ /**
+ * Map key/value data into quote request
+ *
+ * @param {Object} data key/value data
+ *
+ * @return {Object} mapped request data
+ */
+ 'protected mapData'( data )
+ {
+ const rate_date = data.rate_date || data.effective_date || "";
+
+ return {
+ "effective_date": this._formatDate( data.effective_date || "" ),
+ "rate_date": this._formatDate( rate_date ),
+ "insured": {
+ "location": {
+ "city": data.insured_city || "",
+ "state": data.insured_state || "",
+ "zip": data.insured_zip || "",
+ "county": data.insured_county || "",
+ },
+ "business_year_count": +data.business_year_count || 0,
+ },
+ "coverages": ( data.classes || [] ).map(
+ ( class_code, i ) => ( {
+ "class": class_code,
+ "limit": {
+ "occurrence": +( data.limit_occurrence || 0 ),
+ "aggregate": +( data.limit_aggregate || 0 ),
+ },
+ "exposure": +( data.exposure || [] )[ i ] || 0,
+ } )
+ ),
+ "losses": ( data.loss_type || [] ).map(
+ loss_type => ( {
+ type: loss_type,
+ } )
+ ),
+ };
+ },
+
+
+ /**
+ * Append time to ISO 8601 date+time format
+ *
+ * This is required by some services.
+ *
+ * @type {string} date ISO 8601 date (without time)
+ *
+ * @return {string} ISO 8601 combined date and time
+ */
+ 'private _formatDate'( date )
+ {
+ return ( date === "" )
+ ? ""
+ : ( date + "T00:00:00" );
+ },
+} );
diff --git a/src/dapi/RestrictedDataApi.js b/src/dapi/RestrictedDataApi.js
index d4758cf..3648dd4 100644
--- a/src/dapi/RestrictedDataApi.js
+++ b/src/dapi/RestrictedDataApi.js
@@ -85,7 +85,7 @@ module.exports = Class( 'RestrictedDataApi' )
*
* @return {DataApi} self
*/
- 'public request': function( data, callback )
+ 'virtual public request': function( data, callback )
{
data = data || {};
callback = callback || function() {};
diff --git a/src/dapi/format/ResponseApply.js b/src/dapi/format/ResponseApply.js
new file mode 100644
index 0000000..75693f3
--- /dev/null
+++ b/src/dapi/format/ResponseApply.js
@@ -0,0 +1,77 @@
+/**
+ * Applies arbitrary function to response data
+ *
+ * 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 .
+ */
+
+var Trait = require( 'easejs' ).Trait,
+ DataApi = require( '../DataApi' );
+
+
+module.exports = Trait( 'ResponseApply' )
+ .implement( DataApi )
+ .extend(
+{
+ /**
+ * Function to apply to data
+ *
+ * @type {function(*)}
+ */
+ 'private _dataf': () => {},
+
+
+ /**
+ * Initialize with function to apply to return data
+ *
+ * @param {function(*)} req_callback return data function
+ */
+ __mixin( dataf )
+ {
+ if ( typeof dataf !== 'function' )
+ {
+ throw TypeError( 'expected function for #request callback' );
+ }
+
+ this._dataf = dataf;
+ },
+
+
+ /**
+ * Apply function to response
+ *
+ * The function provided during mixin will be applied to the response
+ * data to produce a new response.
+ *
+ * It is not recommended to use this trait for complex transformations;
+ * a new trait should be created instead.
+ *
+ * @param {string} data binary data to transmit
+ * @param {function(?Error,*)} callback continuation upon reply
+ *
+ * @return {DataApi} self
+ */
+ 'virtual abstract override public request'( data, callback )
+ {
+ this.__super( data, ( e, retdata ) =>
+ {
+ callback( e, this._dataf( retdata ) );
+ } );
+
+ return this;
+ },
+} );
diff --git a/src/dapi/http/HttpDataApi.js b/src/dapi/http/HttpDataApi.js
index a72057a..0db81b1 100644
--- a/src/dapi/http/HttpDataApi.js
+++ b/src/dapi/http/HttpDataApi.js
@@ -19,20 +19,22 @@
* along with this program. If not, see .
*/
-var Class = require( 'easejs' ).Class,
- DataApi = require( '../DataApi' ),
- HttpImpl = require( './HttpImpl' ),
+'use strict';
- // RFC 2616 methods
- rfcmethods = {
- DELETE: true,
- GET: true,
- HEAD: true,
- OPTIONS: true,
- POST: true,
- PUT: true,
- TRACE: true
- };
+const { Class } = require( 'easejs' );
+const DataApi = require( '../DataApi' );
+const HttpImpl = require( './HttpImpl' );
+
+// RFC 2616 methods
+const rfcmethods = {
+ DELETE: true,
+ GET: true,
+ HEAD: true,
+ OPTIONS: true,
+ POST: true,
+ PUT: true,
+ TRACE: true
+};
/**
@@ -61,6 +63,12 @@ module.exports = Class( 'HttpDataApi' )
*/
'private _impl': null,
+ /**
+ * MIME media type
+ * @type {string}
+ */
+ 'private _enctype': '',
+
/**
* Initialize Data API with destination and HTTP implementation
@@ -69,15 +77,18 @@ module.exports = Class( 'HttpDataApi' )
* requests, which permits the user to use whatever implementation works
* well with their existing system.
*
+ * Default `enctype` is `application/x-www-form-urlencoded`.
+ *
* TODO: Accept URI encoder.
*
- * @param {string} url destination URL
- * @param {string} method RFC-2616-compliant HTTP method
- * @param {HttpImpl} impl HTTP implementation
+ * @param {string} url destination URL
+ * @param {string} method RFC-2616-compliant HTTP method
+ * @param {HttpImpl} impl HTTP implementation
+ * @param {string=} enctype MIME media type
*
* @throws {TypeError} when non-HttpImpl is provided
*/
- __construct: function( url, method, impl )
+ __construct: function( url, method, impl, enctype )
{
if ( !( Class.isA( HttpImpl, impl ) ) )
{
@@ -87,6 +98,10 @@ module.exports = Class( 'HttpDataApi' )
this._url = ''+url;
this._method = this._validateMethod( method );
this._impl = impl;
+
+ this._enctype = ( enctype )
+ ? ''+enctype
+ : 'application/x-www-form-urlencoded';
},
@@ -127,7 +142,7 @@ module.exports = Class( 'HttpDataApi' )
this._impl.requestData(
this._url,
this._method,
- this._encodeData( data ),
+ this.encodeData( data ),
callback
);
@@ -164,7 +179,7 @@ module.exports = Class( 'HttpDataApi' )
*/
'private _validateDataType': function( data )
{
- var type = typeof data;
+ const type = typeof data;
if( !( ( type === 'string' ) || ( type === 'object' ) ) )
{
@@ -177,51 +192,57 @@ module.exports = Class( 'HttpDataApi' )
/**
- * If the data are an object, it's converted to an encoded key-value
- * URI; otherwise, the original string datum is returned.
+ * Generate params for URI from key-value `data`
*
- * @param {?Object|string=} data raw data or key-value
+ * Conversion depends on the MIME type (enctype) with which this instance
+ * was initialized. For example, `application/x-www-form-urlencoded`
+ * will result in a urlencoded string, whereas `application/json` will
+ * simply be serialized.
*
- * @return {string} encoded data
+ * If `data` is not an object, it will be returned as a string datum.
+ *
+ * @param {Object|string} data key-value request params
+ *
+ * @return {string} generated URI, or empty if no keys
*/
- 'private _encodeData': function( data )
+ 'protected encodeData': function( data )
{
if ( typeof data !== 'object' )
{
return ''+data;
}
- return this._encodeKeys( data );
+ if ( this._method !== 'POST' )
+ {
+ return this._urlEncode( data );
+ }
+
+ switch ( this._enctype )
+ {
+ case 'application/x-www-form-urlencoded':
+ return this._urlEncode( data );
+
+ case 'application/json':
+ return JSON.stringify( data );
+
+ default:
+ throw Error( 'Unknown enctype for POST: ' + this._enctype );
+ }
},
/**
- * Generate params for URI from key-value DATA
+ * urlencode each key of provided object
*
- * @param {Object} data key-value request params
+ * @param {Object} obj key/value
*
- * @return {string} generated URI, or empty if no keys
+ * @return {string} urlencoded string, joined with '&'
*/
- 'private _encodeKeys': function( obj )
+ 'private _urlEncode'( obj )
{
- var uri = '';
-
- // ES3 support
- for ( var key in obj )
- {
- if ( !Object.prototype.hasOwnProperty.call( obj, key ) )
- {
- continue;
- }
-
- uri += ( uri )
- ? '&'
- : '';
-
- uri += encodeURIComponent( key ) + '=' +
- encodeURIComponent( obj[ key ] );
- }
-
- return uri;
- }
+ return Object.keys( obj ).map( key =>
+ encodeURIComponent( key ) + '=' +
+ encodeURIComponent( obj[ key ] )
+ ).join( '&' );
+ },
} );
diff --git a/src/server/DocumentServer.js b/src/server/DocumentServer.js
index b07bf8e..adf0eb7 100644
--- a/src/server/DocumentServer.js
+++ b/src/server/DocumentServer.js
@@ -25,6 +25,7 @@ const {
bucket: {
bucket_filter,
QuoteDataBucket,
+ StagingBucket,
},
dapi: {
@@ -67,7 +68,8 @@ module.exports = Class( 'DocumentServer',
),
apis
),
- DapiMetaSource( QuoteDataBucket )
+ DapiMetaSource( QuoteDataBucket ),
+ StagingBucket
)
),
} );
diff --git a/src/server/Server.js b/src/server/Server.js
index 4771bd8..7a75507 100644
--- a/src/server/Server.js
+++ b/src/server/Server.js
@@ -1140,12 +1140,7 @@ module.exports = Class( 'Server' )
parsed_data, request, program, bucket
);
- quote.setData( filtered );
-
server._monitorMetadataPromise( quote, dapis );
-
- // calculated values (store only)
- program.initQuote( bucket, true );
}
catch ( err )
{
@@ -1185,11 +1180,10 @@ module.exports = Class( 'Server' )
)
)
.catch( e =>
- server.logger.log(
- server.logger.PRIORITY_ERROR,
- "Failed to save field %s[%s] metadata: %s",
- field,
- index,
+ this.logger.log(
+ this.logger.PRIORITY_ERROR,
+ "Failed to save metadata (quote id %d): %s",
+ quote.getId(),
e.message
)
)
diff --git a/src/server/request/DataProcessor.js b/src/server/request/DataProcessor.js
index a7743b5..896251b 100644
--- a/src/server/request/DataProcessor.js
+++ b/src/server/request/DataProcessor.js
@@ -23,7 +23,7 @@
const { Class } = require( 'easejs' );
-const { QuoteDataBucket } = require( '../../' ).bucket;
+const { QuoteDataBucket, StagingBucket } = require( '../../' ).bucket;
/**
@@ -56,15 +56,20 @@ module.exports = Class( 'DataProcessor',
/**
* Initialize processor
*
- * @param {Object} filter bucket filter
- * @param {function()} dapif data API constructor
- * @param {DapiMetaSource} meta_source metadata source
+ * The staging bucket constructor will be used to wrap the bucket for
+ * diff-related operations.
+ *
+ * @param {Object} filter bucket filter
+ * @param {function()} dapif data API constructor
+ * @param {DapiMetaSource} meta_source metadata source
+ * @param {function(Bucket)} staging_ctor staging bucket constructor
*/
- constructor( filter, dapif, meta_source )
+ constructor( filter, dapif, meta_source, staging_ctor )
{
- this._filter = filter;
- this._dapif = dapif;
- this._metaSource = meta_source;
+ this._filter = filter;
+ this._dapif = dapif;
+ this._metaSource = meta_source;
+ this._stagingCtor = staging_ctor;
},
@@ -86,12 +91,21 @@ module.exports = Class( 'DataProcessor',
{
const filtered = this.sanitizeDiff( data, request, program, false );
const dapi_manager = this._dapif( program.apis, request );
+ const staging = this._stagingCtor( bucket );
+
+ // forbidBypass will force diff generation on initQuote
+ staging.setValues( filtered, true );
+ staging.forbidBypass();
+
+ program.initQuote( staging, true );
// array of promises for any dapi requests
const dapis = this._triggerDapis(
- dapi_manager, program, data, bucket
+ dapi_manager, program, staging.getDiff(), staging
);
+ staging.commit();
+
return {
filtered: filtered,
dapis: dapis,
diff --git a/test/bucket/StagingBucketTest.js b/test/bucket/StagingBucketTest.js
index 4f68b6e..6f01e06 100644
--- a/test/bucket/StagingBucketTest.js
+++ b/test/bucket/StagingBucketTest.js
@@ -26,6 +26,7 @@
const { Class } = require( 'easejs' );
const root = require( '../../' );
const expect = require( 'chai' ).expect;
+const sinon = require( 'sinon' );
const {
Bucket,
@@ -111,6 +112,30 @@ describe( 'StagingBucket', () =>
merge_index: true,
is_change: true,
},
+ {
+ initial: { foo: [ 'bar', 'baz' ] },
+ update: { foo: [ 'bar', 'baz', null ] },
+ merge_index: true,
+ is_change: false,
+ },
+ {
+ initial: { foo: [ 'bar', 'baz' ] },
+ update: { foo: [ 'bar', 'baz', null ] },
+ merge_index: false,
+ is_change: false,
+ },
+ {
+ initial: { foo: [ 'bar', 'baz' ] },
+ update: { foo: [ 'bar', 'baz', 'quux' ] },
+ merge_index: true,
+ is_change: true,
+ },
+ {
+ initial: { foo: [ 'bar', 'baz' ] },
+ update: { foo: [ 'bar', 'baz', 'quux' ] },
+ merge_index: false,
+ is_change: true,
+ },
{
initial: { foo: [ 'bar', 'baz' ] },
update: { foo: [] },
@@ -161,6 +186,48 @@ describe( 'StagingBucket', () =>
} );
} );
} );
+
+
+ describe( "#setCommittedValues", () =>
+ {
+ it( "bypasses staging bucket without no bypass flag", () =>
+ {
+ const b = createStubBucket();
+ const bmock = sinon.mock( b );
+ const data = { foo: [ "bar" ] };
+ const sut = Sut( b );
+
+ bmock.expects( 'setValues' )
+ .once()
+ .withExactArgs( data );
+
+ sut.setCommittedValues( data );
+
+ // no diff if bypassed
+ expect( sut.getDiff() ).to.deep.equal( {} );
+
+ bmock.verify();
+ } );
+
+
+ it( "does not bypasses staging bucket with no bypass flag", () =>
+ {
+ const b = createStubBucket();
+ const bmock = sinon.mock( b );
+ const data = { foo: [ "bar" ] };
+ const sut = Sut( b );
+
+ bmock.expects( 'setValues' ).never();
+
+ sut.forbidBypass();
+ sut.setCommittedValues( data );
+
+ // should have been staged
+ expect( sut.getDiff() ).to.deep.equal( data );
+
+ bmock.verify();
+ } );
+ } );
} );
diff --git a/test/dapi/DummyDataApi.js b/test/dapi/DummyDataApi.js
new file mode 100644
index 0000000..fbbfcac
--- /dev/null
+++ b/test/dapi/DummyDataApi.js
@@ -0,0 +1,67 @@
+/**
+ * Applies arbitrary function to response data
+ *
+ * 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 .
+ */
+
+const { Class } = require( 'easejs' );
+const { DataApi } = require( '../../' ).dapi;
+
+
+/**
+ * Dummy DataApi implementation for testing
+ *
+ * This should not be used in production.
+ */
+module.exports = Class( 'DummyDataApi' )
+ .implement( DataApi )
+ .extend(
+{
+ /**
+ * #request callback
+ *
+ * @type {Function} #request method callback
+ */
+ 'private _reqCallback': () => {},
+
+
+ /**
+ * Initialize with `#request` method callback
+ *
+ * @param {Function} req_callback #request method callback
+ */
+ constructor( req_callback )
+ {
+ this._reqCallback = req_callback;
+ },
+
+
+ /**
+ * Dummy method that invokes the callback provided via constructor
+ *
+ * @param {?Object|string} data request params or post data
+ * @param {function(?Error,*):string} callback continuation upon reply
+ *
+ * @return {DataApi} self
+ */
+ 'virtual public request'( data, callback )
+ {
+ this._reqCallback( data, callback );
+ return this;
+ },
+} );
diff --git a/test/dapi/QuoteDataApiTest.js b/test/dapi/QuoteDataApiTest.js
new file mode 100644
index 0000000..eb4abd3
--- /dev/null
+++ b/test/dapi/QuoteDataApiTest.js
@@ -0,0 +1,146 @@
+/**
+ * Tests QuoteDataApi
+ */
+
+'use strict';
+
+const { expect } = require( 'chai' );
+const { Class } = require( 'easejs' );
+const DummyDataApi = require( './DummyDataApi' );
+
+const {
+ DataApi,
+ QuoteDataApi: Sut
+} = require( '../../' ).dapi;
+
+
+describe( 'QuoteDataApi', () =>
+{
+ [
+ // empty request; use defaults
+ {
+ given: {},
+
+ expected: {
+ "effective_date": "",
+ "rate_date": "",
+ "insured": {
+ "location": {
+ "city": "",
+ "state": "",
+ "zip": "",
+ "county": ""
+ },
+ "business_year_count": 0,
+ },
+ "coverages": [],
+ "losses": [],
+ },
+ },
+
+
+ // empty coverage
+ {
+ given: {
+ classes: [ "11111" ],
+ },
+
+ expected: {
+ "effective_date": "",
+ "rate_date": "",
+ "insured": {
+ "location": {
+ "city": "",
+ "state": "",
+ "zip": "",
+ "county": ""
+ },
+ "business_year_count": 0,
+ },
+ "coverages": [
+ {
+ "class": "11111",
+ "limit": {
+ "occurrence": 0,
+ "aggregate": 0,
+ },
+ "exposure": 0,
+ },
+ ],
+ "losses": [],
+ },
+ },
+
+
+ // full request
+ {
+ given: {
+ effective_date: "12345",
+ rate_date: "2345",
+ insured_city: "Buffalo",
+ insured_state: "NY",
+ insured_zip: "14043",
+ insured_county: "Erie",
+ business_year_count: "1",
+ classes: [ "11111", "11112" ],
+ limit_occurrence: "100",
+ limit_aggregate: "200",
+ exposure: [ "200", "300" ],
+ loss_type: [ "gl", "property" ],
+ },
+
+ expected: {
+ "effective_date": "12345T00:00:00",
+ "rate_date": "2345T00:00:00",
+ "insured": {
+ "location": {
+ "city": "Buffalo",
+ "state": "NY",
+ "zip": "14043",
+ "county": "Erie"
+ },
+ "business_year_count": 1,
+ },
+ "coverages": [
+ {
+ "class": "11111",
+ "limit": {
+ "occurrence": 100,
+ "aggregate": 200,
+ },
+ "exposure": 200,
+ },
+ {
+ "class": "11112",
+ "limit": {
+ "occurrence": 100,
+ "aggregate": 200,
+ },
+ "exposure": 300,
+ },
+ ],
+ "losses": [
+ { type: "gl" },
+ { type: "property" },
+ ],
+ },
+ },
+ ].forEach( ( { given, expected }, i ) => {
+ it( `maps input data to structured object (#${i})`, done =>
+ {
+ const dummyc = () => {};
+
+ const mock_dapi = DummyDataApi( ( data, callback ) =>
+ {
+ expect( data ).to.deep.equal( expected );
+ expect( callback ).to.equal( dummyc );
+
+ done();
+ } );
+
+ const sut = Sut( mock_dapi );
+
+ sut.request( given, dummyc );
+ } );
+ } );
+} );
diff --git a/test/dapi/format/ResponseApplyTest.js b/test/dapi/format/ResponseApplyTest.js
new file mode 100644
index 0000000..ffeed2d
--- /dev/null
+++ b/test/dapi/format/ResponseApplyTest.js
@@ -0,0 +1,65 @@
+/**
+ * Tests ResponseApply
+ *
+ * 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 { expect } = require( 'chai' );
+const { Class } = require( 'easejs' );
+const DummyDataApi = require( '../DummyDataApi' );
+
+const {
+ DataApi,
+ format: {
+ ResponseApply: Sut
+ },
+} = require( '../../../' ).dapi;
+
+
+describe( 'ResponseApply', () =>
+{
+ it( 'applies function to response', done =>
+ {
+ const expected = {};
+ const given = { given: 'data' };
+
+ const f = src =>
+ {
+ expect( src ).to.equal( given );
+ return expected;
+ };
+
+ DummyDataApi.use( Sut( f ) )( ( _, c ) => c( null, given ) )
+ .request( given, ( e, data ) =>
+ {
+ expect( data ).to.equal( expected );
+ done();
+ } );
+ } );
+
+
+ it( 'returns self', () =>
+ {
+ const sut = DummyDataApi.use( Sut( _ => {} ) )( _ => {} );
+
+ expect( sut.request( {}, () => {} ) )
+ .to.equal( sut );
+ } );
+} );
diff --git a/test/dapi/http/HttpDataApiTest.js b/test/dapi/http/HttpDataApiTest.js
index e402ae4..df74975 100644
--- a/test/dapi/http/HttpDataApiTest.js
+++ b/test/dapi/http/HttpDataApiTest.js
@@ -105,7 +105,7 @@ describe( 'HttpDataApi', function()
*/
it( 'delegates to provided HTTP implementation', function()
{
- var method = 'POST',
+ var method = 'GET',
data = "ribbit",
c = function() {};
@@ -119,37 +119,58 @@ describe( 'HttpDataApi', function()
} );
- /**
- * It's nice to do this for the HttpImpl so that they don't have to
- * worry about the proper way to handle it, or duplicate the logic.
- */
- describe( 'given key-value data', function()
+ [
+ // default is urlencoded
+ {
+ enctype: '',
+ method: 'POST',
+ data: { foo: "bar=baz", '&bar': "moo%cow" },
+ expected: 'foo=' + encodeURIComponent( 'bar=baz' ) +
+ '&' + encodeURIComponent( '&bar' ) + '=' +
+ encodeURIComponent( 'moo%cow' )
+ },
+
+ // same as above
+ {
+ enctype: 'application/x-www-form-urlencoded',
+ method: 'POST',
+ data: { foo: "bar=baz", '&bar': "moo%cow" },
+ expected: 'foo=' + encodeURIComponent( 'bar=baz' ) +
+ '&' + encodeURIComponent( '&bar' ) + '=' +
+ encodeURIComponent( 'moo%cow' )
+ },
+
+ // empty string
+ {
+ enctype: 'application/x-www-form-urlencoded',
+ method: 'POST',
+ data: {},
+ expected: "",
+ },
+
+ // json
+ {
+ enctype: 'application/json',
+ method: 'POST',
+ data: { foo: 'bar' },
+ expected: '{"foo":"bar"}',
+ },
+
+ // ignored if GET
+ {
+ enctype: 'application/json',
+ method: 'GET',
+ data: { foo: "bar" },
+ expected: "foo=bar",
+ },
+ ].forEach( ( { enctype, method, data, expected }, i ) =>
{
- it( 'converts data into encoded string', function()
+ it( `${method} encodes (${i})`, () =>
{
- var method = 'POST',
- data = { foo: "bar=baz", '&bar': "moo%cow" },
- c = function() {};
+ Sut( dummy_url, method, impl, enctype )
+ .request( data, _ => {} );
- Sut( dummy_url, method, impl ).request( data, c );
-
- expect( impl.provided[ 2 ] ).to.equal(
- 'foo=' + encodeURIComponent( data.foo ) +
- '&' + encodeURIComponent( '&bar' ) + '=' +
- encodeURIComponent( data[ '&bar' ] )
- );
- } );
-
-
- it( 'with no keys, results in empty string', function()
- {
- var method = 'POST',
- data = {},
- c = function() {};
-
- Sut( dummy_url, method, impl ).request( data, c );
-
- expect( impl.provided[ 2 ] ).to.equal( "" );
+ expect( impl.provided[ 2 ] ).to.equal( expected );
} );
} );
diff --git a/test/server/request/DataProcessorTest.js b/test/server/request/DataProcessorTest.js
index bd8f81b..7f5a101 100644
--- a/test/server/request/DataProcessorTest.js
+++ b/test/server/request/DataProcessorTest.js
@@ -90,7 +90,7 @@ describe( 'DataProcessor', () =>
}
};
- Sut( filter, () => {}, meta_source )
+ Sut( filter, () => {}, meta_source, createStubStagingBucket )
.processDiff( data, request, program );
expect( data.filtered ).to.equal( true );
@@ -109,7 +109,7 @@ describe( 'DataProcessor', () =>
done();
}
- Sut( filter, dapi_factory )
+ Sut( filter, dapi_factory, null, createStubStagingBucket )
.processDiff( {}, request, program );
} );
@@ -136,7 +136,12 @@ describe( 'DataProcessor', () =>
meta_source,
} = createStubs( false, {}, getFieldData );
- const sut = Sut( filter, () => dapi_manager, meta_source );
+ const sut = Sut(
+ filter,
+ () => dapi_manager,
+ meta_source,
+ createStubStagingBucket
+ );
program.meta.fields = {
foo: {
@@ -249,7 +254,13 @@ function createSutFromStubs( /* see createStubs */ )
program: program,
filter: filter,
meta_source: meta_source,
- sut: Sut( filter, () => {}, meta_source ),
+
+ sut: Sut(
+ filter,
+ () => {},
+ meta_source,
+ createStubStagingBucket
+ ),
};
}
@@ -281,6 +292,8 @@ function createStubProgram( internals )
internal: internals,
meta: { qtypes: {}, fields: {} },
apis: {},
+
+ initQuote() {},
};
}
@@ -305,3 +318,28 @@ function createStubBucket( data )
},
};
}
+
+
+function createStubStagingBucket( bucket )
+{
+ let data = {};
+
+ return {
+ getDataByName( name )
+ {
+ return bucket.getDataByName( name );
+ },
+
+ setValues( values )
+ {
+ data = values;
+ },
+
+ forbidBypass() {},
+ getDiff()
+ {
+ return data;
+ },
+ commit() {},
+ };
+}