Skip to content

Commit

Permalink
Various small fixes (#117)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Feb 10, 2025
1 parent e968482 commit 46074f7
Show file tree
Hide file tree
Showing 15 changed files with 104 additions and 72 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@ on:
jobs:
unit-tests:
uses: vapor/ci/.github/workflows/run-unit-tests.yml@main
with:
warnings_as_errors: true
with_linting: true
with_windows: true
with_musl: true
ios_scheme_name: multipart-kit
secrets: inherit
4 changes: 2 additions & 2 deletions Benchmarks/Parser/AsyncSyncSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ extension Sequence {
/// An asynchronous sequence composed from a synchronous sequence, that releases the reference to
/// the base sequence when an iterator is created. So you can only iterate once.
///
/// Not safe. Only for testing purposes.
/// Use `swift-algorithms`'s `AsyncSyncSequence`` instead if you're looking for something like this.
/// > Warning: Not safe. Only for testing purposes.
/// Use `swift-algorithms`'s `AsyncSyncSequence` instead if you're looking for something like this.
final class AsyncSyncSequence<Base: Sequence>: AsyncSequence {
typealias Element = Base.Element

Expand Down
11 changes: 9 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,20 @@ let package = Package(
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(name: "Collections", package: "swift-collections"),
],
exclude: ["Docs.docc"]
swiftSettings: swiftSettings
),
.testTarget(
name: "MultipartKitTests",
dependencies: [
.target(name: "MultipartKit")
]
],
swiftSettings: swiftSettings
),
]
)

var swiftSettings: [SwiftSetting] {
[
.enableUpcomingFeature("ExistentialAny")
]
}
2 changes: 2 additions & 0 deletions Sources/MultipartKit/Docs.docc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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

## Overview

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.

### Multipart Form Data
Expand Down
3 changes: 3 additions & 0 deletions Sources/MultipartKit/Docs.docc/theme-settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"multipartkit": "#392048",
"documentation-intro-fill": "radial-gradient(circle at top, var(--color-multipartkit) 30%, #000 100%)",
"documentation-intro-accent": "var(--color-multipartkit)",
"documentation-intro-eyebrow": "white",
"documentation-intro-figure": "white",
"documentation-intro-title": "white",
"logo-base": { "dark": "#fff", "light": "#000" },
"logo-shape": { "dark": "#000", "light": "#fff" },
"fill": { "dark": "#000", "light": "#fff" }
Expand Down
12 changes: 8 additions & 4 deletions Sources/MultipartKit/FormDataDecoder/FormDataDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ public struct FormDataDecoder: Sendable {

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

/// Decodes a `Decodable` item from some``MultipartPartBodyElement`` using the supplied boundary.
///
/// let foo = try FormDataDecoder().decode(Foo.self, from: data, boundary: "123")
/// ```swift
/// let foo = try FormDataDecoder().decode(Foo.self, from: data, boundary: "123")
/// ```
///
/// - Parameters:
/// - decodable: Generic `Decodable` type.
/// - data: some ``MultipartPartBodyElement`` to decode.
/// - buffer: some ``MultipartPartBodyElement`` to decode.
/// - boundary: Multipart boundary to used in the decoding.
/// - Throws: Any errors decoding the model with `Codable` or parsing the data.
/// - Returns: An instance of the decoded type `D`.
Expand Down
16 changes: 10 additions & 6 deletions Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ public struct FormDataEncoder: Sendable {

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

/// Encodes an `Encodable` item into some ``MultipartPartBodyElement`` using the supplied boundary.
///
/// let a = Foo(string: "a", int: 42, double: 3.14, array: [1, 2, 3])
/// var buffer = ByteBuffer()
/// let data = try FormDataEncoder().encode(a, boundary: "123", into: &buffer)
/// ```swift
/// let a = Foo(string: "a", int: 42, double: 3.14, array: [1, 2, 3])
/// var buffer = ByteBuffer()
/// let data = try FormDataEncoder().encode(a, boundary: "123", into: &buffer)
/// ```
///
/// - parameters:
/// - encodable: Generic `Encodable` item.
/// - boundary: Multipart boundary to use for encoding. This must not appear anywhere in the encoded data.
/// - buffer: Buffer to write to.
/// - to: Buffer to write to.
/// - throws: Any errors encoding the model with `Codable` or serializing the data.
public func encode<E: Encodable, Body: MultipartPartBodyElement>(
_ encodable: E,
Expand Down
6 changes: 3 additions & 3 deletions Sources/MultipartKit/MultipartFormData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ enum MultipartFormData<Body: MultipartPartBodyElement>: Sendable {
}

var array: [MultipartFormData]? {
guard case let .array(array) = self else { return nil }
guard case .array(let array) = self else { return nil }
return array
}

var dictionary: Keyed? {
guard case let .keyed(dict) = self else { return nil }
guard case .keyed(let dict) = self else { return nil }
return dict
}

var part: MultipartPart<Body>? {
guard case let .single(part) = self else { return nil }
guard case .single(let part) = self else { return nil }
return part
}

Expand Down
16 changes: 8 additions & 8 deletions Sources/MultipartKit/MultipartParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public struct MultipartParser<Body: MultipartPartBodyElement> where Body: RangeR
case .prematureEnd: // ask for more data and retry
self.state = .parsing(.boundary, buffer)
return .needMoreData
case let .success(index):
case .success(let index):
switch buffer[index...].getIndexAfter(.twoHyphens) { // check if it's the final boundary (ends with "--")
case .success: // if it is, finish
self.state = .finished
Expand Down Expand Up @@ -180,13 +180,13 @@ public struct MultipartParser<Body: MultipartPartBodyElement> where Body: RangeR
}

extension ArraySlice where Element == UInt8 {
/// The result of a `getIndexAfter(_:)` call.
/// - success: The slice was found at the given index. The index is the index after the slice.
/// - wrongCharacter: The buffer did not match the slice. The index is the index of the first mismatching character.
/// - prematureEnd: The buffer was too short to contain the slice. The index is the index of the last character.
/// The result of a ``Swift/ArraySlice/getIndexAfter(_:)`` call.
enum IndexAfterSlice {
/// The slice was found at the given index. The index is the index after the slice.
case success(ArraySlice<UInt8>.Index)
/// The buffer did not match the slice. The index is the index of the first mismatching character.
case wrongCharacter(at: ArraySlice<UInt8>.Index)
/// The buffer was too short to contain the slice. The index is the index of the last character.
case prematureEnd(at: ArraySlice<UInt8>.Index)
}

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

/// The result of a `firstIndexOf(_:)` call.
/// - Parameter success: The slice was found. The associated index is the index before the slice.
/// - Parameter notFound: The slice was not found in the buffer.
/// The result of a ``Swift/ArraySlice/getFirstRange(of:)`` call.
enum FirstIndexOfSliceResult {
/// The slice was found. The associated index is the index before the slice.
case success(Range<Index>)
/// The slice was not found in the buffer.
case notFound
case prematureEnd
}
Expand Down
36 changes: 19 additions & 17 deletions Sources/MultipartKit/MultipartParserAsyncSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,26 @@ import HTTPTypes
/// Different to the ``StreamingMultipartParserAsyncSequence``, this sequence will collate the body
/// chunks into one section rather than yielding them individually.
///
/// let boundary = "boundary123"
/// var message = ArraySlice(...)
/// let stream = AsyncStream { continuation in
/// var offset = message.startIndex
/// while offset < message.endIndex {
/// let endIndex = min(message.endIndex, message.index(offset, offsetBy: 16))
/// continuation.yield(message[offset..<endIndex])
/// offset = endIndex
/// }
/// continuation.finish()
/// }
/// let sequence = MultipartParserAsyncSequence(boundary: boundary, buffer: stream)
/// for try await part in sequence {
/// switch part {
/// case .bodyChunk(let chunk): ...
/// case .headerFields(let field): ...
/// case .boundary: break
/// ```swift
/// let boundary = "boundary123"
/// var message = ArraySlice(...)
/// let stream = AsyncStream { continuation in
/// var offset = message.startIndex
/// while offset < message.endIndex {
/// let endIndex = min(message.endIndex, message.index(offset, offsetBy: 16))
/// continuation.yield(message[offset..<endIndex])
/// offset = endIndex
/// }
/// continuation.finish()
/// }
/// let sequence = MultipartParserAsyncSequence(boundary: boundary, buffer: stream)
/// for try await part in sequence {
/// switch part {
/// case .bodyChunk(let chunk): ...
/// case .headerFields(let field): ...
/// case .boundary: break
/// }
/// ```
///
public struct MultipartParserAsyncSequence<BackingSequence: AsyncSequence>: AsyncSequence
where BackingSequence.Element: MultipartPartBodyElement & RangeReplaceableCollection {
Expand Down
4 changes: 3 additions & 1 deletion Sources/MultipartKit/MultipartPart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public struct MultipartPart<Body: MultipartPartBodyElement>: Sendable {

/// Creates a new ``MultipartPart``.
///
/// let part = MultipartPart(headerFields: [.contentDisposition: "form-data"], body: Array("Hello, world!".utf8))
/// ```swift
/// let part = MultipartPart(headerFields: [.contentDisposition: "form-data"], body: Array("Hello, world!".utf8))
/// ```
///
/// - Parameters:
/// - headerFields: The header fields for this part.
Expand Down
36 changes: 19 additions & 17 deletions Sources/MultipartKit/StreamingMultipartParserAsyncSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,26 @@ import HTTPTypes
/// This sequence is designed to be used with `AsyncStream` to parse a stream of data asynchronously.
/// The sequence will yield ``MultipartSection`` values as they are parsed from the stream.
///
/// let boundary = "boundary123"
/// var message = ArraySlice(...)
/// let stream = AsyncStream { continuation in
/// var offset = message.startIndex
/// while offset < message.endIndex {
/// let endIndex = min(message.endIndex, message.index(offset, offsetBy: 16))
/// continuation.yield(message[offset..<endIndex])
/// offset = endIndex
/// }
/// continuation.finish()
/// }
/// let sequence = StreamingMultipartParserAsyncSequence(boundary: boundary, buffer: stream)
/// for try await part in sequence {
/// switch part {
/// case .bodyChunk(let chunk): ...
/// case .headerFields(let field): ...
/// case .boundary: break
/// ```swift
/// let boundary = "boundary123"
/// var message = ArraySlice(...)
/// let stream = AsyncStream { continuation in
/// var offset = message.startIndex
/// while offset < message.endIndex {
/// let endIndex = min(message.endIndex, message.index(offset, offsetBy: 16))
/// continuation.yield(message[offset..<endIndex])
/// offset = endIndex
/// }
/// continuation.finish()
/// }
/// let sequence = StreamingMultipartParserAsyncSequence(boundary: boundary, buffer: stream)
/// for try await part in sequence {
/// switch part {
/// case .bodyChunk(let chunk): ...
/// case .headerFields(let field): ...
/// case .boundary: break
/// }
/// ```
///
public struct StreamingMultipartParserAsyncSequence<BackingSequence: AsyncSequence>: AsyncSequence
where BackingSequence.Element: MultipartPartBodyElement & RangeReplaceableCollection {
Expand Down
18 changes: 9 additions & 9 deletions Tests/MultipartKitTests/FormDataDecodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Testing
@Suite("Form Data Decoding Tests")
struct FormDataDecodingTests {
@Test("W3 Form Data Decoding")
func testFormDataDecoderW3() throws {
func formDataDecoderW3() throws {
/// Content-Type: multipart/form-data; boundary=12345
let data = """
--12345\r
Expand Down Expand Up @@ -38,7 +38,7 @@ struct FormDataDecodingTests {
}

@Test("Optional Decoding")
func testDecodeOptional() throws {
func decodeOptional() throws {
struct Bar: Decodable {
struct Foo: Decodable {
let int: Int?
Expand All @@ -59,7 +59,7 @@ struct FormDataDecodingTests {
}

@Test("Decode Multiple Items")
func testFormDataDecoderMultiple() throws {
func formDataDecoderMultiple() throws {
/// Content-Type: multipart/form-data; boundary=12345
let data = """
--hello\r
Expand Down Expand Up @@ -110,7 +110,7 @@ struct FormDataDecodingTests {
}

@Test("Decode Multiple Items with Missing Data")
func testFormDataDecoderMultipleWithMissingData() throws {
func formDataDecoderMultipleWithMissingData() throws {
/// Content-Type: multipart/form-data; boundary=hello
let data = """
--hello\r
Expand All @@ -135,7 +135,7 @@ struct FormDataDecodingTests {
Issue.record("Was expecting an error of type DecodingError")
return false
}
guard case let DecodingError.typeMismatch(_, context) = error else {
guard case DecodingError.typeMismatch(_, let context) = error else {
Issue.record("Was expecting an error of type DecodingError.typeMismatch")
return false
}
Expand All @@ -144,7 +144,7 @@ struct FormDataDecodingTests {
}

@Test("Nested Decode")
func testNestedDecode() throws {
func nestedDecode() throws {
struct FormData: Decodable, Equatable {
struct NestedFormData: Decodable, Equatable {
struct AnotherNestedFormData: Decodable, Equatable {
Expand Down Expand Up @@ -252,7 +252,7 @@ struct FormDataDecodingTests {
}

@Test("Decoding Single Value")
func testDecodingSingleValue() throws {
func decodingSingleValue() throws {
let data = """
---\r
Content-Disposition: form-data;\r
Expand All @@ -267,7 +267,7 @@ struct FormDataDecodingTests {
}

@Test("Nesting Depth")
func testNestingDepth() throws {
func nestingDepth() throws {
let nested = """
---\r
Content-Disposition: form-data; name=a[]\r
Expand All @@ -286,7 +286,7 @@ struct FormDataDecodingTests {
}

@Test("Decoding Incorrectly Nested Data")
func testIncorrectlyNestedData() throws {
func incorrectlyNestedData() throws {
struct TestData: Codable {
var x: String
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/MultipartKitTests/ParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ struct ParserTests {
let sequence = StreamingMultipartParserAsyncSequence(boundary: "----WebKitFormBoundaryPVOZifB9OqEwP2fn", buffer: stream)

for try await part in sequence {
if case let .headerFields(fields) = part,
if case .headerFields(let fields) = part,
let contentDispositionField = fields.first(where: { $0.name == .contentDisposition })
{
#expect(contentDispositionField.value.contains(filename))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import MultipartKit
extension MultipartSection: Equatable where Body: Equatable {
public static func == (lhs: MultipartKit.MultipartSection<Body>, rhs: MultipartKit.MultipartSection<Body>) -> Bool {
switch (lhs, rhs) {
case let (.headerFields(lhsFields), .headerFields(rhsFields)):
case (.headerFields(let lhsFields), .headerFields(let rhsFields)):
lhsFields == rhsFields
case let (.bodyChunk(lhsChunk), .bodyChunk(rhsChunk)):
case (.bodyChunk(let lhsChunk), .bodyChunk(let rhsChunk)):
lhsChunk == rhsChunk
case (.boundary, .boundary):
true
Expand Down

0 comments on commit 46074f7

Please sign in to comment.