From e9a99c2fab68a0361cf384017844d5aaec23a7aa Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Wed, 29 Sep 2021 13:55:02 +0200 Subject: [PATCH] Group Tests into pure Multipart & Form Data (#72) --- NOTICE.txt | 2 +- README.md | 114 +++++++ ...partKitTests.swift => FormDataTests.swift} | 297 +----------------- Tests/MultipartKitTests/MultipartTests.swift | 294 +++++++++++++++++ 4 files changed, 410 insertions(+), 297 deletions(-) rename Tests/MultipartKitTests/{MultipartKitTests.swift => FormDataTests.swift} (61%) create mode 100644 Tests/MultipartKitTests/MultipartTests.swift diff --git a/NOTICE.txt b/NOTICE.txt index 4fc2a02..1a9a459 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -17,4 +17,4 @@ Swift Collections. * LICENSE (Apache License 2.0): * https://swift.org/LICENSE.txt * HOMEPAGE: - * https://github.com/apple/swift-collections \ No newline at end of file + * https://github.com/apple/swift-collections diff --git a/README.md b/README.md index 69f66ab..ed1dda2 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,117 @@ Twitter

+ +🏞 Multipart parser and serializer with `Codable` support for Multipart Form Data. + +### Major Releases + +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.2+|`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. + +```swift +.package(url: "https://github.com/vapor/multipart-kit.git", from: ...) +``` + +### Supported Platforms + +MultipartKit supports the following platforms: + +- Ubuntu 18.04+ +- macOS 10.15+ + +## Overview + +MultipartKit is a multipart parsing and serializing library. 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. + +### Multipart Form Data + +Let's define a `Codable` type and a choose a boundary used to separate the multipart parts. + +```swift +struct User: Codable { + let name: String + let email: String +} +let user = User(name: "Ed", email: "ed@example.com") +let boundary = "abc123" +``` + +We can encode this instance of a our type using a `FormDataEncoder`. + +```swift +let encoded = try FormDataEncoder().encode(foo, boundary: boundary) +``` + +The output looks then looks like this. +``` +--abc123 +Content-Disposition: form-data; name="name" + +Ed +--abc123 +Content-Disposition: form-data; name="email" + +ed@example.com +--abc123-- +``` + +In order to _decode_ this message we feed this output and the same boundary to a `FormDataDecoder` and we get back an identical instance to the one we started with. + +```swift +let decoded = try FormDataDecoder().decode(User.self, from: encoded, boundary: boundary) +``` + +### A note on `null` +As there is no standard defined for how to represent `null` in Multipart (unlike, for instance, JSON), FormDataEncoder and FormDataDecoder do not support encoding or decoding `null` respectively. + +### Nesting and Collections + +Nested structures can be represented by naming the parts such that they describe a path using square brackets to denote contained properties or elements in a collection. The following example shows what that looks like in practice. + +```swift +struct Nested: Encodable { + let tag: String + let flag: Bool + let nested: [Nested] +} +let boundary = "abc123" +let nested = Nested(tag: "a", flag: true, nested: [Nested(tag: "b", flag: false, nested: [])]) +let encoded = try FormDataEncoder().encode(nested, boundary: boundary) +``` + +This results in the content below. + +``` +--abc123 +Content-Disposition: form-data; name="tag" + +a +--abc123 +Content-Disposition: form-data; name="flag" + +true +--abc123 +Content-Disposition: form-data; name="nested[0][tag]" + +b +--abc123 +Content-Disposition: form-data; name="nested[0][flag]" + +false +--abc123-- +``` + +Note that the array elements always include the index (as opposed to just `[]`) in order to support complex nesting. + +### Attribution + +This library contains code from the `OrderedCollection` module from https://github.com/apple/swift-collections. See: NOTICE.txt. \ No newline at end of file diff --git a/Tests/MultipartKitTests/MultipartKitTests.swift b/Tests/MultipartKitTests/FormDataTests.swift similarity index 61% rename from Tests/MultipartKitTests/MultipartKitTests.swift rename to Tests/MultipartKitTests/FormDataTests.swift index 70a600c..d9c144a 100644 --- a/Tests/MultipartKitTests/MultipartKitTests.swift +++ b/Tests/MultipartKitTests/FormDataTests.swift @@ -1,97 +1,7 @@ import XCTest import MultipartKit -class MultipartTests: XCTestCase { - let named = """ - test123 - aijdisadi>SDASDdwekqie4u219034u129e0wque90qjsd90asffs - - - SDASD [SubSequence] { - precondition(maxLength > 0, "groups must be greater than zero") - var start = startIndex - return stride(from: 0, to: count, by: maxLength).map { _ in - let end = index(start, offsetBy: maxLength, limitedBy: endIndex) ?? endIndex - defer { start = end } - return self[start.. MultipartParserOutputReceiver { - try collectOutput(ByteBuffer(string: data), boundary: boundary) - } - - static func collectOutput(_ data: ByteBuffer, boundary: String) throws -> MultipartParserOutputReceiver { - let output = MultipartParserOutputReceiver() - let parser = MultipartParser(boundary: boundary) - output.setUp(with: parser) - try parser.execute(data) - return output - } - - func setUp(with parser: MultipartParser) { - parser.onHeader = { (field, value) in - self.headers.replaceOrAdd(name: field, value: value) - } - parser.onBody = { new in - self.body.writeBuffer(&new) - } - parser.onPartComplete = { - let part = MultipartPart(headers: self.headers, body: self.body) - self.headers = [:] - self.body = ByteBuffer() - self.parts.append(part) - } - } -} diff --git a/Tests/MultipartKitTests/MultipartTests.swift b/Tests/MultipartKitTests/MultipartTests.swift new file mode 100644 index 0000000..8ec18f2 --- /dev/null +++ b/Tests/MultipartKitTests/MultipartTests.swift @@ -0,0 +1,294 @@ +import XCTest +import MultipartKit + +class MultipartTests: XCTestCase { + let named = """ + test123 + aijdisadi>SDASDdwekqie4u219034u129e0wque90qjsd90asffs + + + SDASD [SubSequence] { + precondition(maxLength > 0, "groups must be greater than zero") + var start = startIndex + return stride(from: 0, to: count, by: maxLength).map { _ in + let end = index(start, offsetBy: maxLength, limitedBy: endIndex) ?? endIndex + defer { start = end } + return self[start.. MultipartParserOutputReceiver { + try collectOutput(ByteBuffer(string: data), boundary: boundary) + } + + static func collectOutput(_ data: ByteBuffer, boundary: String) throws -> MultipartParserOutputReceiver { + let output = MultipartParserOutputReceiver() + let parser = MultipartParser(boundary: boundary) + output.setUp(with: parser) + try parser.execute(data) + return output + } + + func setUp(with parser: MultipartParser) { + parser.onHeader = { (field, value) in + self.headers.replaceOrAdd(name: field, value: value) + } + parser.onBody = { new in + self.body.writeBuffer(&new) + } + parser.onPartComplete = { + let part = MultipartPart(headers: self.headers, body: self.body) + self.headers = [:] + self.body = ByteBuffer() + self.parts.append(part) + } + } +}