diff options
| author | Karl Seamon | 2011-07-22 15:56:45 -0400 |
|---|---|---|
| committer | Igor Minar | 2011-07-27 15:21:31 -0700 |
| commit | b5594a773a6f07dcba914aa385f92d3305285b24 (patch) | |
| tree | 40823e64e9f74b356a8065edae9bbdf351082164 | |
| parent | f39420e7d7aca2a97eaa01853991facf65dcbd6e (diff) | |
| download | angular.js-b5594a773a6f07dcba914aa385f92d3305285b24.tar.bz2 | |
feat($xhr): add custom error callback to $xhr, $xhr.cache, $xhr.bulk, $resource
Closes #408
| -rw-r--r-- | src/Resource.js | 69 | ||||
| -rw-r--r-- | src/service/resource.js | 10 | ||||
| -rw-r--r-- | src/service/xhr.bulk.js | 60 | ||||
| -rw-r--r-- | src/service/xhr.cache.js | 90 | ||||
| -rw-r--r-- | src/service/xhr.js | 29 | ||||
| -rw-r--r-- | test/ResourceSpec.js | 42 | ||||
| -rw-r--r-- | test/service/xhr.bulkSpec.js | 29 | ||||
| -rw-r--r-- | test/service/xhr.cacheSpec.js | 38 | ||||
| -rw-r--r-- | test/service/xhr.errorSpec.js | 6 | ||||
| -rw-r--r-- | test/service/xhrSpec.js | 42 |
10 files changed, 291 insertions, 124 deletions
diff --git a/src/Resource.js b/src/Resource.js index 5462826d..3c149d8b 100644 --- a/src/Resource.js +++ b/src/Resource.js @@ -67,29 +67,36 @@ ResourceFactory.prototype = { forEach(actions, function(action, name){ var isPostOrPut = action.method == 'POST' || action.method == 'PUT'; - Resource[name] = function (a1, a2, a3) { + Resource[name] = function (a1, a2, a3, a4) { var params = {}; var data; - var callback = noop; + var success = noop; + var error = null; switch(arguments.length) { - case 3: callback = a3; + case 4: + error = a4; + success = a3; + case 3: case 2: if (isFunction(a2)) { - callback = a2; + success = a2; + error = a3; //fallthrough } else { params = a1; data = a2; + success = a3; break; } case 1: - if (isFunction(a1)) callback = a1; + if (isFunction(a1)) success = a1; else if (isPostOrPut) data = a1; else params = a1; break; case 0: break; default: - throw "Expected between 0-3 arguments [params, data, callback], got " + arguments.length + " arguments."; + throw "Expected between 0-4 arguments [params, data, success, error], got " + + arguments.length + " arguments."; } var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); @@ -97,23 +104,20 @@ ResourceFactory.prototype = { action.method, route.url(extend({}, action.params || {}, extractParams(data), params)), data, - function(status, response, clear) { - if (200 <= status && status < 300) { - if (response) { - if (action.isArray) { - value.length = 0; - forEach(response, function(item){ - value.push(new Resource(item)); - }); - } else { - copy(response, value); - } + function(status, response) { + if (response) { + if (action.isArray) { + value.length = 0; + forEach(response, function(item) { + value.push(new Resource(item)); + }); + } else { + copy(response, value); } - (callback||noop)(value); - } else { - throw {status: status, response:response, message: status + ": " + response}; } + (success||noop)(value); }, + error || action.verifyCache, action.verifyCache); return value; }; @@ -122,18 +126,29 @@ ResourceFactory.prototype = { return self.route(url, extend({}, paramDefaults, additionalParamDefaults), actions); }; - Resource.prototype['$' + name] = function(a1, a2){ - var params = extractParams(this); - var callback = noop; + Resource.prototype['$' + name] = function(a1, a2, a3) { + var params = extractParams(this), + success = noop, + error; + switch(arguments.length) { - case 2: params = a1; callback = a2; - case 1: if (typeof a1 == $function) callback = a1; else params = a1; + case 3: params = a1; success = a2; error = a3; break; + case 2: + case 1: + if (isFunction(a1)) { + success = a1; + error = a2; + } else { + params = a1; + success = a2 || noop; + } case 0: break; default: - throw "Expected between 1-2 arguments [params, callback], got " + arguments.length + " arguments."; + throw "Expected between 1-3 arguments [params, success, error], got " + + arguments.length + " arguments."; } var data = isPostOrPut ? this : undefined; - Resource[name].call(this, params, data, callback); + Resource[name].call(this, params, data, success, error); }; }); return Resource; diff --git a/src/service/resource.js b/src/service/resource.js index 31d7ceeb..c11067b1 100644 --- a/src/service/resource.js +++ b/src/service/resource.js @@ -82,9 +82,9 @@ * The action methods on the class object or instance object can be invoked with the following * parameters: * - * - HTTP GET "class" actions: `Resource.action([parameters], [callback])` - * - non-GET "class" actions: `Resource.action(postData, [parameters], [callback])` - * - non-GET instance actions: `instance.$action([parameters], [callback])` + * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` + * - non-GET "class" actions: `Resource.action(postData, [parameters], [success], [error])` + * - non-GET instance actions: `instance.$action([parameters], [success], [error])` * * * @example @@ -142,8 +142,8 @@ }); </pre> * - * It's worth noting that the callback for `get`, `query` and other method gets passed in the - * response that came from the server, so one could rewrite the above example as: + * It's worth noting that the success callback for `get`, `query` and other method gets passed + * in the response that came from the server, so one could rewrite the above example as: * <pre> var User = $resource('/user/:userId', {userId:'@id'}); diff --git a/src/service/xhr.bulk.js b/src/service/xhr.bulk.js index c0940d9d..d7fc7990 100644 --- a/src/service/xhr.bulk.js +++ b/src/service/xhr.bulk.js @@ -15,9 +15,10 @@ angularServiceInject('$xhr.bulk', function($xhr, $error, $log){ var requests = [], scope = this; - function bulkXHR(method, url, post, callback) { + function bulkXHR(method, url, post, success, error) { if (isFunction(post)) { - callback = post; + error = success; + success = post; post = null; } var currentQueue; @@ -28,32 +29,55 @@ angularServiceInject('$xhr.bulk', function($xhr, $error, $log){ }); if (currentQueue) { if (!currentQueue.requests) currentQueue.requests = []; - currentQueue.requests.push({method: method, url: url, data:post, callback:callback}); + var request = { + method: method, + url: url, + data: post, + success: success}; + if (error) request.error = error; + currentQueue.requests.push(request); } else { - $xhr(method, url, post, callback); + $xhr(method, url, post, success, error); } } bulkXHR.urls = {}; - bulkXHR.flush = function(callback){ - forEach(bulkXHR.urls, function(queue, url){ + bulkXHR.flush = function(success, error) { + forEach(bulkXHR.urls, function(queue, url) { var currentRequests = queue.requests; if (currentRequests && currentRequests.length) { queue.requests = []; queue.callbacks = []; - $xhr('POST', url, {requests:currentRequests}, function(code, response){ - forEach(response, function(response, i){ - try { - if (response.status == 200) { - (currentRequests[i].callback || noop)(response.status, response.response); - } else { - $error(currentRequests[i], response); + $xhr('POST', url, {requests: currentRequests}, + function(code, response) { + forEach(response, function(response, i) { + try { + if (response.status == 200) { + (currentRequests[i].success || noop)(response.status, response.response); + } else if (isFunction(currentRequests[i].error)) { + currentRequests[i].error(response.status, response.response); + } else { + $error(currentRequests[i], response); + } + } catch(e) { + $log.error(e); } - } catch(e) { - $log.error(e); - } + }); + (success || noop)(); + }, + function(code, response) { + forEach(currentRequests, function(request, i) { + try { + if (isFunction(request.error)) { + request.error(code, response); + } else { + $error(request, response); + } + } catch(e) { + $log.error(e); + } + }); + (error || noop)(); }); - (callback || noop)(); - }); scope.$eval(); } }); diff --git a/src/service/xhr.cache.js b/src/service/xhr.cache.js index 42b666e1..256b936e 100644 --- a/src/service/xhr.cache.js +++ b/src/service/xhr.cache.js @@ -5,7 +5,11 @@ * @ngdoc service * @name angular.service.$xhr.cache * @function - * @requires $xhr + * + * @requires $xhr.bulk + * @requires $defer + * @requires $xhr.error + * @requires $log * * @description * Acts just like the {@link angular.service.$xhr $xhr} service but caches responses for `GET` @@ -18,27 +22,42 @@ * @param {string} method HTTP method. * @param {string} url Destination URL. * @param {(string|Object)=} post Request body. - * @param {function(number, (string|Object))} callback Response callback. + * @param {function(number, (string|Object))} success Response success callback. + * @param {function(number, (string|Object))=} error Response error callback. * @param {boolean=} [verifyCache=false] If `true` then a result is immediately returned from cache * (if present) while a request is sent to the server for a fresh response that will update the - * cached entry. The `callback` function will be called when the response is received. - * @param {boolean=} [sync=false] in case of cache hit execute `callback` synchronously. + * cached entry. The `success` function will be called when the response is received. + * @param {boolean=} [sync=false] in case of cache hit execute `success` synchronously. */ -angularServiceInject('$xhr.cache', function($xhr, $defer, $log){ +angularServiceInject('$xhr.cache', function($xhr, $defer, $error, $log) { var inflight = {}, self = this; - function cache(method, url, post, callback, verifyCache, sync){ + function cache(method, url, post, success, error, verifyCache, sync) { if (isFunction(post)) { - callback = post; + if (!isFunction(success)) { + verifyCache = success; + sync = error; + error = null; + } else { + sync = verifyCache; + verifyCache = error; + error = success; + } + success = post; post = null; + } else if (!isFunction(error)) { + sync = verifyCache; + verifyCache = error; + error = null; } + if (method == 'GET') { var data, dataCached; if (dataCached = cache.data[url]) { if (sync) { - callback(200, copy(dataCached.value)); + success(200, copy(dataCached.value)); } else { - $defer(function() { callback(200, copy(dataCached.value)); }); + $defer(function() { success(200, copy(dataCached.value)); }); } if (!verifyCache) @@ -46,30 +65,51 @@ angularServiceInject('$xhr.cache', function($xhr, $defer, $log){ } if (data = inflight[url]) { - data.callbacks.push(callback); + data.successes.push(success); + data.errors.push(error); } else { - inflight[url] = {callbacks: [callback]}; - cache.delegate(method, url, post, function(status, response){ - if (status == 200) - cache.data[url] = { value: response }; - var callbacks = inflight[url].callbacks; - delete inflight[url]; - forEach(callbacks, function(callback){ - try { - (callback||noop)(status, copy(response)); - } catch(e) { - $log.error(e); - } + inflight[url] = {successes: [success], errors: [error]}; + cache.delegate(method, url, post, + function(status, response) { + if (status == 200) + cache.data[url] = {value: response}; + var successes = inflight[url].successes; + delete inflight[url]; + forEach(successes, function(success) { + try { + (success||noop)(status, copy(response)); + } catch(e) { + $log.error(e); + } + }); + }, + function(status, response) { + var errors = inflight[url].errors, + successes = inflight[url].successes; + delete inflight[url]; + + forEach(errors, function(error, i) { + try { + if (isFunction(error)) { + error(status, copy(response)); + } else { + $error( + {method: method, url: url, data: post, success: successes[i]}, + {status: status, body: response}); + } + } catch(e) { + $log.error(e); + } + }); }); - }); } } else { cache.data = {}; - cache.delegate(method, url, post, callback); + cache.delegate(method, url, post, success, error); } } cache.data = {}; cache.delegate = $xhr; return cache; -}, ['$xhr.bulk', '$defer', '$log']); +}, ['$xhr.bulk', '$defer', '$xhr.error', '$log']); diff --git a/src/service/xhr.js b/src/service/xhr.js index 5fc5223e..dc18419d 100644 --- a/src/service/xhr.js +++ b/src/service/xhr.js @@ -21,10 +21,10 @@ * {@link angular.service.$resource $resource} service. * * # Error handling - * All XHR responses with response codes other then `2xx` are delegated to - * {@link angular.service.$xhr.error $xhr.error}. The `$xhr.error` can intercept the request - * and process it in application specific way, or resume normal execution by calling the - * request callback method. + * If no `error callback` is specified, XHR response with response code other then `2xx` will be + * delegated to {@link angular.service.$xhr.error $xhr.error}. The `$xhr.error` can intercept the + * request and process it in application specific way, or resume normal execution by calling the + * request `success` method. * * # HTTP Headers * The $xhr service will automatically add certain http headers to all requests. These defaults can @@ -96,14 +96,16 @@ * angular generated callback function. * @param {(string|Object)=} post Request content as either a string or an object to be stringified * as JSON before sent to the server. - * @param {function(number, (string|Object))} callback A function to be called when the response is - * received. The callback will be called with: + * @param {function(number, (string|Object))} success A function to be called when the response is + * received. The success function will be called with: * * - {number} code [HTTP status code](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes) of * the response. This will currently always be 200, since all non-200 responses are routed to - * {@link angular.service.$xhr.error} service. + * {@link angular.service.$xhr.error} service (or custom error callback). * - {string|Object} response Response object as string or an Object if the response was in JSON * format. + * @param {function(number, (string|Object))} error A function to be called if the response code is + * not 2xx.. Accepts the same arguments as success, above. * * @example <doc:example> @@ -158,9 +160,10 @@ angularServiceInject('$xhr', function($browser, $error, $log, $updateView){ patch: {} }; - function xhr(method, url, post, callback){ + function xhr(method, url, post, success, error) { if (isFunction(post)) { - callback = post; + error = success; + success = post; post = null; } if (post && isObject(post)) { @@ -176,11 +179,13 @@ angularServiceInject('$xhr', function($browser, $error, $log, $updateView){ } } if (200 <= code && code < 300) { - callback(code, response); + success(code, response); + } else if (isFunction(error)) { + error(code, response); } else { $error( - {method: method, url:url, data:post, callback:callback}, - {status: code, body:response}); + {method: method, url: url, data: post, success: success}, + {status: code, body: response}); } } catch (e) { $log.error(e); diff --git a/test/ResourceSpec.js b/test/ResourceSpec.js index 0b8d2187..81519f0f 100644 --- a/test/ResourceSpec.js +++ b/test/ResourceSpec.js @@ -1,12 +1,12 @@ 'use strict'; describe("resource", function() { - var xhr, resource, CreditCard, callback; + var xhr, resource, CreditCard, callback, $xhrErr; - beforeEach(function(){ - var browser = new MockBrowser(); - xhr = browser.xhr; - resource = new ResourceFactory(xhr); + beforeEach(function() { + var scope = angular.scope({}, null, {'$xhr.error': $xhrErr = jasmine.createSpy('xhr.error')}); + xhr = scope.$service('$browser').xhr; + resource = new ResourceFactory(scope.$service('$xhr')); CreditCard = resource.route('/CreditCard/:id:verb', {id:'@id.key'}, { charge:{ method:'POST', @@ -242,19 +242,25 @@ describe("resource", function() { dealoc(scope); }); - describe('failure mode', function(){ - it('should report error when non 200', function(){ - xhr.expectGET('/CreditCard/123').respond(500, "Server Error"); - var cc = CreditCard.get({id:123}); - try { - xhr.flush(); - fail('expected exception, non thrown'); - } catch (e) { - expect(e.status).toEqual(500); - expect(e.response).toEqual('Server Error'); - expect(e.message).toEqual('500: Server Error'); - } + describe('failure mode', function() { + var ERROR_CODE = 500, + ERROR_RESPONSE = 'Server Error'; + + beforeEach(function() { + xhr.expectGET('/CreditCard/123').respond(ERROR_CODE, ERROR_RESPONSE); }); - }); + it('should report error when non 2xx if error callback is not provided', function() { + CreditCard.get({id:123}); + xhr.flush(); + expect($xhrErr).toHaveBeenCalled(); + }); + + it('should call the error callback if provided on non 2xx response', function() { + CreditCard.get({id:123}, noop, callback); + xhr.flush(); + expect(callback).toHaveBeenCalledWith(500, ERROR_RESPONSE); + expect($xhrErr).not.toHaveBeenCalled(); + }); + }); }); diff --git a/test/service/xhr.bulkSpec.js b/test/service/xhr.bulkSpec.js index bc8d03f8..adcb61fa 100644 --- a/test/service/xhr.bulkSpec.js +++ b/test/service/xhr.bulkSpec.js @@ -4,13 +4,11 @@ describe('$xhr.bulk', function() { var scope, $browser, $browserXhr, $log, $xhrBulk, $xhrError, log; beforeEach(function(){ - scope = angular.scope({}, angular.service, { - '$xhr.error': $xhrError = jasmine.createSpy('$xhr.error'), - '$log': $log = {} - }); + scope = angular.scope({}, null, {'$xhr.error': $xhrError = jasmine.createSpy('$xhr.error')}); $browser = scope.$service('$browser'); $browserXhr = $browser.xhr; $xhrBulk = scope.$service('$xhr.bulk'); + $log = scope.$service('$log'); log = ''; }); @@ -60,12 +58,29 @@ describe('$xhr.bulk', function() { $browserXhr.flush(); expect($xhrError).toHaveBeenCalled(); - var cb = $xhrError.mostRecentCall.args[0].callback; + var cb = $xhrError.mostRecentCall.args[0].success; expect(typeof cb).toEqual($function); expect($xhrError).toHaveBeenCalledWith( - {url:'/req1', method:'GET', data:null, callback:cb}, - {status:404, response:'NotFound'}); + {url: '/req1', method: 'GET', data: null, success: cb}, + {status: 404, response: 'NotFound'}); expect(log).toEqual('"second";DONE'); }); + + it('should handle non 200 status code by calling error callback if provided', function() { + var callback = jasmine.createSpy('error'); + + $xhrBulk.urls['/'] = {match: /.*/}; + $xhrBulk('GET', '/req1', null, noop, callback); + + $browserXhr.expectPOST('/', { + requests:[{method: 'GET', url: '/req1', data: null}] + }).respond([{status: 404, response: 'NotFound'}]); + + $xhrBulk.flush(); + $browserXhr.flush(); + + expect($xhrError).not.toHaveBeenCalled(); + expect(callback).toHaveBeenCalledWith(404, 'NotFound'); + }); }); diff --git a/test/service/xhr.cacheSpec.js b/test/service/xhr.cacheSpec.js index 905a9dae..f4654cd4 100644 --- a/test/service/xhr.cacheSpec.js +++ b/test/service/xhr.cacheSpec.js @@ -1,10 +1,10 @@ 'use strict'; describe('$xhr.cache', function() { - var scope, $browser, $browserXhr, cache, log; + var scope, $browser, $browserXhr, $xhrErr, cache, log; - beforeEach(function(){ - scope = angular.scope(); + beforeEach(function() { + scope = angular.scope({}, null, {'$xhr.error': $xhrErr = jasmine.createSpy('$xhr.error')}); $browser = scope.$service('$browser'); $browserXhr = $browser.xhr; cache = scope.$service('$xhr.cache'); @@ -143,4 +143,36 @@ describe('$xhr.cache', function() { $browser.defer.flush(); expect(evalSpy).toHaveBeenCalled(); }); + + it('should call the error callback on error if provided', function() { + var errorSpy = jasmine.createSpy('error'), + successSpy = jasmine.createSpy('success'); + + $browserXhr.expectGET('/url').respond(500, 'error'); + + cache('GET', '/url', null, successSpy, errorSpy, false, true); + $browserXhr.flush(); + expect(errorSpy).toHaveBeenCalledWith(500, 'error'); + expect(successSpy).not.toHaveBeenCalled(); + + errorSpy.reset(); + cache('GET', '/url', successSpy, errorSpy, false, true); + $browserXhr.flush(); + expect(errorSpy).toHaveBeenCalledWith(500, 'error'); + expect(successSpy).not.toHaveBeenCalled(); + }); + + it('should call the $xhr.error on error if error callback not provided', function() { + var errorSpy = jasmine.createSpy('error'), + successSpy = jasmine.createSpy('success'); + + $browserXhr.expectGET('/url').respond(500, 'error'); + cache('GET', '/url', null, successSpy, false, true); + $browserXhr.flush(); + + expect(successSpy).not.toHaveBeenCalled(); + expect($xhrErr).toHaveBeenCalledWith( + {method: 'GET', url: '/url', data: null, success: successSpy}, + {status: 500, body: 'error'}); + }); }); diff --git a/test/service/xhr.errorSpec.js b/test/service/xhr.errorSpec.js index fdca93ec..d3af4565 100644 --- a/test/service/xhr.errorSpec.js +++ b/test/service/xhr.errorSpec.js @@ -29,10 +29,10 @@ describe('$xhr.error', function() { $browserXhr.expectPOST('/req', 'MyData').respond(500, 'MyError'); $xhr('POST', '/req', 'MyData', callback); $browserXhr.flush(); - var cb = $xhrError.mostRecentCall.args[0].callback; + var cb = $xhrError.mostRecentCall.args[0].success; expect(typeof cb).toEqual($function); expect($xhrError).toHaveBeenCalledWith( - {url:'/req', method:'POST', data:'MyData', callback:cb}, - {status:500, body:'MyError'}); + {url: '/req', method: 'POST', data: 'MyData', success: cb}, + {status: 500, body: 'MyError'}); }); }); diff --git a/test/service/xhrSpec.js b/test/service/xhrSpec.js index ed7cfc93..9f496535 100644 --- a/test/service/xhrSpec.js +++ b/test/service/xhrSpec.js @@ -1,12 +1,11 @@ 'use strict'; describe('$xhr', function() { - var scope, $browser, $browserXhr, $log, $xhr, log; + var scope, $browser, $browserXhr, $log, $xhr, $xhrErr, log; beforeEach(function(){ - scope = angular.scope({}, angular.service, { '$log': $log = { - error: dump - } }); + var scope = angular.scope({}, null, {'$xhr.error': $xhrErr = jasmine.createSpy('xhr.error')}); + $log = scope.$service('$log'); $browser = scope.$service('$browser'); $browserXhr = $browser.xhr; $xhr = scope.$service('$xhr'); @@ -57,12 +56,11 @@ describe('$xhr', function() { it('should handle exceptions in callback', function(){ - $log.error = jasmine.createSpy('$log.error'); $browserXhr.expectGET('/reqGET').respond('first'); $xhr('GET', '/reqGET', null, function(){ throw "MyException"; }); $browserXhr.flush(); - expect($log.error).toHaveBeenCalledWith("MyException"); + expect($log.error.logs.shift()).toContain('MyException'); }); @@ -104,6 +102,38 @@ describe('$xhr', function() { expect(response).toEqual([1, 'abc', {foo:'bar'}]); }); + it('should call $xhr.error on error if no error callback provided', function() { + var successSpy = jasmine.createSpy('success'); + + $browserXhr.expectGET('/url').respond(500, 'error'); + $xhr('GET', '/url', null, successSpy); + $browserXhr.flush(); + + expect(successSpy).not.toHaveBeenCalled(); + expect($xhrErr).toHaveBeenCalledWith( + {method: 'GET', url: '/url', data: null, success: successSpy}, + {status: 500, body: 'error'} + ); + }); + + it('should call the error callback on error if provided', function() { + var errorSpy = jasmine.createSpy('error'), + successSpy = jasmine.createSpy('success'); + + $browserXhr.expectGET('/url').respond(500, 'error'); + $xhr('GET', '/url', null, successSpy, errorSpy); + $browserXhr.flush(); + + expect(errorSpy).toHaveBeenCalledWith(500, 'error'); + expect(successSpy).not.toHaveBeenCalled(); + + errorSpy.reset(); + $xhr('GET', '/url', successSpy, errorSpy); + $browserXhr.flush(); + + expect(errorSpy).toHaveBeenCalledWith(500, 'error'); + expect(successSpy).not.toHaveBeenCalled(); + }); describe('http headers', function() { |
