Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Indexed Arrays #71

Merged
merged 10 commits into from
Sep 19, 2021
28 changes: 24 additions & 4 deletions Sources/MultipartKit/MultipartFormData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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])
Expand Down
186 changes: 163 additions & 23 deletions Tests/MultipartKitTests/MultipartKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -415,64 +415,204 @@ 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
"""

XCTAssertEqual(data, expected)
}

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 {
Expand Down