Browse Source

Refactored ParameterEncoding into a protocol with url, json and plist structs.

This ends up making a much more flexible set of APIs overall that are much easier to extend.
Christian Noon 9 years ago
parent
commit
65ac1c5f2d

+ 6 - 6
Source/Alamofire.swift

@@ -108,7 +108,7 @@ extension URLRequest {
 /// - parameter urlString:  The URL string.
 /// - parameter method:     The HTTP method. `.get` by default.
 /// - parameter parameters: The parameters. `nil` by default.
-/// - parameter encoding:   The parameter encoding. `.url` by default.
+/// - parameter encoding:   The parameter encoding. `URLEncoding.default` by default.
 /// - parameter headers:    The HTTP headers. `nil` by default.
 ///
 /// - returns: The created `DataRequest`.
@@ -116,8 +116,8 @@ extension URLRequest {
 public func request(
     _ urlString: URLStringConvertible,
     method: HTTPMethod = .get,
-    parameters: [String: Any]? = nil,
-    encoding: ParameterEncoding = .url,
+    parameters: Parameters? = nil,
+    encoding: ParameterEncoding = URLEncoding.default,
     headers: [String: String]? = nil)
     -> DataRequest
 {
@@ -154,7 +154,7 @@ public func request(resource urlRequest: URLRequestConvertible) -> DataRequest {
 /// - parameter urlString:   The URL string.
 /// - parameter method:      The HTTP method. `.get` by default.
 /// - parameter parameters:  The parameters. `nil` by default.
-/// - parameter encoding:    The parameter encoding. `.url` by default.
+/// - parameter encoding:    The parameter encoding. `URLEncoding.default` by default.
 /// - parameter headers:     The HTTP headers. `nil` by default.
 /// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
 ///
@@ -163,8 +163,8 @@ public func request(resource urlRequest: URLRequestConvertible) -> DataRequest {
 public func download(
     _ urlString: URLStringConvertible,
     method: HTTPMethod = .get,
-    parameters: [String: Any]? = nil,
-    encoding: ParameterEncoding = .url,
+    parameters: Parameters? = nil,
+    encoding: ParameterEncoding = URLEncoding.default,
     headers: [String: String]? = nil,
     to destination: DownloadRequest.DownloadFileDestination? = nil)
     -> DownloadRequest

+ 245 - 109
Source/ParameterEncoding.swift

@@ -41,132 +41,105 @@ public enum HTTPMethod: String {
 
 // MARK: -
 
-/// Used to specify the way in which a set of parameters are applied to a URL request.
-///
-/// - url:             Creates a query string to be set as or appended to any existing URL query for `GET`, `HEAD`,
-///                    and `DELETE` requests, or set as the body for requests with any other HTTP method. The
-///                    `Content-Type` HTTP header field of an encoded request with HTTP body is set to
-///                    `application/x-www-form-urlencoded; charset=utf-8`. Since there is no published specification
-///                    for how to encode collection types, the convention of appending `[]` to the key for array
-///                    values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for nested
-///                    dictionary values (`foo[bar]=baz`).
-///
-/// - urlEncodedInURL: Creates query string to be set as or appended to any existing URL query. Uses the same
-///                    implementation as the `.url` case, but always applies the encoded result to the URL.
-///
-/// - json:            Uses `JSONSerialization` to create a JSON representation of the parameters object, which is
-///                    set as the body of the request. The `Content-Type` HTTP header field of an encoded request is
-///                    set to `application/json`.
-///
-/// - propertyList:    Uses `PropertyListSerialization` to create a plist representation of the parameters object,
-///                    according to the associated format and write options values, which is set as the body of the
-///                    request. The `Content-Type` HTTP header field of an encoded request is set to
-///                    `application/x-plist`.
-///
-/// - custom:          Uses the associated closure value to construct a new request given an existing request and
-///                    parameters.
-public enum ParameterEncoding {
-    case url
-    case urlEncodedInURL
-    case json
-    case propertyList(PropertyListSerialization.PropertyListFormat, PropertyListSerialization.WriteOptions)
-    case custom((URLRequestConvertible, [String: Any]?) throws -> URLRequest)
+/// A dictionary of parameters to apply to a `URLRequest`.
+public typealias Parameters = [String: Any]
 
+/// A type used to define how a set of parameters are applied to a `URLRequest`.
+public protocol ParameterEncoding {
     /// Creates a URL request by encoding parameters and applying them onto an existing request.
     ///
     /// - parameter urlRequest: The request to have parameters applied.
     /// - parameter parameters: The parameters to apply.
     ///
-    /// - throws: An `AFError.parameterEncodingFailed` error if json or property list serialization fails.
+    /// - throws: An `AFError.parameterEncodingFailed` error if encoding fails.
     ///
-    /// - returns: A tuple containing the constructed request and the error that occurred during parameter encoding,
-    ///            if any.
-    public func encode(_ urlRequest: URLRequestConvertible, with parameters: [String: Any]?) throws -> URLRequest {
-        var urlRequest = urlRequest.urlRequest
+    /// - returns: The encoded request.
+    func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
+}
 
-        guard let parameters = parameters else { return urlRequest }
+// MARK: -
 
-        switch self {
-        case .url, .urlEncodedInURL:
-            func query(_ parameters: [String: Any]) -> String {
-                var components: [(String, String)] = []
+/// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP
+/// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as
+/// the HTTP body depends on the destination of the encoding.
+///
+/// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to
+/// `application/x-www-form-urlencoded; charset=utf-8`. Since there is no published specification for how to encode 
+/// collection types, the convention of appending `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending
+/// the key surrounded by square brackets for nested dictionary values (`foo[bar]=baz`).
+public struct URLEncoding: ParameterEncoding {
 
-                for key in parameters.keys.sorted(by: <) {
-                    let value = parameters[key]!
-                    components += queryComponents(fromKey: key, value: value)
-                }
+    // MARK: Helper Types
 
-                return components.map { "\($0)=\($1)" }.joined(separator: "&")
-            }
+    /// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the 
+    /// resulting URL request.
+    ///
+    /// - methodDependent: Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE` 
+    ///                    requests and sets as the HTTP body for requests with any other HTTP method.
+    /// - queryString:     Sets or appends encoded query string result to existing query string.
+    /// - httpBody:        Sets encoded query string result as the HTTP body of the URL request.
+    public enum Destination {
+        case methodDependent, queryString, httpBody
+    }
 
-            func encodesParametersInURL(with method: HTTPMethod) -> Bool {
-                switch self {
-                case .urlEncodedInURL:
-                    return true
-                default:
-                    break
-                }
-
-                switch method {
-                case .get, .head, .delete:
-                    return true
-                default:
-                    return false
-                }
-            }
+    // MARK: Properties
 
-            if let method = HTTPMethod(rawValue: urlRequest.httpMethod!), encodesParametersInURL(with: method) {
-                if
-                    var URLComponents = URLComponents(url: urlRequest.url!, resolvingAgainstBaseURL: false),
-                    !parameters.isEmpty
-                {
-                    let percentEncodedQuery = (URLComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
-                    URLComponents.percentEncodedQuery = percentEncodedQuery
-                    urlRequest.url = URLComponents.url
-                }
-            } else {
-                if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
-                    urlRequest.setValue(
-                        "application/x-www-form-urlencoded; charset=utf-8",
-                        forHTTPHeaderField: "Content-Type"
-                    )
-                }
-
-                urlRequest.httpBody = query(parameters).data(
-                    using: String.Encoding.utf8,
-                    allowLossyConversion: false
-                )
-            }
-        case .json:
-            do {
-                let data = try JSONSerialization.data(withJSONObject: parameters, options: [])
+    /// Returns a default `URLEncoding` instance.
+    public static var `default`: URLEncoding { return URLEncoding() }
+
+    /// Returns a `URLEncoding` instance with a `.methodDependent` destination.
+    public static var methodDependent: URLEncoding { return URLEncoding() }
+
+    /// Returns a `URLEncoding` instance with a `.queryString` destination.
+    public static var queryString: URLEncoding { return URLEncoding(destination: .queryString) }
+
+    /// Returns a `URLEncoding` instance with an `.httpBody` destination.
+    public static var httpBody: URLEncoding { return URLEncoding(destination: .httpBody) }
+
+    /// The destination defining where the encoded query string is to be applied to the URL request.
+    public let destination: Destination
+
+    // MARK: Initialization
+
+    /// Creates a `URLEncoding` instance using the specified destination.
+    ///
+    /// - parameter destination: The destination defining where the encoded query string is to be applied.
+    ///
+    /// - returns: The new `URLEncoding` instance.
+    public init(destination: Destination = .methodDependent) {
+        self.destination = destination
+    }
+
+    // MARK: Encoding
+
+    /// Creates a URL request by encoding parameters and applying them onto an existing request.
+    ///
+    /// - parameter urlRequest: The request to have parameters applied.
+    /// - parameter parameters: The parameters to apply.
+    ///
+    /// - throws: An `AFError.parameterEncodingFailed` error if encoding fails.
+    ///
+    /// - returns: The encoded request.
+    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
+        var urlRequest = urlRequest.urlRequest
 
-                if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
-                    urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
-                }
+        guard let parameters = parameters else { return urlRequest }
 
-                urlRequest.httpBody = data
-            } catch {
-                throw AFError.parameterEncodingFailed(reason: .jsonSerializationFailed(error: error))
+        if let method = HTTPMethod(rawValue: urlRequest.httpMethod!), encodesParametersInURL(with: method) {
+            if var URLComponents = URLComponents(url: urlRequest.url!, resolvingAgainstBaseURL: false), !parameters.isEmpty {
+                let percentEncodedQuery = (URLComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
+                URLComponents.percentEncodedQuery = percentEncodedQuery
+                urlRequest.url = URLComponents.url
             }
-        case .propertyList(let format, let options):
-            do {
-                let data = try PropertyListSerialization.data(
-                    fromPropertyList: parameters,
-                    format: format,
-                    options: options
-                )
-
-                if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
-                    urlRequest.setValue("application/x-plist", forHTTPHeaderField: "Content-Type")
-                }
-
-                urlRequest.httpBody = data
-            } catch {
-                throw AFError.parameterEncodingFailed(reason: .propertyListSerializationFailed(error: error))
+        } else {
+            if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
+                urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
             }
-        case .custom(let closure):
-            urlRequest = try closure(urlRequest, parameters)
+
+            urlRequest.httpBody = query(parameters).data(
+                using: String.Encoding.utf8,
+                allowLossyConversion: false
+            )
         }
 
         return urlRequest
@@ -221,4 +194,167 @@ public enum ParameterEncoding {
 
         return string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
     }
+
+    private func query(_ parameters: [String: Any]) -> String {
+        var components: [(String, String)] = []
+
+        for key in parameters.keys.sorted(by: <) {
+            let value = parameters[key]!
+            components += queryComponents(fromKey: key, value: value)
+        }
+
+        return components.map { "\($0)=\($1)" }.joined(separator: "&")
+    }
+
+    private func encodesParametersInURL(with method: HTTPMethod) -> Bool {
+        switch destination {
+        case .queryString:
+            return true
+        case .httpBody:
+            return false
+        default:
+            break
+        }
+
+        switch method {
+        case .get, .head, .delete:
+            return true
+        default:
+            return false
+        }
+    }
+}
+
+// MARK: -
+
+/// Uses `JSONSerialization` to create a JSON representation of the parameters object, which is set as the body of the
+/// request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`.
+public struct JSONEncoding: ParameterEncoding {
+
+    // MARK: Properties
+
+    /// Returns a `JSONEncoding` instance with default writing options.
+    public static var `default`: JSONEncoding { return JSONEncoding() }
+
+    /// Returns a `JSONEncoding` instance with `.prettyPrinted` writing options.
+    public static var prettyPrinted: JSONEncoding { return JSONEncoding(options: .prettyPrinted) }
+
+    /// The options for writing the parameters as JSON data.
+    public let options: JSONSerialization.WritingOptions
+
+    // MARK: Initialization
+
+    /// Creates a `JSONEncoding` instance using the specified options.
+    ///
+    /// - parameter options: The options for writing the parameters as JSON data.
+    ///
+    /// - returns: The new `JSONEncoding` instance.
+    public init(options: JSONSerialization.WritingOptions = []) {
+        self.options = options
+    }
+
+    // MARK: Encoding
+
+    /// Creates a URL request by encoding parameters and applying them onto an existing request.
+    ///
+    /// - parameter urlRequest: The request to have parameters applied.
+    /// - parameter parameters: The parameters to apply.
+    ///
+    /// - throws: An `AFError.parameterEncodingFailed` error if encoding fails.
+    ///
+    /// - returns: The encoded request.
+    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
+        var urlRequest = urlRequest.urlRequest
+
+        guard let parameters = parameters else { return urlRequest }
+
+        do {
+            let data = try JSONSerialization.data(withJSONObject: parameters, options: options)
+
+            if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
+                urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
+            }
+
+            urlRequest.httpBody = data
+        } catch {
+            throw AFError.parameterEncodingFailed(reason: .jsonSerializationFailed(error: error))
+        }
+
+        return urlRequest
+    }
+}
+
+// MARK: -
+
+/// Uses `PropertyListSerialization` to create a plist representation of the parameters object, according to the 
+/// associated format and write options values, which is set as the body of the request. The `Content-Type` HTTP header 
+/// field of an encoded request is set to `application/x-plist`.
+public struct PropertyListEncoding: ParameterEncoding {
+
+    // MARK: Properties
+
+    /// Returns a default `PropertyListEncoding` instance.
+    public static var `default`: PropertyListEncoding { return PropertyListEncoding() }
+
+    /// Returns a `PropertyListEncoding` instance with xml formatting and default writing options.
+    public static var xml: PropertyListEncoding { return PropertyListEncoding(format: .xml) }
+
+    /// Returns a `PropertyListEncoding` instance with binary formatting and default writing options.
+    public static var binary: PropertyListEncoding { return PropertyListEncoding(format: .binary) }
+
+    /// The property list serialization format.
+    public let format: PropertyListSerialization.PropertyListFormat
+
+    /// The options for writing the parameters as plist data.
+    public let options: PropertyListSerialization.WriteOptions
+
+    // MARK: Initialization
+
+    /// Creates a `PropertyListEncoding` instance using the specified format and options.
+    ///
+    /// - parameter format:  The property list serialization format.
+    /// - parameter options: The options for writing the parameters as plist data.
+    ///
+    /// - returns: The new `PropertyListEncoding` instance.
+    public init(
+        format: PropertyListSerialization.PropertyListFormat = .xml,
+        options: PropertyListSerialization.WriteOptions = 0)
+    {
+        self.format = format
+        self.options = options
+    }
+
+    // MARK: Encoding
+
+    /// Creates a URL request by encoding parameters and applying them onto an existing request.
+    ///
+    /// - parameter urlRequest: The request to have parameters applied.
+    /// - parameter parameters: The parameters to apply.
+    ///
+    /// - throws: An `AFError.parameterEncodingFailed` error if encoding fails.
+    ///
+    /// - returns: The encoded request.
+    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
+        var urlRequest = urlRequest.urlRequest
+
+        guard let parameters = parameters else { return urlRequest }
+
+        do {
+            let data = try PropertyListSerialization.data(
+                fromPropertyList: parameters,
+                format: format,
+                options: options
+            )
+
+            if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
+                urlRequest.setValue("application/x-plist", forHTTPHeaderField: "Content-Type")
+            }
+
+            urlRequest.httpBody = data
+        } catch {
+            throw AFError.parameterEncodingFailed(reason: .propertyListSerializationFailed(error: error))
+        }
+        
+        return urlRequest
+    }
 }

+ 6 - 6
Source/SessionManager.swift

@@ -218,7 +218,7 @@ open class SessionManager {
     /// - parameter urlString:  The URL string.
     /// - parameter method:     The HTTP method. `.get` by default.
     /// - parameter parameters: The parameters. `nil` by default.
-    /// - parameter encoding:   The parameter encoding. `.url` by default.
+    /// - parameter encoding:   The parameter encoding. `URLEncoding.default` by default.
     /// - parameter headers:    The HTTP headers. `nil` by default.
     ///
     /// - returns: The created `DataRequest`.
@@ -226,8 +226,8 @@ open class SessionManager {
     open func request(
         _ urlString: URLStringConvertible,
         method: HTTPMethod = .get,
-        parameters: [String: Any]? = nil,
-        encoding: ParameterEncoding = .url,
+        parameters: Parameters? = nil,
+        encoding: ParameterEncoding = URLEncoding.default,
         headers: [String: String]? = nil)
         -> DataRequest
     {
@@ -279,7 +279,7 @@ open class SessionManager {
     /// - parameter urlString:   The URL string.
     /// - parameter method:      The HTTP method. `.get` by default.
     /// - parameter parameters:  The parameters. `nil` by default.
-    /// - parameter encoding:    The parameter encoding. `.url` by default.
+    /// - parameter encoding:    The parameter encoding. `URLEncoding.default` by default.
     /// - parameter headers:     The HTTP headers. `nil` by default.
     /// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
     ///
@@ -288,8 +288,8 @@ open class SessionManager {
     open func download(
         _ urlString: URLStringConvertible,
         method: HTTPMethod = .get,
-        parameters: [String: Any]? = nil,
-        encoding: ParameterEncoding = .url,
+        parameters: Parameters? = nil,
+        encoding: ParameterEncoding = URLEncoding.default,
         headers: [String: String]? = nil,
         to destination: DownloadRequest.DownloadFileDestination? = nil)
         -> DownloadRequest

+ 1 - 1
Tests/CacheTests.swift

@@ -172,7 +172,7 @@ class CacheTestCase: BaseTestCase {
         urlRequest.httpMethod = HTTPMethod.get.rawValue
 
         do {
-            return try ParameterEncoding.url.encode(urlRequest, with: parameters)
+            return try URLEncoding.default.encode(urlRequest, with: parameters)
         } catch {
             return urlRequest
         }

+ 4 - 42
Tests/ParameterEncodingTests.swift

@@ -36,7 +36,7 @@ class URLParameterEncodingTestCase: ParameterEncodingTestCase {
 
     // MARK: Properties
 
-    let encoding: ParameterEncoding = .url
+    let encoding = URLEncoding.default
 
     // MARK: Tests - Parameter Types
 
@@ -547,7 +547,7 @@ class URLParameterEncodingTestCase: ParameterEncodingTestCase {
             let parameters = ["foo": 1, "bar": 2]
 
             // When
-            let urlRequest = try ParameterEncoding.urlEncodedInURL.encode(mutableURLRequest, with: parameters)
+            let urlRequest = try URLEncoding.queryString.encode(mutableURLRequest, with: parameters)
 
             // Then
             XCTAssertEqual(urlRequest.url?.query, "bar=2&foo=1")
@@ -564,7 +564,7 @@ class URLParameterEncodingTestCase: ParameterEncodingTestCase {
 class JSONParameterEncodingTestCase: ParameterEncodingTestCase {
     // MARK: Properties
 
-    let encoding: ParameterEncoding = .json
+    let encoding = JSONEncoding.default
 
     // MARK: Tests
 
@@ -649,7 +649,7 @@ class JSONParameterEncodingTestCase: ParameterEncodingTestCase {
 class PropertyListParameterEncodingTestCase: ParameterEncodingTestCase {
     // MARK: Properties
 
-    let encoding: ParameterEncoding = .propertyList(.xml, 0)
+    let encoding = PropertyListEncoding.default
 
     // MARK: Tests
 
@@ -763,41 +763,3 @@ class PropertyListParameterEncodingTestCase: ParameterEncodingTestCase {
         }
     }
 }
-
-// MARK: -
-
-class CustomParameterEncodingTestCase: ParameterEncodingTestCase {
-
-    // MARK: Tests
-
-    func testCustomParameterEncode() {
-        do {
-            // Given
-            let encodingClosure: (URLRequestConvertible, [String: Any]?) throws -> URLRequest = { urlRequest, parameters in
-                guard let parameters = parameters else { return urlRequest.urlRequest }
-
-                var urlString = urlRequest.urlRequest.urlString + "?"
-
-                parameters.forEach { urlString += "\($0)=\($1)" }
-
-                var mutableURLRequest = urlRequest.urlRequest
-                mutableURLRequest.url = URL(string: urlString)!
-
-                return mutableURLRequest
-            }
-
-            // When
-            let encoding: ParameterEncoding = .custom(encodingClosure)
-
-            // Then
-            let url = URL(string: "https://example.com")!
-            let urlRequest = URLRequest(url: url)
-            let parameters = ["foo": "bar"]
-
-            let result = try encoding.encode(urlRequest, with: parameters)
-            XCTAssertEqual(result.urlString, "https://example.com?foo=bar")
-        } catch {
-            XCTFail("Test encountered unexpected error: \(error)")
-        }
-    }
-}

+ 1 - 1
Tests/RequestTests.swift

@@ -499,7 +499,7 @@ class RequestDebugDescriptionTestCase: BaseTestCase {
         ]
 
         // When
-        let request = manager.request(urlString, method: .post, parameters: parameters, encoding: .json)
+        let request = manager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default)
         let components = cURLCommandComponents(for: request)
 
         // Then

+ 1 - 1
Tests/URLProtocolTests.swift

@@ -62,7 +62,7 @@ class ProxyURLProtocol: URLProtocol {
     override class func canonicalRequest(for request: URLRequest) -> URLRequest {
         if let headers = request.allHTTPHeaderFields {
             do {
-                return try ParameterEncoding.url.encode(request, with: headers)
+                return try URLEncoding.default.encode(request, with: headers)
             } catch {
                 return request
             }