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

Add encoding function that returns Data. Fixes #94 #95

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ let package = Package(
dependencies: [
.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
.product(name: "NIOFoundationCompat", package: "swift-nio"),
.product(name: "Collections", package: "swift-collections"),
]
),
.testTarget(
name: "MultipartKitTests",
dependencies: [
.target(name: "MultipartKit"),
], resources: [
.copy("Utilities/image.jpeg"),
]
),
]
Expand Down
3 changes: 3 additions & 0 deletions [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ let package = Package(
dependencies: [
.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
.product(name: "NIOFoundationCompat", package: "swift-nio"),
.product(name: "Collections", package: "swift-collections"),
],
swiftSettings: [
Expand All @@ -32,6 +33,8 @@ let package = Package(
name: "MultipartKitTests",
dependencies: [
.target(name: "MultipartKit"),
], resources: [
.copy("Utilities/image.jpeg"),
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency=complete"),
Expand Down
28 changes: 22 additions & 6 deletions Sources/MultipartKit/FormDataDecoder/FormDataDecoder.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Foundation
import NIOCore
import NIOHTTP1
import NIOFoundationCompat

/// Decodes `Decodable` types from `multipart/form-data` encoded `Data`.
///
Expand Down Expand Up @@ -28,23 +30,37 @@ public struct FormDataDecoder: Sendable {
/// let foo = try FormDataDecoder().decode(Foo.self, from: "...", boundary: "123")
///
/// - Parameters:
/// - decodable: Generic `Decodable` type.
/// - decodable: A `Decodable` item.
/// - data: String to decode.
/// - boundary: Multipart boundary to used in the decoding.
/// - boundary: The multipart boundary to use for decoding. This string must not appear in the decoded data.
/// - Throws: Any errors decoding the model with `Codable` or parsing the data.
/// - Returns: An instance of the decoded type `D`.
public func decode<D: Decodable>(_ decodable: D.Type, from data: String, boundary: String) throws -> D {
try decode(D.self, from: ByteBuffer(string: data), boundary: boundary)
}

/// Decodes a `Decodable` item from `Data` using the supplied boundary.
///
/// let foo = try FormDataDecoder().decode(Foo.self, from: Data(), boundary: "123")
///
/// - Parameters:
/// - decodable: A `Decodable` item.
/// - data: String to decode.
/// - boundary: The multipart boundary to use for decoding. This string must not appear in the decoded data.
/// - Throws: Any errors decoding the model with `Codable` or parsing the data.
/// - Returns: An instance of the decoded type `D`.
public func decode<D: Decodable>(_ decodable: D.Type, from data: Data, boundary: String) throws -> D {
try decode(D.self, from: ByteBuffer(data: data), boundary: boundary)
}

/// Decodes a `Decodable` item from `Data` using the supplied boundary.
///
/// let foo = try FormDataDecoder().decode(Foo.self, from: data, boundary: "123")
///
/// - Parameters:
/// - decodable: Generic `Decodable` type.
/// - decodable: A `Decodable` item.
/// - data: Data to decode.
/// - boundary: Multipart boundary to used in the decoding.
/// - boundary: The multipart boundary to use for decoding. This string must not appear in the decoded data.
/// - Throws: Any errors decoding the model with `Codable` or parsing the data.
/// - Returns: An instance of the decoded type `D`.
public func decode<D: Decodable>(_ decodable: D.Type, from data: [UInt8], boundary: String) throws -> D {
Expand All @@ -56,9 +72,9 @@ public struct FormDataDecoder: Sendable {
/// let foo = try FormDataDecoder().decode(Foo.self, from: data, boundary: "123")
///
/// - Parameters:
/// - decodable: Generic `Decodable` type.
/// - decodable: A `Decodable` item.
/// - data: Data to decode.
/// - boundary: Multipart boundary to used in the decoding.
/// - boundary: The multipart boundary to use for decoding. This string must not appear in the decoded data.
/// - Throws: Any errors decoding the model with `Codable` or parsing the data.
/// - Returns: An instance of the decoded type `D`.
public func decode<D: Decodable>(_ decodable: D.Type, from buffer: ByteBuffer, boundary: String) throws -> D {
Expand Down
39 changes: 27 additions & 12 deletions Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Foundation
import NIOCore

/// Encodes `Encodable` items to `multipart/form-data` encoded `Data`.
Expand All @@ -18,31 +19,45 @@ public struct FormDataEncoder: Sendable {
/// 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.
/// - boundary: Multipart boundary to use for encoding. This must not appear anywhere in the encoded data.
/// - throws: Any errors encoding the model with `Codable` or serializing the data.
/// - returns: `multipart/form-data`-encoded `String`.
public func encode<E: Encodable>(_ encodable: E, boundary: String) throws -> String {
/// - Parameters:
/// - encodable: An `Encodable` item.
/// - boundary: The multipart boundary to use for encoding. This string must not appear in the encoded data.
/// - Throws: Any errors encoding the model with `Codable` or serializing the data.
/// - Returns: A `multipart/form-data`-encoded `String`.
public func encode(_ encodable: some Encodable, boundary: String) throws -> String {
try MultipartSerializer().serialize(parts: parts(from: encodable), boundary: boundary)
}

/// Encodes an `Encodable` item to `Data` using the supplied boundary.
///
/// let a = Foo(string: "a", int: 42, double: 3.14, array: [1, 2, 3])
/// let data = try FormDataEncoder().encodeToData(a, boundary: "123")
///
/// - Parameters:
/// - encodable: An `Encodable` item.
/// - boundary: The multipart boundary to use for encoding. This string must not appear in the encoded data.
/// - Throws: Any errors encoding the model or serializing the data.
/// - Returns: A `multipart/form-data`-encoded `String`.
public func encodeToData(_ encodable: some Encodable, boundary: String) throws -> Data {
try MultipartSerializer().serializeToData(parts: parts(from: encodable), boundary: boundary)
}

/// Encodes an `Encodable` item into a `ByteBuffer` 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)
///
/// - parameters:
/// - encodable: Generic `Encodable` item.
/// - boundary: Multipart boundary to use for encoding. This must not appear anywhere in the encoded data.
/// - Parameters:
/// - encodable: An `Encodable` item.
/// - boundary: The multipart boundary to use for encoding. This string must not appear in the encoded data.
/// - buffer: Buffer to write to.
/// - throws: Any errors encoding the model with `Codable` or serializing the data.
public func encode<E: Encodable>(_ encodable: E, boundary: String, into buffer: inout ByteBuffer) throws {
/// - Throws: Any errors encoding the model with `Codable` or serializing the data.
public func encode(_ encodable: some Encodable, boundary: String, into buffer: inout ByteBuffer) throws {
try MultipartSerializer().serialize(parts: parts(from: encodable), boundary: boundary, into: &buffer)
}

private func parts<E: Encodable>(from encodable: E) throws -> [MultipartPart] {
private func parts(from encodable: some Encodable) throws -> [MultipartPart] {
let encoder = Encoder(codingPath: [], userInfo: userInfo)
try encodable.encode(to: encoder)
return encoder.storage.data?.namedParts() ?? []
Expand Down
36 changes: 27 additions & 9 deletions Sources/MultipartKit/MultipartSerializer.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Foundation
import NIOCore
import NIOFoundationCompat

/// Serializes `MultipartForm`s to `Data`.
///
Expand All @@ -13,15 +15,31 @@ public final class MultipartSerializer: Sendable {
/// let data = try MultipartSerializer().serialize(parts: [part], boundary: "123")
/// print(data) // multipart-encoded
///
/// - parameters:
/// - parts: One or more `MultipartPart`s to serialize into `Data`.
/// - boundary: Multipart boundary to use for encoding. This must not appear anywhere in the encoded data.
/// - throws: Any errors that may occur during serialization.
/// - returns: `multipart`-encoded `Data`.
/// - Parameters:
/// - parts: One or more `MultipartPart`s to serialize into `String`.
/// - boundary: The multipart boundary to use for encoding. This string must not appear in the encoded data.
/// - Throws: Any errors that may occur during serialization.
/// - Returns: A `multipart`-encoded `Data`.
public func serialize(parts: [MultipartPart], boundary: String) throws -> String {
var buffer = ByteBufferAllocator().buffer(capacity: 0)
try self.serialize(parts: parts, boundary: boundary, into: &buffer)
return String(decoding: buffer.readableBytesView, as: UTF8.self)
return String(buffer: buffer)
}

/// Serializes the `MultipartForm` to data.
///
/// let data = try MultipartSerializer().serializeToData(parts: [part], boundary: "123")
/// print(data) // multipart-encoded
///
/// - Parameters:
/// - parts: One or more `MultipartPart`s to serialize into `Data`.
/// - boundary: The multipart boundary to use for encoding. This string must not appear in the encoded data.
/// - Throws: Any errors that may occur during serialization.
/// - Returns: A `multipart`-encoded `Data`.
public func serializeToData(parts: [MultipartPart], boundary: String) throws -> Data {
var buffer = ByteBufferAllocator().buffer(capacity: 0)
try self.serialize(parts: parts, boundary: boundary, into: &buffer)
return Data(buffer: buffer, byteTransferStrategy: .automatic)
}

/// Serializes the `MultipartForm` into a `ByteBuffer`.
Expand All @@ -30,11 +48,11 @@ public final class MultipartSerializer: Sendable {
/// try MultipartSerializer().serialize(parts: [part], boundary: "123", into: &buffer)
/// print(String(buffer: buffer)) // multipart-encoded
///
/// - parameters:
/// - Parameters:
/// - parts: One or more `MultipartPart`s to serialize into `Data`.
/// - boundary: Multipart boundary to use for encoding. This must not appear anywhere in the encoded data.
/// - boundary: The multipart boundary to use for encoding. This string must not appear in the encoded data.
/// - buffer: Buffer to write to.
/// - throws: Any errors that may occur during serialization.
/// - Throws: Any errors that may occur during serialization.
public func serialize(parts: [MultipartPart], boundary: String, into buffer: inout ByteBuffer) throws {
for part in parts {
buffer.writeString("--")
Expand Down
40 changes: 40 additions & 0 deletions Tests/MultipartKitTests/MultipartTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,46 @@ class MultipartTests: XCTestCase {
}
}

func testFormDataCodingToFromStringWithJPEG() throws {
guard let resourceURL = Bundle.module.url(forResource: "image", withExtension: "jpeg") else {
XCTFail("image.jpeg file missing from package resource")
return
}
let originalData = try Data(contentsOf: resourceURL)

struct ObjectToEncode: Codable {
let data: Data
}

let object = ObjectToEncode(data: originalData)
let boundary = UUID().uuidString

let encoded = try FormDataEncoder().encode(object, boundary: boundary)
let decoded = try FormDataDecoder().decode(ObjectToEncode.self, from: encoded, boundary: boundary)

XCTAssertEqual(originalData, decoded.data)
}

func testFormDataCodingToFromDataWithJPEG() throws {
guard let resourceURL = Bundle.module.url(forResource: "image", withExtension: "jpeg") else {
XCTFail("image.jpeg file missing from package resource")
return
}
let originalData = try Data(contentsOf: resourceURL)

struct ObjectToEncode: Codable {
let data: Data
}

let object = ObjectToEncode(data: originalData)
let boundary = UUID().uuidString

let encoded = try FormDataEncoder().encodeToData(object, boundary: boundary)
let decoded = try FormDataDecoder().decode(ObjectToEncode.self, from: encoded, boundary: boundary)

XCTAssertEqual(originalData, decoded.data)
}

func testDocBlocks() throws {
do {
/// Content-Type: multipart/form-data; boundary=123
Expand Down
Binary file added Tests/MultipartKitTests/Utilities/image.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading