Browse Source

Add URLResponseSerializer. (#3343)

Jon Shier 5 years ago
parent
commit
d9259d107a

+ 5 - 5
Documentation/Usage.md

@@ -681,14 +681,14 @@ However, headers that must be part of all requests are often better handled as p
 In addition to fetching data into memory, Alamofire also provides the `Session.download`, `DownloadRequest`, and `DownloadResponse<Success, Failure: Error>` APIs to facilitate downloading to disk. While downloading into memory works great for small payloads like most JSON API responses, fetching larger assets like images and videos should be downloaded to disk to avoid memory issues with your application.
 
 ```swift
-AF.download("https://httpbin.org/image/png").responseData { response in
-    if let data = response.value {
-        let image = UIImage(data: data)
-    }
+AF.download("https://httpbin.org/image/png").responseURL { response in
+    // Read file from provided URL.
 }
 ```
 
-> `DownloadRequest` has most of the same `response` handlers that `DataRequest` does. However, since it downloads data to disk, serializing the response involves reading from disk, and may also involve reading large amounts of data into memory. It's important to keep these facts in mind when architecting your download handling.
+In addition to having the same response handlers that `DataRequest` does, `DownloadRequest` also includes `responseURL`. Unlike the other response handlers, this handler just returns the `URL` containing the location of the downloaded data and does not read the `Data` from disk.
+
+Other response handlers, like `responseDecodable`, involve reading the response `Data` from disk. This may involve reading large amounts of data into memory, so it's important to keep that in mind when using those handlers.
 
 #### Download File Destination
 

+ 14 - 2
Source/Combine.swift

@@ -503,17 +503,29 @@ extension DownloadRequest {
         DownloadResponsePublisher(self, queue: queue, serializer: serializer)
     }
 
-    /// Creates a `DataResponsePublisher` for this instance and uses a `DataResponseSerializer` to serialize the
+    /// Creates a `DownloadResponsePublisher` for this instance and uses a `URLResponseSerializer` to serialize the
+    /// response.
+    ///
+    /// - Parameter queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default.
+    ///
+    /// - Returns:         The `DownloadResponsePublisher`.
+    @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
+    public func publishURL(queue: DispatchQueue = .main) -> DownloadResponsePublisher<URL> {
+        publishResponse(using: URLResponseSerializer(), on: queue)
+    }
+
+    /// Creates a `DownloadResponsePublisher` for this instance and uses a `DataResponseSerializer` to serialize the
     /// response.
     ///
     /// - Parameters:
-    ///   - queue:               `DispatchQueue` on which the `DataResponse` will be published. `.main` by default.
+    ///   - queue:               `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default.
     ///   - preprocessor:        `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()`
     ///                          by default.
     ///   - emptyResponseCodes:  `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by
     ///                          default.
     ///   - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless of
     ///                          status code. `[.head]` by default.
+    ///
     /// - Returns:               The `DownloadResponsePublisher`.
     @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
     public func publishData(queue: DispatchQueue = .main,

+ 37 - 0
Source/ResponseSerialization.swift

@@ -394,6 +394,43 @@ extension DownloadRequest {
     }
 }
 
+// MARK: - URL
+
+/// A `DownloadResponseSerializerProtocol` that performs only `Error` checking and ensures that a downloaded `fileURL`
+/// is present.
+public struct URLResponseSerializer: DownloadResponseSerializerProtocol {
+    /// Creates an instance.
+    public init() {}
+
+    public func serializeDownload(request: URLRequest?,
+                                  response: HTTPURLResponse?,
+                                  fileURL: URL?,
+                                  error: Error?) throws -> URL {
+        guard error == nil else { throw error! }
+
+        guard let url = fileURL else {
+            throw AFError.responseSerializationFailed(reason: .inputFileNil)
+        }
+
+        return url
+    }
+}
+
+extension DownloadRequest {
+    /// Adds a handler using a `URLResponseSerializer` to be called once the request is finished.
+    ///
+    /// - Parameters:
+    ///   - queue:             The queue on which the completion handler is called. `.main` by default.
+    ///   - completionHandler: A closure to be executed once the request has finished.
+    ///
+    /// - Returns:             The request.
+    @discardableResult
+    public func responseURL(queue: DispatchQueue = .main,
+                            completionHandler: @escaping (AFDownloadResponse<URL>) -> Void) -> Self {
+        response(queue: queue, responseSerializer: URLResponseSerializer(), completionHandler: completionHandler)
+    }
+}
+
 // MARK: - Data
 
 /// A `ResponseSerializer` that performs minimal response checking and returns any response `Data` as-is. By default, a

+ 21 - 0
Tests/CombineTests.swift

@@ -1090,6 +1090,27 @@ final class DownloadRequestCombineTests: CombineTestCase {
         XCTAssertTrue(response?.result.isSuccess == true)
     }
 
+    @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
+    func testThatDownloadRequestCanPublishURL() {
+        // Given
+        let responseReceived = expectation(description: "response should be received")
+        let completionReceived = expectation(description: "publisher should complete")
+        var response: DownloadResponse<URL, AFError>?
+
+        // When
+        store {
+            AF.download(URLRequest.makeHTTPBinRequest())
+                .publishURL()
+                .sink(receiveCompletion: { _ in completionReceived.fulfill() },
+                      receiveValue: { response = $0; responseReceived.fulfill() })
+        }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertTrue(response?.result.isSuccess == true)
+    }
+
     @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
     func testThatDownloadRequestCanPublishWithMultipleHandlers() {
         // Given

+ 27 - 0
Tests/DownloadTests.swift

@@ -112,6 +112,32 @@ class DownloadResponseTestCase: BaseTestCase {
         }
     }
 
+    func testDownloadRequestResponseURLProducesURL() throws {
+        // Given
+        let request = URLRequest.makeHTTPBinRequest()
+        let expectation = self.expectation(description: "Download request should download data")
+        var response: DownloadResponse<URL, AFError>?
+
+        // When
+        AF.download(request)
+            .responseURL { resp in
+                response = resp
+                expectation.fulfill()
+            }
+
+        waitForExpectations(timeout: timeout, handler: nil)
+
+        // Then
+        XCTAssertNotNil(response?.request)
+        XCTAssertNotNil(response?.response)
+        XCTAssertNotNil(response?.fileURL)
+        XCTAssertNil(response?.resumeData)
+        XCTAssertNil(response?.error)
+
+        let url = try XCTUnwrap(response?.value)
+        XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
+    }
+
     func testCancelledDownloadRequest() {
         // Given
         let fileURL = randomCachesFileURL
@@ -585,6 +611,7 @@ final class DownloadResumeDataTestCase: BaseTestCase {
         XCTAssertEqual(response?.resumeData, download.resumeData)
     }
 
+    // Disabled until we can find another source which supports resume ranges.
     func _testThatCancelledDownloadCanBeResumedWithResumeData() {
         // Given
         let expectation1 = expectation(description: "Download should be cancelled")

+ 54 - 0
Tests/ResponseSerializationTests.swift

@@ -458,6 +458,60 @@ final class DataResponseSerializationTestCase: BaseTestCase {
 
 // MARK: -
 
+final class URLResponseSerializerTests: BaseTestCase {
+    func testThatURLResponseSerializerProducesURLOnSuccess() {
+        // Given
+        let serializer = URLResponseSerializer()
+        let request = URLRequest.makeHTTPBinRequest()
+        let response = HTTPURLResponse(statusCode: 200)
+        let url = URL(fileURLWithPath: "/")
+
+        // When
+        let result = Result { try serializer.serializeDownload(request: request,
+                                                               response: response,
+                                                               fileURL: url,
+                                                               error: nil) }
+
+        // Then
+        XCTAssertEqual(result.success, url)
+    }
+
+    func testThatURLResponseSerializerProducesErrorFromIncomingErrors() {
+        // Given
+        let serializer = URLResponseSerializer()
+        let request = URLRequest.makeHTTPBinRequest()
+        let response = HTTPURLResponse(statusCode: 200)
+        let error = AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: 404))
+
+        // When
+        let result = Result { try serializer.serializeDownload(request: request,
+                                                               response: response,
+                                                               fileURL: nil,
+                                                               error: error) }
+
+        // Then
+        XCTAssertEqual(result.failure?.localizedDescription, error.localizedDescription)
+    }
+
+    func testThatURLResponseSerializerProducesInputFileNilErrorWhenNoURL() {
+        // Given
+        let serializer = URLResponseSerializer()
+        let request = URLRequest.makeHTTPBinRequest()
+        let response = HTTPURLResponse(statusCode: 200)
+
+        // When
+        let result = Result { try serializer.serializeDownload(request: request,
+                                                               response: response,
+                                                               fileURL: nil,
+                                                               error: nil) }
+
+        // Then
+        XCTAssertTrue(result.failure?.asAFError?.isInputFileNil == true)
+    }
+}
+
+// MARK: -
+
 // used by testThatDecodableResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseConformingTypeAndEmptyResponseStatusCode
 extension Bool: EmptyResponse {
     public static func emptyValue() -> Bool {