Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion dist/hypermedia.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ angular.module('hypermedia')

angular.module('hypermedia')

.config(function ($httpProvider) {
$httpProvider.interceptors.push('errorInterceptor');
})

/**
* @ngdoc type
* @name ResourceContext
Expand All @@ -104,7 +108,7 @@ angular.module('hypermedia')
* Context for working with hypermedia resources. The context has methods
* for making HTTP requests and acts as an identity map.
*/
.factory('ResourceContext', ['$http', '$log', '$q', 'Resource', function ($http, $log, $q, Resource) {
.factory('ResourceContext', ['$http', '$log', '$q', 'Resource', 'errorInterceptor', function ($http, $log, $q, Resource, errorInterceptor) {

var busyRequests = 0;

Expand Down Expand Up @@ -300,6 +304,10 @@ angular.module('hypermedia')
*/
busyRequests: {get: function () {
return busyRequests;
}},

registerErrorHandler: {value: function (contentType, handler) {
errorInterceptor.registerErrorHandler(contentType, handler);
}}
});

Expand All @@ -323,6 +331,22 @@ angular.module('hypermedia')
}
}])

.factory('errorInterceptor', function ($q) {
var handlers;
return {
'responseError': function (response) {
var contentType = response.headers('Content-Type');
var handler = handlers[contentType];
response.error = (handler ? handler(response) : {message: response.statusText});
return $q.reject(response);
},
'registerErrorHandler': function (contentType, handler) {
if (!handlers) handlers = {};
handlers[contentType] = handler;
}
};
})

;

/**
Expand Down Expand Up @@ -985,3 +1009,38 @@ angular.module('hypermedia')
})

;

'use strict';

angular.module('hypermedia')

.run(function ($q, ResourceContext, VndError) {
var vndErrorHandler = function (response) {
return new VndError(response.data);
};

ResourceContext.registerErrorHandler('application/vnd+error', vndErrorHandler);
})

.factory('VndError', function () {
var HalError = function (data) {
var self = this;
this.message = data.message;
this.errors = [];

var embeds = (data._embedded ? data._embedded.errors : undefined);
if (embeds) {
if (!Array.isArray(embeds)) {
embeds = [embeds];
}
embeds.forEach(function (embed) {
self.errors.push(new HalError(embed));
});
}
};

return HalError;
})


;
35 changes: 34 additions & 1 deletion src/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

angular.module('hypermedia')

.config(function ($httpProvider) {
$httpProvider.interceptors.push('errorInterceptor');
})

/**
* @ngdoc type
* @name ResourceContext
Expand All @@ -10,7 +14,7 @@ angular.module('hypermedia')
* Context for working with hypermedia resources. The context has methods
* for making HTTP requests and acts as an identity map.
*/
.factory('ResourceContext', ['$http', '$log', '$q', 'Resource', function ($http, $log, $q, Resource) {
.factory('ResourceContext', ['$http', '$log', '$q', 'Resource', 'errorInterceptor', function ($http, $log, $q, Resource, errorInterceptor) {

var busyRequests = 0;

Expand Down Expand Up @@ -206,6 +210,10 @@ angular.module('hypermedia')
*/
busyRequests: {get: function () {
return busyRequests;
}},

registerErrorHandler: {value: function (contentType, handler) {
errorInterceptor.registerErrorHandler(contentType, handler);
}}
});

Expand All @@ -229,6 +237,31 @@ angular.module('hypermedia')
}
}])

/**
* @ngdoc service
* @name errorInterceptor
* @description
*
* Intercepts error from server and invokes error handler for content-type,
* or default error handler if none is found. Error with message is published
* on response under 'error' key.
*/
.factory('errorInterceptor', function ($q) {
var handlers;
return {
'responseError': function (response) {
var contentType = response.headers('Content-Type');
var handler = handlers[contentType];
response.error = (handler ? handler(response) : {message: response.statusText});
return $q.reject(response);
},
'registerErrorHandler': function (contentType, handler) {
if (!handlers) handlers = {};
handlers[contentType] = handler;
}
};
})

;

/**
Expand Down
62 changes: 60 additions & 2 deletions src/context.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ describe('ResourceContext', function () {

// Setup

var $httpBackend, ResourceContext, context, resource;
var $httpBackend, $q, ResourceContext, context, errorInterceptor, resource;
var problemJson = 'application/problem+json';

beforeEach(inject(function (_$httpBackend_, _ResourceContext_) {
beforeEach(inject(function (_$httpBackend_, _$q_, _ResourceContext_, _errorInterceptor_) {
$httpBackend = _$httpBackend_;
$q = _$q_;
ResourceContext = _ResourceContext_;
errorInterceptor = _errorInterceptor_;
context = new ResourceContext();
resource = context.get('http://example.com');
}));
Expand All @@ -23,6 +26,61 @@ describe('ResourceContext', function () {

// Tests

it('registers error handler', function () {
var func = function () {};
spyOn(errorInterceptor, 'registerErrorHandler');

ResourceContext.registerErrorHandler(problemJson, func);

expect(errorInterceptor.registerErrorHandler).toHaveBeenCalledWith(problemJson, func);
});

it('invokes error handler for content type', function () {
var spy = jasmine.createSpy('spy').and.callFake(function (response) {
return {};
});
ResourceContext.registerErrorHandler(problemJson, spy);

context.httpGet(resource);
$httpBackend.expectGET(resource.$uri, {'Accept': 'application/json'})
.respond(500, null, {'Content-Type': problemJson});
$httpBackend.flush();

expect(spy).toHaveBeenCalled();
});

it('rejects response with error if no matching error handler', function () {
var statusText = 'Validation error';
var promiseResult = null;
var spy = jasmine.createSpy('spy');

ResourceContext.registerErrorHandler('application/json', spy);
context.httpGet(resource).catch(function (result) {
promiseResult = result;
});
$httpBackend.expectGET(resource.$uri, {'Accept': 'application/json'})
.respond(500, {}, {'Content-Type': problemJson}, statusText);
$httpBackend.flush();

expect(spy).not.toHaveBeenCalled();
expect(promiseResult.error.message).toBe(statusText);
expect(promiseResult.status).toBe(500);
});

it('invokes default error handler for content type "application/vnd+error"', function () {
var promiseResult;
var msg = 'Validatie fout';
context.httpGet(resource).catch(function (result) {
promiseResult = result;
});
$httpBackend.expectGET(resource.$uri, {'Accept': 'application/json'})
.respond(500, {message: msg}, {'Content-Type': 'application/vnd+error'});
$httpBackend.flush();

expect(promiseResult.error).toBeDefined();
expect(promiseResult.error.message).toBe(msg);
});

it('creates unique resources', function () {
expect(context.get('http://example.com')).toBe(resource);
expect(context.get('http://example.com/other')).not.toBe(resource);
Expand Down
42 changes: 42 additions & 0 deletions src/vnderror.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

angular.module('hypermedia')

.run(function ($q, ResourceContext, VndError) {
var vndErrorHandler = function (response) {
return new VndError(response.data);
};

ResourceContext.registerErrorHandler('application/vnd+error', vndErrorHandler);
})

/**
* @ngdoc type
* @name VndError
* @description
*
* VndError represents errors from server with content type 'application/vnd+error',
* see: https://github.com/blongden/vnd.error
*/
.factory('VndError', function () {
var HalError = function (data) {
var self = this;
this.message = data.message;
this.errors = [];

var embeds = (data._embedded ? data._embedded.errors : undefined);
if (embeds) {
if (!Array.isArray(embeds)) {
embeds = [embeds];
}
embeds.forEach(function (embed) {
self.errors.push(new HalError(embed));
});
}
};

return HalError;
})


;
29 changes: 29 additions & 0 deletions src/vnderror.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

describe('HalError', function () {
beforeEach(module('hypermedia'));

var VndError;

beforeEach(inject(function (_VndError_) {
VndError = _VndError_;
}));

it('can be constructed with embedded errors', function () {
var data = {
'message': 'Validatie fout',
'_links': {'profile': {'href': 'http://nocarrier.co.uk/profiles/vnd.error/'}},
'_embedded': {
'errors': {
'message': 'Invalide nummer',
'_links': {'profile': {'href': 'http://nocarrier.co.uk/profiles/vnd.error/'}}
}
}
};

var error = new VndError(data);

expect(error.message).toBe('Validatie fout');
expect(error.errors.length).toBe(1);
});
});