Skip to content

Commit

Permalink
Conform URL to MutipartPartConvertible (#97)
Browse files Browse the repository at this point in the history
* Package and docs updates
* General code cleanup
* Conform URL to MultipartPartConvertible, add test.
* Update CI
  • Loading branch information
gwynne authored May 23, 2024
1 parent 3c9558f commit a31236f
Show file tree
Hide file tree
Showing 22 changed files with 137 additions and 99 deletions.
12 changes: 0 additions & 12 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
version: 2
enable-beta-ecosystems: true
updates:
- package-ecosystem: "github-actions"
directory: "/"
Expand All @@ -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:
- "*"
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ let package = Package(
.product(name: "Collections", package: "swift-collections"),
],
swiftSettings: [
.enableUpcomingFeature("ExistentialAny"),
.enableExperimentalFeature("StrictConcurrency=complete"),
]
),
Expand All @@ -34,6 +35,7 @@ let package = Package(
.target(name: "MultipartKit"),
],
swiftSettings: [
.enableUpcomingFeature("ExistentialAny"),
.enableExperimentalFeature("StrictConcurrency=complete"),
]
),
Expand Down
15 changes: 3 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<a href="https://docs.vapor.codes/4.0/"><img src="https://design.vapor.codes/images/readthedocs.svg" alt="Documentation"></a>
<a href="https://discord.gg/vapor"><img src="https://design.vapor.codes/images/discordchat.svg" alt="Team Chat"></a>
<a href="LICENSE"><img src="https://design.vapor.codes/images/mitlicense.svg" alt="MIT License"></a>
<a href="https://github.com/vapor/multipart-kit/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/vapor/multipart-kit/test.yml?event=push&style=plastic&logo=github&label=test&logoColor=%23ccc" alt="Continuous Integration"></a>
<a href="https://github.com/vapor/multipart-kit/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/vapor/multipart-kit/test.yml?event=push&style=plastic&logo=github&label=tests&logoColor=%23ccc" alt="Continuous Integration"></a>
<a href="https://codecov.io/github/vapor/multipart-kit"><img src="https://img.shields.io/codecov/c/github/vapor/multipart-kit?style=plastic&logo=codecov&label=Codecov&token=yDzzHja8lt"></a>
<a href="https://swift.org"><img src="https://design.vapor.codes/images/swift57up.svg" alt="Swift 5.7+"></a>
</p>
Expand All @@ -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: [
Expand All @@ -38,7 +29,7 @@ dependencies: [
]
```

Add MultiPartKit to your target's dependencies:
Add MultipartKit to your target's dependencies:

```swift
targets: [
Expand Down
4 changes: 3 additions & 1 deletion Sources/MultipartKit/Docs.docc/index.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion Sources/MultipartKit/Docs.docc/theme-settings.json
Original file line number Diff line number Diff line change
@@ -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" },
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ extension FormDataDecoder.KeyedContainer: KeyedDecodingContainerProtocol {
data.keys.compactMap(K.init(stringValue:))
}

var codingPath: [CodingKey] {
var codingPath: [any CodingKey] {
decoder.codingPath
}

func contains(_ key: K) -> Bool {
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(
Expand All @@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extension FormDataDecoder.Decoder: SingleValueDecodingContainer {
func decode<T: Decodable>(_: 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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/MultipartKit/FormDataDecoder/FormDataDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
extension FormDataEncoder {
struct Encoder {
let codingPath: [CodingKey]
let codingPath: [any CodingKey]
let storage = Storage()
let userInfo: [CodingUserInfoKey: Any]
}
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extension FormDataEncoder {
}

extension FormDataEncoder.KeyedContainer: KeyedEncodingContainerProtocol {
var codingPath: [CodingKey] {
var codingPath: [any CodingKey] {
encoder.codingPath
}

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ extension FormDataEncoder.Encoder: SingleValueEncodingContainer {

func encode<T: Encodable>(_ value: T) throws {
if
let convertible = value as? MultipartPartConvertible,
let convertible = value as? any MultipartPartConvertible,
let part = convertible.multipart
{
storage.dataContainer = SingleValueDataContainer(part: part)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extension FormDataEncoder {
}

extension FormDataEncoder.UnkeyedContainer: UnkeyedEncodingContainer {
var codingPath: [CodingKey] {
var codingPath: [any CodingKey] {
encoder.codingPath
}

Expand All @@ -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()
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/MultipartKit/FormDataEncoder/FormDataEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() { }
Expand Down
2 changes: 1 addition & 1 deletion Sources/MultipartKit/FormDataEncoder/Storage.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Collections

final class Storage {
var dataContainer: DataContainer? = nil
var dataContainer: (any DataContainer)? = nil
var data: MultipartFormData? {
dataContainer?.data
}
Expand Down
9 changes: 2 additions & 7 deletions Sources/MultipartKit/MultipartPart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
}
Loading

0 comments on commit a31236f

Please sign in to comment.