diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7810eff..14c39b4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,4 @@ version: 2 -enable-beta-ecosystems: true updates: - package-ecosystem: "github-actions" directory: "/" @@ -11,14 +10,3 @@ updates: dependencies: patterns: - "*" - - package-ecosystem: "swift" - directory: "/" - schedule: - interval: "daily" - open-pull-requests-limit: 6 - allow: - - dependency-type: all - groups: - all-dependencies: - patterns: - - "*" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc2abf2..51c0262 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,4 +5,5 @@ on: jobs: unit-tests: - uses: vapor/ci/.github/workflows/run-unit-tests.yml@reusable-workflows + uses: vapor/ci/.github/workflows/run-unit-tests.yml@main + secrets: inherit diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index 1cad414..365360c 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -25,6 +25,7 @@ let package = Package( .product(name: "Collections", package: "swift-collections"), ], swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), .enableExperimentalFeature("StrictConcurrency=complete"), ] ), @@ -34,6 +35,7 @@ let package = Package( .target(name: "MultipartKit"), ], swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), .enableExperimentalFeature("StrictConcurrency=complete"), ] ), diff --git a/README.md b/README.md index 72fd4bd..3ab2ec9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Documentation Team Chat MIT License -Continuous Integration +Continuous Integration Swift 5.7+

@@ -18,18 +18,9 @@ ### Installation -The table below shows a list of MultipartKit major releases alongside their compatible NIO and Swift versions. - -|Version|NIO|Swift|SPM| -|---|---|---|---| -|4.0|2.2|5.4+|`from: "4.0.0"`| -|3.0|1.0|4.0+|`from: "3.0.0"`| -|2.0|N/A|3.1+|`from: "2.0.0"`| -|1.0|N/A|3.1+|`from: "1.0.0"`| - Use the SPM string to easily include the dependency in your `Package.swift` file. -Add MultiPartKit to your package dependencies: +Add MultipartKit to your package dependencies: ```swift dependencies: [ @@ -38,7 +29,7 @@ dependencies: [ ] ``` -Add MultiPartKit to your target's dependencies: +Add MultipartKit to your target's dependencies: ```swift targets: [ diff --git a/Sources/MultipartKit/Docs.docc/index.md b/Sources/MultipartKit/Docs.docc/index.md index fb613dd..88d1c8f 100644 --- a/Sources/MultipartKit/Docs.docc/index.md +++ b/Sources/MultipartKit/Docs.docc/index.md @@ -1,6 +1,8 @@ # ``MultipartKit`` -MultipartKit is a Swift package for parsing and serializing multipart/form-data requests. It provides hooks for encoding and decoding requests in Swift. It provides `Codable` support for the special case of the `multipart/form-data` media type through a ``FormDataEncoder`` and ``FormDataDecoder``. The parser delivers its output as it is parsed through callbacks suitable for streaming. +Parser, serializer, and `Codable` support for `multipart/form-data`. + +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 diff --git a/Sources/MultipartKit/Docs.docc/theme-settings.json b/Sources/MultipartKit/Docs.docc/theme-settings.json index 09c9bb3..6e551b7 100644 --- a/Sources/MultipartKit/Docs.docc/theme-settings.json +++ b/Sources/MultipartKit/Docs.docc/theme-settings.json @@ -1,6 +1,6 @@ { "theme": { - "aside": { "border-radius": "6px", "border-style": "double", "border-width": "3px" }, + "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" }, "border-radius": "0", "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, diff --git a/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.Decoder.swift b/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.Decoder.swift index d33d539..cbc3ae4 100644 --- a/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.Decoder.swift +++ b/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.Decoder.swift @@ -1,12 +1,12 @@ extension FormDataDecoder { struct Decoder { - let codingPath: [CodingKey] + let codingPath: [any CodingKey] let data: MultipartFormData let userInfo: [CodingUserInfoKey: Any] - let previousCodingPath : [CodingKey]? - let previousType: Decodable.Type? + let previousCodingPath: [any CodingKey]? + let previousType: (any Decodable.Type)? - init(codingPath: [CodingKey], data: MultipartFormData, userInfo: [CodingUserInfoKey: Any], previousCodingPath: [CodingKey]? = nil, previousType: Decodable.Type? = nil) { + init(codingPath: [any CodingKey], data: MultipartFormData, userInfo: [CodingUserInfoKey: Any], previousCodingPath: [any CodingKey]? = nil, previousType: (any Decodable.Type)? = nil) { self.codingPath = codingPath self.data = data self.userInfo = userInfo @@ -24,26 +24,26 @@ extension FormDataDecoder.Decoder: Decoder { return KeyedDecodingContainer(FormDataDecoder.KeyedContainer(data: dictionary, decoder: self)) } - func unkeyedContainer() throws -> UnkeyedDecodingContainer { + func unkeyedContainer() throws -> any UnkeyedDecodingContainer { guard let array = data.array else { throw decodingError(expectedType: "array") } return FormDataDecoder.UnkeyedContainer(data: array, decoder: self) } - func singleValueContainer() throws -> SingleValueDecodingContainer { + func singleValueContainer() throws -> any SingleValueDecodingContainer { self } } extension FormDataDecoder.Decoder { - func nested(at key: CodingKey, with data: MultipartFormData) -> Self { + func nested(at key: any CodingKey, with data: MultipartFormData) -> Self { .init(codingPath: codingPath + [key], data: data, userInfo: userInfo) } } private extension FormDataDecoder.Decoder { - func decodingError(expectedType: String) -> Error { + func decodingError(expectedType: String) -> any Error { let encounteredType: Any.Type let encounteredTypeDescription: String diff --git a/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.KeyedContainer.swift b/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.KeyedContainer.swift index e9fb246..e523e23 100644 --- a/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.KeyedContainer.swift +++ b/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.KeyedContainer.swift @@ -10,7 +10,7 @@ extension FormDataDecoder.KeyedContainer: KeyedDecodingContainerProtocol { data.keys.compactMap(K.init(stringValue:)) } - var codingPath: [CodingKey] { + var codingPath: [any CodingKey] { decoder.codingPath } @@ -18,7 +18,7 @@ extension FormDataDecoder.KeyedContainer: KeyedDecodingContainerProtocol { data.keys.contains(key.stringValue) } - func getValue(forKey key: CodingKey) throws -> MultipartFormData { + func getValue(forKey key: any CodingKey) throws -> MultipartFormData { guard let value = data[key.stringValue] else { throw DecodingError.keyNotFound( key, .init( @@ -42,19 +42,19 @@ extension FormDataDecoder.KeyedContainer: KeyedDecodingContainerProtocol { try decoderForKey(key).container(keyedBy: keyType) } - func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer { + func nestedUnkeyedContainer(forKey key: K) throws -> any UnkeyedDecodingContainer { try decoderForKey(key).unkeyedContainer() } - func superDecoder() throws -> Decoder { + func superDecoder() throws -> any Decoder { try decoderForKey(BasicCodingKey.super) } - func superDecoder(forKey key: K) throws -> Decoder { + func superDecoder(forKey key: K) throws -> any Decoder { try decoderForKey(key) } - func decoderForKey(_ key: CodingKey) throws -> FormDataDecoder.Decoder { + func decoderForKey(_ key: any CodingKey) throws -> FormDataDecoder.Decoder { decoder.nested(at: key, with: try getValue(forKey: key)) } } diff --git a/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.SingleValueContainer.swift b/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.SingleValueContainer.swift index 749e0a6..5108ce2 100644 --- a/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.SingleValueContainer.swift +++ b/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.SingleValueContainer.swift @@ -6,7 +6,7 @@ extension FormDataDecoder.Decoder: SingleValueDecodingContainer { func decode(_: T.Type = T.self) throws -> T { guard let part = data.part, - let Convertible = T.self as? MultipartPartConvertible.Type + let Convertible = T.self as? any MultipartPartConvertible.Type else { guard previousCodingPath?.count != codingPath.count || previousType != T.self else { throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Decoding caught in recursion loop")) diff --git a/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.UnkeyedContainer.swift b/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.UnkeyedContainer.swift index 66294e4..d4779bd 100644 --- a/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.UnkeyedContainer.swift +++ b/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.UnkeyedContainer.swift @@ -7,11 +7,11 @@ extension FormDataDecoder { } extension FormDataDecoder.UnkeyedContainer: UnkeyedDecodingContainer { - var codingPath: [CodingKey] { + var codingPath: [any CodingKey] { decoder.codingPath } var count: Int? { data.count } - var index: CodingKey { BasicCodingKey.index(currentIndex) } + var index: any CodingKey { BasicCodingKey.index(currentIndex) } var isAtEnd: Bool { currentIndex >= data.count } mutating func decodeNil() throws -> Bool { @@ -26,11 +26,11 @@ extension FormDataDecoder.UnkeyedContainer: UnkeyedDecodingContainer { try decoderAtIndex().container(keyedBy: keyType) } - mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer { try decoderAtIndex().unkeyedContainer() } - mutating func superDecoder() throws -> Decoder { + mutating func superDecoder() throws -> any Decoder { try decoderAtIndex() } diff --git a/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.swift b/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.swift index 7e6f2c8..a793ed8 100644 --- a/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.swift +++ b/Sources/MultipartKit/FormDataDecoder/FormDataDecoder.swift @@ -15,7 +15,7 @@ public struct FormDataDecoder: Sendable { let nestingDepth: Int /// Any contextual information set by the user for decoding. - public var userInfo: [CodingUserInfoKey: Sendable] = [:] + public var userInfo: [CodingUserInfoKey: any Sendable] = [:] /// Creates a new `FormDataDecoder`. /// - Parameter nestingDepth: maximum allowed nesting depth of the decoded structure. Defaults to 8. diff --git a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.Encoder.swift b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.Encoder.swift index 6fbd11e..7e0a41c 100644 --- a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.Encoder.swift +++ b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.Encoder.swift @@ -1,6 +1,6 @@ extension FormDataEncoder { struct Encoder { - let codingPath: [CodingKey] + let codingPath: [any CodingKey] let storage = Storage() let userInfo: [CodingUserInfoKey: Any] } @@ -13,19 +13,19 @@ extension FormDataEncoder.Encoder: Encoder { return .init(container) } - func unkeyedContainer() -> UnkeyedEncodingContainer { + func unkeyedContainer() -> any UnkeyedEncodingContainer { let container = FormDataEncoder.UnkeyedContainer(encoder: self) storage.dataContainer = container.dataContainer return container } - func singleValueContainer() -> SingleValueEncodingContainer { + func singleValueContainer() -> any SingleValueEncodingContainer { self } } extension FormDataEncoder.Encoder { - func nested(at key: CodingKey) -> FormDataEncoder.Encoder { + func nested(at key: any CodingKey) -> FormDataEncoder.Encoder { .init(codingPath: codingPath + [key], userInfo: userInfo) } } diff --git a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.KeyedContainer.swift b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.KeyedContainer.swift index 6046d66..e72ec72 100644 --- a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.KeyedContainer.swift +++ b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.KeyedContainer.swift @@ -6,7 +6,7 @@ extension FormDataEncoder { } extension FormDataEncoder.KeyedContainer: KeyedEncodingContainerProtocol { - var codingPath: [CodingKey] { + var codingPath: [any CodingKey] { encoder.codingPath } @@ -22,19 +22,19 @@ extension FormDataEncoder.KeyedContainer: KeyedEncodingContainerProtocol { encoderForKey(key).container(keyedBy: keyType) } - func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer { encoderForKey(key).unkeyedContainer() } - func superEncoder() -> Encoder { + func superEncoder() -> any Encoder { encoderForKey(BasicCodingKey.super) } - func superEncoder(forKey key: Key) -> Encoder { + func superEncoder(forKey key: Key) -> any Encoder { encoderForKey(key) } - func encoderForKey(_ key: CodingKey) -> FormDataEncoder.Encoder { + func encoderForKey(_ key: any CodingKey) -> FormDataEncoder.Encoder { let encoder = self.encoder.nested(at: key) dataContainer.value[key.stringValue] = encoder.storage return encoder diff --git a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.SingleValueContainer.swift b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.SingleValueContainer.swift index b905f22..d5c9f60 100644 --- a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.SingleValueContainer.swift +++ b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.SingleValueContainer.swift @@ -5,7 +5,7 @@ extension FormDataEncoder.Encoder: SingleValueEncodingContainer { func encode(_ value: T) throws { if - let convertible = value as? MultipartPartConvertible, + let convertible = value as? any MultipartPartConvertible, let part = convertible.multipart { storage.dataContainer = SingleValueDataContainer(part: part) diff --git a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.UnkeyedContainer.swift b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.UnkeyedContainer.swift index 35ca9ee..6344a2f 100644 --- a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.UnkeyedContainer.swift +++ b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.UnkeyedContainer.swift @@ -6,7 +6,7 @@ extension FormDataEncoder { } extension FormDataEncoder.UnkeyedContainer: UnkeyedEncodingContainer { - var codingPath: [CodingKey] { + var codingPath: [any CodingKey] { encoder.codingPath } @@ -26,11 +26,11 @@ extension FormDataEncoder.UnkeyedContainer: UnkeyedEncodingContainer { nextEncoder().container(keyedBy: keyType) } - func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { nextEncoder().unkeyedContainer() } - func superEncoder() -> Encoder { + func superEncoder() -> any Encoder { nextEncoder() } diff --git a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift index f48066d..2579ff7 100644 --- a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift +++ b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift @@ -8,7 +8,7 @@ import NIOCore public struct FormDataEncoder: Sendable { /// Any contextual information set by the user for encoding. - public var userInfo: [CodingUserInfoKey: Sendable] = [:] + public var userInfo: [CodingUserInfoKey: any Sendable] = [:] /// Creates a new `FormDataEncoder`. public init() { } diff --git a/Sources/MultipartKit/FormDataEncoder/Storage.swift b/Sources/MultipartKit/FormDataEncoder/Storage.swift index 23a081b..7a7c01d 100644 --- a/Sources/MultipartKit/FormDataEncoder/Storage.swift +++ b/Sources/MultipartKit/FormDataEncoder/Storage.swift @@ -1,7 +1,7 @@ import Collections final class Storage { - var dataContainer: DataContainer? = nil + var dataContainer: (any DataContainer)? = nil var data: MultipartFormData? { dataContainer?.data } diff --git a/Sources/MultipartKit/MultipartPart.swift b/Sources/MultipartKit/MultipartPart.swift index 2281fc1..8e3a709 100644 --- a/Sources/MultipartKit/MultipartPart.swift +++ b/Sources/MultipartKit/MultipartPart.swift @@ -53,16 +53,11 @@ public struct MultipartPart: Equatable, Sendable { extension Array where Element == MultipartPart { /// Returns the first `MultipartPart` with matching name attribute in `"Content-Disposition"` header. public func firstPart(named name: String) -> MultipartPart? { - for el in self { - if el.name == name { - return el - } - } - return nil + self.first { $0.name == name } } /// Returns all `MultipartPart`s with matching name attribute in `"Content-Disposition"` header. public func allParts(named name: String) -> [MultipartPart] { - filter { $0.name == name } + self.filter { $0.name == name } } } diff --git a/Sources/MultipartKit/MultipartPartConvertible.swift b/Sources/MultipartKit/MultipartPartConvertible.swift index ae070b7..5a637c1 100644 --- a/Sources/MultipartKit/MultipartPartConvertible.swift +++ b/Sources/MultipartKit/MultipartPartConvertible.swift @@ -1,14 +1,18 @@ import struct Foundation.Data -import struct Foundation.UUID +import struct Foundation.URL +/// A protocol to provide custom behaviors for parsing and serializing types from and to multipart data. public protocol MultipartPartConvertible { var multipart: MultipartPart? { get } + init?(multipart: MultipartPart) } +// MARK: MultipartPart self-conformance + extension MultipartPart: MultipartPartConvertible { public var multipart: MultipartPart? { - return self + self } public init?(multipart: MultipartPart) { @@ -16,9 +20,11 @@ extension MultipartPart: MultipartPartConvertible { } } +// MARK: String + extension String: MultipartPartConvertible { public var multipart: MultipartPart? { - return MultipartPart(body: self) + .init(body: self) } public init?(multipart: MultipartPart) { @@ -26,16 +32,15 @@ extension String: MultipartPartConvertible { } } +// MARK: Numbers + extension FixedWidthInteger { public var multipart: MultipartPart? { - return MultipartPart(body: self.description) + .init(body: self.description) } public init?(multipart: MultipartPart) { - guard let string = String(multipart: multipart) else { - return nil - } - self.init(string) + self.init(String(multipart: multipart)!) // String.init(multipart:) never returns nil } } @@ -52,49 +57,54 @@ extension UInt64: MultipartPartConvertible { } extension Float: MultipartPartConvertible { public var multipart: MultipartPart? { - return MultipartPart(body: self.description) + .init(body: self.description) } public init?(multipart: MultipartPart) { - guard let string = String(multipart: multipart) else { - return nil - } - self.init(string) + self.init(String(multipart: multipart)!) // String.init(multipart:) never returns nil } } extension Double: MultipartPartConvertible { public var multipart: MultipartPart? { - return MultipartPart(body: self.description) + .init(body: self.description) } public init?(multipart: MultipartPart) { - guard let string = String(multipart: multipart) else { - return nil - } - self.init(string) + self.init(String(multipart: multipart)!) // String.init(multipart:) never returns nil } } +// MARK: Bool + extension Bool: MultipartPartConvertible { public var multipart: MultipartPart? { - return MultipartPart(body: self.description) + .init(body: self.description) } public init?(multipart: MultipartPart) { - guard let string = String(multipart: multipart) else { - return nil - } - self.init(string) + self.init(String(multipart: multipart)!) // String.init(multipart:) never returns nil } } +// MARK: Foundation types + extension Data: MultipartPartConvertible { public var multipart: MultipartPart? { - return MultipartPart(body: self) + .init(body: self) } public init?(multipart: MultipartPart) { self.init(multipart.body.readableBytesView) } } + +extension URL: MultipartPartConvertible { + public var multipart: MultipartPart? { + .init(body: self.absoluteString) + } + + public init?(multipart: MultipartPart) { + self.init(string: String(multipart: multipart)!) // String.init(multipart:) never returns nil + } +} diff --git a/Sources/MultipartKit/Utilities.swift b/Sources/MultipartKit/Utilities.swift index fbaf0be..3586f5d 100644 --- a/Sources/MultipartKit/Utilities.swift +++ b/Sources/MultipartKit/Utilities.swift @@ -19,16 +19,19 @@ extension HTTPHeaders { defaultValue: String ) { var current: [String] + if let existing = self.headerParts(name: name) { current = existing.filter { !$0.hasPrefix("\(key)=") } } else { current = [defaultValue] } + if let value = value { current.append("\(key)=\"\(value)\"") } - let new = current.joined(separator: "; ") - .trimmingCharacters(in: .whitespaces) + + let new = current.joined(separator: "; ").trimmingCharacters(in: .whitespaces) + self.replaceOrAdd(name: name, value: new) } diff --git a/Tests/MultipartKitTests/FormDataTests.swift b/Tests/MultipartKitTests/FormDataTests.swift index 20d6d56..3199f55 100644 --- a/Tests/MultipartKitTests/FormDataTests.swift +++ b/Tests/MultipartKitTests/FormDataTests.swift @@ -1,7 +1,7 @@ import XCTest import MultipartKit -class FormDataTests: XCTestCase { +final class FormDataTests: XCTestCase { func testFormDataEncoder() throws { struct Foo: Encodable { var string: String @@ -159,7 +159,11 @@ class FormDataTests: XCTestCase { """ struct Foo: Decodable { - var link: URL + struct Bar: Decodable { + var relative: String + var base: String? + } + var link: Bar } XCTAssertThrowsError(try FormDataDecoder().decode(Foo.self, from: data, boundary: "hello")) { error in @@ -391,7 +395,7 @@ class FormDataTests: XCTestCase { let success: Bool - init(from _: Decoder) throws { + init(from _: any Decoder) throws { success = false } init?(multipart: MultipartPart) { @@ -507,4 +511,46 @@ class FormDataTests: XCTestCase { """ XCTAssertThrowsError (try FormDataDecoder().decode(TestData.self, from: multipart, boundary: "-")) } + + func testCodingDataTypes() throws { + struct AllTypes: Codable, Equatable { + let string: String + let int: Int, int8: Int8, int16: Int16, int32: Int32, int64: Int64 + let uint: UInt, uint8: UInt8, uint16: UInt16, uint32: UInt32, uint64: UInt64 + let float: Float, double: Double + let bool: Bool + let data: Data, url: URL + } + let value = AllTypes( + string: "string", + int: 1, int8: 2, int16: 3, int32: 4, int64: 5, + uint: 6, uint8: 7, uint16: 8, uint32: 9, uint64: 0, + float: 1.0, double: -1.0, + bool: false, + data: .init([.init(ascii: "A")]), url: .init(string: "https://apple.com/")! + ) + let multipart = """ + ---\r\nContent-Disposition: form-data; name="string"\r\n\r\nstring\r + ---\r\nContent-Disposition: form-data; name="int"\r\n\r\n1\r + ---\r\nContent-Disposition: form-data; name="int8"\r\n\r\n2\r + ---\r\nContent-Disposition: form-data; name="int16"\r\n\r\n3\r + ---\r\nContent-Disposition: form-data; name="int32"\r\n\r\n4\r + ---\r\nContent-Disposition: form-data; name="int64"\r\n\r\n5\r + ---\r\nContent-Disposition: form-data; name="uint"\r\n\r\n6\r + ---\r\nContent-Disposition: form-data; name="uint8"\r\n\r\n7\r + ---\r\nContent-Disposition: form-data; name="uint16"\r\n\r\n8\r + ---\r\nContent-Disposition: form-data; name="uint32"\r\n\r\n9\r + ---\r\nContent-Disposition: form-data; name="uint64"\r\n\r\n0\r + ---\r\nContent-Disposition: form-data; name="float"\r\n\r\n1.0\r + ---\r\nContent-Disposition: form-data; name="double"\r\n\r\n-1.0\r + ---\r\nContent-Disposition: form-data; name="bool"\r\n\r\nfalse\r + ---\r\nContent-Disposition: form-data; name="data"\r\n\r\nA\r + ---\r\nContent-Disposition: form-data; name="url"\r\n\r\nhttps://apple.com/\r + -----\r\n + """ + + XCTAssertEqual(try FormDataEncoder().encode(value, boundary: "-"), multipart) + XCTAssertEqual(try FormDataDecoder().decode(AllTypes.self, from: multipart, boundary: "-"), value) + + } } diff --git a/Tests/MultipartKitTests/MultipartTests.swift b/Tests/MultipartKitTests/MultipartTests.swift index 6101999..af08574 100644 --- a/Tests/MultipartKitTests/MultipartTests.swift +++ b/Tests/MultipartKitTests/MultipartTests.swift @@ -3,7 +3,7 @@ import MultipartKit import NIOCore import NIOHTTP1 -class MultipartTests: XCTestCase { +final class MultipartTests: XCTestCase { let named = """ test123 aijdisadi>SDASD