Skip to content

Commit 481a446

Browse files
authored
Add back MultipartPartConvertible (#130)
* Add back `MultipartPartConvertible` * Add tests and fix bug
1 parent b186a55 commit 481a446

19 files changed

+169
-23
lines changed

Sources/MultipartKit/FormDataDecoder/FormDataDecoder+SingleValueContainer.swift

+2
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ extension FormDataDecoder.Decoder: SingleValueDecodingContainer {
9393

9494
let decoded =
9595
switch T.self {
96+
case let multipartConvertible as any MultipartPartConvertible.Type:
97+
multipartConvertible.init(multipart: part) as? T
9698
case is MultipartPart<Body>.Type:
9799
part as? T
98100
case is Data.Type:

Sources/MultipartKit/FormDataDecoder/FormDataDecoder.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public struct FormDataDecoder: Sendable {
5454
_ decodable: D.Type,
5555
from buffer: Body,
5656
boundary: String
57-
) throws -> D where Body: RangeReplaceableCollection, Body.SubSequence: Equatable & Sendable {
57+
) throws -> D where Body.SubSequence: Equatable & Sendable {
5858
let parts = try MultipartParser(boundary: boundary).parse(buffer)
5959
let data = MultipartFormData(parts: parts, nestingDepth: nestingDepth)
6060
let decoder = FormDataDecoder.Decoder(codingPath: [], data: data, userInfo: userInfo)

Sources/MultipartKit/FormDataEncoder/FormDataEncoder+Encoder.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
extension FormDataEncoder {
2-
struct Encoder<Body: MultipartPartBodyElement> where Body: RangeReplaceableCollection {
2+
struct Encoder<Body: MultipartPartBodyElement> {
33
let codingPath: [any CodingKey]
44
let storage = Storage<Body>()
55
let sendableUserInfo: [CodingUserInfoKey: any Sendable]

Sources/MultipartKit/FormDataEncoder/FormDataEncoder+KeyedContainer.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
extension FormDataEncoder {
2-
struct KeyedContainer<Key: CodingKey, Body: MultipartPartBodyElement> where Body: RangeReplaceableCollection {
2+
struct KeyedContainer<Key: CodingKey, Body: MultipartPartBodyElement> {
33
let dataContainer = KeyedDataContainer<Body>()
44
let encoder: Encoder<Body>
55
}

Sources/MultipartKit/FormDataEncoder/FormDataEncoder+SingleValueContainer.swift

+5
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ extension FormDataEncoder.Encoder: SingleValueEncodingContainer {
4646

4747
func encode<T: Encodable>(_ value: T) throws {
4848
switch value {
49+
case let multipartConvertible as any MultipartPartConvertible<Body>:
50+
guard let multipart = multipartConvertible.multipart else {
51+
return try value.encode(to: self)
52+
}
53+
storage.dataContainer = SingleValueDataContainer(part: multipart)
4954
case let multipart as MultipartPart<Body>:
5055
storage.dataContainer = SingleValueDataContainer(part: multipart)
5156
case let data as Data:

Sources/MultipartKit/FormDataEncoder/FormDataEncoder+UnkeyedContainer.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
extension FormDataEncoder {
2-
struct UnkeyedContainer<Body: MultipartPartBodyElement> where Body: RangeReplaceableCollection {
2+
struct UnkeyedContainer<Body: MultipartPartBodyElement> {
33
let dataContainer = UnkeyedDataContainer<Body>()
44
let encoder: FormDataEncoder.Encoder<Body>
55
}

Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,12 @@ public struct FormDataEncoder: Sendable {
4545
_ encodable: E,
4646
boundary: String,
4747
to: Body.Type = Body.self
48-
) throws -> Body where Body: RangeReplaceableCollection {
48+
) throws -> Body {
4949
let parts: [MultipartPart<Body>] = try self.parts(from: encodable)
5050
return MultipartSerializer(boundary: boundary).serialize(parts: parts)
5151
}
5252

53-
private func parts<E: Encodable, Body: MultipartPartBodyElement>(from encodable: E) throws -> [MultipartPart<Body>]
54-
where Body: RangeReplaceableCollection {
53+
private func parts<E: Encodable, Body: MultipartPartBodyElement>(from encodable: E) throws -> [MultipartPart<Body>] {
5554
let encoder = Encoder<Body>(codingPath: [], userInfo: userInfo)
5655
try encodable.encode(to: encoder)
5756
return encoder.storage.data?.namedParts() ?? []

Sources/MultipartKit/MultipartParser+parse.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import HTTPTypes
22

33
extension MultipartParser {
44
/// Synchronously parse the multipart data into an array of ``MultipartPart``.
5-
public func parse(_ data: Body) throws -> [MultipartPart<Body>] where Body: RangeReplaceableCollection {
5+
public func parse(_ data: Body) throws -> [MultipartPart<Body>] {
66
var output: [MultipartPart<Body>] = []
77
var parser = MultipartParser(boundary: self.boundary)
88

Sources/MultipartKit/MultipartParser.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import HTTPTypes
22

33
/// Parses any kind of multipart encoded data into ``MultipartSection``s.
4-
public struct MultipartParser<Body: MultipartPartBodyElement> where Body: RangeReplaceableCollection {
4+
public struct MultipartParser<Body: MultipartPartBodyElement> {
55
enum State: Equatable {
66
enum Part: Equatable {
77
case boundary

Sources/MultipartKit/MultipartParserAsyncSequence.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import HTTPTypes
2828
/// ```
2929
///
3030
public struct MultipartParserAsyncSequence<BackingSequence: AsyncSequence>: AsyncSequence
31-
where BackingSequence.Element: MultipartPartBodyElement & RangeReplaceableCollection {
31+
where BackingSequence.Element: MultipartPartBodyElement {
3232
let streamingSequence: StreamingMultipartParserAsyncSequence<BackingSequence>
3333

3434
public init(boundary: String, buffer: BackingSequence) {

Sources/MultipartKit/MultipartPart.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import HTTPTypes
22

3-
public typealias MultipartPartBodyElement = Collection<UInt8> & Sendable
3+
public typealias MultipartPartBodyElement = Collection<UInt8> & Sendable & RangeReplaceableCollection
44

55
/// Represents a single part of a multipart-encoded message.
66
public struct MultipartPart<Body: MultipartPartBodyElement>: Sendable {
@@ -19,7 +19,7 @@ public struct MultipartPart<Body: MultipartPartBodyElement>: Sendable {
1919
/// - Parameters:
2020
/// - headerFields: The header fields for this part.
2121
/// - body: The body of this part.
22-
public init(headerFields: HTTPFields, body: Body) {
22+
public init(headerFields: HTTPFields = .init(), body: Body) {
2323
self.headerFields = headerFields
2424
self.body = body
2525
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// A protocol to provide custom behaviors for parsing and serializing types from and to multipart data.
2+
public protocol MultipartPartConvertible<Body> {
3+
associatedtype Body: MultipartPartBodyElement
4+
5+
var multipart: MultipartPart<Body>? { get }
6+
init?(multipart: MultipartPart<some MultipartPartBodyElement>)
7+
}
8+
9+
extension MultipartPart: MultipartPartConvertible {
10+
public var multipart: MultipartPart<Body>? {
11+
self
12+
}
13+
14+
public init?(multipart: MultipartPart<some MultipartPartBodyElement>) {
15+
self = .init(headerFields: multipart.headerFields, body: .init(multipart.body))
16+
}
17+
}

Sources/MultipartKit/MultipartSerializer.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public struct MultipartSerializer: Sendable {
1919
public func serialize<Body: MultipartPartBodyElement>(
2020
parts: [MultipartPart<some MultipartPartBodyElement>],
2121
into: Body.Type = Body.self
22-
) -> Body where Body: RangeReplaceableCollection {
22+
) -> Body {
2323
var buffer = Body()
2424
self.serialize(parts: parts, into: &buffer)
2525
return buffer
@@ -40,7 +40,7 @@ public struct MultipartSerializer: Sendable {
4040
public func serialize<OutputBody: MultipartPartBodyElement>(
4141
parts: [MultipartPart<some MultipartPartBodyElement>],
4242
into buffer: inout OutputBody
43-
) where OutputBody: RangeReplaceableCollection {
43+
) {
4444
for part in parts {
4545
buffer.append(.hyphen)
4646
buffer.append(.hyphen)

Sources/MultipartKit/StreamingMultipartParserAsyncSequence.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import HTTPTypes
2727
/// ```
2828
///
2929
public struct StreamingMultipartParserAsyncSequence<BackingSequence: AsyncSequence>: AsyncSequence
30-
where BackingSequence.Element: MultipartPartBodyElement & RangeReplaceableCollection {
30+
where BackingSequence.Element: MultipartPartBodyElement {
3131
let parser: MultipartParser<BackingSequence.Element>
3232
let buffer: BackingSequence
3333

Sources/MultipartKit/Utilities.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import HTTPTypes
44
extension HTTPFields {
55
func getParameter(_ name: HTTPField.Name, _ key: String) -> String? {
66
headerParts(name: name)?
7-
.filter { $0.contains("\(key)=") }
7+
.filter { $0.starts(with: "\(key)=") }
88
.first?
99
.split(separator: "=")
1010
.last?

Tests/MultipartKitTests/FormDataDecodingTests.swift

+38-2
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,7 @@ struct FormDataDecodingTests {
304304
}
305305
}
306306

307-
// https://github.com/vapor/multipart-kit/issues/123
308-
@Test("Decoding with key containing square bracket")
307+
@Test("Decoding with key containing square bracket", .bug("https://github.com/vapor/multipart-kit/issues/123"))
309308
func decodeWithKeyContainingBracket() async throws {
310309
struct HasADict: Codable, Equatable {
311310
var hints: [String: String]
@@ -344,5 +343,42 @@ struct FormDataDecodingTests {
344343
#expect(deserializedBar == bar)
345344
}
346345

346+
@Test("Decode simil-Vapor File type")
347+
func decodeSimilVaporFileType() async throws {
348+
struct User: Codable {
349+
var name: String
350+
var age: Int
351+
var image: File
352+
}
353+
354+
let user = User(
355+
name: "Vapor",
356+
age: 4,
357+
image: File(filename: "droplet.png", data: Array("<contents of image>".utf8)))
358+
359+
let message = ArraySlice(
360+
"""
361+
--helloBoundary\r
362+
Content-Disposition: form-data; name="name"\r
363+
\r
364+
Vapor\r
365+
--helloBoundary\r
366+
Content-Disposition: form-data; name="age"\r
367+
\r
368+
4\r
369+
--helloBoundary\r
370+
Content-Disposition: form-data; filename="droplet.png"; name="image"\r
371+
\r
372+
<contents of image>\r
373+
--helloBoundary--\r\n
374+
""".utf8)
375+
376+
let decoded = try FormDataDecoder().decode(User.self, from: message, boundary: "helloBoundary")
377+
378+
#expect(decoded.name == user.name)
379+
#expect(decoded.age == user.age)
380+
#expect(decoded.image == user.image)
381+
}
382+
347383
}
348384
#endif // canImport(Testing)

Tests/MultipartKitTests/FormDataEncodingTests.swift

+37-2
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,7 @@ struct FormDataEncodingTests {
177177
#expect(try FormDataDecoder().decode(UUID.self, from: multipart, boundary: "-") == uuid)
178178
}
179179

180-
// https://github.com/vapor/multipart-kit/issues/65
181-
@Test("Encoding and Decoding Non-Multipart Part Convertible Codable Types")
180+
@Test("Encoding and Decoding Non-Multipart Part Convertible Codable Types", .bug("https://github.com/vapor/multipart-kit/issues/65"))
182181
func encodeAndDecodeNonMultipartPartConvertibleCodableTypes() async throws {
183182
enum License: String, Codable, CaseIterable, Equatable {
184183
case dme1
@@ -284,5 +283,41 @@ struct FormDataEncodingTests {
284283
#expect(try FormDataEncoder().encode(value, boundary: "-") == multipart)
285284
#expect(try FormDataDecoder().decode(AllTypes.self, from: multipart, boundary: "-") == value)
286285
}
286+
287+
@Test("Encode simil-Vapor File type")
288+
func encodeSimilVaporFileType() async throws {
289+
struct User: Codable {
290+
var name: String
291+
var age: Int
292+
var image: File
293+
}
294+
295+
let user = User(
296+
name: "Vapor",
297+
age: 4,
298+
image: File(filename: "droplet.png", data: Array("<contents of image>".utf8)))
299+
300+
let encoder = FormDataEncoder()
301+
let boundary = "helloBoundary"
302+
let encoded = try encoder.encode(user, boundary: boundary)
303+
304+
let expected = """
305+
--helloBoundary\r
306+
Content-Disposition: form-data; name="name"\r
307+
\r
308+
Vapor\r
309+
--helloBoundary\r
310+
Content-Disposition: form-data; name="age"\r
311+
\r
312+
4\r
313+
--helloBoundary\r
314+
Content-Disposition: form-data; filename="droplet.png"; name="image"\r
315+
\r
316+
<contents of image>\r
317+
--helloBoundary--\r\n
318+
"""
319+
320+
#expect(encoded == expected)
321+
}
287322
}
288323
#endif // canImport(Testing)

Tests/MultipartKitTests/ParserTests.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ struct ParserTests {
150150
var iterator = StreamingMultipartParserAsyncSequence(boundary: boundary, buffer: stream).makeAsyncIterator()
151151

152152
await #expect(throws: MultipartParserError.unexpectedEndOfFile) {
153-
while (try await iterator.next()) != nil {}
153+
while try await iterator.next() != nil {}
154154
}
155155
}
156156

@@ -175,7 +175,7 @@ struct ParserTests {
175175
var iterator = StreamingMultipartParserAsyncSequence(boundary: boundary, buffer: stream).makeAsyncIterator()
176176

177177
await #expect(throws: MultipartParserError.invalidHeader(reason: "Invalid header name")) {
178-
while (try await iterator.next()) != nil {}
178+
while try await iterator.next() != nil {}
179179
}
180180
}
181181

@@ -225,7 +225,7 @@ struct ParserTests {
225225
var iterator = StreamingMultipartParserAsyncSequence(boundary: boundary, buffer: stream).makeAsyncIterator()
226226

227227
await #expect(throws: MultipartParserError.unexpectedEndOfFile) {
228-
while (try await iterator.next()) != nil {}
228+
while try await iterator.next() != nil {}
229229
}
230230
}
231231

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import MultipartKit
2+
3+
struct File: Codable, Equatable, MultipartPartConvertible {
4+
let filename: String
5+
let data: [UInt8]
6+
7+
enum CodingKeys: String, CodingKey {
8+
case data, filename
9+
}
10+
11+
init(filename: String, data: [UInt8]) {
12+
self.filename = filename
13+
self.data = data
14+
}
15+
16+
init(from decoder: any Decoder) throws {
17+
let container = try decoder.container(keyedBy: CodingKeys.self)
18+
let data = try container.decode([UInt8].self, forKey: .data)
19+
let filename = try container.decode(String.self, forKey: .filename)
20+
self.init(filename: filename, data: data)
21+
}
22+
23+
func encode(to encoder: any Encoder) throws {
24+
var container = encoder.container(keyedBy: CodingKeys.self)
25+
try container.encode(data, forKey: .data)
26+
try container.encode(self.filename, forKey: .filename)
27+
}
28+
29+
var multipart: MultipartPart<[UInt8]>? {
30+
let part = MultipartPart(
31+
headerFields: [.contentDisposition: "form-data; name=\"image\"; filename=\"\(filename)\""],
32+
body: self.data)
33+
return part
34+
}
35+
36+
init?(multipart: MultipartPart<some MultipartPartBodyElement>) {
37+
let contentDisposition = multipart.headerFields[.contentDisposition] ?? ""
38+
let filenamePattern = "filename=\"([^\"]+)\""
39+
let filename: String
40+
41+
if let range = contentDisposition.range(of: filenamePattern, options: .regularExpression) {
42+
let match = contentDisposition[range]
43+
let startIndex = match.index(match.startIndex, offsetBy: 10) // Skip 'filename="'
44+
let endIndex = match.index(before: match.endIndex) // Skip closing quote
45+
filename = String(contentDisposition[startIndex..<endIndex])
46+
} else {
47+
return nil
48+
}
49+
50+
self.init(filename: filename, data: Array(multipart.body))
51+
}
52+
}

0 commit comments

Comments
 (0)