Browse Source

Brotli Support (#2442)

* Drop iOS 8 / macOS 10.10 support and remove all workarounds.

* First draft of response serializer refactor and decodable serializers.

* Refactor serializer protocols and implementations.

* Finish refactors, update inline docs.

* Remove download serializer from Data, as it’s default now.

* Update whitespace.

* Add failure expectation test.

* Initial versions of Mutex and Protector.

* Rename value to unsafeValue.

* Add UnfairLock.

* Use UnfairLock on supported OSes, hide everything.

* Clean whitespace.

* Initial protocol abstraction and multiple evaluation implementation.

* Go back to simpler API, add composite case.

* Refactor away from enum.

* Remove custom tests.

* Move key comparison to AnyHashable.

* Update all documenation and naming.

* Add brotli support to default Accept-Encoding header.

* Add tests for encodings, make listing dynamic.
Jon Shier 7 years ago
parent
commit
ab63fa2952

+ 35 - 2
Source/Mutex+Protector.swift

@@ -24,6 +24,7 @@
 
 
 import Foundation
 import Foundation
 
 
+
 /// A lock abstraction.
 /// A lock abstraction.
 private protocol Lock {
 private protocol Lock {
     func lock()
     func lock()
@@ -78,7 +79,23 @@ final class Mutex: Lock {
     }
     }
 }
 }
 
 
-// MARK: -
+    /// Execute a value producing closure while aquiring the mutex.
+    ///
+    /// - Parameter closure: The closure to run.
+    /// - Returns:           The value the closure generated.
+    func around<T>(_ closure: () -> T) -> T {
+        lock(); defer { unlock() }
+        return closure()
+    }
+
+    /// Execute a closure while aquiring the mutex.
+    ///
+    /// - Parameter closure: The closure to run.
+    func around(_ closure: () -> Void) {
+        lock(); defer { unlock() }
+        return closure()
+    }
+}
 
 
 /// An `os_unfair_lock` wrapper.
 /// An `os_unfair_lock` wrapper.
 @available (iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *)
 @available (iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *)
@@ -96,7 +113,23 @@ final class UnfairLock: Lock {
     }
     }
 }
 }
 
 
-// MARK: -
+    /// Execute a value producing closure while aquiring the lock.
+    ///
+    /// - Parameter closure: The closure to run.
+    /// - Returns:           The value the closure generated.
+    func around<T>(_ closure: () -> T) -> T {
+        lock(); defer { unlock() }
+        return closure()
+    }
+
+    /// Execute a closure while aquiring the lock.
+    ///
+    /// - Parameter closure: The closure to run.
+    func around(_ closure: () -> Void) {
+        lock(); defer { unlock() }
+        return closure()
+    }
+}
 
 
 /// A thread-safe wrapper around a value.
 /// A thread-safe wrapper around a value.
 final class Protector<T> {
 final class Protector<T> {

+ 1 - 1
Source/Request.swift

@@ -450,7 +450,7 @@ open class DownloadRequest: Request {
     enum Downloadable: TaskConvertible {
     enum Downloadable: TaskConvertible {
         case request(URLRequest)
         case request(URLRequest)
         case resumeData(Data)
         case resumeData(Data)
-
+        // TODO: Ask about this use of queue. Perhaps just to protect session and adapter?
         func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
         func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
             do {
             do {
                 let task: URLSessionTask
                 let task: URLSessionTask

+ 65 - 0
Source/ResponseSerialization.swift

@@ -65,6 +65,7 @@ public extension DownloadResponseSerializerProtocol where Self: DataResponseSeri
         }
         }
 
 
         do {
         do {
+            let data = try Data(contentsOf: fileURL)
             return try serialize(request: request, response: response, data: data, error: error)
             return try serialize(request: request, response: response, data: data, error: error)
         } catch {
         } catch {
             throw error
             throw error
@@ -124,6 +125,7 @@ public final class AnyResponseSerializer<Value>: ResponseSerializer {
             }
             }
 
 
             do {
             do {
+                let data = try Data(contentsOf: fileURL)
                 return try serialize(request: request, response: response, data: data, error: error)
                 return try serialize(request: request, response: response, data: data, error: error)
             } catch {
             } catch {
                 throw error
                 throw error
@@ -603,5 +605,68 @@ extension DataRequest {
     }
     }
 }
 }
 
 
+extension DownloadRequest {
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - Parameters:
+    ///   - queue:             The queue on which the completion handler is dispatched. Defaults to `nil`, which means
+    ///                        the handler is called on `.main`.
+    ///   - options:           The property list reading options. Defaults to `[]`.
+    ///   - completionHandler: A closure to be executed once the request has finished.
+    /// - Returns:             The request.
+    @discardableResult
+    public func responsePropertyList(
+        queue: DispatchQueue? = nil,
+        options: PropertyListSerialization.ReadOptions = [],
+        completionHandler: @escaping (DownloadResponse<Any>) -> Void)
+        -> Self
+    {
+        return response(
+            queue: queue,
+            responseSerializer: PropertyListResponseSerializer(options: options),
+            completionHandler: completionHandler
+        )
+    }
+}
+
+// MARK: - PropertyList Decodable
+
+/// A `ResponseSerializer` that decodes the response data as a generic value using a `PropertyListDecoder`. 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.response` value is returned.
+public final class PropertyListDecodableResponseSerializer<T: Decodable>: ResponseSerializer {
+    let decoder: PropertyListDecoder
+
+
+    /// Creates an instance with the given `JSONDecoder` instance.
+    ///
+    /// - Parameter decoder: A decoder. Defaults to a `PropertyListDecoder` with default settings.
+    public init(decoder: PropertyListDecoder = PropertyListDecoder()) {
+        self.decoder = decoder
+    }
+
+    public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) -> Result<T> {
+        guard error == nil else { return .failure(error!) }
+
+        guard let validData = data, validData.count > 0 else {
+            if let response = response, emptyDataStatusCodes.contains(response.statusCode) {
+                guard let emptyResponse = Empty.response as? T else {
+                    return .failure(AFError.responseSerializationFailed(reason: .invalidEmptyResponse(type: "\(T.self)")))
+                }
+
+                return .success(emptyResponse)
+            }
+
+            return .failure(AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength))
+        }
+
+        do {
+            return .success(try decoder.decode(T.self, from: validData))
+        } catch {
+            return .failure(error)
+        }
+    }
+}
+
 /// A set of HTTP response status code that do not contain response data.
 /// A set of HTTP response status code that do not contain response data.
 private let emptyDataStatusCodes: Set<Int> = [204, 205]
 private let emptyDataStatusCodes: Set<Int> = [204, 205]

+ 13 - 1
Source/SessionManager.swift

@@ -55,7 +55,19 @@ open class SessionManager {
     /// Creates default values for the "Accept-Encoding", "Accept-Language" and "User-Agent" headers.
     /// Creates default values for the "Accept-Encoding", "Accept-Language" and "User-Agent" headers.
     open static let defaultHTTPHeaders: HTTPHeaders = {
     open static let defaultHTTPHeaders: HTTPHeaders = {
         // Accept-Encoding HTTP Header; see https://tools.ietf.org/html/rfc7230#section-4.2.3
         // Accept-Encoding HTTP Header; see https://tools.ietf.org/html/rfc7230#section-4.2.3
-        let acceptEncoding: String = "gzip;q=1.0, compress;q=0.5"
+        let acceptEncoding: String = {
+            let encodings: [String]
+            if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) {
+                encodings = ["br", "gzip", "deflate"]
+            } else {
+                encodings = ["gzip", "deflate"]
+            }
+            
+            return encodings.enumerated().map { (index, encoding) in
+                let quality = 1.0 - (Double(index) * 0.1)
+                return "\(encoding);q=\(quality)"
+                }.joined(separator: ", ")
+        }()
 
 
         // Accept-Language HTTP Header; see https://tools.ietf.org/html/rfc7231#section-5.3.5
         // Accept-Language HTTP Header; see https://tools.ietf.org/html/rfc7231#section-5.3.5
         let acceptLanguage = Locale.preferredLanguages.prefix(6).enumerated().map { index, languageCode in
         let acceptLanguage = Locale.preferredLanguages.prefix(6).enumerated().map { index, languageCode in

+ 43 - 0
Tests/SessionManagerTests.swift

@@ -254,6 +254,49 @@ class SessionManagerTestCase: BaseTestCase {
         let expectedUserAgent = "Unknown/Unknown (Unknown; build:Unknown; \(osNameVersion)) \(alamofireVersion)"
         let expectedUserAgent = "Unknown/Unknown (Unknown; build:Unknown; \(osNameVersion)) \(alamofireVersion)"
         XCTAssertEqual(userAgent, expectedUserAgent)
         XCTAssertEqual(userAgent, expectedUserAgent)
     }
     }
+    
+    // MARK: Tests - Supported Accept-Encodings
+    
+    func testDefaultAcceptEncodingSupportsAppropriateEncodingsOnAppropriateSystems() {
+        // Given
+        let brotliURL = URL(string: "https://httpbin.org/brotli")!
+        let gzipURL = URL(string: "https://httpbin.org/gzip")!
+        let deflateURL = URL(string: "https://httpbin.org/deflate")!
+        let brotliExpectation = expectation(description: "brotli request should complete")
+        let gzipExpectation = expectation(description: "gzip request should complete")
+        let deflateExpectation = expectation(description: "deflate request should complete")
+        var brotliResponse: DataResponse<Any>?
+        var gzipResponse: DataResponse<Any>?
+        var deflateResponse: DataResponse<Any>?
+        
+        // When
+        Alamofire.request(brotliURL).responseJSON { (response) in
+            brotliResponse = response
+            brotliExpectation.fulfill()
+        }
+        
+        Alamofire.request(gzipURL).responseJSON { (response) in
+            gzipResponse = response
+            gzipExpectation.fulfill()
+        }
+        
+        Alamofire.request(deflateURL).responseJSON { (response) in
+            deflateResponse = response
+            deflateExpectation.fulfill()
+        }
+        
+        waitForExpectations(timeout: 5, handler: nil)
+        
+        // Then
+        if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) {
+            XCTAssertTrue(brotliResponse?.result.isSuccess == true)
+        } else {
+            XCTAssertFalse(brotliResponse?.result.isSuccess == true)
+        }
+        
+        XCTAssertTrue(gzipResponse?.result.isSuccess == true)
+        XCTAssertTrue(deflateResponse?.result.isSuccess == true)
+    }
 
 
     // MARK: Tests - Start Requests Immediately
     // MARK: Tests - Start Requests Immediately