Prechádzať zdrojové kódy

Alamofire 5: Abstract decoders for decodable response serialization (#2657)

* Cleanup previous changes.

* Add DataDecoder, refactor DecodableResponseSerializer.

* Add top level doc comment for DataDecoder.
Jon Shier 7 rokov pred
rodič
commit
2417ae3b2a

+ 0 - 2
Alamofire.xcodeproj/project.pbxproj

@@ -424,7 +424,6 @@
 		4CFB02F31D7D2FA20056F249 /* utf32_string.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = utf32_string.txt; sourceTree = "<group>"; };
 		4CFB02F41D7D2FA20056F249 /* utf8_string.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = utf8_string.txt; sourceTree = "<group>"; };
 		4DD67C0B1A5C55C900ED2280 /* Alamofire.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; };
-		72998D721BF26173006D3F69 /* Info-tvOS.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-tvOS.plist"; sourceTree = "<group>"; };
 		B39E2F831C1A72F8002DA1A9 /* certDER.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = certDER.cer; path = selfSignedAndMalformedCerts/certDER.cer; sourceTree = "<group>"; };
 		B39E2F841C1A72F8002DA1A9 /* certDER.crt */ = {isa = PBXFileReference; lastKnownFileType = file; name = certDER.crt; path = selfSignedAndMalformedCerts/certDER.crt; sourceTree = "<group>"; };
 		B39E2F851C1A72F8002DA1A9 /* certDER.der */ = {isa = PBXFileReference; lastKnownFileType = file; name = certDER.der; path = selfSignedAndMalformedCerts/certDER.der; sourceTree = "<group>"; };
@@ -834,7 +833,6 @@
 			children = (
 				F8111E3819A95C8B0040E7D1 /* Alamofire.h */,
 				F8111E3719A95C8B0040E7D1 /* Info.plist */,
-				72998D721BF26173006D3F69 /* Info-tvOS.plist */,
 			);
 			name = "Supporting Files";
 			sourceTree = "<group>";

+ 10 - 11
Source/AFError.swift

@@ -121,22 +121,21 @@ public enum AFError: Error {
     }
 
     /// The underlying reason the response serialization error occurred.
-    ///
-    /// - inputDataNil:                    The server response contained no data.
-    /// - inputDataNilOrZeroLength:        The server response contained no data or the data was zero length.
-    /// - inputFileNil:                    The file containing the server response did not exist.
-    /// - inputFileReadFailed:             The file containing the server response could not be read.
-    /// - stringSerializationFailed:       String serialization failed using the provided `String.Encoding`.
-    /// - jsonSerializationFailed:         JSON serialization failed with an underlying system error.
-    /// - invalidEmptyResponse:            Generic serialization failed for an empty response that wasn't type `Empty`.
     public enum ResponseSerializationFailureReason {
+        /// The server response contained no data or the data was zero length.
         case inputDataNilOrZeroLength
+        /// The file containing the server response did not exist.
         case inputFileNil
+        /// The file containing the server response could not be read from the associated `URL`.
         case inputFileReadFailed(at: URL)
+        /// String serialization failed using the provided `String.Encoding`.
         case stringSerializationFailed(encoding: String.Encoding)
+        /// JSON serialization failed with an underlying system error.
         case jsonSerializationFailed(error: Error)
+        /// A `DataDecoder` failed to decode the response due to the associated `Error`.
+        case decodingFailed(error: Error)
+        /// Generic serialization failed for an empty response that wasn't type `Empty` but instead the associated type.
         case invalidEmptyResponse(type: String)
-        case jsonDecodingFailed(error: Error)
     }
 
     /// Underlying reason a server trust evaluation error occured.
@@ -546,8 +545,8 @@ extension AFError.ResponseSerializationFailureReason {
             return "JSON could not be serialized because of error:\n\(error.localizedDescription)"
         case .invalidEmptyResponse(let type):
             return "Empty response could not be serialized to type: \(type). Use Empty as the expected type for such responses."
-        case .jsonDecodingFailed(let error):
-            return "JSON could not be decoded because of error:\n\(error.localizedDescription)"
+        case .decodingFailed(let error):
+            return "Response could not be decoded because of error:\n\(error.localizedDescription)"
         }
     }
 }

+ 33 - 15
Source/ResponseSerialization.swift

@@ -521,14 +521,32 @@ public struct Empty: Decodable {
     public static let value = Empty()
 }
 
-// MARK: - JSON Decodable
+// MARK: - DataDecoder Protocol
 
-/// A `ResponseSerializer` that decodes the response data as a generic value using a `JSONDecoder`. By default, a
-/// request returning `nil` or no data is considered an error. However, if the response is has a status code valid for
-/// empty responses (`204`, `205`), then the `Empty.value` value is returned.
-public final class JSONDecodableResponseSerializer<T: Decodable>: ResponseSerializer {
+/// Any type which can decode `Data`.
+public protocol DataDecoder {
+    /// Decode `Data` into the provided type.
+    ///
+    /// - Parameters:
+    ///   - type:  The `Type` to be decoded.
+    ///   - data:  The `Data`
+    /// - Returns: The decoded value of type `D`.
+    /// - Throws:  Any error that occurs during decode.
+    func decode<D: Decodable>(_ type: D.Type, from data: Data) throws -> D
+}
+
+/// `JSONDecoder` automatically conforms to `DataDecoder`.
+extension JSONDecoder: DataDecoder { }
+
+// MARK: - Decodable
+
+/// A `ResponseSerializer` that decodes the response data as a generic value using any type that conforms to
+/// `DataDecoder`. By default, this is an instance of `JSONDecoder`. Additionally, a request returning `nil` or no data
+/// is considered an error. However, if the response is has a status code valid for empty responses (`204`, `205`), then
+/// the `Empty.value` value is returned.
+public final class DecodableResponseSerializer<T: Decodable>: ResponseSerializer {
     /// The `JSONDecoder` instance used to decode responses.
-    public let decoder: JSONDecoder
+    public let decoder: DataDecoder
     /// HTTP response codes for which empty responses are allowed.
     public let emptyResponseCodes: Set<Int>
     /// HTTP request methods for which empty responses are allowed.
@@ -541,9 +559,9 @@ public final class JSONDecodableResponseSerializer<T: Decodable>: ResponseSerial
     ///   - emptyResponseCodes:  The HTTP response codes for which empty responses are allowed. Defaults to
     ///                          `[204, 205]`.
     ///   - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. Defaults to `[.head]`.
-    public init(decoder: JSONDecoder = JSONDecoder(),
-                emptyResponseCodes: Set<Int> = JSONDecodableResponseSerializer.defaultEmptyResponseCodes,
-                emptyRequestMethods: Set<HTTPMethod> = JSONDecodableResponseSerializer.defaultEmptyRequestMethods) {
+    public init(decoder: DataDecoder = JSONDecoder(),
+                emptyResponseCodes: Set<Int> = DecodableResponseSerializer.defaultEmptyResponseCodes,
+                emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer.defaultEmptyRequestMethods) {
         self.decoder = decoder
         self.emptyResponseCodes = emptyResponseCodes
         self.emptyRequestMethods = emptyRequestMethods
@@ -567,7 +585,7 @@ public final class JSONDecodableResponseSerializer<T: Decodable>: ResponseSerial
         do {
             return try decoder.decode(T.self, from: data)
         } catch {
-            throw AFError.responseSerializationFailed(reason: .jsonDecodingFailed(error: error))
+            throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error))
         }
     }
 }
@@ -578,16 +596,16 @@ extension DataRequest {
     /// - Parameters:
     ///   - queue:             The queue on which the completion handler is dispatched. Defaults to `nil`, which means
     ///                        the handler is called on `.main`.
-    ///   - decoder:           The decoder to use to decode the response. Defaults to a `JSONDecoder` with default
+    ///   - decoder:           The `DataDecoder` to use to decode the response. Defaults to a `JSONDecoder` with default
     ///                        settings.
     ///   - completionHandler: A closure to be executed once the request has finished.
     /// - Returns:             The request.
     @discardableResult
-    public func responseJSONDecodable<T: Decodable>(queue: DispatchQueue? = nil,
-                                                    decoder: JSONDecoder = JSONDecoder(),
-                                                    completionHandler: @escaping (DataResponse<T>) -> Void) -> Self {
+    public func responseDecodable<T: Decodable>(queue: DispatchQueue? = nil,
+                                                decoder: DataDecoder = JSONDecoder(),
+                                                completionHandler: @escaping (DataResponse<T>) -> Void) -> Self {
         return response(queue: queue,
-                        responseSerializer: JSONDecodableResponseSerializer(decoder: decoder),
+                        responseSerializer: DecodableResponseSerializer(decoder: decoder),
                         completionHandler: completionHandler)
     }
 }

+ 3 - 3
Tests/AFError+AlamofireTests.swift

@@ -133,7 +133,7 @@ extension AFError {
     }
 
     var isJSONDecodingFailed: Bool {
-        if case let .responseSerializationFailed(reason) = self, reason.isJSONDecodingFailed { return true }
+        if case let .responseSerializationFailed(reason) = self, reason.isDecodingFailed { return true }
         return false
     }
 
@@ -276,8 +276,8 @@ extension AFError.ResponseSerializationFailureReason {
         return false
     }
 
-    var isJSONDecodingFailed: Bool {
-        if case .jsonDecodingFailed = self { return true }
+    var isDecodingFailed: Bool {
+        if case .decodingFailed = self { return true }
         return false
     }
 }

+ 3 - 5
Tests/ParameterEncoderTests.swift

@@ -103,13 +103,11 @@ final class JSONParameterEncoderTests: BaseTestCase {
                     """
         XCTAssertEqual(encoded.httpBody?.asString, expected)
     }
+}
 
-    @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
+@available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
+final class SortedKeysJSONParameterEncoderTests: BaseTestCase {
     func testTestJSONEncoderSortedKeysHasSortedKeys() throws {
-        // Apparently marking the method as unavailable doesn't prevent it from running on older OSes.
-        guard #available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) else {
-            return
-        }
         // Given
         let encoder = JSONParameterEncoder.sortedKeys
         let request = URLRequest.makeHTTPBinRequest()

+ 3 - 3
Tests/RequestTests.swift

@@ -214,7 +214,7 @@ class RequestResponseTestCase: BaseTestCase {
 
         // When
         AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default)
-          .responseJSONDecodable { (response: DataResponse<HTTPBinResponse>) in
+          .responseDecodable { (response: DataResponse<HTTPBinResponse>) in
               receivedResponse = response
               expect.fulfill()
           }
@@ -233,7 +233,7 @@ class RequestResponseTestCase: BaseTestCase {
 
         // When
         AF.request("https://httpbin.org/get", method: .get, parameters: parameters)
-          .responseJSONDecodable { (response: DataResponse<HTTPBinResponse>) in
+          .responseDecodable { (response: DataResponse<HTTPBinResponse>) in
               receivedResponse = response
               expect.fulfill()
           }
@@ -252,7 +252,7 @@ class RequestResponseTestCase: BaseTestCase {
 
         // When
         AF.request("https://httpbin.org/post", method: .post, parameters: parameters)
-            .responseJSONDecodable { (response: DataResponse<HTTPBinResponse>) in
+            .responseDecodable { (response: DataResponse<HTTPBinResponse>) in
                 receivedResponse = response
                 expect.fulfill()
         }

+ 8 - 8
Tests/ResponseSerializationTests.swift

@@ -438,7 +438,7 @@ class DataResponseSerializationTestCase: BaseTestCase {
         }
     }
 
-    // MARK: JSONDecodableResponseSerializer
+    // MARK: DecodableResponseSerializer
 
     struct DecodableValue: Codable {
         let string: String
@@ -446,7 +446,7 @@ class DataResponseSerializationTestCase: BaseTestCase {
 
     func testThatJSONDecodableResponseSerializerFailsWhenDataIsNil() {
         // Given
-        let serializer = JSONDecodableResponseSerializer<DecodableValue>()
+        let serializer = DecodableResponseSerializer<DecodableValue>()
 
         // When
         let result = Result { try serializer.serialize(request: nil, response: nil, data: nil, error: nil) }
@@ -465,7 +465,7 @@ class DataResponseSerializationTestCase: BaseTestCase {
 
     func testThatJSONDecodableResponseSerializerFailsWhenDataIsEmpty() {
         // Given
-        let serializer = JSONDecodableResponseSerializer<DecodableValue>()
+        let serializer = DecodableResponseSerializer<DecodableValue>()
 
         // When
         let result = Result { try serializer.serialize(request: nil, response: nil, data: Data(), error: nil) }
@@ -485,7 +485,7 @@ class DataResponseSerializationTestCase: BaseTestCase {
     func testThatJSONDecodableResponseSerializerSucceedsWhenDataIsValidJSON() {
         // Given
         let data = Data("{\"string\":\"string\"}".utf8)
-        let serializer = JSONDecodableResponseSerializer<DecodableValue>()
+        let serializer = DecodableResponseSerializer<DecodableValue>()
 
         // When
         let result = Result { try serializer.serialize(request: nil, response: nil, data: data, error: nil) }
@@ -499,7 +499,7 @@ class DataResponseSerializationTestCase: BaseTestCase {
 
     func testThatJSONDecodableResponseSerializerFailsWhenDataIsInvalidJSON() {
         // Given
-        let serializer = JSONDecodableResponseSerializer<DecodableValue>()
+        let serializer = DecodableResponseSerializer<DecodableValue>()
         let data = Data("definitely not valid json".utf8)
 
         // When
@@ -513,7 +513,7 @@ class DataResponseSerializationTestCase: BaseTestCase {
 
     func testThatJSONDecodableResponseSerializerFailsWhenErrorIsNotNil() {
         // Given
-        let serializer = JSONDecodableResponseSerializer<DecodableValue>()
+        let serializer = DecodableResponseSerializer<DecodableValue>()
 
         // When
         let result = Result { try serializer.serialize(request: nil, response: nil, data: nil, error: error) }
@@ -532,7 +532,7 @@ class DataResponseSerializationTestCase: BaseTestCase {
 
     func testThatJSONDecodableResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() {
         // Given
-        let serializer = JSONDecodableResponseSerializer<DecodableValue>()
+        let serializer = DecodableResponseSerializer<DecodableValue>()
         let response = HTTPURLResponse(statusCode: 200)
 
         // When
@@ -552,7 +552,7 @@ class DataResponseSerializationTestCase: BaseTestCase {
 
     func testThatJSONDecodableResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() {
         // Given
-        let serializer = JSONDecodableResponseSerializer<Empty>()
+        let serializer = DecodableResponseSerializer<Empty>()
         let response = HTTPURLResponse(statusCode: 204)
 
         // When

+ 2 - 2
Tests/ResponseTests.swift

@@ -302,7 +302,7 @@ class ResponseJSONDecodableTestCase: BaseTestCase {
         var response: DataResponse<HTTPBinResponse>?
 
         // When
-        AF.request(urlString, parameters: [:]).responseJSONDecodable { (resp: DataResponse<HTTPBinResponse>) in
+        AF.request(urlString, parameters: [:]).responseDecodable { (resp: DataResponse<HTTPBinResponse>) in
             response = resp
             expectation.fulfill()
         }
@@ -326,7 +326,7 @@ class ResponseJSONDecodableTestCase: BaseTestCase {
         var response: DataResponse<HTTPBinResponse>?
 
         // When
-        AF.request(urlString, parameters: [:]).responseJSONDecodable { (resp: DataResponse<HTTPBinResponse>) in
+        AF.request(urlString, parameters: [:]).responseDecodable { (resp: DataResponse<HTTPBinResponse>) in
             response = resp
             expectation.fulfill()
         }