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

Various small fixes #117

Merged
merged 10 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
fpseverino marked this conversation as resolved.
Show resolved Hide resolved
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.
fpseverino marked this conversation as resolved.
Show resolved Hide resolved
/// 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")
Copy link
Contributor

@MahdiBM MahdiBM Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a change in this PR but just want to mention i don't like how we have:

@Suite("Form Data Decoding Tests")
struct FormDataDecodingTests {

It literally says the same "Form Data Decoding Tests" twice which is of little value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It says "Form Data Decoding Tests" instead of "FormDataDecodingTests". It's a small thing, but readability improves.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I should specify that I'm referring to readability of the output during tests at runtime, not of the code.)

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