diff --git a/bower.json b/bower.json index b60f078..dd62b6f 100644 --- a/bower.json +++ b/bower.json @@ -35,7 +35,8 @@ "devDependencies": { "sinon": "~1.10.3", "angular-mocks": ">=1.2.23", - "angular-resource": ">=1.2.23" + "angular-resource": ">=1.2.23", + "angular-route": ">=1.2.23" }, "dependencies": { "angular": ">= 1.2.23" diff --git a/package.json b/package.json index b89c20f..a02dac0 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "karma-phantomjs-launcher": "^0.1.4", "karma-sinon": "^1.0.3", "mocha": "^1.20.1", + "phantomjs-polyfill": "0.*", "sinon": "^1.10.2" }, "ignore": [ diff --git a/src/services/adapters/npmBatchRequestAdapter.js b/src/services/adapters/npmBatchRequestAdapter.js new file mode 100644 index 0000000..9bd0268 --- /dev/null +++ b/src/services/adapters/npmBatchRequestAdapter.js @@ -0,0 +1,133 @@ +(function() { + 'use strict'; + + var adapterKey = 'npmBatchRequestAdapter'; + var HttpBatchResponseData = window.ahb.HttpBatchResponseData; + + /** + * HTTP Adapter for angular-http-batcher for converting multiple requests into a single + * request using the batch-request format. + */ + function NpmBatchRequestAdapter() { + this.key = adapterKey; + } + + /** + * Transforms a GET request to the format expected by batch-request and attaches it to + * the httpConfig Object. + * + * @param requestIndex (Integer) - the index of the request in the pool of requests + * @param request (Object) - the Angular $http config Object for the request + * @param httpConfig (Object) - the Angular $http config Object for the batched request + */ + function transformGETRequest(requestIndex, request, httpConfig) { + var paramSerializer; + + httpConfig.data[requestIndex] = { + method: request.method, + uri: request.url, + headers: request.headers + }; + } + + /** + * Transforms any request with a body to the format expected by batch-request and attaches + * it to the httpConfig Object. + * + * @param requestIndex (Integer) - the index of the request in the pool of requests + * @param request (Object) - the Angular $http config Object for the request + * @param httpConfig (Object) - the Angular $http config Object for the batched request + */ + function transformRequestWithBody(requestIndex, request, httpConfig) { + httpConfig.data[requestIndex] = { + method: request.method, + uri: request.url, + headers: request.headers, + body: request.data + }; + } + + /** + * Builds the single batch request from the given batch of pending requests. + * + * @throws (Error) If the requests do not use the same HTTP method + * + * @param requests (Object[]) - the collection of standard Angular $http config Objects + * that should be bundled into a single batch request + * @param config (Object) - the http-batch configuration Object + * + * @return (Object) a standard Angular $http config Object for a batch request that + * represents all the provided requests + */ + NpmBatchRequestAdapter.prototype.buildRequest = function buildRequest(requests, config) { + var requestIndex; + var transformRequest; + var httpConfig = { + method: 'POST', + url: config.batchEndpointUrl, + headers: config.batchRequestHeaders || {}, + data: {} + }; + + for(requestIndex = 0; requestIndex < requests.length; requestIndex++) { + switch(requests[requestIndex].method) { + case 'GET': + transformRequest = transformGETRequest; + + break; + default: + transformRequest = transformRequestWithBody; + } + + transformRequest(requestIndex, requests[requestIndex], httpConfig); + } + + return httpConfig; + }; + + /** + * Parses the raw response from the server and maps each response to the request to which + * the server is responding. + * + * @param requests (Object[]) - the collection of standard Angular $http config Objects + * originally provided when generating the batch request + * @param rawResponse (Object) - the raw response returned from the server + * + * @return (HttpBatchResponseData[]) an array of the HttpBatchResponseData generated from + * the rawResponse + */ + NpmBatchRequestAdapter.prototype.parseResponse = function parseResponse(requests, rawResponse) { + var requestIndex; + var batchResponses = []; + var response; + + for(requestIndex = 0; requestIndex < requests.length; requestIndex++) { + response = rawResponse.data[requestIndex]; + + batchResponses.push(new HttpBatchResponseData( + requests[requestIndex], + response.statusCode, + '', + response.body, + response.headers + )); + } + + return batchResponses; + }; + + /** + * Guard method. Always returns true. + * + * @param request (Object) - the standard Angular $http config Object for the request that + * might be pooled with other requests + * + * @return (Boolean) true iff the request can be batched with other requests; false + * otherwise + */ + NpmBatchRequestAdapter.prototype.canBatchRequest = function canBatchRequest(request) { + return true; + }; + + angular.module(window.ahb.name).service(adapterKey, NpmBatchRequestAdapter); +})(); diff --git a/src/services/httpBatcher.js b/src/services/httpBatcher.js index eb0bc19..42c20bd 100644 --- a/src/services/httpBatcher.js +++ b/src/services/httpBatcher.js @@ -92,12 +92,13 @@ BatchRequestManager.prototype.send = sendFn; BatchRequestManager.prototype.addRequest = addRequestFn; BatchRequestManager.prototype.flush = flushFn; -function HttpBatcherFn($injector, $timeout, httpBatchConfig, httpBatchAdapter, nodeJsMultiFetchAdapter) { +function HttpBatcherFn($injector, $timeout, httpBatchConfig, httpBatchAdapter, nodeJsMultiFetchAdapter, npmBatchRequestAdapter) { var self = this, currentBatchedRequests = {}, adapters = { - httpBatchAdapter: httpBatchAdapter, - nodeJsMultiFetchAdapter: nodeJsMultiFetchAdapter + httpBatchAdapter: httpBatchAdapter, + nodeJsMultiFetchAdapter: nodeJsMultiFetchAdapter, + npmBatchRequestAdapter: npmBatchRequestAdapter }; self.canBatchRequest = canBatchRequestFn; diff --git a/tests/karma.conf.shared.js b/tests/karma.conf.shared.js index 1dc561f..f8a0ad9 100644 --- a/tests/karma.conf.shared.js +++ b/tests/karma.conf.shared.js @@ -73,10 +73,9 @@ shared.files = [ 'src/angular-http-batch.js', 'src/providers/httpBatchConfig.js', 'src/services/httpBatcher.js', - 'src/services/adapters/httpBatchResponseData.js', - 'src/services/adapters/httpAdapter.js', - 'src/services/adapters/nodeJsMultiFetchAdapter.js', - 'src/config/httpBackendDecorator.js' + 'src/services/adapters/*.js', + 'src/config/httpBackendDecorator.js', + './node_modules/phantomjs-polyfill/bind-polyfill.js' ]; module.exports = shared; \ No newline at end of file diff --git a/tests/services/adapters/npmBatchRequestAdapter.spec.js b/tests/services/adapters/npmBatchRequestAdapter.spec.js new file mode 100644 index 0000000..117867b --- /dev/null +++ b/tests/services/adapters/npmBatchRequestAdapter.spec.js @@ -0,0 +1,250 @@ +(function(angular, sinon) { + 'use strict'; + + describe('npmBatchRequestAdapter', function() { + beforeEach(module(window.ahb.name)); + + beforeEach(inject(function($injector) { + this.adapter = $injector.get('npmBatchRequestAdapter'); + this.sandbox = sinon.sandbox.create(); + })); + + afterEach(function() { + this.sandbox.restore(); + }); + + it('should be defined', function() { + expect(this.adapter).to.exist(); + }); + + describe('buildRequest()', function() { + beforeEach(function() { + this.config = { + batchEndpointUrl: 'https://website.com/api/batch' + }; + }); + + beforeEach(function() { + var self = this; + + this.testBatchRequest = function testBatchRequest(batchRequest, requests) { + var requestIndex; + var requestData; + + expect(batchRequest.method).to.equal('POST'); + expect(batchRequest.url).to.equal(self.config.batchEndpointUrl); + + if(self.config.batchRequestHeaders) { + expect(batchRequest.headers).to.deep.equal(self.config.batchRequestHeaders); + } + + for(requestIndex = 0; requestIndex < requests.length; requestIndex++) { + requestData = batchRequest.data[requestIndex]; + + expect(requestData).to.include.keys('method', 'uri'); + expect(requestData.method).to.equal(requests[requestIndex].method); + expect(requestData.uri).to.equal(requests[requestIndex].url); + + if(requestData.headers) { + expect(requestData).to.include.keys('headers'); + + expect(requestData.headers).to.deep.equal(requests[requestIndex].headers); + }; + + if(requestData.method !== 'GET') { + expect(requestData).to.include.keys('body'); + + expect(requestData.body).to.deep.equal(requests[requestIndex].data); + } + } + + expect(requestIndex).to.equal(requests.length); + }; + }); + + it('should build the correct request for a single GET request', function() { + var requests = [{ + url: '/api/resources/resourceId', + method: 'GET' + }]; + + this.testBatchRequest(this.adapter.buildRequest(requests, this.config), requests); + }); + + it('should build the correct request for multiple GET requests', function() { + var requests = [ + { + url: '/api/resources/resourceId1', + method: 'GET' + }, + { + url: '/api/resources/resourceId2', + method: 'GET' + } + ]; + + this.testBatchRequest(this.adapter.buildRequest(requests, this.config), requests); + }); + + it('should build the correct request for a single POST request', function() { + var requests = [{ + url: '/api/resources', + method: 'POST', + data: { + field: 'value' + } + }]; + + this.testBatchRequest(this.adapter.buildRequest(requests, this.config), requests); + }); + + it('should build the correct request for multiple POST request', function() { + var requests = [ + { + url: '/api/resources', + method: 'POST', + data: { + field: 'value' + } + }, + { + url: '/api/resources', + method: 'POST', + data: { + field: 'value2' + } + } + ]; + + this.testBatchRequest(this.adapter.buildRequest(requests, this.config), requests); + }); + + it('should build the correct request for a mix of GET and POST requests', function() { + var requests = [ + { + url: '/api/resources', + method: 'POST', + data: { + field: 'value' + } + }, + { + url: '/api/resources/resourceId2', + method: 'GET' + } + ]; + + this.testBatchRequest(this.adapter.buildRequest(requests, this.config), requests); + }); + + it('should include batch headers in requests if specified', function() { + var requests = [ + { + url: '/api/resources', + method: 'POST', + data: { + field: 'value' + } + }, + { + url: '/api/resources/resourceId2', + method: 'GET' + } + ]; + + this.config.batchRequestHeaders = { + MyHeader: 'Header-Value' + }; + + this.testBatchRequest(this.adapter.buildRequest(requests, this.config), requests); + }); + }); + + describe('parseResponse()', function() { + beforeEach(function() { + this.testResponse = function testResponse(request, responseData, parsedResponse) { + expect(parsedResponse).to.be.instanceof(window.ahb.HttpBatchResponseData); + expect(parsedResponse.request).to.deep.equal(request); + expect(parsedResponse.statusCode).to.equal(responseData.statusCode); + expect(parsedResponse.data).to.deep.equal(responseData.body); + expect(parsedResponse.headers).to.deep.equal(responseData.headers); + }; + }); + + it('should parse a single response', function() { + var request; + var response; + var parsedResponses; + + request = {}; + + response = { + data: { + 0: { + statusCode: 200, + body: { + data: 1 + }, + headers: { + header: 'One' + } + } + } + }; + + parsedResponses = this.adapter.parseResponse([request], response); + + expect(parsedResponses).to.be.an('array').with.lengthOf(1); + + this.testResponse(request, response.data[0], parsedResponses[0]); + }); + + it('should parse multiple responses and multiplex them appropriately', function() { + var requests; + var response; + var parsedResponses; + + requests = [{ + url: 'https://api.website.com/resource/{id}?filter=test', + method: 'GET' + }, { + url: 'https://api.website.com/resources/{id}', + method: 'POST', + body: { + data: 'something_important' + } + }]; + + response = { + data: { + 0: { + statusCode: 200, + body: { + data: 1 + }, + headers: { + header: 'One' + } + }, + 1: { + statusCode: 204, + body: { + data: 1 + }, + headers: { + header: 'Two' + } + } + } + }; + + parsedResponses = this.adapter.parseResponse(requests, response); + + expect(parsedResponses).to.be.an('array').with.lengthOf(2); + + this.testResponse(requests[0], response.data[0], parsedResponses[0]); + this.testResponse(requests[1], response.data[1], parsedResponses[1]); + }); + }); + }); +}(angular, sinon)); \ No newline at end of file