From ab9d2c322335fdd57e52db6b9e67d6e32b9eadcd Mon Sep 17 00:00:00 2001 From: Lily Ballard Date: Tue, 23 Apr 2019 18:57:27 -0700 Subject: [PATCH] Add computed properties to HTTPManagerError * 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. --- README.md | 4 +++ Sources/HTTPManager.swift | 56 +++++++++++++++++++++++++++++++++++++++ Sources/PMHTTPError.h | 9 +++++++ Sources/PMHTTPError.m | 17 +++++++----- Tests/PMHTTPErrorTests.m | 21 +++++++++++++++ Tests/PMHTTPTests.swift | 38 ++++++++++++++++++++++++++ 6 files changed, 139 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e7acd58..5a7592c 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/Sources/HTTPManager.swift b/Sources/HTTPManager.swift index 08e6def..8f211f5 100644 --- a/Sources/HTTPManager.swift +++ b/Sources/HTTPManager.swift @@ -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): diff --git a/Sources/PMHTTPError.h b/Sources/PMHTTPError.h index afb544f..7cd4ca2 100644 --- a/Sources/PMHTTPError.h +++ b/Sources/PMHTTPError.h @@ -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); diff --git a/Sources/PMHTTPError.m b/Sources/PMHTTPError.m index 1f5b59f..e912879 100644 --- a/Sources/PMHTTPError.m +++ b/Sources/PMHTTPError.m @@ -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; } diff --git a/Tests/PMHTTPErrorTests.m b/Tests/PMHTTPErrorTests.m index 2afb6ce..533a406 100644 --- a/Tests/PMHTTPErrorTests.m +++ b/Tests/PMHTTPErrorTests.m @@ -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"); @@ -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 diff --git a/Tests/PMHTTPTests.swift b/Tests/PMHTTPTests.swift index 01bff86..7fb61a0 100644 --- a/Tests/PMHTTPTests.swift +++ b/Tests/PMHTTPTests.swift @@ -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