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/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
const Class = require( 'easejs' ).Class;
|
const Class = require( 'easejs' ).Class;
|
||||||
const HttpDataApi = require( './http/HttpDataApi' );
|
const HttpDataApi = require( './http/HttpDataApi' );
|
||||||
const XhrHttpImpl = require( './http/XhrHttpImpl' );
|
const XhrHttpImpl = require( './http/XhrHttpImpl' );
|
||||||
const JsonResponse = require( './format/JsonResponse' );
|
const JsonResponse = require( './format/JsonResponse' );
|
||||||
|
const ResponseApply = require( './format/ResponseApply' );
|
||||||
const RestrictedDataApi = require( './RestrictedDataApi' );
|
const RestrictedDataApi = require( './RestrictedDataApi' );
|
||||||
const StaticAdditionDataApi = require( './StaticAdditionDataApi' );
|
const StaticAdditionDataApi = require( './StaticAdditionDataApi' );
|
||||||
const BucketDataApi = require( './BucketDataApi' );
|
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
|
* Return a DataApi instance for the requested service type
|
||||||
*
|
*
|
||||||
* The source and method have type-specific meaning; that is, "source" may
|
* The source and method have type-specific meaning; that is, "source"
|
||||||
* be a URL and "method" may be get/post for a RESTful service.
|
* may be a URL and "method" may be get/post for a RESTful service.
|
||||||
*
|
*
|
||||||
* @param {string} type service type (e.g. "rest")
|
* @param {string} type service type (e.g. "rest")
|
||||||
* @param {Object} desc API description
|
* @param {Object} desc API description
|
||||||
|
@ -46,39 +50,11 @@ module.exports = Class( 'DataApiFactory',
|
||||||
*/
|
*/
|
||||||
'public fromType': function( type, desc, bucket )
|
'public fromType': function( type, desc, bucket )
|
||||||
{
|
{
|
||||||
var api = null,
|
const static_data = ( desc['static'] || [] );
|
||||||
source = ( desc.source || '' ),
|
const nonempty = !!desc.static_nonempty;
|
||||||
method = ( desc.method || '' ),
|
const multiple = !!desc.static_multiple;
|
||||||
|
|
||||||
static_data = ( desc['static'] || [] ),
|
const api = this._createDataApi( type, desc, bucket );
|
||||||
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 );
|
|
||||||
};
|
|
||||||
|
|
||||||
return RestrictedDataApi(
|
return RestrictedDataApi(
|
||||||
StaticAdditionDataApi( api, nonempty, multiple, static_data ),
|
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'()
|
'virtual protected createHttpImpl'()
|
||||||
{
|
{
|
||||||
return XhrHttpImpl( XMLHttpRequest );
|
return XhrHttpImpl( XMLHttpRequest );
|
||||||
|
|
|
@ -19,20 +19,22 @@
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Class = require( 'easejs' ).Class,
|
'use strict';
|
||||||
DataApi = require( '../DataApi' ),
|
|
||||||
HttpImpl = require( './HttpImpl' ),
|
|
||||||
|
|
||||||
// RFC 2616 methods
|
const { Class } = require( 'easejs' );
|
||||||
rfcmethods = {
|
const DataApi = require( '../DataApi' );
|
||||||
DELETE: true,
|
const HttpImpl = require( './HttpImpl' );
|
||||||
GET: true,
|
|
||||||
HEAD: true,
|
// RFC 2616 methods
|
||||||
OPTIONS: true,
|
const rfcmethods = {
|
||||||
POST: true,
|
DELETE: true,
|
||||||
PUT: true,
|
GET: true,
|
||||||
TRACE: true
|
HEAD: true,
|
||||||
};
|
OPTIONS: true,
|
||||||
|
POST: true,
|
||||||
|
PUT: true,
|
||||||
|
TRACE: true
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,6 +63,12 @@ module.exports = Class( 'HttpDataApi' )
|
||||||
*/
|
*/
|
||||||
'private _impl': null,
|
'private _impl': null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIME media type
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
'private _enctype': '',
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Data API with destination and HTTP implementation
|
* 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
|
* requests, which permits the user to use whatever implementation works
|
||||||
* well with their existing system.
|
* well with their existing system.
|
||||||
*
|
*
|
||||||
|
* Default `enctype` is `application/x-www-form-urlencoded`.
|
||||||
|
*
|
||||||
* TODO: Accept URI encoder.
|
* TODO: Accept URI encoder.
|
||||||
*
|
*
|
||||||
* @param {string} url destination URL
|
* @param {string} url destination URL
|
||||||
* @param {string} method RFC-2616-compliant HTTP method
|
* @param {string} method RFC-2616-compliant HTTP method
|
||||||
* @param {HttpImpl} impl HTTP implementation
|
* @param {HttpImpl} impl HTTP implementation
|
||||||
|
* @param {string=} enctype MIME media type
|
||||||
*
|
*
|
||||||
* @throws {TypeError} when non-HttpImpl is provided
|
* @throws {TypeError} when non-HttpImpl is provided
|
||||||
*/
|
*/
|
||||||
__construct: function( url, method, impl )
|
__construct: function( url, method, impl, enctype )
|
||||||
{
|
{
|
||||||
if ( !( Class.isA( HttpImpl, impl ) ) )
|
if ( !( Class.isA( HttpImpl, impl ) ) )
|
||||||
{
|
{
|
||||||
|
@ -87,6 +98,10 @@ module.exports = Class( 'HttpDataApi' )
|
||||||
this._url = ''+url;
|
this._url = ''+url;
|
||||||
this._method = this._validateMethod( method );
|
this._method = this._validateMethod( method );
|
||||||
this._impl = impl;
|
this._impl = impl;
|
||||||
|
|
||||||
|
this._enctype = ( enctype )
|
||||||
|
? ''+enctype
|
||||||
|
: 'application/x-www-form-urlencoded';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
@ -127,7 +142,7 @@ module.exports = Class( 'HttpDataApi' )
|
||||||
this._impl.requestData(
|
this._impl.requestData(
|
||||||
this._url,
|
this._url,
|
||||||
this._method,
|
this._method,
|
||||||
this._encodeData( data ),
|
this.encodeData( data ),
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -164,7 +179,7 @@ module.exports = Class( 'HttpDataApi' )
|
||||||
*/
|
*/
|
||||||
'private _validateDataType': function( data )
|
'private _validateDataType': function( data )
|
||||||
{
|
{
|
||||||
var type = typeof data;
|
const type = typeof data;
|
||||||
|
|
||||||
if( !( ( type === 'string' ) || ( type === 'object' ) ) )
|
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
|
* Generate params for URI from key-value `data`
|
||||||
* URI; otherwise, the original string datum is returned.
|
|
||||||
*
|
*
|
||||||
* @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' )
|
if ( typeof data !== 'object' )
|
||||||
{
|
{
|
||||||
return ''+data;
|
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 = '';
|
return Object.keys( obj ).map( key =>
|
||||||
|
encodeURIComponent( key ) + '=' +
|
||||||
if ( this._method === 'POST' )
|
encodeURIComponent( obj[ key ] )
|
||||||
{
|
).join( '&' );
|
||||||
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;
|
|
||||||
}
|
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -119,51 +119,58 @@ describe( 'HttpDataApi', function()
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
||||||
/**
|
[
|
||||||
* It's nice to do this for the HttpImpl so that they don't have to
|
// default is urlencoded
|
||||||
* worry about the proper way to handle it, or duplicate the logic.
|
{
|
||||||
*/
|
enctype: '',
|
||||||
describe( 'given key-value data', function()
|
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',
|
Sut( dummy_url, method, impl, enctype )
|
||||||
data = { foo: "bar=baz", '&bar': "moo%cow" },
|
.request( data, _ => {} );
|
||||||
c = function() {};
|
|
||||||
|
|
||||||
Sut( dummy_url, method, impl ).request( data, c );
|
expect( impl.provided[ 2 ] ).to.equal( expected );
|
||||||
|
|
||||||
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 )
|
|
||||||
);
|
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue