Browse Source

Add .indexInBrackets array serialization option for URL parameters (#3516)

* Add the indexInBrackets option to URLEncoding

* Add the indexInBrackets option to URLEncodedFormEncoder

* Add .indexInBrackets tests for structs and classes

* Address PR comments regarding .indexInBrackets serialization
Tiago Maia 4 years ago
parent
commit
92c76834f0

+ 7 - 3
Source/ParameterEncoding.swift

@@ -85,13 +85,17 @@ public struct URLEncoding: ParameterEncoding {
         case brackets
         /// No brackets are appended. The key is encoded as is.
         case noBrackets
+        /// Brackets containing the item index are appended. This matches the jQuery and Node.js behavior.
+        case indexInBrackets
 
-        func encode(key: String) -> String {
+        func encode(key: String, atIndex index: Int) -> String {
             switch self {
             case .brackets:
                 return "\(key)[]"
             case .noBrackets:
                 return key
+            case .indexInBrackets:
+                return "\(key)[\(index)]"
             }
         }
     }
@@ -193,8 +197,8 @@ public struct URLEncoding: ParameterEncoding {
                 components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
             }
         case let array as [Any]:
-            for value in array {
-                components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
+            for (index, value) in array.enumerated() {
+                components += queryComponents(fromKey: arrayEncoding.encode(key: key, atIndex: index), value: value)
             }
         case let number as NSNumber:
             if number.isBool {

+ 11 - 5
Source/URLEncodedFormEncoder.swift

@@ -48,15 +48,21 @@ public final class URLEncodedFormEncoder {
         case brackets
         /// No brackets are appended to the key and the key is encoded as is.
         case noBrackets
+        /// Brackets containing the item index are appended. This matches the jQuery and Node.js behavior.
+        case indexInBrackets
 
         /// Encodes the key according to the encoding.
         ///
-        /// - Parameter key: The `key` to encode.
-        /// - Returns:       The encoded key.
-        func encode(_ key: String) -> String {
+        /// - Parameters:
+        ///     - key:      The `key` to encode.
+        ///     - index:   When this enum instance is `.indexInBrackets`, the `index` to encode.
+        ///
+        /// - Returns:          The encoded key.
+        func encode(_ key: String, atIndex index: Int) -> String {
             switch self {
             case .brackets: return "\(key)[]"
             case .noBrackets: return key
+            case .indexInBrackets: return "\(key)[\(index)]"
             }
         }
     }
@@ -930,8 +936,8 @@ final class URLEncodedFormSerializer {
     }
 
     func serialize(_ array: [URLEncodedFormComponent], forKey key: String) -> String {
-        var segments: [String] = array.map { component in
-            let keyPath = arrayEncoding.encode(key)
+        var segments: [String] = array.enumerated().map { index, component in
+            let keyPath = arrayEncoding.encode(key, atIndex: index)
             return serialize(component, forKey: keyPath)
         }
         segments = alphabetizeKeyValuePairs ? segments.sorted() : segments

+ 100 - 0
Tests/ParameterEncoderTests.swift

@@ -511,6 +511,106 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
         // Then
         XCTAssertFalse(result.isSuccess)
     }
+    
+    func testThatEncodableSuperclassCanBeEncodedWithIndexInBrackets() {
+        // Given
+        let encoder = URLEncodedFormEncoder(arrayEncoding: .indexInBrackets)
+        let parameters = ["foo": [EncodableSuperclass()]]
+        
+        // When
+        let result = Result<String, Error> { try encoder.encode(parameters) }
+        
+        // Then
+        XCTAssertEqual(result.success, "foo%5B0%5D%5Bone%5D=one&foo%5B0%5D%5Bthree%5D=1&foo%5B0%5D%5Btwo%5D=2")
+    }
+    
+    func testThatEncodableSubclassCanBeEncodedWithIndexInBrackets() {
+        // Given
+        let encoder = URLEncodedFormEncoder(arrayEncoding: .indexInBrackets)
+        let parameters = EncodableSubclass()
+
+        // When
+        let result = Result<String, Error> { try encoder.encode(parameters) }
+
+        // Then
+        let expected = "five%5Ba%5D=a&five%5Bb%5D=b&four%5B0%5D=1&four%5B1%5D=2&four%5B2%5D=3&one=one&three=1&two=2"
+        XCTAssertEqual(result.success, expected)
+    }
+    
+    func testThatManuallyEncodableSubclassCanBeEncodedWithIndexInBrackets() {
+        // Given
+        let encoder = URLEncodedFormEncoder(arrayEncoding: .indexInBrackets)
+        let parameters = ManuallyEncodableSubclass()
+
+        // When
+        let result = Result<String, Error> { try encoder.encode(parameters) }
+
+        // Then
+        let expected = "five%5Ba%5D=a&five%5Bb%5D=b&four%5Bfive%5D=2&four%5Bfour%5D=one"
+        XCTAssertEqual(result.success, expected)
+    }
+    
+    func testThatEncodableStructCanBeEncodedWithIndexInBrackets() {
+        // Given
+        let encoder = URLEncodedFormEncoder(arrayEncoding: .indexInBrackets)
+        let parameters = EncodableStruct()
+
+        // When
+        let result = Result<String, Error> { try encoder.encode(parameters) }
+
+        // Then
+        let expected = "five%5Ba%5D=a&four%5B0%5D=1&four%5B1%5D=2&four%5B2%5D=3&one=one&seven%5Ba%5D=a&six%5Ba%5D%5Bb%5D=b&three=1&two=2"
+        XCTAssertEqual(result.success, expected)
+    }
+    
+    func testThatManuallyEncodableStructCanBeEncodedWithIndexInBrackets() {
+        // Given
+        let encoder = URLEncodedFormEncoder(arrayEncoding: .indexInBrackets)
+        let parameters = ManuallyEncodableStruct()
+
+        // When
+        let result = Result<String, Error> { try encoder.encode(parameters) }
+
+        // then
+        let expected = "root%5B0%5D%5B0%5D=1&root%5B0%5D%5B1%5D=2&root%5B0%5D%5B2%5D=3&root%5B1%5D%5Ba%5D%5Bstring%5D=string&root%5B2%5D%5B0%5D%5B0%5D=1&root%5B2%5D%5B0%5D%5B1%5D=2&root%5B2%5D%5B0%5D%5B2%5D=3"
+        XCTAssertEqual(result.success, expected)
+    }
+    
+    func testThatArrayNestedDictionaryIntValueCanBeEncodedWithIndexInBrackets() {
+        // Given
+        let encoder = URLEncodedFormEncoder(arrayEncoding: .indexInBrackets)
+        let parameters = ["foo": [["bar": 2], ["qux": 3], ["quy": 4]]]
+        
+        // When
+        let result = Result<String, Error> { try encoder.encode(parameters) }
+        
+        // Then
+        XCTAssertEqual(result.success, "foo%5B0%5D%5Bbar%5D=2&foo%5B1%5D%5Bqux%5D=3&foo%5B2%5D%5Bquy%5D=4")
+    }
+    
+    func testThatArrayNestedDictionaryStringValueCanBeEncodedWithIndexInBrackets() {
+        // Given
+        let encoder = URLEncodedFormEncoder(arrayEncoding: .indexInBrackets)
+        let parameters = ["foo": [["bar": "2"], ["qux": "3"], ["quy": "4"]]]
+        
+        // When
+        let result = Result<String, Error> { try encoder.encode(parameters) }
+        
+        // Then
+        XCTAssertEqual(result.success, "foo%5B0%5D%5Bbar%5D=2&foo%5B1%5D%5Bqux%5D=3&foo%5B2%5D%5Bquy%5D=4")
+    }
+    
+    func testThatArrayNestedDictionaryBoolValueCanBeEncodedWithIndexInBrackets() {
+        // Given
+        let encoder = URLEncodedFormEncoder(arrayEncoding: .indexInBrackets)
+        let parameters = ["foo": [["bar": true], ["qux": false], ["quy": true]]]
+        
+        // When
+        let result = Result<String, Error> { try encoder.encode(parameters) }
+        
+        // Then
+        XCTAssertEqual(result.success, "foo%5B0%5D%5Bbar%5D=1&foo%5B1%5D%5Bqux%5D=0&foo%5B2%5D%5Bquy%5D=1")
+    }
 
     func testThatArraysCanBeEncodedWithoutBrackets() {
         // Given

+ 16 - 0
Tests/ParameterEncodingTests.swift

@@ -205,6 +205,22 @@ final class URLParameterEncodingTestCase: ParameterEncodingTestCase {
             XCTFail("Test encountered unexpected error: \(error)")
         }
     }
+    
+    func testURLParameterEncodeArrayNestedDictionaryValueParameterWithIndex() {
+        do {
+            // Given
+            let encoding = URLEncoding(arrayEncoding: .indexInBrackets)
+            let parameters = ["foo": ["a", 1, true, ["bar": 2], ["qux": 3], ["quy": ["quz": 3]]]]
+            
+            // When
+            let urlRequest = try encoding.encode(self.urlRequest, with: parameters)
+            
+            // Then
+            XCTAssertEqual(urlRequest.url?.query, "foo%5B0%5D=a&foo%5B1%5D=1&foo%5B2%5D=1&foo%5B3%5D%5Bbar%5D=2&foo%5B4%5D%5Bqux%5D=3&foo%5B5%5D%5Bquy%5D%5Bquz%5D=3")
+        } catch {
+            XCTFail("Test encountered unexpected error: \(error)")
+        }
+    }
 
     func testURLParameterEncodeStringKeyArrayValueParameterWithoutBrackets() {
         do {