From c1b6f796fe7507360e41f6025e3525fe9276328e Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 8 Aug 2017 11:49:35 -0400 Subject: [PATCH] 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. --- src/dapi/DataApiFactory.js | 128 ++++++++++++++++++++++-------- src/dapi/http/HttpDataApi.js | 124 ++++++++++++++++------------- test/dapi/http/HttpDataApiTest.js | 91 +++++++++++---------- 3 files changed, 213 insertions(+), 130 deletions(-) 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/http/HttpDataApi.js b/src/dapi/http/HttpDataApi.js index 48d498b..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,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=} 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 = ''; - - 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( '&' ); + }, } ); diff --git a/test/dapi/http/HttpDataApiTest.js b/test/dapi/http/HttpDataApiTest.js index 8e46ca3..df74975 100644 --- a/test/dapi/http/HttpDataApiTest.js +++ b/test/dapi/http/HttpDataApiTest.js @@ -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 ); } ); } );