Skip to content

Commit

Permalink
Add computed properties to HTTPManagerError
Browse files Browse the repository at this point in the history
* Add properties for accessing fields common to two or more variants.
* Add `statusCode` property that returns the status code for all errors
  except for `.unexpectedContentType`.
* Add `PMHTTPErrorGetStatusCode()` function for Obj-C.

Fixes #60.
  • Loading branch information
lilyball committed Apr 24, 2019
1 parent b2d5b92 commit ab9d2c3
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 6 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,11 @@ work by you shall be dual licensed as above, without any additional terms or con
#### Development

* Fix a bug when parsing images where we passed the wrong value for the type identifier hint, resulting in a warning being logged to the console ([#62][]).
* Add computed properties on `HTTPManagerError` for convenient access to the associated values (e.g. `.response`, `.body`, etc).
* Add computed property `HTTPManagerError.statusCode` that returns the failing status code for the error, or `nil` for `.unexpectedContentType` ([#60][]).
* Add Obj-C function `PMHTTPErrorGetStatusCode()` that returns the failing status code for the error, or `nil` for `PMHTTPErrorUnexpectedContentType` or for non-PMHTTP errors ([#60][]).

[#60]: https://github.com/postmates/PMHTTP/issues/60 "HTTPManagerError should have .statusCode property · Issue #60 · postmates/PMHTTP"
[#62]: https://github.com/postmates/PMHTTP/issues/62 "Unknown Hint Identifier for Image MIME Types · Issue #62 · postmates/PMHTTP"

#### v4.3.3 (2019-04-07)
Expand Down
56 changes: 56 additions & 0 deletions Sources/HTTPManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1316,6 +1316,62 @@ public enum HTTPManagerError: Error, CustomStringConvertible, CustomDebugStringC
/// - Parameter body: The body of the response, if any.
case unexpectedRedirect(statusCode: Int, location: URL?, response: HTTPURLResponse, body: Data)

/// Returns the HTTP status code for the error.
///
/// If the error does not represent a response with a non-successful status code (e.g.
/// `unexpectedContentType`), this returns `nil` instead.
public var statusCode: Int? {
switch self {
case .failedResponse(let statusCode, _, _, _),
.unexpectedRedirect(let statusCode, _, _, _):
return statusCode
case .unauthorized: return 401
case .unexpectedContentType: return nil
case .unexpectedNoContent: return 204
}
}

/// Returns the `HTTPURLResponse` for the error.
public var response: HTTPURLResponse {
switch self {
case .failedResponse(_, let response, _, _),
.unauthorized(_, let response, _, _),
.unexpectedContentType(_, let response, _),
.unexpectedNoContent(let response),
.unexpectedRedirect(_, _, let response, _):
return response
}
}

/// Returns the response body for the error, or `nil` for `.unexpectedNoContent`.
public var body: Data? {
switch self {
case .failedResponse(_, _, let body, _),
.unauthorized(_, _, let body, _),
.unexpectedContentType(_, _, let body),
.unexpectedRedirect(_, _, _, let body):
return body
case .unexpectedNoContent: return nil
}
}

/// Returns the response body for the error as a `JSON`, if possible.
///
/// If a `.failedResponse` or `.unauthorized` response declares a `Content-Type` of
/// `application/json` or `text/json`, this returns the results of decoding the body as JSON.
/// Otherwise, or if the decoding fails, this returns `nil`.
public var bodyJson: JSON? {
switch self {
case .failedResponse(_, _, _, let bodyJson),
.unauthorized(_, _, _, let bodyJson):
return bodyJson
case .unexpectedContentType,
.unexpectedNoContent,
.unexpectedRedirect:
return nil
}
}

public var description: String {
switch self {
case let .failedResponse(statusCode, response, body, json):
Expand Down
9 changes: 9 additions & 0 deletions Sources/PMHTTPError.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,12 @@ extern NSString * _Nonnull const PMHTTPCredentialErrorKey NS_UNAVAILABLE;
/// PMHTTPErrorUnexpectedRedirect in addition to \c PMHTTPErrorFailedResponse.
NS_SWIFT_UNAVAILABLE("use pattern matching against HTTPManagerError")
BOOL PMHTTPErrorIsFailedResponse(NSError * _Nullable error, NSInteger statusCode);

/// Returns the HTTP status code from a PMHTTP error.
///
/// \param error The \c NSError to test.
/// \returns An \c NSNumber containing the HTTP status code represented by the error if the error is
/// a PMHTTP error that corresponds to a specific status code, otherwise \c nil if the error is not
/// a PMHTTP error or is an error that represents something other than a non-successful status code.
NS_SWIFT_UNAVAILABLE("use HTTPManagerError.statusCode")
NSNumber * _Nullable PMHTTPErrorGetStatusCode(NSError * _Nullable error);
17 changes: 11 additions & 6 deletions Sources/PMHTTPError.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,24 @@
NSString * const PMHTTPLocationErrorKey = @"location";

BOOL PMHTTPErrorIsFailedResponse(NSError * _Nullable error, NSInteger statusCode) {
if (![error.domain isEqualToString:PMHTTPErrorDomain]) return NO;
NSNumber *errorStatusCode = PMHTTPErrorGetStatusCode(error);
return errorStatusCode && errorStatusCode.integerValue == statusCode;
}

NSNumber * _Nullable PMHTTPErrorGetStatusCode(NSError * _Nullable error) {
if (![error.domain isEqualToString:PMHTTPErrorDomain]) return nil;
switch ((PMHTTPError)error.code) {
case PMHTTPErrorFailedResponse:
case PMHTTPErrorUnexpectedRedirect: {
NSNumber *errorStatusCode = error.userInfo[PMHTTPStatusCodeErrorKey];
return [errorStatusCode isKindOfClass:[NSNumber class]] && errorStatusCode.integerValue == statusCode;
return [errorStatusCode isKindOfClass:[NSNumber class]] ? errorStatusCode : nil;
}
case PMHTTPErrorUnauthorized:
return statusCode == 401;
return @401;
case PMHTTPErrorUnexpectedContentType:
return NO;
return nil;
case PMHTTPErrorUnexpectedNoContent:
return statusCode == 204;
return @204;
}
return NO;
return nil;
}
21 changes: 21 additions & 0 deletions Tests/PMHTTPErrorTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ - (void)testPMHTTPErrorIsFailedResponse {
// unexpectedContentType
XCTAssertFalse(PMHTTPErrorIsFailedResponse([ObjCTestSupport createUnexpectedContentTypeErrorWithContentType:@"text/plain" response:response body:[NSData data]], 500), @"unexpectedContentType");
XCTAssertFalse(PMHTTPErrorIsFailedResponse([ObjCTestSupport createUnexpectedContentTypeErrorWithContentType:@"text/plain" response:response body:[NSData data]], response.statusCode), @"unexpectedContentType");
XCTAssertFalse(PMHTTPErrorIsFailedResponse([ObjCTestSupport createUnexpectedContentTypeErrorWithContentType:@"text/plain" response:response body:[NSData data]], 0), @"unexpectedContentType");

// unexpectedNoContent
XCTAssert(PMHTTPErrorIsFailedResponse([ObjCTestSupport createUnexpectedNoContentErrorWith:response], 204), @"unexpectedNoContent");
Expand All @@ -61,6 +62,26 @@ - (void)testPMHTTPErrorIsFailedResponse {
XCTAssertFalse(PMHTTPErrorIsFailedResponse([ObjCTestSupport createUnexpectedRedirectErrorWithStatusCode:301 location:nil response:response body:[NSData data]], 304), @"unexpectedRedirect code 301");
XCTAssertFalse(PMHTTPErrorIsFailedResponse([ObjCTestSupport createUnexpectedRedirectErrorWithStatusCode:304 location:nil response:response body:[NSData data]], 301), @"unexpectedRedirect code 304");
XCTAssertFalse(PMHTTPErrorIsFailedResponse([ObjCTestSupport createUnexpectedRedirectErrorWithStatusCode:301 location:nil response:response body:[NSData data]], response.statusCode), @"unexpectedRedirect code 301");

// Dummy error with the userInfo from a PMHTTP error
NSError *dummyError = [NSError errorWithDomain:@"DummyErrorDomain" code:PMHTTPErrorFailedResponse
userInfo:[ObjCTestSupport createFailedResponseErrorWithStatusCode:500 response:response body:[NSData data] bodyJson:nil].userInfo];
XCTAssertFalse(PMHTTPErrorIsFailedResponse(dummyError, 500));
XCTAssertFalse(PMHTTPErrorIsFailedResponse(dummyError, 419));
}

- (void)testPMHTTPErrorGetStatusCode {
// Use a dummy response for all errors. The status code of the response doesn't matter,
// PMHTTPErrorGetStatusCode looks at the error keys instead.
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://example.com"] statusCode:419 HTTPVersion:nil headerFields:nil];

XCTAssertEqualObjects(PMHTTPErrorGetStatusCode([ObjCTestSupport createFailedResponseErrorWithStatusCode:500 response:response body:[NSData data] bodyJson:nil]), @500, @"failedResponse code 500");
XCTAssertEqualObjects(PMHTTPErrorGetStatusCode([ObjCTestSupport createFailedResponseErrorWithStatusCode:404 response:response body:[NSData data] bodyJson:nil]), @404, @"failedResponse code 404");
XCTAssertEqualObjects(PMHTTPErrorGetStatusCode([ObjCTestSupport createUnauthorizedErrorWith:nil response:response body:[NSData data] bodyJson:nil]), @401, @"unauthorized");
XCTAssertNil(PMHTTPErrorGetStatusCode([ObjCTestSupport createUnexpectedContentTypeErrorWithContentType:@"text/plain" response:response body:[NSData data]]), @"unexpectedContentType");
XCTAssertEqualObjects(PMHTTPErrorGetStatusCode([ObjCTestSupport createUnexpectedNoContentErrorWith:response]), @204, @"unexpectedNoContent");
XCTAssertEqualObjects(PMHTTPErrorGetStatusCode([ObjCTestSupport createUnexpectedRedirectErrorWithStatusCode:301 location:nil response:response body:[NSData data]]), @301, @"unexpectedRedirect code 301");
XCTAssertEqualObjects(PMHTTPErrorGetStatusCode([ObjCTestSupport createUnexpectedRedirectErrorWithStatusCode:304 location:nil response:response body:[NSData data]]), @304, @"unexpectedRedirect code 304");
}

@end
38 changes: 38 additions & 0 deletions Tests/PMHTTPTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,44 @@ final class PMHTTPTests: PMHTTPTestCase {
}
}

func testErrorProperties() {
let response = HTTPURLResponse(url: URL(string: "http://example.com")!, statusCode: 419, httpVersion: nil, headerFields: nil)!

// statusCode
XCTAssertEqual(HTTPManagerError.failedResponse(statusCode: 500, response: response, body: Data(), bodyJson: nil).statusCode, 500, "statusCode - failedResponse code 500")
XCTAssertEqual(HTTPManagerError.failedResponse(statusCode: 404, response: response, body: Data(), bodyJson: nil).statusCode, 404, "statusCode - failedResponse code 404")
XCTAssertEqual(HTTPManagerError.unauthorized(auth: nil, response: response, body: Data(), bodyJson: nil).statusCode, 401, "statusCode - unauthorized")
XCTAssertNil(HTTPManagerError.unexpectedContentType(contentType: "text/plain", response: response, body: Data()).statusCode, "statusCode - unexpectedContentType")
XCTAssertEqual(HTTPManagerError.unexpectedNoContent(response: response).statusCode, 204, "statusCode - unexpectedNoContent")
XCTAssertEqual(HTTPManagerError.unexpectedRedirect(statusCode: 301, location: nil, response: response, body: Data()).statusCode, 301, "statusCode - unexpectedRedirect code 301")
XCTAssertEqual(HTTPManagerError.unexpectedRedirect(statusCode: 304, location: nil, response: response, body: Data()).statusCode, 304, "statusCode - unexpectedRedirect code 304")

// response
XCTAssertEqual(HTTPManagerError.failedResponse(statusCode: 400, response: response, body: Data(), bodyJson: nil).response, response, "response - failedResponse")
XCTAssertEqual(HTTPManagerError.unauthorized(auth: nil, response: response, body: Data(), bodyJson: nil).response, response, "response - unauthorized")
XCTAssertEqual(HTTPManagerError.unexpectedContentType(contentType: "text/plain", response: response, body: Data()).response, response, "response - unexpectedContentType")
XCTAssertEqual(HTTPManagerError.unexpectedNoContent(response: response).response, response, "response - unexpectedNoContent")
XCTAssertEqual(HTTPManagerError.unexpectedRedirect(statusCode: 304, location: nil, response: response, body: Data()).response, response, "response - unexpectedRedirect")

// body
let data = "hello world".data(using: .utf8)!
XCTAssertEqual(HTTPManagerError.failedResponse(statusCode: 400, response: response, body: data, bodyJson: nil).body, data, "body - failedResponse")
XCTAssertEqual(HTTPManagerError.unauthorized(auth: nil, response: response, body: data, bodyJson: nil).body, data, "body - unauthorized")
XCTAssertEqual(HTTPManagerError.unexpectedContentType(contentType: "text/plain", response: response, body: data).body, data, "body - unexpectedContentType")
XCTAssertNil(HTTPManagerError.unexpectedNoContent(response: response).body, "body - unexpectedNoContent")
XCTAssertEqual(HTTPManagerError.unexpectedRedirect(statusCode: 304, location: nil, response: response, body: data).body, data, "body - unexpectedRedirect")

// bodyJson
let json: JSON = ["ok": true, "msg": "Hello world"]
XCTAssertEqual(HTTPManagerError.failedResponse(statusCode: 500, response: response, body: Data(), bodyJson: json).bodyJson, json, "bodyJson - failedResponse with json")
XCTAssertNil(HTTPManagerError.failedResponse(statusCode: 500, response: response, body: Data(), bodyJson: nil).bodyJson, "bodyJson - failedResponse without json")
XCTAssertEqual(HTTPManagerError.unauthorized(auth: nil, response: response, body: Data(), bodyJson: json).bodyJson, json, "bodyJson - unauthorized with json")
XCTAssertNil(HTTPManagerError.unauthorized(auth: nil, response: response, body: Data(), bodyJson: nil).bodyJson, "bodyJson - unauthorized without json")
XCTAssertNil(HTTPManagerError.unexpectedContentType(contentType: "text/plain", response: response, body: Data()).bodyJson, "bodyJson - unexpectedContentType")
XCTAssertNil(HTTPManagerError.unexpectedNoContent(response: response).bodyJson, "bodyJson - unexpectedNoContent")
XCTAssertNil(HTTPManagerError.unexpectedRedirect(statusCode: 304, location: nil, response: response, body: Data()).bodyJson, "bodyJson - unexpectedRedirect")
}

func testEnvironmentWithPath() {
HTTP.environment = HTTPManager.Environment(string: "http://\(httpServer.address)/api/v1")!
expectationForHTTPRequest(httpServer, path: "/api/v1/foo") { request, completionHandler in
Expand Down

0 comments on commit ab9d2c3

Please sign in to comment.