Browse Source

Add configurable parameter handling to URLEncoding. (#2431)

* Add configurable array parameter handling to URLEncoding.

* Add configurable boolean parameter handling to URLEncoding.

* Addressing PR feedback for URLEncoding.

* Documentation for BoolEncoding and ArrayEncoding options in URLEncoding.
Morten Heiberg 8 years ago
parent
commit
fe06db7a80
3 changed files with 155 additions and 7 deletions
  1. 32 0
      Documentation/Usage.md
  2. 58 7
      Source/ParameterEncoding.swift
  3. 65 0
      Tests/ParameterEncodingTests.swift

+ 32 - 0
Documentation/Usage.md

@@ -279,6 +279,38 @@ Alamofire.request("https://httpbin.org/post", method: .post, parameters: paramet
 // HTTP body: foo=bar&baz[]=a&baz[]=1&qux[x]=1&qux[y]=2&qux[z]=3
 // HTTP body: foo=bar&baz[]=a&baz[]=1&qux[x]=1&qux[y]=2&qux[z]=3
 ```
 ```
 
 
+##### Configuring the Encoding of `Bool` Parameters
+
+The `URLEncoding.BoolEncoding` enumeration provides the following methods for encoding `Bool` parameters:
+
+- `.numeric` - Encode `true` as `1` and `false` as `0`.
+- `.literal` - Encode `true` and `false` as string literals.
+
+By default, Alamofire uses the `.numeric` encoding.
+
+You can create your own `URLEncoding` and specify the desired `Bool` encoding in the initializer:
+
+```swift
+let encoding = URLEncoding(boolEncoding: .literal)
+```
+
+##### Configuring the Encoding of `Array` Parameters
+
+The `URLEncoding.ArrayEncoding` enumeration provides the following methods for encoding `Array` parameters:
+
+- `.brackets` - An empty set of square brackets is appended to the key for every value.
+- `.noBrackets` - No brackets are appended. The key is encoded as is.
+
+By default, Alamofire uses the `.brackets` encoding, where `foo=[1,2]` is encoded as `foo[]=1&foo[]=2`.
+
+Using the `.noBrackets` encoding will encode `foo=[1,2]` as `foo=1&foo=2`.
+
+You can create your own `URLEncoding` and specify the desired `Array` encoding in the initializer:
+
+```swift
+let encoding = URLEncoding(arrayEncoding: .noBrackets)
+```
+
 #### JSON Encoding
 #### JSON Encoding
 
 
 The `JSONEncoding` type creates a JSON representation of the parameters object, which is set as the HTTP body of the request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`.
 The `JSONEncoding` type creates a JSON representation of the parameters object, which is set as the HTTP body of the request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`.

+ 58 - 7
Source/ParameterEncoding.swift

@@ -64,9 +64,15 @@ public protocol ParameterEncoding {
 /// the HTTP body depends on the destination of the encoding.
 /// 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
 /// 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`).
+/// `application/x-www-form-urlencoded; charset=utf-8`.
+///
+/// There is no published specification for how to encode collection types. By default 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`) is used. Optionally, `ArrayEncoding` can be used to omit the
+/// square brackets appended to array keys.
+///
+/// `BoolEncoding` can be used to configure how boolean values are encoded. The default behavior is to encode
+/// `true` as 1 and `false` as 0.
 public struct URLEncoding: ParameterEncoding {
 public struct URLEncoding: ParameterEncoding {
 
 
     // MARK: Helper Types
     // MARK: Helper Types
@@ -82,6 +88,41 @@ public struct URLEncoding: ParameterEncoding {
         case methodDependent, queryString, httpBody
         case methodDependent, queryString, httpBody
     }
     }
 
 
+    /// Configures how `Array` parameters are encoded.
+    ///
+    /// - brackets:        An empty set of square brackets is appended to the key for every value.
+    ///                    This is the default behavior.
+    /// - noBrackets:      No brackets are appended. The key is encoded as is.
+    public enum ArrayEncoding {
+        case brackets, noBrackets
+
+        func encode(key: String) -> String {
+            switch self {
+            case .brackets:
+                return "\(key)[]"
+            case .noBrackets:
+                return key
+            }
+        }
+    }
+
+    /// Configures how `Bool` parameters are encoded.
+    ///
+    /// - numeric:         Encode `true` as `1` and `false` as `0`. This is the default behavior.
+    /// - literal:         Encode `true` and `false` as string literals.
+    public enum BoolEncoding {
+        case numeric, literal
+
+        func encode(value: Bool) -> String {
+            switch self {
+            case .numeric:
+                return value ? "1" : "0"
+            case .literal:
+                return value ? "true" : "false"
+            }
+        }
+    }
+
     // MARK: Properties
     // MARK: Properties
 
 
     /// Returns a default `URLEncoding` instance.
     /// Returns a default `URLEncoding` instance.
@@ -99,15 +140,25 @@ public struct URLEncoding: ParameterEncoding {
     /// The destination defining where the encoded query string is to be applied to the URL request.
     /// The destination defining where the encoded query string is to be applied to the URL request.
     public let destination: Destination
     public let destination: Destination
 
 
+    /// The encoding to use for `Array` parameters.
+    public let arrayEncoding: ArrayEncoding
+
+    /// The encoding to use for `Bool` parameters.
+    public let boolEncoding: BoolEncoding
+
     // MARK: Initialization
     // MARK: Initialization
 
 
     /// Creates a `URLEncoding` instance using the specified destination.
     /// Creates a `URLEncoding` instance using the specified destination.
     ///
     ///
     /// - parameter destination: The destination defining where the encoded query string is to be applied.
     /// - parameter destination: The destination defining where the encoded query string is to be applied.
+    /// - parameter arrayEncoding: The encoding to use for `Array` parameters.
+    /// - parameter boolEncoding: The encoding to use for `Bool` parameters.
     ///
     ///
     /// - returns: The new `URLEncoding` instance.
     /// - returns: The new `URLEncoding` instance.
-    public init(destination: Destination = .methodDependent) {
+    public init(destination: Destination = .methodDependent, arrayEncoding: ArrayEncoding = .brackets, boolEncoding: BoolEncoding = .numeric) {
         self.destination = destination
         self.destination = destination
+        self.arrayEncoding = arrayEncoding
+        self.boolEncoding = boolEncoding
     }
     }
 
 
     // MARK: Encoding
     // MARK: Encoding
@@ -161,16 +212,16 @@ public struct URLEncoding: ParameterEncoding {
             }
             }
         } else if let array = value as? [Any] {
         } else if let array = value as? [Any] {
             for value in array {
             for value in array {
-                components += queryComponents(fromKey: "\(key)[]", value: value)
+                components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
             }
             }
         } else if let value = value as? NSNumber {
         } else if let value = value as? NSNumber {
             if value.isBool {
             if value.isBool {
-                components.append((escape(key), escape((value.boolValue ? "1" : "0"))))
+                components.append((escape(key), escape(boolEncoding.encode(value: value.boolValue))))
             } else {
             } else {
                 components.append((escape(key), escape("\(value)")))
                 components.append((escape(key), escape("\(value)")))
             }
             }
         } else if let bool = value as? Bool {
         } else if let bool = value as? Bool {
-            components.append((escape(key), escape((bool ? "1" : "0"))))
+            components.append((escape(key), escape(boolEncoding.encode(value: bool))))
         } else {
         } else {
             components.append((escape(key), escape("\(value)")))
             components.append((escape(key), escape("\(value)")))
         }
         }

+ 65 - 0
Tests/ParameterEncodingTests.swift

@@ -207,6 +207,22 @@ class URLParameterEncodingTestCase: ParameterEncodingTestCase {
         }
         }
     }
     }
 
 
+    func testURLParameterEncodeStringKeyArrayValueParameterWithoutBrackets() {
+        do {
+            // Given
+            let encoding = URLEncoding(arrayEncoding: .noBrackets)
+            let parameters = ["foo": ["a", 1, true]]
+
+            // When
+            let urlRequest = try encoding.encode(self.urlRequest, with: parameters)
+
+            // Then
+            XCTAssertEqual(urlRequest.url?.query, "foo=a&foo=1&foo=1")
+        } catch {
+            XCTFail("Test encountered unexpected error: \(error)")
+        }
+    }
+
     func testURLParameterEncodeStringKeyDictionaryValueParameter() {
     func testURLParameterEncodeStringKeyDictionaryValueParameter() {
         do {
         do {
             // Given
             // Given
@@ -253,6 +269,55 @@ class URLParameterEncodingTestCase: ParameterEncodingTestCase {
         }
         }
     }
     }
 
 
+    func testURLParameterEncodeStringKeyNestedDictionaryArrayValueParameterWithoutBrackets() {
+        do {
+            // Given
+            let encoding = URLEncoding(arrayEncoding: .noBrackets)
+            let parameters = ["foo": ["bar": ["baz": ["a", 1, true]]]]
+
+            // When
+            let urlRequest = try encoding.encode(self.urlRequest, with: parameters)
+
+            // Then
+            let expectedQuery = "foo%5Bbar%5D%5Bbaz%5D=a&foo%5Bbar%5D%5Bbaz%5D=1&foo%5Bbar%5D%5Bbaz%5D=1"
+            XCTAssertEqual(urlRequest.url?.query, expectedQuery)
+        } catch {
+            XCTFail("Test encountered unexpected error: \(error)")
+        }
+    }
+
+    func testURLParameterLiteralBoolEncodingWorksAndDoesNotAffectNumbers() {
+        do {
+            // Given
+            let encoding = URLEncoding(boolEncoding: .literal)
+            let parameters: [String: Any] = [
+                // Must still encode to numbers
+                "a": 1,
+                "b": 0,
+                "c": 1.0,
+                "d": 0.0,
+                "e": NSNumber(value: 1),
+                "f": NSNumber(value: 0),
+                "g": NSNumber(value: 1.0),
+                "h": NSNumber(value: 0.0),
+
+                // Must encode to literals
+                "i": true,
+                "j": false,
+                "k": NSNumber(value: true),
+                "l": NSNumber(value: false)
+            ]
+
+            // When
+            let urlRequest = try encoding.encode(self.urlRequest, with: parameters)
+
+            // Then
+            XCTAssertEqual(urlRequest.url?.query, "a=1&b=0&c=1&d=0&e=1&f=0&g=1&h=0&i=true&j=false&k=true&l=false")
+        } catch {
+            XCTFail("Test encountered unexpected error: \(error)")
+        }
+    }
+
     // MARK: Tests - All Reserved / Unreserved / Illegal Characters According to RFC 3986
     // MARK: Tests - All Reserved / Unreserved / Illegal Characters According to RFC 3986
 
 
     func testThatReservedCharactersArePercentEscapedMinusQuestionMarkAndForwardSlash() {
     func testThatReservedCharactersArePercentEscapedMinusQuestionMarkAndForwardSlash() {