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
parent
d31e12193a
commit
c1b6f796fe
|
@ -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 );
|
||||
|
|
|
@ -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( '&' );
|
||||
},
|
||||
} );
|
||||
|
|
|
@ -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 );
|
||||
} );
|
||||
} );
|
||||
|
||||
|
|
Loading…
Reference in New Issue