From d9a056eb517266ec3017b75b44c18a772479d053 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Wed, 6 Nov 2024 12:29:15 +0100 Subject: [PATCH] Make encoding work again --- .../FormDataEncoder.Encoder.swift | 54 ++-- .../FormDataEncoder.KeyedContainer.swift | 84 +++--- ...FormDataEncoder.SingleValueContainer.swift | 28 +- .../FormDataEncoder.UnkeyedContainer.swift | 84 +++--- .../FormDataEncoder/FormDataEncoder.swift | 88 +++--- .../FormDataEncoder/Storage.swift | 56 ++-- Sources/MultipartKit/MultipartFormData.swift | 244 ++++++++-------- Sources/MultipartKit/MultipartParser.swift | 2 +- .../MultipartPartConvertible.swift | 265 ++++++++++-------- .../MultipartKit/MultipartSerializer.swift | 2 +- Tests/MultipartKitTests/FormDataTests.swift | 94 +++---- Tests/MultipartKitTests/ParserTests.swift | 16 +- Tests/MultipartKitTests/SerializerTests.swift | 11 +- 13 files changed, 541 insertions(+), 487 deletions(-) diff --git a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.Encoder.swift b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.Encoder.swift index 6dc34df..7e0a41c 100644 --- a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.Encoder.swift +++ b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.Encoder.swift @@ -1,31 +1,31 @@ -// extension FormDataEncoder { -// struct Encoder { -// let codingPath: [any CodingKey] -// let storage = Storage() -// let userInfo: [CodingUserInfoKey: Any] -// } -// } +extension FormDataEncoder { + struct Encoder { + let codingPath: [any CodingKey] + let storage = Storage() + let userInfo: [CodingUserInfoKey: Any] + } +} -// extension FormDataEncoder.Encoder: Encoder { -// func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { -// let container = FormDataEncoder.KeyedContainer(encoder: self) -// storage.dataContainer = container.dataContainer -// return .init(container) -// } +extension FormDataEncoder.Encoder: Encoder { + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { + let container = FormDataEncoder.KeyedContainer(encoder: self) + storage.dataContainer = container.dataContainer + return .init(container) + } -// func unkeyedContainer() -> any UnkeyedEncodingContainer { -// let container = FormDataEncoder.UnkeyedContainer(encoder: self) -// storage.dataContainer = container.dataContainer -// return container -// } + func unkeyedContainer() -> any UnkeyedEncodingContainer { + let container = FormDataEncoder.UnkeyedContainer(encoder: self) + storage.dataContainer = container.dataContainer + return container + } -// func singleValueContainer() -> any SingleValueEncodingContainer { -// self -// } -// } + func singleValueContainer() -> any SingleValueEncodingContainer { + self + } +} -// extension FormDataEncoder.Encoder { -// func nested(at key: any CodingKey) -> FormDataEncoder.Encoder { -// .init(codingPath: codingPath + [key], userInfo: userInfo) -// } -// } +extension 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 87e145e..e72ec72 100644 --- a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.KeyedContainer.swift +++ b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.KeyedContainer.swift @@ -1,42 +1,42 @@ -// extension FormDataEncoder { -// struct KeyedContainer { -// let dataContainer = KeyedDataContainer() -// let encoder: Encoder -// } -// } - -// extension FormDataEncoder.KeyedContainer: KeyedEncodingContainerProtocol { -// var codingPath: [any CodingKey] { -// encoder.codingPath -// } - -// func encodeNil(forKey _: Key) throws { -// // skip -// } - -// func encode(_ value: T, forKey key: Key) throws { -// try encoderForKey(key).encode(value) -// } - -// func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { -// encoderForKey(key).container(keyedBy: keyType) -// } - -// func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer { -// encoderForKey(key).unkeyedContainer() -// } - -// func superEncoder() -> any Encoder { -// encoderForKey(BasicCodingKey.super) -// } - -// func superEncoder(forKey key: Key) -> any Encoder { -// encoderForKey(key) -// } - -// func encoderForKey(_ key: any CodingKey) -> FormDataEncoder.Encoder { -// let encoder = self.encoder.nested(at: key) -// dataContainer.value[key.stringValue] = encoder.storage -// return encoder -// } -// } +extension FormDataEncoder { + struct KeyedContainer { + let dataContainer = KeyedDataContainer() + let encoder: Encoder + } +} + +extension FormDataEncoder.KeyedContainer: KeyedEncodingContainerProtocol { + var codingPath: [any CodingKey] { + encoder.codingPath + } + + func encodeNil(forKey _: Key) throws { + // skip + } + + func encode(_ value: T, forKey key: Key) throws { + try encoderForKey(key).encode(value) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { + encoderForKey(key).container(keyedBy: keyType) + } + + func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer { + encoderForKey(key).unkeyedContainer() + } + + func superEncoder() -> any Encoder { + encoderForKey(BasicCodingKey.super) + } + + func superEncoder(forKey key: Key) -> any Encoder { + encoderForKey(key) + } + + 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 c03c9cc..3f9b114 100644 --- a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.SingleValueContainer.swift +++ b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.SingleValueContainer.swift @@ -1,15 +1,15 @@ -// extension FormDataEncoder.Encoder: SingleValueEncodingContainer { -// func encodeNil() throws { -// // skip -// } +extension FormDataEncoder.Encoder: SingleValueEncodingContainer { + func encodeNil() throws { + // skip + } -// func encode(_ value: T) throws { -// if let convertible = value as? any MultipartPartConvertible, -// let part = convertible.multipart -// { -// storage.dataContainer = SingleValueDataContainer(part: part) -// } else { -// try value.encode(to: self) -// } -// } -// } + func encode(_ value: T) throws { + if let convertible = value as? any MultipartPartConvertible<[UInt8]>, + let part = convertible.multipart + { + storage.dataContainer = SingleValueDataContainer(part: part) + } else { + try value.encode(to: self) + } + } +} diff --git a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.UnkeyedContainer.swift b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.UnkeyedContainer.swift index 9043d8b..6344a2f 100644 --- a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.UnkeyedContainer.swift +++ b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.UnkeyedContainer.swift @@ -1,42 +1,42 @@ -// extension FormDataEncoder { -// struct UnkeyedContainer { -// let dataContainer = UnkeyedDataContainer() -// let encoder: FormDataEncoder.Encoder -// } -// } - -// extension FormDataEncoder.UnkeyedContainer: UnkeyedEncodingContainer { -// var codingPath: [any CodingKey] { -// encoder.codingPath -// } - -// var count: Int { -// dataContainer.value.count -// } - -// func encodeNil() throws { -// // skip -// } - -// func encode(_ value: T) throws { -// try nextEncoder().encode(value) -// } - -// func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer { -// nextEncoder().container(keyedBy: keyType) -// } - -// func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { -// nextEncoder().unkeyedContainer() -// } - -// func superEncoder() -> any Encoder { -// nextEncoder() -// } - -// func nextEncoder() -> FormDataEncoder.Encoder { -// let encoder = self.encoder.nested(at: BasicCodingKey.index(count)) -// dataContainer.value.append(encoder.storage) -// return encoder -// } -// } +extension FormDataEncoder { + struct UnkeyedContainer { + let dataContainer = UnkeyedDataContainer() + let encoder: FormDataEncoder.Encoder + } +} + +extension FormDataEncoder.UnkeyedContainer: UnkeyedEncodingContainer { + var codingPath: [any CodingKey] { + encoder.codingPath + } + + var count: Int { + dataContainer.value.count + } + + func encodeNil() throws { + // skip + } + + func encode(_ value: T) throws { + try nextEncoder().encode(value) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer { + nextEncoder().container(keyedBy: keyType) + } + + func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { + nextEncoder().unkeyedContainer() + } + + func superEncoder() -> any Encoder { + nextEncoder() + } + + func nextEncoder() -> FormDataEncoder.Encoder { + let encoder = self.encoder.nested(at: BasicCodingKey.index(count)) + dataContainer.value.append(encoder.storage) + return encoder + } +} diff --git a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift index 131c27b..f363d8f 100644 --- a/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift +++ b/Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift @@ -1,50 +1,48 @@ -// import NIOCore +/// Encodes `Encodable` items to `multipart/form-data` encoded `Data`. +/// +/// See [RFC#2388](https://tools.ietf.org/html/rfc2388) for more information about `multipart/form-data` encoding. +/// +/// Seealso `MultipartParser` for more information about the `multipart` encoding. +public struct FormDataEncoder: Sendable { -// /// Encodes `Encodable` items to `multipart/form-data` encoded `Data`. -// /// -// /// See [RFC#2388](https://tools.ietf.org/html/rfc2388) for more information about `multipart/form-data` encoding. -// /// -// /// Seealso `MultipartParser` for more information about the `multipart` encoding. -// public struct FormDataEncoder: Sendable { + /// Any contextual information set by the user for encoding. + public var userInfo: [CodingUserInfoKey: any Sendable] = [:] -// /// Any contextual information set by the user for encoding. -// public var userInfo: [CodingUserInfoKey: any Sendable] = [:] + /// Creates a new `FormDataEncoder`. + public init() {} -// /// Creates a new `FormDataEncoder`. -// public init() {} + /// 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") + /// + /// - 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(_ encodable: E, boundary: String) throws -> String { + try MultipartSerializer(boundary: boundary).serialize(parts: parts(from: encodable)) + } -// /// 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") -// /// -// /// - 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(_ encodable: E, boundary: String) throws -> String { -// try MultipartSerializer().serialize(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. + /// - buffer: Buffer to write to. + /// - throws: Any errors encoding the model with `Codable` or serializing the data. + public func encode(_ encodable: E, boundary: String, into buffer: inout [UInt8]) throws { + try MultipartSerializer(boundary: boundary).serialize(parts: parts(from: encodable), into: &buffer) + } -// /// 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. -// /// - buffer: Buffer to write to. -// /// - throws: Any errors encoding the model with `Codable` or serializing the data. -// public func encode(_ encodable: E, boundary: String, into buffer: inout ByteBuffer) throws { -// try MultipartSerializer().serialize(parts: parts(from: encodable), boundary: boundary, into: &buffer) -// } - -// private func parts(from encodable: E) throws -> [MultipartPart] { -// let encoder = Encoder(codingPath: [], userInfo: userInfo) -// try encodable.encode(to: encoder) -// return encoder.storage.data?.namedParts() ?? [] -// } -// } + private func parts(from encodable: E) throws -> [MultipartPart<[UInt8]>] { + let encoder = Encoder(codingPath: [], userInfo: userInfo) + try encodable.encode(to: encoder) + return encoder.storage.data?.namedParts() ?? [] + } +} diff --git a/Sources/MultipartKit/FormDataEncoder/Storage.swift b/Sources/MultipartKit/FormDataEncoder/Storage.swift index e38f17a..d3d4352 100644 --- a/Sources/MultipartKit/FormDataEncoder/Storage.swift +++ b/Sources/MultipartKit/FormDataEncoder/Storage.swift @@ -1,33 +1,33 @@ -// import Collections +import Collections -// final class Storage { -// var dataContainer: (any DataContainer)? = nil -// var data: MultipartFormData? { -// dataContainer?.data -// } -// } +final class Storage { + var dataContainer: (any DataContainer)? = nil + var data: MultipartFormData? { + dataContainer?.data + } +} -// protocol DataContainer { -// var data: MultipartFormData { get } -// } +protocol DataContainer { + var data: MultipartFormData { get } +} -// struct SingleValueDataContainer: DataContainer { -// init(part: MultipartPart) { -// data = .single(part) -// } -// let data: MultipartFormData -// } +struct SingleValueDataContainer: DataContainer { + init(part: MultipartPart<[UInt8]>) { + data = .single(part) + } + let data: MultipartFormData +} -// final class KeyedDataContainer: DataContainer { -// var value: OrderedDictionary = [:] -// var data: MultipartFormData { -// .keyed(value.compactMapValues(\.data)) -// } -// } +final class KeyedDataContainer: DataContainer { + var value: OrderedDictionary = [:] + var data: MultipartFormData { + .keyed(value.compactMapValues(\.data)) + } +} -// final class UnkeyedDataContainer: DataContainer { -// var value: [Storage] = [] -// var data: MultipartFormData { -// .array(value.compactMap(\.data)) -// } -// } +final class UnkeyedDataContainer: DataContainer { + var value: [Storage] = [] + var data: MultipartFormData { + .array(value.compactMap(\.data)) + } +} diff --git a/Sources/MultipartKit/MultipartFormData.swift b/Sources/MultipartKit/MultipartFormData.swift index 1322e31..f43f7c2 100644 --- a/Sources/MultipartKit/MultipartFormData.swift +++ b/Sources/MultipartKit/MultipartFormData.swift @@ -1,121 +1,123 @@ -// import Collections - -// enum MultipartFormData: Equatable, Sendable { -// typealias Keyed = OrderedDictionary - -// case single(MultipartPart) -// case array([MultipartFormData]) -// case keyed(Keyed) -// case nestingDepthExceeded - -// init(parts: [MultipartPart], nestingDepth: Int) { -// self = parts.reduce(into: .empty) { result, part in -// result.insert( -// part, -// at: part.name.map(makePath) ?? [], -// remainingNestingDepth: nestingDepth -// ) -// } -// } - -// static let empty = MultipartFormData.keyed([:]) - -// var array: [MultipartFormData]? { -// guard case let .array(array) = self else { return nil } -// return array -// } - -// var dictionary: Keyed? { -// guard case let .keyed(dict) = self else { return nil } -// return dict -// } - -// var part: MultipartPart? { -// guard case let .single(part) = self else { return nil } -// return part -// } - -// var hasExceededNestingDepth: Bool { -// guard case .nestingDepthExceeded = self else { -// return false -// } -// return true -// } -// } - -// private func makePath(from string: String) -> ArraySlice { -// ArraySlice(string.replacingOccurrences(of: "]", with: "").split(omittingEmptySubsequences: false) { $0 == "[" }) -// } - -// extension MultipartFormData { -// func namedParts() -> [MultipartPart] { -// Self.namedParts(from: self) -// } - -// private static func namedParts(from data: MultipartFormData, path: String? = nil) -> [MultipartPart] { -// switch data { -// case .array(let array): -// return array.enumerated().flatMap { offset, element in -// namedParts(from: element, path: path.map { "\($0)[\(offset)]" }) -// } -// case .single(var part): -// part.name = path -// return [part] -// case .keyed(let dictionary): -// return dictionary.flatMap { key, value in -// namedParts(from: value, path: path.map { "\($0)[\(key)]" } ?? key) -// } -// case .nestingDepthExceeded: -// return [] -// } -// } -// } - -// extension MultipartFormData { -// fileprivate mutating func insert(_ part: MultipartPart, at path: ArraySlice, remainingNestingDepth: Int) { -// self = inserting(part, at: path, remainingNestingDepth: remainingNestingDepth) -// } - -// fileprivate func inserting(_ part: MultipartPart, at path: ArraySlice, remainingNestingDepth: Int) -> MultipartFormData { -// guard let head = path.first else { -// return .single(part) -// } - -// guard remainingNestingDepth > 1 else { -// return .nestingDepthExceeded -// } - -// func insertPart(into data: inout MultipartFormData) { -// data.insert(part, at: path.dropFirst(), remainingNestingDepth: remainingNestingDepth - 1) -// } - -// func insertingPart(at index: Int?) -> MultipartFormData { -// var array = self.array ?? [] -// let count = array.count -// let index = index ?? count - -// switch index { -// case count: -// array.append(.empty) -// case 0.. + + case single(MultipartPart<[UInt8]>) + case array([MultipartFormData]) + case keyed(Keyed) + case nestingDepthExceeded + + init(parts: [MultipartPart<[UInt8]>], nestingDepth: Int) { + self = parts.reduce(into: .empty) { result, part in + result.insert( + part, + at: part.name.map(makePath) ?? [], + remainingNestingDepth: nestingDepth + ) + } + } + + static let empty = MultipartFormData.keyed([:]) + + var array: [MultipartFormData]? { + guard case let .array(array) = self else { return nil } + return array + } + + var dictionary: Keyed? { + guard case let .keyed(dict) = self else { return nil } + return dict + } + + var part: MultipartPart<[UInt8]>? { + guard case let .single(part) = self else { return nil } + return part + } + + var hasExceededNestingDepth: Bool { + guard case .nestingDepthExceeded = self else { + return false + } + return true + } +} + +private func makePath(from string: String) -> ArraySlice { + ArraySlice(string.replacingOccurrences(of: "]", with: "").split(omittingEmptySubsequences: false) { $0 == "[" }) +} + +extension MultipartFormData { + func namedParts() -> [MultipartPart<[UInt8]>] { + Self.namedParts(from: self) + } + + private static func namedParts(from data: MultipartFormData, path: String? = nil) -> [MultipartPart<[UInt8]>] { + switch data { + case .array(let array): + return array.enumerated().flatMap { offset, element in + namedParts(from: element, path: path.map { "\($0)[\(offset)]" }) + } + case .single(var part): + part.name = path + return [part] + case .keyed(let dictionary): + return dictionary.flatMap { key, value in + namedParts(from: value, path: path.map { "\($0)[\(key)]" } ?? key) + } + case .nestingDepthExceeded: + return [] + } + } +} + +extension MultipartFormData { + fileprivate mutating func insert(_ part: MultipartPart<[UInt8]>, at path: ArraySlice, remainingNestingDepth: Int) { + self = inserting(part, at: path, remainingNestingDepth: remainingNestingDepth) + } + + fileprivate func inserting(_ part: MultipartPart<[UInt8]>, at path: ArraySlice, remainingNestingDepth: Int) + -> MultipartFormData + { + guard let head = path.first else { + return .single(part) + } + + guard remainingNestingDepth > 1 else { + return .nestingDepthExceeded + } + + func insertPart(into data: inout MultipartFormData) { + data.insert(part, at: path.dropFirst(), remainingNestingDepth: remainingNestingDepth - 1) + } + + func insertingPart(at index: Int?) -> MultipartFormData { + var array = self.array ?? [] + let count = array.count + let index = index ?? count + + switch index { + case count: + array.append(.empty) + case 0..) { - self.boundary = .init([45, 45] + boundary) + self.boundary = .init(boundary) self.state = .initial } diff --git a/Sources/MultipartKit/MultipartPartConvertible.swift b/Sources/MultipartKit/MultipartPartConvertible.swift index 81274d1..f58cd82 100644 --- a/Sources/MultipartKit/MultipartPartConvertible.swift +++ b/Sources/MultipartKit/MultipartPartConvertible.swift @@ -1,110 +1,155 @@ -// import struct Foundation.Data -// 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? { -// self -// } - -// public init?(multipart: MultipartPart) { -// self = multipart -// } -// } - -// // MARK: String - -// extension String: MultipartPartConvertible { -// public var multipart: MultipartPart? { -// .init(body: self) -// } - -// public init?(multipart: MultipartPart) { -// self.init(decoding: multipart.body.readableBytesView, as: UTF8.self) -// } -// } - -// // MARK: Numbers - -// extension FixedWidthInteger { -// public var multipart: MultipartPart? { -// .init(body: self.description) -// } - -// public init?(multipart: MultipartPart) { -// self.init(String(multipart: multipart)!) // String.init(multipart:) never returns nil -// } -// } - -// extension Int: MultipartPartConvertible { } -// extension Int8: MultipartPartConvertible { } -// extension Int16: MultipartPartConvertible { } -// extension Int32: MultipartPartConvertible { } -// extension Int64: MultipartPartConvertible { } -// extension UInt: MultipartPartConvertible { } -// extension UInt8: MultipartPartConvertible { } -// extension UInt16: MultipartPartConvertible { } -// extension UInt32: MultipartPartConvertible { } -// extension UInt64: MultipartPartConvertible { } - -// extension Float: MultipartPartConvertible { -// public var multipart: MultipartPart? { -// .init(body: self.description) -// } - -// public init?(multipart: MultipartPart) { -// self.init(String(multipart: multipart)!) // String.init(multipart:) never returns nil -// } -// } - -// extension Double: MultipartPartConvertible { -// public var multipart: MultipartPart? { -// .init(body: self.description) -// } - -// public init?(multipart: MultipartPart) { -// self.init(String(multipart: multipart)!) // String.init(multipart:) never returns nil -// } -// } - -// // MARK: Bool - -// extension Bool: MultipartPartConvertible { -// public var multipart: MultipartPart? { -// .init(body: self.description) -// } - -// public init?(multipart: MultipartPart) { -// self.init(String(multipart: multipart)!) // String.init(multipart:) never returns nil -// } -// } - -// // MARK: Foundation types - -// extension Data: MultipartPartConvertible { -// public var multipart: MultipartPart? { -// .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 -// } -// } +import struct Foundation.Data +import struct Foundation.URL + +/// A protocol to provide custom behaviors for parsing and serializing types from and to multipart data. +public protocol MultipartPartConvertible { + associatedtype Body: MultipartPartBodyElement + + var multipart: MultipartPart? { get } + init?(multipart: MultipartPart) +} + +// MARK: MultipartPart self-conformance + +extension MultipartPart: MultipartPartConvertible { + public var multipart: MultipartPart? { + self + } + + public init?(multipart: MultipartPart) { + self = multipart + } +} + +// MARK: String + +extension String: MultipartPartConvertible { + public typealias Body = [UInt8] + + public var multipart: MultipartPart<[UInt8]>? { + MultipartPart(headerFields: [:], body: Array(self.utf8)) + } + + public init?(multipart: MultipartPart<[UInt8]>) { + guard let string = String(bytes: multipart.body, encoding: .utf8) else { + return nil + } + self = string + } +} + +// MARK: Numbers + +extension FixedWidthInteger { + public typealias Body = [UInt8] + + public var multipart: MultipartPart<[UInt8]>? { + self.description.multipart + } + + public init?(multipart: MultipartPart<[UInt8]>) { + guard let str = String(bytes: multipart.body, encoding: .utf8), + let value = Self(str) + else { + return nil + } + self = value + } +} + +extension Int: MultipartPartConvertible {} +extension Int8: MultipartPartConvertible {} +extension Int16: MultipartPartConvertible {} +extension Int32: MultipartPartConvertible {} +extension Int64: MultipartPartConvertible {} +extension UInt: MultipartPartConvertible {} +extension UInt8: MultipartPartConvertible {} +extension UInt16: MultipartPartConvertible {} +extension UInt32: MultipartPartConvertible {} +extension UInt64: MultipartPartConvertible {} + +// MARK: Floating Point Numbers + +extension Float: MultipartPartConvertible { + public typealias Body = [UInt8] + + public var multipart: MultipartPart<[UInt8]>? { + self.description.multipart + } + + public init?(multipart: MultipartPart<[UInt8]>) { + guard let str = String(bytes: multipart.body, encoding: .utf8), + let value = Float(str) + else { + return nil + } + self = value + } +} + +extension Double: MultipartPartConvertible { + public typealias Body = [UInt8] + + public var multipart: MultipartPart<[UInt8]>? { + self.description.multipart + } + + public init?(multipart: MultipartPart<[UInt8]>) { + guard let str = String(bytes: multipart.body, encoding: .utf8), + let value = Double(str) + else { + return nil + } + self = value + } +} + +// MARK: Bool + +extension Bool: MultipartPartConvertible { + public typealias Body = [UInt8] + + public var multipart: MultipartPart<[UInt8]>? { + self.description.multipart + } + + public init?(multipart: MultipartPart<[UInt8]>) { + guard let str = String(bytes: multipart.body, encoding: .utf8), + let value = Bool(str) + else { + return nil + } + self = value + } +} + +// MARK: Foundation types + +extension Data: MultipartPartConvertible { + public typealias Body = [UInt8] + + public var multipart: MultipartPart<[UInt8]>? { + MultipartPart(headerFields: [:], body: Array(self)) + } + + public init?(multipart: MultipartPart<[UInt8]>) { + self.init(multipart.body) + } +} + +extension URL: MultipartPartConvertible { + public typealias Body = [UInt8] + + public var multipart: MultipartPart<[UInt8]>? { + self.absoluteString.multipart + } + + public init?(multipart: MultipartPart<[UInt8]>) { + guard let str = String(bytes: multipart.body, encoding: .utf8), + let url = URL(string: str) + else { + return nil + } + self = url + } +} diff --git a/Sources/MultipartKit/MultipartSerializer.swift b/Sources/MultipartKit/MultipartSerializer.swift index c918771..6e2a8b9 100644 --- a/Sources/MultipartKit/MultipartSerializer.swift +++ b/Sources/MultipartKit/MultipartSerializer.swift @@ -5,7 +5,7 @@ public struct MultipartSerializer: Sendable { let boundary: String /// Creates a new `MultipartSerializer`. - init(boundary: String) { + public init(boundary: String) { self.boundary = boundary } diff --git a/Tests/MultipartKitTests/FormDataTests.swift b/Tests/MultipartKitTests/FormDataTests.swift index aa2b427..2c73db4 100644 --- a/Tests/MultipartKitTests/FormDataTests.swift +++ b/Tests/MultipartKitTests/FormDataTests.swift @@ -1,49 +1,49 @@ -// import XCTest -// import MultipartKit - -// final class FormDataTests: XCTestCase { -// func testFormDataEncoder() throws { -// struct Foo: Encodable { -// var string: String -// var int: Int -// var double: Double -// var array: [Int] -// var bool: Bool -// } -// let a = Foo(string: "a", int: 42, double: 3.14, array: [1, 2, 3], bool: true) -// let data = try FormDataEncoder().encode(a, boundary: "hello") -// XCTAssertEqual(data, """ -// --hello\r -// Content-Disposition: form-data; name="string"\r -// \r -// a\r -// --hello\r -// Content-Disposition: form-data; name="int"\r -// \r -// 42\r -// --hello\r -// Content-Disposition: form-data; name="double"\r -// \r -// 3.14\r -// --hello\r -// Content-Disposition: form-data; name="array[0]"\r -// \r -// 1\r -// --hello\r -// Content-Disposition: form-data; name="array[1]"\r -// \r -// 2\r -// --hello\r -// Content-Disposition: form-data; name="array[2]"\r -// \r -// 3\r -// --hello\r -// Content-Disposition: form-data; name="bool"\r -// \r -// true\r -// --hello--\r\n -// """) -// } + import XCTest + import MultipartKit + +final class FormDataTests: XCTestCase { + func testFormDataEncoder() throws { + struct Foo: Encodable { + var string: String + var int: Int + var double: Double + var array: [Int] + var bool: Bool + } + let a = Foo(string: "a", int: 42, double: 3.14, array: [1, 2, 3], bool: true) + let data = try FormDataEncoder().encode(a, boundary: "hello") + XCTAssertEqual(data, """ + --hello\r + Content-Disposition: form-data; name="string"\r + \r + a\r + --hello\r + Content-Disposition: form-data; name="int"\r + \r + 42\r + --hello\r + Content-Disposition: form-data; name="double"\r + \r + 3.14\r + --hello\r + Content-Disposition: form-data; name="array[0]"\r + \r + 1\r + --hello\r + Content-Disposition: form-data; name="array[1]"\r + \r + 2\r + --hello\r + Content-Disposition: form-data; name="array[2]"\r + \r + 3\r + --hello\r + Content-Disposition: form-data; name="bool"\r + \r + true\r + --hello--\r\n + """) + } // func testFormDataDecoderW3() throws { // /// Content-Type: multipart/form-data; boundary=12345 @@ -553,4 +553,4 @@ // XCTAssertEqual(try FormDataDecoder().decode(AllTypes.self, from: multipart, boundary: "-"), value) // } -// } +} diff --git a/Tests/MultipartKitTests/ParserTests.swift b/Tests/MultipartKitTests/ParserTests.swift index fcabc31..32530eb 100644 --- a/Tests/MultipartKitTests/ParserTests.swift +++ b/Tests/MultipartKitTests/ParserTests.swift @@ -34,15 +34,15 @@ struct ParserTests { 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, ] - let boundary = "--boundary123" + let boundary = "boundary123" var message = ArraySlice( """ - \(boundary)\r + --\(boundary)\r Content-Disposition: form-data; name="id"\r Content-Type: text/plain\r \r 123e4567-e89b-12d3-a456-426655440000\r - \(boundary)\r + --\(boundary)\r Content-Disposition: form-data; name="address"\r Content-Type: application/json\r \r @@ -50,13 +50,13 @@ struct ParserTests { "street": "3, Garden St",\r "city": "Hillsbery, UT"\r }\r - \(boundary)\r + --\(boundary)\r Content-Disposition: form-data; name="profileImage"; filename="image1.png"\r Content-Type: image/png\r \r\n """.utf8) message.append(contentsOf: pngData) - message.append(contentsOf: "\r\n\(boundary)--".utf8) + message.append(contentsOf: "\r\n--\(boundary)--".utf8) let stream = makeParsingStream(for: message) let sequence = MultipartParserAsyncSequence(boundary: boundary, buffer: stream) @@ -114,7 +114,7 @@ struct ParserTests { """.utf8) let stream = makeParsingStream(for: data) - let sequence = MultipartParserAsyncSequence(boundary: "------WebKitFormBoundaryPVOZifB9OqEwP2fn", buffer: stream) + let sequence = MultipartParserAsyncSequence(boundary: "----WebKitFormBoundaryPVOZifB9OqEwP2fn", buffer: stream) for try await part in sequence { switch part { @@ -131,12 +131,12 @@ struct ParserTests { func parseSynchronously() async throws { let boundary = "boundary123" let message = """ - \(boundary)\r + --\(boundary)\r Content-Disposition: form-data; name="id"\r Content-Type: text/plain\r \r 123e4567-e89b-12d3-a456-426655440000\r - \(boundary)-- + --\(boundary)-- """ let parts = try MultipartParser(boundary: boundary) diff --git a/Tests/MultipartKitTests/SerializerTests.swift b/Tests/MultipartKitTests/SerializerTests.swift index 0a9ecd0..16146c7 100644 --- a/Tests/MultipartKitTests/SerializerTests.swift +++ b/Tests/MultipartKitTests/SerializerTests.swift @@ -16,6 +16,15 @@ struct SerializerTests { ) ] - let serialized = try MultipartSerializer.serialize(parts: example, boundary: "boundary123") + let serialized = try MultipartSerializer(boundary: "boundary123").serialize(parts: example) + let expected = """ + --boundary123\r + Content-Disposition: form-data; name="file"; filename="hello.txt"\r + Content-Type: text/plain\r + \r + Hello, world!\r + --boundary123--\r\n + """ + #expect(serialized == expected) } }