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