1
0
Fork 0

Add `quote' Data API type

* src/dapi/DataApiFactory.js (_createDataApi): Add support for
  `quote'.
* src/dapi/http/HttpDataApi.js (__construct): New `enctype' argument.
  (_encodeData, _encodeKeys): Remove former, rename latter to former.
  (encodeData, _urlEncode): Encode based on enctype and method.
  (*): Strict mode, es6 style.
master
Mike Gerwitz 2017-08-08 11:49:35 -04:00
parent d31e12193a
commit c1b6f796fe
3 changed files with 213 additions and 130 deletions

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

@ -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,56 +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 = '';
if ( this._method === 'POST' )
{
return JSON.stringify( obj );
}
// 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

@ -119,51 +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 = 'GET',
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 = 'GET',
data = {},
c = function() {};
Sut( dummy_url, method, impl ).request( data, c );
expect( impl.provided[ 2 ] ).to.equal( "" );
} );
it( 'encodes JSON on POST', () =>
{
var method = 'POST',
data = { foo: 'bar' },
c = () => {};
Sut( dummy_url, method, impl ).request( data, c );
expect( impl.provided[ 2 ] ).to.equal(
JSON.stringify( data )
);
expect( impl.provided[ 2 ] ).to.equal( expected );
} );
} );