Skip to content

Commit 46074f7

Browse files
fpseverinoptoffyMahdiBM
authored
Various small fixes (#117)
* Add linting to CI * Add "ExistentialAny" Swift setting and remove superfluous exclude of DocC catalog * Remove `test` prefix from test function names * Improve DocC * Make the linter happy * Add Windows, Musl and iOS to CI * Update `theme-settings.json` --------- Co-authored-by: Paul Toffoloni <[email protected]> Co-authored-by: Mahdi Bahrami <[email protected]>
1 parent e968482 commit 46074f7

15 files changed

+104
-72
lines changed

.github/workflows/test.yml

+6
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,10 @@ on:
99
jobs:
1010
unit-tests:
1111
uses: vapor/ci/.github/workflows/run-unit-tests.yml@main
12+
with:
13+
warnings_as_errors: true
14+
with_linting: true
15+
with_windows: true
16+
with_musl: true
17+
ios_scheme_name: multipart-kit
1218
secrets: inherit

Benchmarks/Parser/AsyncSyncSequence.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ extension Sequence {
77
/// An asynchronous sequence composed from a synchronous sequence, that releases the reference to
88
/// the base sequence when an iterator is created. So you can only iterate once.
99
///
10-
/// Not safe. Only for testing purposes.
11-
/// Use `swift-algorithms`'s `AsyncSyncSequence`` instead if you're looking for something like this.
10+
/// > Warning: Not safe. Only for testing purposes.
11+
/// Use `swift-algorithms`'s `AsyncSyncSequence` instead if you're looking for something like this.
1212
final class AsyncSyncSequence<Base: Sequence>: AsyncSequence {
1313
typealias Element = Base.Element
1414

Package.swift

+9-2
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,20 @@ let package = Package(
2525
.product(name: "HTTPTypes", package: "swift-http-types"),
2626
.product(name: "Collections", package: "swift-collections"),
2727
],
28-
exclude: ["Docs.docc"]
28+
swiftSettings: swiftSettings
2929
),
3030
.testTarget(
3131
name: "MultipartKitTests",
3232
dependencies: [
3333
.target(name: "MultipartKit")
34-
]
34+
],
35+
swiftSettings: swiftSettings
3536
),
3637
]
3738
)
39+
40+
var swiftSettings: [SwiftSetting] {
41+
[
42+
.enableUpcomingFeature("ExistentialAny")
43+
]
44+
}

Sources/MultipartKit/Docs.docc/index.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Parser, serializer, and `Codable` support for `multipart/form-data`.
44

5+
## Overview
6+
57
MultipartKit is a Swift package for parsing and serializing `multipart/form-data` requests. It provides hooks for encoding and decoding requests in Swift and `Codable` support for handling `multipart/form-data` data through a ``FormDataEncoder`` and ``FormDataDecoder``. The parser delivers its output as it is parsed through callbacks suitable for streaming.
68

79
### Multipart Form Data

Sources/MultipartKit/Docs.docc/theme-settings.json

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
"multipartkit": "#392048",
99
"documentation-intro-fill": "radial-gradient(circle at top, var(--color-multipartkit) 30%, #000 100%)",
1010
"documentation-intro-accent": "var(--color-multipartkit)",
11+
"documentation-intro-eyebrow": "white",
12+
"documentation-intro-figure": "white",
13+
"documentation-intro-title": "white",
1114
"logo-base": { "dark": "#fff", "light": "#000" },
1215
"logo-shape": { "dark": "#000", "light": "#fff" },
1316
"fill": { "dark": "#000", "light": "#fff" }

Sources/MultipartKit/FormDataDecoder/FormDataDecoder.swift

+8-4
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ public struct FormDataDecoder: Sendable {
2424

2525
/// Decodes a `Decodable` item from `String` using the supplied boundary.
2626
///
27-
/// let foo = try FormDataDecoder().decode(Foo.self, from: "...", boundary: "123")
27+
/// ```swift
28+
/// let foo = try FormDataDecoder().decode(Foo.self, from: "...", boundary: "123")
29+
/// ```
2830
///
2931
/// - Parameters:
3032
/// - decodable: Generic `Decodable` type.
31-
/// - data: `String` to decode.
33+
/// - string: `String` to decode.
3234
/// - boundary: Multipart boundary to used in the decoding.
3335
/// - Throws: Any errors decoding the model with `Codable` or parsing the data.
3436
/// - Returns: An instance of the decoded type `D`.
@@ -38,11 +40,13 @@ public struct FormDataDecoder: Sendable {
3840

3941
/// Decodes a `Decodable` item from some``MultipartPartBodyElement`` using the supplied boundary.
4042
///
41-
/// let foo = try FormDataDecoder().decode(Foo.self, from: data, boundary: "123")
43+
/// ```swift
44+
/// let foo = try FormDataDecoder().decode(Foo.self, from: data, boundary: "123")
45+
/// ```
4246
///
4347
/// - Parameters:
4448
/// - decodable: Generic `Decodable` type.
45-
/// - data: some ``MultipartPartBodyElement`` to decode.
49+
/// - buffer: some ``MultipartPartBodyElement`` to decode.
4650
/// - boundary: Multipart boundary to used in the decoding.
4751
/// - Throws: Any errors decoding the model with `Codable` or parsing the data.
4852
/// - Returns: An instance of the decoded type `D`.

Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift

+10-6
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ public struct FormDataEncoder: Sendable {
1212

1313
/// Encodes an `Encodable` item to `String` using the supplied boundary.
1414
///
15-
/// let a = Foo(string: "a", int: 42, double: 3.14, array: [1, 2, 3])
16-
/// let data = try FormDataEncoder().encode(a, boundary: "123")
15+
/// ```swift
16+
/// let a = Foo(string: "a", int: 42, double: 3.14, array: [1, 2, 3])
17+
/// let data = try FormDataEncoder().encode(a, boundary: "123")
18+
/// ```
1719
///
1820
/// - parameters:
1921
/// - encodable: Generic `Encodable` item.
@@ -27,14 +29,16 @@ public struct FormDataEncoder: Sendable {
2729

2830
/// Encodes an `Encodable` item into some ``MultipartPartBodyElement`` using the supplied boundary.
2931
///
30-
/// let a = Foo(string: "a", int: 42, double: 3.14, array: [1, 2, 3])
31-
/// var buffer = ByteBuffer()
32-
/// let data = try FormDataEncoder().encode(a, boundary: "123", into: &buffer)
32+
/// ```swift
33+
/// let a = Foo(string: "a", int: 42, double: 3.14, array: [1, 2, 3])
34+
/// var buffer = ByteBuffer()
35+
/// let data = try FormDataEncoder().encode(a, boundary: "123", into: &buffer)
36+
/// ```
3337
///
3438
/// - parameters:
3539
/// - encodable: Generic `Encodable` item.
3640
/// - boundary: Multipart boundary to use for encoding. This must not appear anywhere in the encoded data.
37-
/// - buffer: Buffer to write to.
41+
/// - to: Buffer to write to.
3842
/// - throws: Any errors encoding the model with `Codable` or serializing the data.
3943
public func encode<E: Encodable, Body: MultipartPartBodyElement>(
4044
_ encodable: E,

Sources/MultipartKit/MultipartFormData.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,17 @@ enum MultipartFormData<Body: MultipartPartBodyElement>: Sendable {
2424
}
2525

2626
var array: [MultipartFormData]? {
27-
guard case let .array(array) = self else { return nil }
27+
guard case .array(let array) = self else { return nil }
2828
return array
2929
}
3030

3131
var dictionary: Keyed? {
32-
guard case let .keyed(dict) = self else { return nil }
32+
guard case .keyed(let dict) = self else { return nil }
3333
return dict
3434
}
3535

3636
var part: MultipartPart<Body>? {
37-
guard case let .single(part) = self else { return nil }
37+
guard case .single(let part) = self else { return nil }
3838
return part
3939
}
4040

Sources/MultipartKit/MultipartParser.swift

+8-8
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public struct MultipartParser<Body: MultipartPartBodyElement> where Body: RangeR
7171
case .prematureEnd: // ask for more data and retry
7272
self.state = .parsing(.boundary, buffer)
7373
return .needMoreData
74-
case let .success(index):
74+
case .success(let index):
7575
switch buffer[index...].getIndexAfter(.twoHyphens) { // check if it's the final boundary (ends with "--")
7676
case .success: // if it is, finish
7777
self.state = .finished
@@ -180,13 +180,13 @@ public struct MultipartParser<Body: MultipartPartBodyElement> where Body: RangeR
180180
}
181181

182182
extension ArraySlice where Element == UInt8 {
183-
/// The result of a `getIndexAfter(_:)` call.
184-
/// - success: The slice was found at the given index. The index is the index after the slice.
185-
/// - wrongCharacter: The buffer did not match the slice. The index is the index of the first mismatching character.
186-
/// - prematureEnd: The buffer was too short to contain the slice. The index is the index of the last character.
183+
/// The result of a ``Swift/ArraySlice/getIndexAfter(_:)`` call.
187184
enum IndexAfterSlice {
185+
/// The slice was found at the given index. The index is the index after the slice.
188186
case success(ArraySlice<UInt8>.Index)
187+
/// The buffer did not match the slice. The index is the index of the first mismatching character.
189188
case wrongCharacter(at: ArraySlice<UInt8>.Index)
189+
/// The buffer was too short to contain the slice. The index is the index of the last character.
190190
case prematureEnd(at: ArraySlice<UInt8>.Index)
191191
}
192192

@@ -211,11 +211,11 @@ extension ArraySlice where Element == UInt8 {
211211
return .success(resultIndex)
212212
}
213213

214-
/// The result of a `firstIndexOf(_:)` call.
215-
/// - Parameter success: The slice was found. The associated index is the index before the slice.
216-
/// - Parameter notFound: The slice was not found in the buffer.
214+
/// The result of a ``Swift/ArraySlice/getFirstRange(of:)`` call.
217215
enum FirstIndexOfSliceResult {
216+
/// The slice was found. The associated index is the index before the slice.
218217
case success(Range<Index>)
218+
/// The slice was not found in the buffer.
219219
case notFound
220220
case prematureEnd
221221
}

Sources/MultipartKit/MultipartParserAsyncSequence.swift

+19-17
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,26 @@ import HTTPTypes
66
/// Different to the ``StreamingMultipartParserAsyncSequence``, this sequence will collate the body
77
/// chunks into one section rather than yielding them individually.
88
///
9-
/// let boundary = "boundary123"
10-
/// var message = ArraySlice(...)
11-
/// let stream = AsyncStream { continuation in
12-
/// var offset = message.startIndex
13-
/// while offset < message.endIndex {
14-
/// let endIndex = min(message.endIndex, message.index(offset, offsetBy: 16))
15-
/// continuation.yield(message[offset..<endIndex])
16-
/// offset = endIndex
17-
/// }
18-
/// continuation.finish()
19-
/// }
20-
/// let sequence = MultipartParserAsyncSequence(boundary: boundary, buffer: stream)
21-
/// for try await part in sequence {
22-
/// switch part {
23-
/// case .bodyChunk(let chunk): ...
24-
/// case .headerFields(let field): ...
25-
/// case .boundary: break
9+
/// ```swift
10+
/// let boundary = "boundary123"
11+
/// var message = ArraySlice(...)
12+
/// let stream = AsyncStream { continuation in
13+
/// var offset = message.startIndex
14+
/// while offset < message.endIndex {
15+
/// let endIndex = min(message.endIndex, message.index(offset, offsetBy: 16))
16+
/// continuation.yield(message[offset..<endIndex])
17+
/// offset = endIndex
2618
/// }
19+
/// continuation.finish()
20+
/// }
21+
/// let sequence = MultipartParserAsyncSequence(boundary: boundary, buffer: stream)
22+
/// for try await part in sequence {
23+
/// switch part {
24+
/// case .bodyChunk(let chunk): ...
25+
/// case .headerFields(let field): ...
26+
/// case .boundary: break
27+
/// }
28+
/// ```
2729
///
2830
public struct MultipartParserAsyncSequence<BackingSequence: AsyncSequence>: AsyncSequence
2931
where BackingSequence.Element: MultipartPartBodyElement & RangeReplaceableCollection {

Sources/MultipartKit/MultipartPart.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ public struct MultipartPart<Body: MultipartPartBodyElement>: Sendable {
1212

1313
/// Creates a new ``MultipartPart``.
1414
///
15-
/// let part = MultipartPart(headerFields: [.contentDisposition: "form-data"], body: Array("Hello, world!".utf8))
15+
/// ```swift
16+
/// let part = MultipartPart(headerFields: [.contentDisposition: "form-data"], body: Array("Hello, world!".utf8))
17+
/// ```
1618
///
1719
/// - Parameters:
1820
/// - headerFields: The header fields for this part.

Sources/MultipartKit/StreamingMultipartParserAsyncSequence.swift

+19-17
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,26 @@ import HTTPTypes
55
/// This sequence is designed to be used with `AsyncStream` to parse a stream of data asynchronously.
66
/// The sequence will yield ``MultipartSection`` values as they are parsed from the stream.
77
///
8-
/// let boundary = "boundary123"
9-
/// var message = ArraySlice(...)
10-
/// let stream = AsyncStream { continuation in
11-
/// var offset = message.startIndex
12-
/// while offset < message.endIndex {
13-
/// let endIndex = min(message.endIndex, message.index(offset, offsetBy: 16))
14-
/// continuation.yield(message[offset..<endIndex])
15-
/// offset = endIndex
16-
/// }
17-
/// continuation.finish()
18-
/// }
19-
/// let sequence = StreamingMultipartParserAsyncSequence(boundary: boundary, buffer: stream)
20-
/// for try await part in sequence {
21-
/// switch part {
22-
/// case .bodyChunk(let chunk): ...
23-
/// case .headerFields(let field): ...
24-
/// case .boundary: break
8+
/// ```swift
9+
/// let boundary = "boundary123"
10+
/// var message = ArraySlice(...)
11+
/// let stream = AsyncStream { continuation in
12+
/// var offset = message.startIndex
13+
/// while offset < message.endIndex {
14+
/// let endIndex = min(message.endIndex, message.index(offset, offsetBy: 16))
15+
/// continuation.yield(message[offset..<endIndex])
16+
/// offset = endIndex
2517
/// }
18+
/// continuation.finish()
19+
/// }
20+
/// let sequence = StreamingMultipartParserAsyncSequence(boundary: boundary, buffer: stream)
21+
/// for try await part in sequence {
22+
/// switch part {
23+
/// case .bodyChunk(let chunk): ...
24+
/// case .headerFields(let field): ...
25+
/// case .boundary: break
26+
/// }
27+
/// ```
2628
///
2729
public struct StreamingMultipartParserAsyncSequence<BackingSequence: AsyncSequence>: AsyncSequence
2830
where BackingSequence.Element: MultipartPartBodyElement & RangeReplaceableCollection {

Tests/MultipartKitTests/FormDataDecodingTests.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Testing
44
@Suite("Form Data Decoding Tests")
55
struct FormDataDecodingTests {
66
@Test("W3 Form Data Decoding")
7-
func testFormDataDecoderW3() throws {
7+
func formDataDecoderW3() throws {
88
/// Content-Type: multipart/form-data; boundary=12345
99
let data = """
1010
--12345\r
@@ -38,7 +38,7 @@ struct FormDataDecodingTests {
3838
}
3939

4040
@Test("Optional Decoding")
41-
func testDecodeOptional() throws {
41+
func decodeOptional() throws {
4242
struct Bar: Decodable {
4343
struct Foo: Decodable {
4444
let int: Int?
@@ -59,7 +59,7 @@ struct FormDataDecodingTests {
5959
}
6060

6161
@Test("Decode Multiple Items")
62-
func testFormDataDecoderMultiple() throws {
62+
func formDataDecoderMultiple() throws {
6363
/// Content-Type: multipart/form-data; boundary=12345
6464
let data = """
6565
--hello\r
@@ -110,7 +110,7 @@ struct FormDataDecodingTests {
110110
}
111111

112112
@Test("Decode Multiple Items with Missing Data")
113-
func testFormDataDecoderMultipleWithMissingData() throws {
113+
func formDataDecoderMultipleWithMissingData() throws {
114114
/// Content-Type: multipart/form-data; boundary=hello
115115
let data = """
116116
--hello\r
@@ -135,7 +135,7 @@ struct FormDataDecodingTests {
135135
Issue.record("Was expecting an error of type DecodingError")
136136
return false
137137
}
138-
guard case let DecodingError.typeMismatch(_, context) = error else {
138+
guard case DecodingError.typeMismatch(_, let context) = error else {
139139
Issue.record("Was expecting an error of type DecodingError.typeMismatch")
140140
return false
141141
}
@@ -144,7 +144,7 @@ struct FormDataDecodingTests {
144144
}
145145

146146
@Test("Nested Decode")
147-
func testNestedDecode() throws {
147+
func nestedDecode() throws {
148148
struct FormData: Decodable, Equatable {
149149
struct NestedFormData: Decodable, Equatable {
150150
struct AnotherNestedFormData: Decodable, Equatable {
@@ -252,7 +252,7 @@ struct FormDataDecodingTests {
252252
}
253253

254254
@Test("Decoding Single Value")
255-
func testDecodingSingleValue() throws {
255+
func decodingSingleValue() throws {
256256
let data = """
257257
---\r
258258
Content-Disposition: form-data;\r
@@ -267,7 +267,7 @@ struct FormDataDecodingTests {
267267
}
268268

269269
@Test("Nesting Depth")
270-
func testNestingDepth() throws {
270+
func nestingDepth() throws {
271271
let nested = """
272272
---\r
273273
Content-Disposition: form-data; name=a[]\r
@@ -286,7 +286,7 @@ struct FormDataDecodingTests {
286286
}
287287

288288
@Test("Decoding Incorrectly Nested Data")
289-
func testIncorrectlyNestedData() throws {
289+
func incorrectlyNestedData() throws {
290290
struct TestData: Codable {
291291
var x: String
292292
}

Tests/MultipartKitTests/ParserTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ struct ParserTests {
194194
let sequence = StreamingMultipartParserAsyncSequence(boundary: "----WebKitFormBoundaryPVOZifB9OqEwP2fn", buffer: stream)
195195

196196
for try await part in sequence {
197-
if case let .headerFields(fields) = part,
197+
if case .headerFields(let fields) = part,
198198
let contentDispositionField = fields.first(where: { $0.name == .contentDisposition })
199199
{
200200
#expect(contentDispositionField.value.contains(filename))

Tests/MultipartKitTests/Utilities/MultipartSection+Equatable.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import MultipartKit
33
extension MultipartSection: Equatable where Body: Equatable {
44
public static func == (lhs: MultipartKit.MultipartSection<Body>, rhs: MultipartKit.MultipartSection<Body>) -> Bool {
55
switch (lhs, rhs) {
6-
case let (.headerFields(lhsFields), .headerFields(rhsFields)):
6+
case (.headerFields(let lhsFields), .headerFields(let rhsFields)):
77
lhsFields == rhsFields
8-
case let (.bodyChunk(lhsChunk), .bodyChunk(rhsChunk)):
8+
case (.bodyChunk(let lhsChunk), .bodyChunk(let rhsChunk)):
99
lhsChunk == rhsChunk
1010
case (.boundary, .boundary):
1111
true

0 commit comments

Comments
 (0)