Skip to content

Commit b186a55

Browse files
authored
Adopt typed throws and isolation based methods for iterators (#124)
* Use typed throws and isolation based iterator for parsing * Remove `MultipartMessageError` * Format * Add default isolation inference
1 parent 8634be5 commit b186a55

8 files changed

+119
-14
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Package.pins
77
Package.resolved
88
DerivedData
99
.swiftpm
10+
.index-build/

Sources/MultipartKit/MultipartMessageError.swift

-4
This file was deleted.

Sources/MultipartKit/MultipartParser+parse.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ extension MultipartParser {
3636
case .needMoreData:
3737
// In synchronous parsing with all data provided upfront,
3838
// needing more data indicates an incomplete/corrupted message
39-
throw MultipartMessageError.unexpectedEndOfFile
39+
throw MultipartParserError.unexpectedEndOfFile
4040

4141
case .error(let error):
4242
throw error

Sources/MultipartKit/MultipartParserAsyncSequence.swift

+8-1
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,16 @@ where BackingSequence.Element: MultipartPartBodyElement & RangeReplaceableCollec
3838
public struct AsyncIterator: AsyncIteratorProtocol {
3939
var streamingIterator: StreamingMultipartParserAsyncSequence<BackingSequence>.AsyncIterator
4040

41-
public mutating func next() async throws -> MultipartSection<BackingSequence.Element>? {
41+
public mutating func next() async throws(MultipartParserError) -> MultipartSection<BackingSequence.Element>? {
4242
try await streamingIterator.nextCollatedPart()
4343
}
44+
45+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
46+
public mutating func next(isolation actor: isolated (any Actor)? = #isolation) async throws(MultipartParserError)
47+
-> MultipartSection<BackingSequence.Element>?
48+
{
49+
try await streamingIterator.nextCollatedPart(isolation: actor)
50+
}
4451
}
4552

4653
public func makeAsyncIterator() -> AsyncIterator {

Sources/MultipartKit/MultipartParserError.swift

+10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ public struct MultipartParserError: Swift.Error, Equatable, Sendable {
66
case invalidBoundary
77
case invalidHeader
88
case invalidBody
9+
case unexpectedEndOfFile
10+
case backingSequenceError
911
}
1012

1113
let base: Base
@@ -17,6 +19,8 @@ public struct MultipartParserError: Swift.Error, Equatable, Sendable {
1719
public static let invalidBoundary = Self(.invalidBoundary)
1820
public static let invalidHeader = Self(.invalidHeader)
1921
public static let invalidBody = Self(.invalidBody)
22+
public static let unexpectedEndOfFile = Self(.unexpectedEndOfFile)
23+
public static let backingSequenceError = Self(.backingSequenceError)
2024

2125
public var description: String {
2226
base.rawValue
@@ -43,6 +47,8 @@ public struct MultipartParserError: Swift.Error, Equatable, Sendable {
4347

4448
public static let invalidBoundary = Self(errorType: .invalidBoundary)
4549

50+
public static let unexpectedEndOfFile = Self(errorType: .unexpectedEndOfFile)
51+
4652
public static func invalidHeader(reason: String) -> Self {
4753
.init(backing: .init(errorType: .invalidHeader, reason: reason))
4854
}
@@ -51,6 +57,10 @@ public struct MultipartParserError: Swift.Error, Equatable, Sendable {
5157
.init(backing: .init(errorType: .invalidBody, reason: reason))
5258
}
5359

60+
public static func backingSequenceError(reason: String) -> Self {
61+
.init(backing: .init(errorType: .backingSequenceError, reason: reason))
62+
}
63+
5464
public var description: String {
5565
var result = "MultipartParserError(errorType: \(errorType)"
5666

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import HTTPTypes
2+
3+
extension StreamingMultipartParserAsyncSequence.AsyncIterator {
4+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
5+
public mutating func next(isolation actor: isolated (any Actor)? = #isolation) async throws(MultipartParserError) -> Self.Element? {
6+
if let pendingBodyChunk {
7+
defer { self.pendingBodyChunk = nil }
8+
return .bodyChunk(pendingBodyChunk)
9+
}
10+
11+
var headerFields = HTTPFields()
12+
13+
while true {
14+
switch parser.read() {
15+
case .success(let optionalPart):
16+
switch optionalPart {
17+
case .none: continue
18+
case .some(let part):
19+
switch part {
20+
case .headerFields(let fields):
21+
headerFields.append(contentsOf: fields)
22+
continue
23+
case .bodyChunk(let chunk):
24+
if !headerFields.isEmpty {
25+
pendingBodyChunk = chunk
26+
let returningFields = headerFields
27+
headerFields = .init()
28+
return .headerFields(returningFields)
29+
}
30+
return .bodyChunk(chunk)
31+
case .boundary:
32+
return part
33+
}
34+
}
35+
case .needMoreData:
36+
let next: BackingSequence.Element?
37+
do {
38+
next = try await iterator.next(isolation: actor)
39+
} catch {
40+
throw MultipartParserError.backingSequenceError(reason: "\(error)")
41+
}
42+
if let next {
43+
parser.append(buffer: next)
44+
} else {
45+
switch parser.state {
46+
case .initial, .finished:
47+
return nil
48+
case .parsing:
49+
throw MultipartParserError.unexpectedEndOfFile
50+
}
51+
}
52+
case .error(let error):
53+
throw error
54+
case .finished:
55+
return nil
56+
}
57+
}
58+
}
59+
60+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
61+
public mutating func nextCollatedPart(isolation actor: isolated (any Actor)? = #isolation) async throws(MultipartParserError)
62+
-> MultipartSection<BackingSequence.Element>?
63+
{
64+
var headerFields = HTTPFields()
65+
66+
while let part = try await self.next(isolation: actor) {
67+
switch part {
68+
case .headerFields(let fields):
69+
headerFields.append(contentsOf: fields)
70+
case .bodyChunk(let chunk):
71+
self.currentCollatedBody.append(contentsOf: chunk)
72+
if !headerFields.isEmpty {
73+
defer { headerFields = .init() }
74+
return .headerFields(headerFields)
75+
}
76+
case .boundary:
77+
if !currentCollatedBody.isEmpty {
78+
defer { currentCollatedBody = .init() }
79+
return .bodyChunk(currentCollatedBody)
80+
}
81+
}
82+
}
83+
return nil
84+
}
85+
}

Sources/MultipartKit/StreamingMultipartParserAsyncSequence.swift

+10-4
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ where BackingSequence.Element: MultipartPartBodyElement & RangeReplaceableCollec
4646

4747
var currentCollatedBody = BackingSequence.Element()
4848

49-
public mutating func next() async throws -> MultipartSection<BackingSequence.Element>? {
49+
public mutating func next() async throws(MultipartParserError) -> MultipartSection<BackingSequence.Element>? {
5050
if let pendingBodyChunk {
5151
defer { self.pendingBodyChunk = nil }
5252
return .bodyChunk(pendingBodyChunk)
@@ -77,14 +77,20 @@ where BackingSequence.Element: MultipartPartBodyElement & RangeReplaceableCollec
7777
}
7878
}
7979
case .needMoreData:
80-
if let next = try await iterator.next() {
80+
let next: BackingSequence.Element?
81+
do {
82+
next = try await iterator.next()
83+
} catch {
84+
throw MultipartParserError.backingSequenceError(reason: "\(error)")
85+
}
86+
if let next {
8187
parser.append(buffer: next)
8288
} else {
8389
switch parser.state {
8490
case .initial, .finished:
8591
return nil
8692
case .parsing:
87-
throw MultipartMessageError.unexpectedEndOfFile
93+
throw MultipartParserError.unexpectedEndOfFile
8894
}
8995
}
9096
case .error(let error):
@@ -95,7 +101,7 @@ where BackingSequence.Element: MultipartPartBodyElement & RangeReplaceableCollec
95101
}
96102
}
97103

98-
public mutating func nextCollatedPart() async throws -> MultipartSection<BackingSequence.Element>? {
104+
public mutating func nextCollatedPart() async throws(MultipartParserError) -> MultipartSection<BackingSequence.Element>? {
99105
var headerFields = HTTPFields()
100106

101107
while let part = try await self.next() {

Tests/MultipartKitTests/ParserTests.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,15 @@ struct ParserTests {
141141
""".utf8
142142
)
143143

144-
#expect(throws: MultipartMessageError.unexpectedEndOfFile) {
144+
#expect(throws: MultipartParserError.unexpectedEndOfFile) {
145145
_ = try MultipartParser<[UInt8]>(boundary: boundary)
146146
.parse([UInt8](message))
147147
}
148148

149149
let stream = makeParsingStream(for: message)
150150
var iterator = StreamingMultipartParserAsyncSequence(boundary: boundary, buffer: stream).makeAsyncIterator()
151151

152-
await #expect(throws: MultipartMessageError.unexpectedEndOfFile) {
152+
await #expect(throws: MultipartParserError.unexpectedEndOfFile) {
153153
while (try await iterator.next()) != nil {}
154154
}
155155
}
@@ -216,15 +216,15 @@ struct ParserTests {
216216
""".utf8
217217
)
218218

219-
#expect(throws: MultipartMessageError.unexpectedEndOfFile) {
219+
#expect(throws: MultipartParserError.unexpectedEndOfFile) {
220220
_ = try MultipartParser<[UInt8]>(boundary: boundary)
221221
.parse([UInt8](message))
222222
}
223223

224224
let stream = makeParsingStream(for: message)
225225
var iterator = StreamingMultipartParserAsyncSequence(boundary: boundary, buffer: stream).makeAsyncIterator()
226226

227-
await #expect(throws: MultipartMessageError.unexpectedEndOfFile) {
227+
await #expect(throws: MultipartParserError.unexpectedEndOfFile) {
228228
while (try await iterator.next()) != nil {}
229229
}
230230
}

0 commit comments

Comments
 (0)