1
0
Fork 0

Better server-side change detection, QuoteDataApi

master
Mike Gerwitz 2017-08-14 08:41:41 -04:00
commit b7139bdc6a
17 changed files with 1024 additions and 154 deletions

View File

@ -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;
},

View File

@ -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 [];
};

View File

@ -19,7 +19,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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,string>|string} data params or post data
* @param {function(?Error,*):string} callback continuation upon reply
*
* @return {DataApi} self
*/

View File

@ -19,13 +19,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'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 );

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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" );
},
} );

View File

@ -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() {};

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
},
} );

View File

@ -19,20 +19,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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,string>|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,string>|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<string,string>} 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( '&' );
},
} );

View File

@ -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
)
),
} );

View File

@ -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
)
)

View File

@ -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,

View File

@ -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();
} );
} );
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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,string>|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;
},
} );

View File

@ -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 );
} );
} );
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
'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 );
} );
} );

View File

@ -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 );
} );
} );

View File

@ -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() {},
};
}