diff --git a/src/dapi/AutoRetry.js b/src/dapi/AutoRetry.js
new file mode 100644
index 0000000..810677c
--- /dev/null
+++ b/src/dapi/AutoRetry.js
@@ -0,0 +1,186 @@
+/**
+ * DataApi auto-retry requests on specified failure
+ *
+ * Copyright (C) 2015 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var Trait = require( 'easejs' ).Trait,
+ DataApi = require( './DataApi' );
+
+
+/**
+ * Automatically retries requests while satisfying a given predicate
+ *
+ * It is important to distinguish between the concept of a request failure
+ * and a retry predicate: the former represents a problem with the request,
+ * whereas the latter indicates that a retry should be performed, but may
+ * not necessarily imply a request failure.
+ */
+module.exports = Trait( 'AutoRetry' )
+ .implement( DataApi )
+ .extend(
+{
+ /**
+ * Predicate function determining whether a retry is needed
+ * @var {function(?Error,*): boolean}
+ */
+ 'private _pred': '',
+
+ /**
+ * Maximum number of tries (including initial request)
+ * @var {number}
+ */
+ 'private _tries': 0,
+
+ /**
+ * Function to be passed a continuation to introduce a delay between
+ * requests
+ *
+ * @var {function(number,function(),function())} delay
+ */
+ 'private _delay': null,
+
+
+ /**
+ * Initialize auto-retry
+ *
+ * If TRIES is negative, then requests will continue indefinitely while
+ * the retry predicate is true, or is aborted by DELAY. If TRIES is 0,
+ * then no requests will be performed.
+ *
+ * If DELAY is a function, then it invoked with a retry continuation
+ * before each retry, the number of tries remaining, and a failure
+ * continuation that may be used to abort the process at an arbitrary
+ * time.
+ *
+ * @param {function(?Error,*): boolean} pred predicate determining if
+ * a retry is needed
+ * @param {number} tries maximum number of tries,
+ * including the initial
+ * request
+ *
+ * @param {?function(number,function(),function())} delay
+ * an optional function
+ * accepting a continuation
+ * to continue with the next
+ * request
+ *
+ * @return {undefined}
+ */
+ __mixin: function( pred, tries, delay )
+ {
+ if ( typeof pred !== 'function' )
+ {
+ throw TypeError( 'Predicate must be a function' );
+ }
+ if ( delay && ( typeof delay !== 'function' ) )
+ {
+ throw TypeError( "Delay must be a function" );
+ }
+
+ this._pred = pred;
+ this._tries = +tries;
+ this._delay = delay || function( _, c, __ ) { c(); };
+ },
+
+
+ /**
+ * Perform an asynchronous request and invoke CALLBACK with the
+ * reply
+ *
+ * In the special case that the number of tries is set to zero, CALLBACK
+ * will be immediately invoked with a null error and result (but not
+ * necessarily asynchronously---that remains undefined).
+ *
+ * Otherwise, requests will continue to be re-issued until either the
+ * retry predicate fails or the number of retries are exhausted,
+ * whichever comes first. Once the retries are exhausted, the error and
+ * output data from the final request are returned.
+ *
+ * If the number of tries is negative, then requests will be performed
+ * indefinitely while the retry predicate is met; the delay function (as
+ * provided via the constructor) may be used to abort in this case.
+ *
+ * @param {string} input binary data to transmit
+ * @param {function(?Error,*)} callback continuation upon reply
+ *
+ * @return {DataApi} self
+ */
+ 'abstract override public request': function( input, callback )
+ {
+ this._try( input, callback, this._tries );
+
+ return this;
+ },
+
+
+ /**
+ * Recursively perform request until retry predicate failure or try
+ * count exhaustion
+ *
+ * For more information, see `#request'.
+ *
+ * @param {string} input binary data to transmit
+ * @param {function(?Error,*)} callback continuation upon reply
+ * @param {number} n number of retries remaining
+ *
+ * @return {undefined}
+ */
+ 'private _try': function( input, callback, n )
+ {
+ var _self = this;
+
+ // the special case of 0 retries still invokes the callback, but has
+ // no data to return
+ if ( n === 0 )
+ {
+ callback( null, null );
+ return;
+ }
+
+ this.request.super.call( this, input, function( err, output )
+ {
+ var complete = function()
+ {
+ callback( err, output );
+ };
+
+ // predicate determines whether a retry is necessary
+ if ( !!_self._pred( err, output ) === false )
+ {
+ return complete();
+ }
+
+ // note that we intentionally do not want to check <= 1, so that
+ // we can proceed indefinitely (JavaScript does not wrap on overflow)
+ if ( n === 1 )
+ {
+ return complete();
+ }
+
+ _self._delay(
+ ( n - 1 ),
+ function()
+ {
+ _self._try( input, callback, ( n - 1 ) );
+ },
+ complete
+ );
+ } );
+ },
+} );
diff --git a/src/dapi/DataApi.js b/src/dapi/DataApi.js
new file mode 100644
index 0000000..b1bc59d
--- /dev/null
+++ b/src/dapi/DataApi.js
@@ -0,0 +1,51 @@
+/**
+ * Generic interface for data transmission
+ *
+ * Copyright (C) 2014 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var Interface = require( 'easejs' ).Interface;
+
+
+/**
+ * Provies a generic interface for data transmission. The only assumption that a
+ * user of this API shall make is that data may be sent and received in some
+ * arbitrary, implementation-defined format, and that every request for data
+ * shall yield some sort of response via a callback.
+ */
+module.exports = Interface( 'DataApi',
+{
+ /**
+ * Perform an asynchronous request and invoke the callback with the reply
+ *
+ * If an implementation is synchronous, the callback must still be invoked.
+ *
+ * The data format is implementation-defined. The data parameter is
+ * documented as binary as it is the most permissive, but any data may be
+ * transferred that is supported by the protocol.
+ *
+ * The first parameter of the callback shall contain an Error in the event
+ * of a failure; otherwise, it shall be null.
+ *
+ * @param {string} data binary data to transmit
+ * @param {function(?Error,*)} callback continuation upon reply
+ *
+ * @return {DataApi} self
+ */
+ 'public request': [ 'data', 'callback' ]
+} );
diff --git a/src/dapi/format/JsonResponse.js b/src/dapi/format/JsonResponse.js
new file mode 100644
index 0000000..b88ad9a
--- /dev/null
+++ b/src/dapi/format/JsonResponse.js
@@ -0,0 +1,118 @@
+/**
+ * Processes DataApi return data as JSON
+ *
+ * Copyright (C) 2014, 2015 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var Trait = require( 'easejs' ).Trait,
+ DataApi = require( '../DataApi' );
+
+
+/**
+ * Processes DataApi return data as JSON
+ */
+module.exports = Trait( 'JsonResponse' )
+ .implement( DataApi )
+ .extend(
+{
+ /**
+ * Processes response as JSON
+ *
+ * If the response is not valid JSON, an error will be returned. The
+ * output value will be an object with a single
+ * property---`text`---containing the response text that failed to
+ * parse.
+ *
+ * If a request error occurs in conjunction with a parse error, then
+ * both errors will be returned in a single error object under the
+ * `list` property.
+ *
+ * @param {string} data binary data to transmit
+ * @param {function(?Error,*)} callback continuation upon reply
+ *
+ * @return {DataApi} self
+ */
+ 'virtual abstract override public request': function( data, callback )
+ {
+ var _self = this;
+
+ this.__super( data, function( err, resp )
+ {
+ _self._tryParse( err, resp, callback );
+ } );
+
+ return this;
+ },
+
+
+ /**
+ * Attempt to parse SRC as JSON and invoke callback according to the
+ * rules of `#request`
+ *
+ * @param {?Error} err response error
+ * @param {string} src JSON string
+ * @param {function(?Error,*)} callback continuation
+ *
+ * @return {undefined}
+ */
+ 'private _tryParse': function( err, src, callback )
+ {
+ try
+ {
+ var data = JSON.parse( src );
+ }
+ catch ( e )
+ {
+ // parsing failed; provide response text in addition to
+ // original data so that the caller can handle how they
+ // please
+ callback(
+ this._getReturnError( err, e ),
+ { text: src }
+ );
+
+ return;
+ }
+
+ callback( err, data );
+ },
+
+
+ /**
+ * Produce the parse error, or a combined error containing both the
+ * original and parse errors
+ *
+ * @param {?Error} orig response error
+ * @param {Error} parse parse error
+ *
+ * @return {Error} parse error or combined error
+ */
+ 'private _getReturnError': function( orig, parse )
+ {
+ if ( !orig )
+ {
+ return parse;
+ }
+
+ var e = Error( "Multiple errors occurred; see `list` property" );
+ e.list = [ orig, parse ];
+
+ return e;
+ }
+} );
+
diff --git a/src/dapi/http/HttpDataApi.js b/src/dapi/http/HttpDataApi.js
new file mode 100644
index 0000000..e2debb6
--- /dev/null
+++ b/src/dapi/http/HttpDataApi.js
@@ -0,0 +1,227 @@
+/**
+ * Data transmission over HTTP(S)
+ *
+ * Copyright (C) 2014, 2015 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var Class = require( 'easejs' ).Class,
+ DataApi = require( '../DataApi' ),
+ HttpImpl = require( './HttpImpl' ),
+
+ // RFC 2616 methods
+ rfcmethods = {
+ DELETE: true,
+ GET: true,
+ HEAD: true,
+ OPTIONS: true,
+ POST: true,
+ PUT: true,
+ TRACE: true
+ };
+
+
+/**
+ * HTTP request abstraction. Does minor validation, but delegates to a specific
+ * HTTP implementation for the actual request.
+ */
+module.exports = Class( 'HttpDataApi' )
+ .implement( DataApi )
+ .extend(
+{
+ /**
+ * Request URL
+ * @type {string}
+ */
+ 'private _url': '',
+
+ /**
+ * HTTP method
+ * @type {string}
+ */
+ 'private _method': '',
+
+ /**
+ * HTTP implementation to perfom request
+ * @type {HttpImpl}
+ */
+ 'private _impl': null,
+
+
+ /**
+ * Initialize Data API with destination and HTTP implementation
+ *
+ * The supplied HTTP implementation will be used to perform the HTTP
+ * requests, which permits the user to use whatever implementation works
+ * well with their existing system.
+ *
+ * TODO: Accept URI encoder.
+ *
+ * @param {string} url destination URL
+ * @param {string} method RFC-2616-compliant HTTP method
+ * @param {HttpImpl} impl HTTP implementation
+ *
+ * @throws {TypeError} when non-HttpImpl is provided
+ */
+ __construct: function( url, method, impl )
+ {
+ if ( !( Class.isA( HttpImpl, impl ) ) )
+ {
+ throw TypeError( "Expected HttpImpl" );
+ }
+
+ this._url = ''+url;
+ this._method = this._validateMethod( method );
+ this._impl = impl;
+ },
+
+
+ /**
+ * Perform an asynchronous request and invoke the callback with the reply
+ *
+ * DATA must be either a string or an object; the latter is treated as a
+ * key-value parameter list, which will have each key and value
+ * individually URI-encoded and converted into a string, delimited by
+ * ampersands. `null` may be used to indicate that no data should be
+ * sent.
+ *
+ * In the event of an error, the first parameter is the error; otherwise, it
+ * is null. The return data shall not be used in the event of an error.
+ *
+ * The return value shall be a raw string; conversion to other formats must
+ * be handled by a wrapper.
+ *
+ * @param {?Object|string} data request params or post data
+ *
+ * @param {function(?Error,*):string} callback continuation upon reply
+ *
+ * @return {DataApi} self
+ *
+ * @throws {TypeError} on validation failure
+ */
+ 'virtual public request': function( data, callback )
+ {
+ // null is a good indicator of "I have no intent to send any data";
+ // empty strings and objects are not, since those are valid data
+ if ( data === null )
+ {
+ data = "";
+ }
+
+ this._validateDataType( data );
+
+ this._impl.requestData(
+ this._url,
+ this._method,
+ this._encodeData( data ),
+ callback
+ );
+
+ return this;
+ },
+
+
+ /**
+ * Ensures that the provided method conforms to RFC 2616
+ *
+ * @param {string} method HTTP method
+ * @return {string} provided method
+ *
+ * @throws {Error} on non-conforming method
+ */
+ 'private _validateMethod': function( method )
+ {
+ if ( !( rfcmethods[ method ] ) )
+ {
+ throw Error( "Invalid RFC 2616 method: " + method );
+ }
+
+ return method;
+ },
+
+
+ /**
+ * Validates that the provided data type is accepted by the Data API
+ *
+ * @param {*} data data to validate
+ * @return {undefined}
+ *
+ * @throws {TypeError} on validation failure
+ */
+ 'private _validateDataType': function( data )
+ {
+ var type = typeof data;
+
+ if( !( ( type === 'string' ) || ( type === 'object' ) ) )
+ {
+ throw TypeError(
+ "Data must be a string of raw data or object containing " +
+ "key-value params"
+ );
+ }
+ },
+
+
+ /**
+ * If the data are an object, it's converted to an encoded key-value
+ * URI; otherwise, the original string datum is returned.
+ *
+ * @param {?Object|string=} data raw data or key-value
+ *
+ * @return {string} encoded data
+ */
+ 'private _encodeData': function( data )
+ {
+ if ( typeof data !== 'object' )
+ {
+ return ''+data;
+ }
+
+ return this._encodeKeys( data );
+ },
+
+
+ /**
+ * Generate params for URI from key-value DATA
+ *
+ * @param {Object} data key-value request params
+ *
+ * @return {string} generated URI, or empty if no keys
+ */
+ 'private _encodeKeys': function( 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;
+ },
+} );
diff --git a/src/dapi/http/HttpImpl.js b/src/dapi/http/HttpImpl.js
new file mode 100644
index 0000000..dd214d5
--- /dev/null
+++ b/src/dapi/http/HttpImpl.js
@@ -0,0 +1,51 @@
+/**
+ * HTTP protocol implementation
+ *
+ * Copyright (C) 2014 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var Interface = require( 'easejs' ).Interface;
+
+
+/**
+ * HTTP protocol implementation that will perform the actual transfer. This
+ * abstraction allows use of whatever library the user prefers (e.g.
+ * XMLHttpRequest, jQuery, etc).
+ */
+module.exports = Interface( 'HttpImpl',
+{
+ /**
+ * Perform HTTP request
+ *
+ * If the request is synchronous, it must still return the data via the
+ * provided callback. The provided data is expected to be key-value if an
+ * object is given, otherwise a string of binary data.
+ *
+ * An implementation is not required to implement every HTTP method,
+ * although that is certainly preferred; a user of the API is expected to
+ * know when an implementation does not support a given method.
+ *
+ * @param {string} url destination URL
+ * @param {string} method RFC-2616-compliant HTTP method
+ * @param {Object|string} data request params
+ * @param {function(Error, Object)} callback server response callback
+ *
+ * @return {HttpImpl} self
+ */
+ 'public requestData': [ 'url', 'method', 'data', 'callback' ]
+} );
diff --git a/src/dapi/http/XhrHttpImpl.js b/src/dapi/http/XhrHttpImpl.js
new file mode 100644
index 0000000..622b5c7
--- /dev/null
+++ b/src/dapi/http/XhrHttpImpl.js
@@ -0,0 +1,272 @@
+/**
+ * XMLHttpRequest HTTP protocol implementation
+ *
+ * Copyright (C) 2014, 2015 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var Class = require( 'easejs' ).Class,
+ HttpImpl = require( './HttpImpl' );
+
+
+/**
+ * An HTTP implementation using the standardized XMLHttpRequest prototype.
+ */
+module.exports = Class( 'XhrHttpImpl' )
+ .implement( HttpImpl )
+ .extend(
+{
+ /**
+ * XMLHttpRequest constructor
+ * @type {XMLHttpRequest}
+ * @constructor
+ */
+ 'private _Xhr': null,
+
+
+ /**
+ * Initializes with constructor to the object through which XHRs will be
+ * made
+ *
+ * @param {Object} XMLHttpRequest ctor to object to perform requests
+ */
+ __construct: function( XMLHttpRequest )
+ {
+ this._Xhr = XMLHttpRequest;
+ },
+
+
+ /**
+ * Perform HTTP request using the standard XMLHttpRequest
+ *
+ * If METHOD is `"GET"`, the data will be appended to the URL;
+ * otherwise, the URL remains unchanged.
+ *
+ * No additional encoding is preformed on DATA; that is assumed to have
+ * already been performed.
+ *
+ * @param {string} url base request URL
+ * @param {string} method RFC-2616-compliant HTTP method
+ *
+ * @param {string} data request data
+ *
+ * @param {function(Error, Object)} callback server response callback
+ *
+ * @return {HttpImpl} self
+ */
+ 'public requestData': function( url, method, data, callback )
+ {
+ if ( typeof data !== 'string' )
+ {
+ throw TypeError(
+ "Request data must be a string; " + typeof data + " given"
+ );
+ }
+
+ var req = new this._Xhr(),
+ url = this._genUrl( url, method, data );
+
+ try
+ {
+ this.openRequest( req, url, method );
+ this.onLoad( req, function( err, resp )
+ {
+ callback( err, resp );
+ } );
+
+ req.send( this._getSendData( method, data ) );
+ }
+ catch ( e )
+ {
+ callback( e, null );
+ }
+
+ return this;
+ },
+
+
+ /**
+ * Generate URL according to METHOD and provided DATA
+ *
+ * See `#requestData` for more information.
+ *
+ * @param {string} url base request URL
+ * @param {string} method RFC-2616-compliant HTTP method
+ *
+ * @param {?Object|string=} data request params or
+ * post data
+ *
+ * @return {string} original URL, or appended with params
+ */
+ 'private _genUrl': function( url, method, data )
+ {
+ if ( method !== 'GET' )
+ {
+ return url;
+ }
+
+ return url +
+ ( ( data )
+ ? ( '?' + data )
+ : ''
+ );
+ },
+
+
+ /**
+ * Determine what DATA to post to the server
+ *
+ * If method is GET, no data are posted
+ *
+ * @param {string} url base request URL
+ * @param {?Object|string=} data post data
+ *
+ * @return {string|undefined} data to post to server
+ */
+ 'private _getSendData': function( method, data )
+ {
+ if ( method === 'GET' )
+ {
+ return undefined;
+ }
+
+ // TODO: reject nonsense types, including arrays
+ switch ( typeof data )
+ {
+ case 'object':
+ return this._encodeKeys( data );
+
+ default:
+ return data;
+ }
+ },
+
+
+ /**
+ * Prepares a request to the given URL using the given HTTP method
+ *
+ * This method may be overridden by subtypes to set authentication data,
+ * modify headers, hook XHR callbacks, etc.
+ *
+ * Subtypes may throw exceptions; the caller of this method catches and
+ * properly forwards them to the callback.
+ *
+ * This method must be synchronous.
+ *
+ * @param {XMLHttpRequest} req request to prepare
+ * @param {string} url destination URL
+ * @param {string} method HTTP method
+ *
+ * @return {undefined}
+ */
+ 'virtual protected openRequest': function( req, url, method )
+ {
+ // alway async
+ req.open( method, url, true );
+ },
+
+
+ /**
+ * Hooks ready state change to handle data
+ *
+ * Subtypes may override this method to alter the ready state change
+ * actions taken (e.g. to display progress, handle errors, etc.) If
+ * only the HTTP status needs to be checked, subtypes may override
+ * success/failure determination via `#isSuccessful'. If the error
+ * response needs to be customized, override `#serveError'.
+ *
+ * When overriding this method, please either call the parent method or
+ * invoke the aforementioned two methods.
+ *
+ * @param {XMLHttpRequest} req request to hook
+ * @param {function(?Error,string)} callback continuation to invoke with
+ * response
+ *
+ * @return {undefined}
+ *
+ * @throws {Error} if non-200 response received from server
+ */
+ 'virtual protected onLoad': function( req, callback )
+ {
+ var _self = this;
+
+ req.onreadystatechange = function()
+ {
+ // ready state of 4 (DONE) indicates that the request is complete
+ if ( req.readyState !== 4 )
+ {
+ return;
+ }
+ else if ( !( _self.isSuccessful( req.status ) ) )
+ {
+ _self.serveError( req, callback );
+ return;
+ }
+
+ // successful
+ callback( null, req.responseText );
+ };
+ },
+
+
+ /**
+ * Determine whether the given HTTP status indicates a success or
+ * failure
+ *
+ * The default implementation is to consider any 2xx status code to be
+ * successful, as indicated by RFC 2616.
+ *
+ * @param {number} status HTTP response status
+ *
+ * @return {bool} whether HTTP status represents a success
+ */
+ 'virtual protected isSuccessful': function( status )
+ {
+ return ( +status >= 200 ) && ( +status < 300 );
+ },
+
+
+ /**
+ * Serve an error response
+ *
+ * The default behavior is to return an Error with the status code as a
+ * `status` property, and the original response text as the output
+ * value; the philosophy here is that we should never modify the output,
+ * since a certain format may be expected as the result.
+ *
+ * When overriding this method, keep in mind that it should always
+ * return an Error for the first argument, or set it to null, indicating
+ * a success.
+ *
+ * This method exposes the original XMLHttpRequest used to make the
+ * request, so it can be used to perform additional analysis for error
+ * handling, or to override the error and instead return a successful
+ * response.
+ *
+ * @param {XMLHttpRequest} req request to hook
+ * @param {function(?Error,string)} callback continuation to invoke with
+ * response
+ * @return {undefined}
+ */
+ 'virtual protected serveError': function( req, callback )
+ {
+ var e = Error( req.status + " error from server" );
+ e.status = req.status;
+
+ callback( e, req.responseText );
+ }
+} );
diff --git a/test/dapi/AutoRetryTest.js b/test/dapi/AutoRetryTest.js
new file mode 100644
index 0000000..b9c6867
--- /dev/null
+++ b/test/dapi/AutoRetryTest.js
@@ -0,0 +1,293 @@
+/**
+ * Test case for AutoRetry
+ *
+ * Copyright (C) 2015 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var dapi = require( '../../' ).dapi,
+ expect = require( 'chai' ).expect,
+ Class = require( 'easejs' ).Class,
+ DataApi = dapi.DataApi,
+ Sut = dapi.AutoRetry;
+
+var _void = function() {},
+ _true = function() { return true; };
+
+
+describe( 'dapi.AutoRetry trait', function()
+{
+ /**
+ * If there are no retries, then AutoRetry has no observable effects.
+ */
+ describe( 'when the request does not need retrying', function()
+ {
+ it( 'makes only one request', function( done )
+ {
+ var given = {};
+
+ // success (but note the number of retries presented)
+ var stub = _createStub( null, '' )
+ .use( Sut( _void, 5 ) )
+ ();
+
+ stub.request( given, function()
+ {
+ expect( stub.given ).to.equal( given );
+ expect( stub.requests ).to.equal( 1 );
+ done();
+ } );
+
+ } );
+
+
+ /**
+ * We expect that any error will be proxied back to us; this is an
+ * important concept, since it allow separating the idea of a
+ * "retry" from that of a "failure": the latter represents a problem
+ * with the request, whereas the former indicates that a request
+ * should be performed once again.
+ */
+ it( 'returns the response data, including any error', function( done )
+ {
+ var chk = { foo: 'bar' },
+ chk_err = Error( 'foo' );
+
+ var stub = _createStub( chk_err, chk )
+ .use( Sut( _void, 1 ) )
+ ();
+
+ stub.request( '', function( err, data )
+ {
+ expect( err ).to.equal( chk_err );
+ expect( data ).to.equal( chk );
+ done();
+ } );
+ } );
+ } );
+
+
+ /**
+ * This is when we care.
+ */
+ describe( 'when the retry predicate is true', function()
+ {
+ it( 'will re-perform request N-1 times until false', function( done )
+ {
+ var n = 5;
+
+ var stub = _createStub( {}, {} )
+ .use( Sut( _true, n ) )
+ ();
+
+ stub.request( {}, function( err, _ )
+ {
+ expect( stub.requests ).to.equal( n );
+ done();
+ } );
+ } );
+
+
+ it( 'will return most recent error and output data', function( done )
+ {
+ var e = Error( 'foo' ),
+ output = {};
+
+ // XXX: this does not test for most recent, because the return
+ // data are static for each request
+ var stub = _createStub( e, output )
+ .use( Sut( _true, 1 ) )
+ ();
+
+ stub.request( {}, function( err, data )
+ {
+ expect( err ).to.equal( e );
+ expect( data ).to.equal( output );
+ done();
+ } );
+ } );
+
+
+ describe( 'given a negative number of tries', function()
+ {
+ it( 'will continue until a successful request', function( done )
+ {
+ var n = 10,
+ pred = function( _, __ )
+ {
+ return --n > 0;
+ };
+
+ var stub = _createStub()
+ .use( Sut( pred, -1 ) )
+ ();
+
+ stub.request( {}, function( _, __ )
+ {
+ expect( n ).to.equal( 0 );
+ done();
+ } );
+ } );
+ } );
+ } );
+
+
+ describe( 'when the number of tries is zero', function()
+ {
+ it( 'will perform zero requests with null results', function( done )
+ {
+ var stub = _createStub( {}, {} )
+ .use( Sut( _void, 0 ) )
+ ();
+
+ stub.request( {}, function( err, data )
+ {
+ expect( stub.requests ).to.equal( 0 );
+ expect( err ).to.equal( null );
+ expect( data ).to.equal( null );
+ done();
+ } );
+ } );
+ } );
+
+
+ describe( 'when a delay function is provided', function()
+ {
+ it( 'will wait for continuation before retry', function( done )
+ {
+ var waited = false;
+
+ var wait = function( _, c )
+ {
+ waited = true;
+ c();
+ };
+
+ var stub = _createStub( {}, {} )
+ .use( Sut( _true, 2, wait ) )
+ ();
+
+ stub.request( {}, function( _, __ )
+ {
+ expect( waited ).to.equal( true );
+ done();
+ } );
+ } );
+
+
+ it( 'will not process if continuation is not called', function()
+ {
+ var waited = false;
+ var wait = function( _, c )
+ {
+ waited = true;
+ /* do not invoke */
+ };
+
+ var stub = _createStub( {}, {} )
+ .use( Sut( _true, 2, wait ) )
+ ();
+
+ // this works because we know that our stub is invoked
+ // synchronously
+ stub.request( {}, function( _, __ )
+ {
+ throw Error( "Should not have been called!" );
+ } );
+
+ expect( waited ).to.equal( true );
+ } );
+
+
+ it( 'will call delay function until predicate falsity', function()
+ {
+ var n = 5;
+ var wait = function( tries_left, c )
+ {
+ n--;
+
+ // the first argument is the number of tries left
+ expect( tries_left ).to.equal( n );
+ c();
+ };
+
+ var pred = function()
+ {
+ return n > 0;
+ };
+
+ var stub = _createStub( {}, {} )
+ .use( Sut( pred, n, wait ) )
+ ();
+
+ // this works because we know that our stub is invoked
+ // synchronously
+ stub.request( {}, _void );
+
+ // the first request counts as one, which brings us down to 4,
+ // but the wait function has not been called at this point; so,
+ // we expect that it will only be called four times
+ expect( n ).to.equal( 1 );
+ } );
+
+
+ it( 'allows aborting via failure continuation', function( done )
+ {
+ var err_expect = {},
+ out_expect = [];
+
+ var wait = function( _, __, abort )
+ {
+ abort();
+ };
+
+ // without aborting, this would never finish
+ var stub = _createStub( err_expect, out_expect )
+ .use( Sut( _true, -1, wait ) )
+ ();
+
+ // this works because we know that our stub is invoked
+ // synchronously
+ stub.request( {}, function( err, output )
+ {
+ expect( err ).to.equal( err_expect );
+ expect( output ).to.equal( out_expect );
+
+ done();
+ } );
+ } );
+ } );
+} );
+
+
+
+function _createStub( err, resp )
+{
+ return Class.implement( DataApi ).extend(
+ {
+ given: null,
+ requests: 0,
+
+ 'virtual public request': function( data, callback )
+ {
+ this.given = data;
+ this.requests++;
+
+ callback( err, resp );
+ }
+ } );
+}
diff --git a/test/dapi/format/JsonResponseTest.js b/test/dapi/format/JsonResponseTest.js
new file mode 100644
index 0000000..f74ea50
--- /dev/null
+++ b/test/dapi/format/JsonResponseTest.js
@@ -0,0 +1,152 @@
+/**
+ * Test case for JSON formatting of API result
+ *
+ * Copyright (C) 2014, 2015 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var dapi = require( '../../../' ).dapi,
+ expect = require( 'chai' ).expect,
+ Class = require( 'easejs' ).Class,
+ DataApi = dapi.DataApi,
+ Sut = dapi.format.JsonResponse;
+
+
+describe( 'dapi.format.JsonRepsonse trait', function()
+{
+ describe( '.request', function()
+ {
+ it( 'passes data to encapsulated DataApi', function()
+ {
+ var stubs = _createStubbedDapi( null, '0' ),
+ expected = {};
+
+ stubs.request( expected, function() {} );
+ expect( stubs.given ).to.equal( expected );
+ } );
+
+
+ it( 'converts response to JSON', function( done )
+ {
+ var raw = '{"foo": "bar"}';
+
+ _createStubbedDapi( null, raw )
+ .request( '', function( err, data )
+ {
+ // should have been converted to JSON
+ expect( data ).to.deep.equal( { foo: 'bar' } );
+ expect( err ).to.equal( null );
+ done();
+ } );
+ } );
+
+
+ describe( 'when JSON parsing fails', function()
+ {
+ it( 'returns error', function( done )
+ {
+ _createStubbedDapi( null, 'ERR' )
+ .request( '', function( err, data )
+ {
+ expect( err ).to.be.instanceOf( SyntaxError );
+ done();
+ } );
+ } );
+
+
+ it( 'provides bad text as object.text', function( done )
+ {
+ var text = 'NOT JSON';
+
+ _createStubbedDapi( null, text )
+ .request( '', function( err, data )
+ {
+ expect( data ).to.be.a( 'object' );
+ expect( data.text ).to.equal( text );
+ done();
+ } );
+ } );
+ } );
+
+
+ describe( 'on request error from supertype', function()
+ {
+ it( 'attempts to format output as JSON', function( done )
+ {
+ var chk = '{"foo": "bar"}';
+
+ _createStubbedDapi( null, chk )
+ .request( '', function( _, data )
+ {
+ expect( data ).to.be.a( 'object' );
+ expect( data.foo ).to.equal( "bar" );
+ done();
+ } );
+ } );
+
+
+ it( 'proxies error when JSON output valid', function( done )
+ {
+ var e = Error( 'foo' );
+
+ _createStubbedDapi( e, '{}' )
+ .request( '', function( err, _ )
+ {
+ expect( err ).to.equal( e );
+ done();
+ } );
+ } );
+
+
+ it( 'produces both errors on bad JSON output', function( done )
+ {
+ var e = Error( 'foo' );
+
+ _createStubbedDapi( e, 'BAD JSON' )
+ .request( '', function( err, _ )
+ {
+ // the main error should indicate both
+ expect( err ).to.be.instanceOf( Error );
+
+ // and we should provide references to both
+ expect( err.list[ 0 ] ).to.equal( e );
+ expect( err.list[ 1 ] ).to.be.instanceOf(
+ SyntaxError
+ );
+
+ done();
+ } );
+ } );
+ } );
+ } );
+} );
+
+
+function _createStubbedDapi( err, resp )
+{
+ return Class.implement( DataApi ).extend(
+ {
+ given: null,
+
+ 'virtual public request': function( data, callback )
+ {
+ this.given = data;
+ callback( err, resp );
+ }
+ } ).use( Sut )();
+}
+
diff --git a/test/dapi/http/HttpDataApiTest.js b/test/dapi/http/HttpDataApiTest.js
new file mode 100644
index 0000000..a5cdba2
--- /dev/null
+++ b/test/dapi/http/HttpDataApiTest.js
@@ -0,0 +1,227 @@
+/**
+ * Test case for data transmission over HTTP(S)
+ *
+ * Copyright (C) 2014, 2015 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var dapi = require( '../../../' ).dapi,
+ expect = require( 'chai' ).expect,
+ Class = require( 'easejs' ).Class,
+ Sut = dapi.http.HttpDataApi,
+
+ dummy_url = 'http://foo',
+ dummy_impl = Class
+ .implement( dapi.http.HttpImpl )
+ .extend( { requestData: function( _, __, ___, ____ ) {} } )(),
+
+ dummy_sut = Sut( dummy_url, 'GET', dummy_impl );
+
+
+describe( 'HttpDataApi', function()
+{
+ it( 'is a DataApi', function()
+ {
+ expect( Class.isA( dapi.DataApi, dummy_sut ) ).to.be.ok;
+ } );
+
+
+ it( 'permits RFC 2616 HTTP methods', function()
+ {
+ var m = [ 'GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'TRACE' ];
+
+ m.forEach( function( method )
+ {
+ expect( function()
+ {
+ Sut( dummy_url, method, dummy_impl );
+ } ).to.not.throw( Error );
+ } );
+ } );
+
+
+ it( 'does not permit non-RFC-2616 HTTP methods', function()
+ {
+ expect( function()
+ {
+ Sut( dummy_url, 'FOO', dummy_impl );
+ } ).to.throw( Error, 'FOO' );
+ } );
+
+
+ it( 'rejects non-HttpImpl objects', function()
+ {
+ expect( function()
+ {
+ Sut( dummy_url, 'GET', {} );
+ } ).to.throw( TypeError, 'HttpImpl' );
+ } );
+
+
+ describe( '.request', function()
+ {
+ var impl = Class( 'StubHttpImpl' )
+ .implement( dapi.http.HttpImpl )
+ .extend(
+ {
+ provided: [],
+ data: "",
+ err: null,
+
+ requestData: function( url, method, data, c )
+ {
+ this.provided = arguments;
+ c( this.err, this.data );
+ }
+ } )();
+
+
+ /**
+ * The actual request is performed by some underling implementation.
+ * This additional level of indirection allows the general concept of an
+ * "HTTP Data API" to vary from an underyling HTTP protocol
+ * implementation; they are separate concerns, although the distinction
+ * may seem subtle.
+ */
+ it( 'delegates to provided HTTP implementation', function()
+ {
+ var method = 'POST',
+ data = "ribbit",
+ c = function() {};
+
+ Sut( dummy_url, method, impl ).request( data, c );
+
+ var provided = impl.provided;
+ expect( provided[ 0 ] ).to.equal( dummy_url );
+ expect( provided[ 1 ] ).to.equal( method );
+ expect( provided[ 2 ] ).to.equal( data );
+ expect( provided[ 3 ] ).to.equal( c );
+ } );
+
+
+ /**
+ * 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()
+ {
+ it( 'converts data into encoded string', function()
+ {
+ var method = 'POST',
+ data = { foo: "bar=baz", '&bar': "moo%cow" },
+ c = function() {};
+
+ 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( "" );
+ } );
+ } );
+
+
+ /**
+ * Method chaining
+ */
+ it( 'returns self', function()
+ {
+ var sut = Sut( dummy_url, 'GET', impl ),
+ ret = sut.request( "", function() {} );
+
+ expect( ret ).to.equal( sut );
+ } );
+
+
+ /**
+ * String requests are intended to be raw messages, whereas objects are
+ * treated as key-value params.
+ */
+ it( 'accepts string and object data', function()
+ {
+ expect( function()
+ {
+ Sut( dummy_url, 'GET', impl )
+ .request( "", function() {} ) // string
+ .request( {}, function() {} ); // object
+ } ).to.not.throw( Error );
+ } );
+
+
+ it( 'accepts null data, converting to empty string', function()
+ {
+ expect( function()
+ {
+ Sut( dummy_url, 'GET', impl )
+ .request( null, function()
+ {
+ expect( impl.provided[ 2 ] ).to.equal( "" );
+ } );
+ } ).to.not.throw( Error );
+ } );
+
+
+ it( 'rejects all other data types', function()
+ {
+ var sut = Sut( dummy_url, 'GET', impl );
+
+ [ 123, Infinity, undefined, NaN, function() {} ]
+ .forEach( function( data )
+ {
+ expect( function()
+ {
+ sut.request( data, function() {} );
+ } ).to.throw( TypeError );
+ } );
+ } );
+
+
+ it( 'returns error provided by HTTP implementation', function( done )
+ {
+ impl.err = Error( "Test impl error" );
+ Sut( dummy_url, 'GET', impl ).request( "", function( err, resp )
+ {
+ expect( err ).to.equal( impl.err );
+ done();
+ } );
+ } );
+
+
+ it( 'returns response provided by HTTP implementation', function( done )
+ {
+ impl.data = {};
+ Sut( dummy_url, 'GET', impl ).request( "", function( err, resp )
+ {
+ expect( resp ).to.equal( impl.data );
+ done();
+ } );
+ } );
+ } );
+} );
diff --git a/test/dapi/http/XhrHttpImplTest.js b/test/dapi/http/XhrHttpImplTest.js
new file mode 100644
index 0000000..ffa5ca2
--- /dev/null
+++ b/test/dapi/http/XhrHttpImplTest.js
@@ -0,0 +1,354 @@
+/**
+ * Test case for XMLHttpRequest HTTP protocol implementation
+ *
+ * Copyright (C) 2014, 2015 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var dapi = require( '../../../' ).dapi,
+ expect = require( 'chai' ).expect,
+ Class = require( 'easejs' ).Class,
+ HttpImpl = dapi.http.HttpImpl,
+ Sut = dapi.http.XhrHttpImpl,
+
+ DummyXhr = function()
+ {
+ this.open = function()
+ {
+ DummyXhr.args = arguments;
+ };
+ };
+
+
+describe( 'XhrHttpImpl', function()
+{
+ /**
+ * Since ECMAScript does not have return typing, we won't know if the ctor
+ * actually returns an XMLHttpRequest until we try.
+ */
+ it( 'will accept any constructor', function()
+ {
+ expect( function()
+ {
+ Sut( function() {} );
+ } ).to.not.throw( Error );
+ } );
+
+
+ it( 'is an HttpImpl', function()
+ {
+ var sut = Sut( function() {} );
+ expect( Class.isA( HttpImpl, sut ) ).to.be.ok;
+ } );
+
+
+ describe( '.requestData', function()
+ {
+ it( 'requests a connection using the given method', function()
+ {
+ var method = 'GET',
+ sut = Sut( DummyXhr );
+
+ sut.requestData( 'http://foo', method, "", function() {} );
+
+ var args = DummyXhr.args;
+ expect( args[ 0 ] ).to.equal( method );
+ } );
+
+
+ /**
+ * Since the request is asynchronous, we should be polite and not return
+ * errors in two different formats; we will catch it and instead pass it
+ * back via the callback.
+ */
+ it( 'returns XHR open() errors via callback', function( done )
+ {
+ var e = Error( "Test error" ),
+ Xhr = function()
+ {
+ this.open = function()
+ {
+ throw e;
+ };
+ };
+
+ // should not throw an exception
+ expect( function()
+ {
+ // should instead provide to callback
+ Sut( Xhr )
+ .requestData( 'http://foo', 'GET', "", function( err, data )
+ {
+ expect( err ).to.equal( e );
+ expect( data ).to.equal( null );
+ done();
+ } );
+ } ).to.not.throw( Error );
+ } );
+
+
+ it( 'returns XHR response via callback when no error', function( done )
+ {
+ var retdata = "foobar",
+ StubXhr = createStubXhr();
+
+ StubXhr.prototype.responseText = retdata;
+ StubXhr.prototype.readyState = 4; // done
+ StubXhr.prototype.status = 200; // OK
+
+ Sut( StubXhr )
+ .requestData( 'http://bar', 'GET', "", function( err, resp )
+ {
+ expect( err ).to.equal( null );
+ expect( resp ).to.equal( retdata );
+ done();
+ } );
+ } );
+
+
+ describe( 'HTTP method is GET', function()
+ {
+ it( 'appends data to URL', function( done )
+ {
+ var url = 'http://bar',
+ src = "moocow%foocow%",
+ StubXhr = createStubXhr();
+
+ StubXhr.prototype.readyState = 4; // done
+ StubXhr.prototype.status = 200; // OK
+
+ StubXhr.prototype.open = function( _, given_url )
+ {
+ // no additional encoding should be performed; it's
+ // assumed to have already been done
+ expect( given_url ).to.equal(
+ url + '?' + src
+ );
+ };
+
+ StubXhr.prototype.send = function( data )
+ {
+ // no posting on GET
+ expect( data ).is.equal( undefined );
+ StubXhr.inst.onreadystatechange();
+ };
+
+ Sut( StubXhr )
+ .requestData( url, 'GET', src, done );
+ } );
+
+
+ it( 'leaves URL unaltered when data is empty', function( done )
+ {
+ var url = 'http://bar',
+ StubXhr = createStubXhr();
+
+ StubXhr.prototype.readyState = 4; // done
+ StubXhr.prototype.status = 200; // OK
+
+ StubXhr.prototype.open = function( _, given_url )
+ {
+ // unaltered
+ expect( given_url ).to.equal( url );
+ };
+
+ Sut( StubXhr )
+ .requestData( url, 'GET', "", done );
+ } );
+
+ } );
+
+
+ describe( 'HTTP method is not GET', function()
+ {
+ it( 'sends data verbatim', function( done )
+ {
+ var url = 'http://bar',
+ src = "moocow",
+ StubXhr = createStubXhr();
+
+ StubXhr.prototype.readyState = 4; // done
+ StubXhr.prototype.status = 200; // OK
+
+ StubXhr.prototype.open = function( _, given_url )
+ {
+ // URL should be unchanged
+ expect( given_url ).to.equal( url );
+ };
+
+ StubXhr.prototype.send = function( data )
+ {
+ expect( data ).is.equal( src );
+ StubXhr.inst.onreadystatechange();
+ };
+
+ Sut( StubXhr )
+ .requestData( url, 'POST', src, done );
+ } );
+ } );
+
+
+ describe( 'if return status code is not successful', function()
+ {
+ /**
+ * This is the default behavior, but can be changed by overriding
+ * the onLoad method.
+ */
+ it( 'returns error to callback with status code', function( done )
+ {
+ var StubXhr = createStubXhr();
+ StubXhr.prototype.status = 404;
+
+ Sut( StubXhr )
+ .requestData( 'http://foo', 'GET', '', function( err, _ )
+ {
+ expect( err ).to.be.instanceOf( Error );
+
+ expect( err.message ).to.contain(
+ StubXhr.prototype.status
+ );
+
+ expect( err.status ).to.equal(
+ StubXhr.prototype.status
+ );
+
+ done();
+ } );
+ } );
+
+
+ it( 'returns response text as output', function( done )
+ {
+ var StubXhr = createStubXhr(),
+ status = 404,
+ reply = 'foobunny';
+
+ StubXhr.prototype.responseText = reply;
+
+ Sut( StubXhr )
+ .requestData( 'http://foo', 'GET', '', function( _, resp )
+ {
+ expect( resp ).to.equal( reply );
+ done();
+ } );
+ } );
+ } );
+
+
+ it( 'considers any 2xx status to be successful', function( done )
+ {
+ var StubXhr = createStubXhr();
+ StubXhr.prototype.status = 250;
+
+ Sut( StubXhr )
+ .requestData( 'http://foo', 'GET', '', function( err, _ )
+ {
+ expect( err ).to.equal( null );
+ done();
+ } );
+ } );
+
+
+ it( 'allows overriding notion of success/failure', function( done )
+ {
+ var chk = 12345;
+
+ // succeed on CHK
+ var StubXhr = createStubXhr();
+ StubXhr.prototype.status = chk;
+
+ Sut.extend(
+ {
+ 'override protected isSuccessful': function( status )
+ {
+ return status === chk;
+ },
+ } )( StubXhr )
+ .requestData( 'http://foo', 'GET', '', function( err, resp )
+ {
+ expect( err ).to.equal( null );
+ done();
+ } );
+ } );
+
+
+ it( 'allows customizing error', function( done )
+ {
+ var _self = this,
+ chk = {};
+
+ var StubXhr = createStubXhr();
+ StubXhr.prototype.status = 404;
+
+ Sut.extend(
+ {
+ 'override protected serveError': function( req, callback )
+ {
+ var e = Error( 'foobunny' );
+ e.foo = true;
+
+ expect( req ).to.be.an.instanceOf( StubXhr );
+
+ callback( e, chk );
+ },
+ } )( StubXhr )
+ .requestData( 'http://foo', 'GET', '', function( err, resp )
+ {
+ expect( ( err || {} ).foo ).to.be.ok;
+ expect( resp ).to.equal( chk );
+
+ done();
+ } );
+ } );
+
+
+ it( 'returns self', function()
+ {
+ var sut = Sut( function() {} ),
+ ret = sut.requestData(
+ 'http://foo', 'GET', "", function() {}
+ );
+
+ expect( ret ).to.equal( sut );
+ } );
+ } );
+} );
+
+
+function createStubXhr()
+{
+ var StubXhr = function()
+ {
+ StubXhr.inst = this;
+ };
+
+ StubXhr.prototype = {
+ onreadystatechange: null,
+ responseText: '',
+ readyState: 4, // don,
+ status: 200, // O,
+
+ open: function() {},
+ send: function( data )
+ {
+ this.onreadystatechange();
+ }
+ };
+
+ return StubXhr;
+}
+
diff --git a/tools/gen-index b/tools/gen-index
index 7d78470..9c97ef8 100755
--- a/tools/gen-index
+++ b/tools/gen-index
@@ -19,7 +19,7 @@
# along with this program. If not, see .
##
-shopt -s extglob
+shopt -s extglob nullglob
destpath="${1?Destination path required}"