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

MultipartKit V5 #100

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
70 changes: 70 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"fileScopedDeclarationPrivacy": {
"accessLevel": "private"
},
"indentation": {
"spaces": 4
},
"indentConditionalCompilationBlocks": true,
"indentSwitchCaseLabels": false,
"lineBreakAroundMultilineExpressionChainComponents": false,
"lineBreakBeforeControlFlowKeywords": false,
"lineBreakBeforeEachArgument": false,
"lineBreakBeforeEachGenericRequirement": false,
"lineLength": 140,
"maximumBlankLines": 1,
"multiElementCollectionTrailingCommas": true,
"noAssignmentInExpressions": {
"allowedFunctions": [
"XCTAssertNoThrow"
]
},
"prioritizeKeepingFunctionOutputTogether": false,
"respectsExistingLineBreaks": true,
"rules": {
"AllPublicDeclarationsHaveDocumentation": false,
"AlwaysUseLiteralForEmptyCollectionInit": false,
"AlwaysUseLowerCamelCase": true,
"AmbiguousTrailingClosureOverload": true,
"BeginDocumentationCommentWithOneLineSummary": false,
"DoNotUseSemicolons": true,
"DontRepeatTypeInStaticProperties": true,
"FileScopedDeclarationPrivacy": true,
"FullyIndirectEnum": true,
"GroupNumericLiterals": true,
"IdentifiersMustBeASCII": true,
"NeverForceUnwrap": false,
"NeverUseForceTry": false,
"NeverUseImplicitlyUnwrappedOptionals": false,
"NoAccessLevelOnExtensionDeclaration": true,
"NoAssignmentInExpressions": true,
"NoBlockComments": true,
"NoCasesWithOnlyFallthrough": true,
"NoEmptyTrailingClosureParentheses": true,
"NoLabelsInCasePatterns": true,
"NoLeadingUnderscores": false,
"NoParensAroundConditions": true,
"NoPlaygroundLiterals": true,
"NoVoidReturnOnFunctionSignature": true,
"OmitExplicitReturns": false,
"OneCasePerLine": true,
"OneVariableDeclarationPerLine": true,
"OnlyOneTrailingClosureArgument": true,
"OrderedImports": true,
"ReplaceForEachWithForLoop": true,
"ReturnVoidInsteadOfEmptyTuple": true,
"TypeNamesShouldBeCapitalized": true,
"UseEarlyExits": false,
"UseExplicitNilCheckInConditions": true,
"UseLetInEveryBoundCaseVariable": true,
"UseShorthandTypeNames": true,
"UseSingleLinePropertyGetter": true,
"UseSynthesizedInitializer": true,
"UseTripleSlashForDocumentationComments": true,
"UseWhereClausesInForLoops": false,
"ValidateDocumentationComments": false
},
"spacesAroundRangeFormationOperators": false,
"tabWidth": 4,
"version": 1
}
19 changes: 9 additions & 10 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
// swift-tools-version:5.7
// swift-tools-version:6.0
import PackageDescription
ptoffy marked this conversation as resolved.
Show resolved Hide resolved

let package = Package(
name: "multipart-kit",
platforms: [
.macOS(.v10_15),
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
.macOS(.v15),
.iOS(.v15),
.tvOS(.v15),
.watchOS(.v8),
],
ptoffy marked this conversation as resolved.
Show resolved Hide resolved
products: [
.library(name: "MultipartKit", targets: ["MultipartKit"]),
.library(name: "MultipartKit", targets: ["MultipartKit"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
ptoffy marked this conversation as resolved.
Show resolved Hide resolved
],
targets: [
.target(
name: "MultipartKit",
dependencies: [
.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(name: "Collections", package: "swift-collections"),
]
),
.testTarget(
name: "MultipartKitTests",
dependencies: [
.target(name: "MultipartKit"),
.target(name: "MultipartKit")
]
),
]
Expand Down
43 changes: 0 additions & 43 deletions Package@swift-5.9.swift

This file was deleted.

11 changes: 0 additions & 11 deletions Sources/MultipartKit/Deprecated/MultipartError.swift

This file was deleted.

13 changes: 0 additions & 13 deletions Sources/MultipartKit/Exports.swift

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
extension FormDataDecoder {
struct Decoder {
struct Decoder<Body: MultipartPartBodyElement>: Sendable {
let codingPath: [any CodingKey]
let data: MultipartFormData
let userInfo: [CodingUserInfoKey: Any]
let data: MultipartFormData<Body>
let sendableUserInfo: [CodingUserInfoKey: any Sendable]
let previousCodingPath: [any CodingKey]?
let previousType: (any Decodable.Type)?

init(codingPath: [any CodingKey], data: MultipartFormData, userInfo: [CodingUserInfoKey: Any], previousCodingPath: [any CodingKey]? = nil, previousType: (any Decodable.Type)? = nil) {
var userInfo: [CodingUserInfoKey: Any] { sendableUserInfo }

init(
codingPath: [any CodingKey],
data: MultipartFormData<Body>,
userInfo: [CodingUserInfoKey: any Sendable] = [:],
previousCodingPath: [any CodingKey]? = nil,
previousType: (any Decodable.Type)? = nil
) {
self.codingPath = codingPath
self.data = data
self.userInfo = userInfo
self.sendableUserInfo = userInfo
self.previousCodingPath = previousCodingPath
self.previousType = previousType
}
Expand Down Expand Up @@ -37,41 +45,42 @@ extension FormDataDecoder.Decoder: Decoder {
}

extension FormDataDecoder.Decoder {
func nested(at key: any CodingKey, with data: MultipartFormData) -> Self {
.init(codingPath: codingPath + [key], data: data, userInfo: userInfo)
func nested(at key: any CodingKey, with data: MultipartFormData<Body>) -> Self {
.init(codingPath: codingPath + [key], data: data, userInfo: sendableUserInfo)
}
}

private extension FormDataDecoder.Decoder {
func decodingError(expectedType: String) -> any Error {
extension FormDataDecoder.Decoder {
fileprivate func decodingError(expectedType: String) -> any Error {
let encounteredType: Any.Type
let encounteredTypeDescription: String

switch data {
case .nestingDepthExceeded:
return DecodingError.dataCorrupted(.init(
codingPath: codingPath,
debugDescription: "Nesting depth exceeded while expecting \(expectedType).",
underlyingError: nil
))
return DecodingError.dataCorrupted(
.init(
codingPath: codingPath,
debugDescription: "Nesting depth exceeded while expecting \(expectedType).",
underlyingError: nil
))
case .array:
encounteredType = [MultipartFormData].self
encounteredType = [MultipartFormData<Body>].self
encounteredTypeDescription = "array"
case .keyed:
encounteredType = MultipartFormData.Keyed.self
encounteredType = MultipartFormData<Body>.Keyed.self
encounteredTypeDescription = "dictionary"
case .single:
encounteredType = MultipartPart.self
encounteredType = MultipartPart<Body>.self
encounteredTypeDescription = "single value"
}

return DecodingError.typeMismatch(
encounteredType,
.init(
codingPath: codingPath,
debugDescription: "Expected \(expectedType) but encountered \(encounteredTypeDescription).",
underlyingError: nil
)
.init(
codingPath: codingPath,
debugDescription: "Expected \(expectedType) but encountered \(encounteredTypeDescription).",
underlyingError: nil
)
)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
extension FormDataDecoder {
struct KeyedContainer<K: CodingKey> {
let data: MultipartFormData.Keyed
let decoder: FormDataDecoder.Decoder
struct KeyedContainer<K: CodingKey, Body: MultipartPartBodyElement>: Sendable {
let data: MultipartFormData<Body>.Keyed
let decoder: FormDataDecoder.Decoder<Body>
}
}

Expand All @@ -18,10 +18,11 @@ extension FormDataDecoder.KeyedContainer: KeyedDecodingContainerProtocol {
data.keys.contains(key.stringValue)
}

func getValue(forKey key: any CodingKey) throws -> MultipartFormData {
func getValue(forKey key: any CodingKey) throws -> MultipartFormData<Body> {
guard let value = data[key.stringValue] else {
throw DecodingError.keyNotFound(
key, .init(
key,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "No value associated with key \"\(key.stringValue)\"."
)
Expand Down Expand Up @@ -54,7 +55,7 @@ extension FormDataDecoder.KeyedContainer: KeyedDecodingContainerProtocol {
try decoderForKey(key)
}

func decoderForKey(_ key: any CodingKey) throws -> FormDataDecoder.Decoder {
func decoderForKey(_ key: any CodingKey) throws -> FormDataDecoder.Decoder<Body> {
decoder.nested(at: key, with: try getValue(forKey: key))
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,59 @@
import Foundation
ptoffy marked this conversation as resolved.
Show resolved Hide resolved

extension FormDataDecoder.Decoder: SingleValueDecodingContainer {
func decodeNil() -> Bool {
false
}

func decode<T: Decodable>(_: T.Type = T.self) throws -> T {
guard
let part = data.part,
let Convertible = T.self as? any MultipartPartConvertible.Type
else {
guard let part = data.part else {
guard previousCodingPath?.count != codingPath.count || previousType != T.self else {
throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Decoding caught in recursion loop"))
}
return try T(from: FormDataDecoder.Decoder(codingPath: codingPath, data: data, userInfo: userInfo, previousCodingPath: codingPath, previousType: T.self))
}

guard !data.hasExceededNestingDepth else {
throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Nesting depth exceeded.", underlyingError: nil))
return try T(
from: FormDataDecoder.Decoder(
codingPath: codingPath, data: data, userInfo: sendableUserInfo, previousCodingPath: codingPath, previousType: T.self
)
)
}

guard
let decoded = Convertible.init(multipart: part) as? T
else {
let path = codingPath.map(\.stringValue).joined(separator: ".")
throw DecodingError.dataCorrupted(
.init(
codingPath: codingPath,
debugDescription: #"Could not convert value at "\#(path)" to type \#(T.self) from multipart part."#
let decoded =
switch T.self {
case is MultipartPart<Body>.Type:
part as? T
case is String.Type:
String(bytes: part.body, encoding: .utf8) as? T
case let IntType as any FixedWidthInteger.Type:
String(bytes: part.body, encoding: .utf8).flatMap(IntType.init) as? T
case is Float.Type:
String(bytes: part.body, encoding: .utf8).flatMap(Float.init) as? T
case is Double.Type:
String(bytes: part.body, encoding: .utf8).flatMap(Double.init) as? T
case is Bool.Type:
String(bytes: part.body, encoding: .utf8).flatMap(Bool.init) as? T
case is Data.Type:
Data(part.body) as? T
case is URL.Type:
String(bytes: part.body, encoding: .utf8).flatMap(URL.init(string:)) as? T
default:
T?.none
}

guard let decoded else {
guard !data.hasExceededNestingDepth else {
throw DecodingError.dataCorrupted(
.init(codingPath: codingPath, debugDescription: "Nesting depth exceeded.", underlyingError: nil)
)
}

return try T(
from: FormDataDecoder.Decoder(
codingPath: codingPath, data: data, userInfo: sendableUserInfo, previousCodingPath: codingPath, previousType: T.self
)
)
}

return decoded
}
}
Loading
Loading