Repository: vapor/multipart-kit (swift)

Issue:
Unable to decode nested `multipart` array.
<!-- 🚀 Thank you for contributing! --->

### Describe the bug

```swift
struct Payload: Content {
    struct NestedPayload: Content {
        let name: String
        let age: Int
    }
    let nestedPayload: [NestedPayload]
}

let encoder = FormDataEncoder()
let encoded = try encoder.encode(Payload(nestedPayload: [.init(name: "abc", age: 10),
                                                         .init(name: "def", age: 20)]),
                                 boundary: "-")
let decoder = FormDataDecoder()
let decoded = try decoder.decode(Payload.self, from: encoded, boundary: "-")

```
got output `[ WARNING ] Value required for key 'nestedPayload.0.age'.`

### Environment

<!-- We must know your exact environment or it is very difficult to help. -->
<!-- Hint: use `vapor --version` in the root directory of your Vapor project. -->

* Vapor Framework version: 4.27.0
* Multipart-Kit Framework version: 4.2.1
* Vapor Toolbox version: 18.3.3
* OS version: Mac OS 11.3

Pull request:
Supports indexed array elements when decoding and encoding form data (fixes #67, replaces #69).

Indicating that a value belongs to an array can now be done by appending `[0]`, `[1]`, etc. to the name, as an alternative to the already supported `[]`. This allows for representing nested data _inside_ elements in an array.

For instance, `let matrix: [[Int]] = [[42,21]]` can now be represented using part names `matrix[0][0]` and `matrix[0][1]`. Without indexed array elements there is no way to distinguish the above from `[[42],[21]]`.

> Note: `FormDataEncoder` now also includes indexes for array types in its output.

Credits go to @chocoford for identifying the issue, the test cases and the initial PR!

Patch:
diff --git a/Sources/MultipartKit/MultipartFormData.swift b/Sources/MultipartKit/MultipartFormData.swift
index 861751f9..6f0b8c55 100644
--- a/Sources/MultipartKit/MultipartFormData.swift
+++ b/Sources/MultipartKit/MultipartFormData.swift
@@ -53,7 +53,8 @@ extension MultipartFormData {
     private static func namedParts(from data: MultipartFormData, path: String? = nil) -> [MultipartPart] {
         switch data {
         case .array(let array):
-            return array.flatMap { namedParts(from: $0, path: path.map { "\($0)[]" }) }
+            return array.enumerated().flatMap { offset, element in
+                namedParts(from: element, path: path.map { "\($0)[\(offset)]" }) }
         case .single(var part):
             part.name = path
             return [part]
@@ -85,10 +86,29 @@ private extension MultipartFormData {
             data.insert(part, at: path.dropFirst(), remainingNestingDepth: remainingNestingDepth - 1)
         }
 
+        func insertingPart(at index: Int?) -> MultipartFormData {
+            var array = self.array ?? []
+            let count = array.count
+            let index = index ?? count
+
+            switch index {
+            case count:
+                array.append(.empty)
+            case 0..<count:
+                break
+            default:
+                // ignore indices outside the range of 0...count
+                return self
+            }
+
+            insertPart(into: &array[index])
+            return .array(array)
+        }
+
         if head.isEmpty {
-            var tail = MultipartFormData.empty
-            insertPart(into: &tail)
-            return .array((array ?? []) + [tail])
+            return insertingPart(at: nil)
+        } else if let index = Int(head) {
+            return insertingPart(at: index)
         } else {
             var dictionary = self.dictionary ?? [:]
             insertPart(into: &dictionary[String(head), default: .empty])


Test patch:
diff --git a/Tests/MultipartKitTests/MultipartKitTests.swift b/Tests/MultipartKitTests/MultipartKitTests.swift
index fc74f263..70a600c1 100644
--- a/Tests/MultipartKitTests/MultipartKitTests.swift
+++ b/Tests/MultipartKitTests/MultipartKitTests.swift
@@ -116,15 +116,15 @@ class MultipartTests: XCTestCase {
         \r
         3.14\r
         --hello\r
-        Content-Disposition: form-data; name="array[]"\r
+        Content-Disposition: form-data; name="array[0]"\r
         \r
         1\r
         --hello\r
-        Content-Disposition: form-data; name="array[]"\r
+        Content-Disposition: form-data; name="array[1]"\r
         \r
         2\r
         --hello\r
-        Content-Disposition: form-data; name="array[]"\r
+        Content-Disposition: form-data; name="array[2]"\r
         \r
         3\r
         --hello\r
@@ -415,29 +415,99 @@ class MultipartTests: XCTestCase {
     }
 
     func testNestedEncode() throws {
-        struct Foo: Encodable {
-            struct Bar: Encodable {
-                let baz: Int
+        struct Formdata: Encodable, Equatable {
+            struct NestedFormdata: Encodable, Equatable {
+                struct AnotherNestedFormdata: Encodable, Equatable {
+                    let int: Int
+                    let string: String
+                    let strings: [String]
+                }
+                let int: String
+                let string: Int
+                let strings: [String]
+                let anotherNestedFormdata: AnotherNestedFormdata
+                let anotherNestedFormdataList: [AnotherNestedFormdata]
             }
-            let bar: Bar
-            let bars: [Bar]
+            let nestedFormdata: [NestedFormdata]
         }
 
         let encoder = FormDataEncoder()
-        let data = try encoder.encode(Foo(bar: .init(baz: 1), bars: [.init(baz: 2), .init(baz: 3)]), boundary: "-")
+        let data = try encoder.encode(Formdata(nestedFormdata: [
+            .init(
+                int: "1",
+                string: 1,
+                strings: ["2", "3"],
+                anotherNestedFormdata: .init(int: 4, string: "5", strings: ["6", "7"]),
+                anotherNestedFormdataList: [
+                    .init(int: 10, string: "11", strings: ["12", "13"]),
+                    .init(int: 20, string: "21", strings: ["22", "33"])
+                ])
+        ]), boundary: "-")
         let expected = """
         ---\r
-        Content-Disposition: form-data; name="bar[baz]"\r
+        Content-Disposition: form-data; name="nestedFormdata[0][int]"\r
         \r
         1\r
         ---\r
-        Content-Disposition: form-data; name="bars[][baz]"\r
+        Content-Disposition: form-data; name="nestedFormdata[0][string]"\r
+        \r
+        1\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][strings][0]"\r
         \r
         2\r
         ---\r
-        Content-Disposition: form-data; name="bars[][baz]"\r
+        Content-Disposition: form-data; name="nestedFormdata[0][strings][1]"\r
         \r
         3\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdata][int]"\r
+        \r
+        4\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdata][string]"\r
+        \r
+        5\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdata][strings][0]"\r
+        \r
+        6\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdata][strings][1]"\r
+        \r
+        7\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][0][int]"\r
+        \r
+        10\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][0][string]"\r
+        \r
+        11\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][0][strings][0]"\r
+        \r
+        12\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][0][strings][1]"\r
+        \r
+        13\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][1][int]"\r
+        \r
+        20\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][1][string]"\r
+        \r
+        21\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][1][strings][0]"\r
+        \r
+        22\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][1][strings][1]"\r
+        \r
+        33\r
         -----\r\n
         """
 
@@ -445,34 +515,104 @@ class MultipartTests: XCTestCase {
     }
 
     func testNestedDecode() throws {
-        struct Foo: Decodable, Equatable {
-            struct Bar: Decodable, Equatable {
-                let baz: Int
+        struct Formdata: Decodable, Equatable {
+            struct NestedFormdata: Decodable, Equatable {
+                struct AnotherNestedFormdata: Decodable, Equatable {
+                    let int: Int
+                    let string: String
+                    let strings: [String]
+                }
+                let int: String
+                let string: Int
+                let strings: [String]
+                let anotherNestedFormdata: AnotherNestedFormdata
+                let anotherNestedFormdataList: [AnotherNestedFormdata]
             }
-            let bar: Bar
-            let bars: [Bar]
+            let nestedFormdata: [NestedFormdata]
         }
 
         let data = """
         ---\r
-        Content-Disposition: form-data; name="bar[baz]"\r
+        Content-Disposition: form-data; name="nestedFormdata[0][int]"\r
+        \r
+        1\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][string]"\r
         \r
         1\r
         ---\r
-        Content-Disposition: form-data; name="bars[][baz]"\r
+        Content-Disposition: form-data; name="nestedFormdata[0][strings][0]"\r
         \r
         2\r
         ---\r
-        Content-Disposition: form-data; name="bars[][baz]"\r
+        Content-Disposition: form-data; name="nestedFormdata[0][strings][1]"\r
         \r
         3\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdata][int]"\r
+        \r
+        4\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdata][string]"\r
+        \r
+        5\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdata][strings][0]"\r
+        \r
+        6\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdata][strings][1]"\r
+        \r
+        7\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][0][int]"\r
+        \r
+        10\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][0][string]"\r
+        \r
+        11\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][0][strings][0]"\r
+        \r
+        12\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][0][strings][1]"\r
+        \r
+        13\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][1][int]"\r
+        \r
+        20\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][1][string]"\r
+        \r
+        21\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][1][strings][0]"\r
+        \r
+        22\r
+        ---\r
+        Content-Disposition: form-data; name="nestedFormdata[0][anotherNestedFormdataList][1][strings][1]"\r
+        \r
+        33\r
         -----\r\n
         """
 
         let decoder = FormDataDecoder()
-        let foo = try decoder.decode(Foo.self, from: data, boundary: "-")
-
-        XCTAssertEqual(foo, Foo(bar: .init(baz: 1), bars: [.init(baz: 2), .init(baz: 3)]))
+        let formdata = try decoder.decode(Formdata.self, from: data, boundary: "-")
+
+        XCTAssertEqual(formdata, Formdata(nestedFormdata: [
+            .init(
+                int: "1",
+                string: 1,
+                strings: ["2", "3"],
+                anotherNestedFormdata: .init(int: 4, string: "5", strings: ["6", "7"]),
+                anotherNestedFormdataList: [
+                    .init(int: 10, string: "11", strings: ["12", "13"]),
+                    .init(int: 20, string: "21", strings: ["22", "33"])
+                ])
+        ]))
     }
 
     func testDecodingSingleValue() throws {