Skip to content

Commit

Permalink
Support nested multipart encoding (#57)
Browse files Browse the repository at this point in the history
* Support nested multipart encoding

* Clean up some comments and access modifiers

* Remove unneeded return

* Fix access modifiers

* Add support for nested decode

* Extend nested coding tests

* Include part of Swift Collections for Swift 5.2

* Remove testing-only file that break 5.2 compat

* Remove more unused files

* Clean up and finalize (?) implementation

- remove unused code
- deprecate MultipartError
- simplify decoding
- augment error messages
- remove attempt at null decoding
- reenable test for URL decoding but change behavior slightly

* Fix MultipartConvertible regression

Also enables decoding single MultipartConvertible values

* Remove dependency on swift-collections

Thanks to @fabianfett for reminding that we should not rely on pre 1.0
dependencies.

* Fix whitespaces and typo

* Remove OrderedCollections as a target

* Add OrderedCollections as an internal dependency

* Comment out unused code

This makes it build but not run yet

* Uncomment enough to pass tests

* Remove commented out code

* Make OrderedCollections code internal

* Update and rename NOTICES.txt

* Remove redundant `//`s

* Add changes summarize to vendored files

* Guard against deeply nested objects

Makes nesting depth configurable through the initializer.

* Fix crash on failing to initialize from part

* Centralize decoding value from MultipartPart

- define decoding single value once on MultipartPart
- avoid force unwrap

* Improve error message
  • Loading branch information
siemensikkema authored Jun 9, 2021
1 parent 2376b49 commit 8c666b7
Show file tree
Hide file tree
Showing 33 changed files with 3,561 additions and 279 deletions.
20 changes: 20 additions & 0 deletions NOTICE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Vapor open source project
//
// Copyright (c) 2017-2021 Vapor project authors
// Licensed under MIT
//
// See LICENSE for license information
//
// SPDX-License-Identifier: MIT
//
//===----------------------------------------------------------------------===//

This product contains a selection from the `OrderedCollection` module from
Swift Collections.

* LICENSE (Apache License 2.0):
* https://swift.org/LICENSE.txt
* HOMEPAGE:
* https://github.com/apple/swift-collections
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ let package = Package(
.library(name: "MultipartKit", targets: ["MultipartKit"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.2.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.2.0")
],
targets: [
.target(name: "MultipartKit", dependencies: [
Expand Down
1 change: 0 additions & 1 deletion Sources/MultipartKit/BasicCodingKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,3 @@ internal enum BasicCodingKey: CodingKey {
self = .index(intValue)
}
}

11 changes: 11 additions & 0 deletions Sources/MultipartKit/Deprecated/MultipartError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@available(*, deprecated)
public enum MultipartError: Error, CustomStringConvertible {
case invalidFormat
case convertibleType(Any.Type)
case convertiblePart(Any.Type, MultipartPart)
case nesting
case missingPart(String)
case missingFilename

public var description: String { "" }
}
249 changes: 125 additions & 124 deletions Sources/MultipartKit/FormDataDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,28 @@
///
/// Seealso `MultipartParser` for more information about the `multipart` encoding.
public struct FormDataDecoder {
/// Maximum nesting depth to allow when decoding the input.
/// - 1 corresponds to a single value
/// - 2 corresponds to an an object with non-nested properties or an 1 dimensional array
/// - 3... corresponds to nested objects or multi-dimensional arrays or combinations thereof
let nestingDepth: Int

/// Creates a new `FormDataDecoder`.
public init() { }
/// - Parameter nestingDepth: maximum allowed nesting depth of the decoded structure. Defaults to 8.
public init(nestingDepth: Int = 8) {
self.nestingDepth = nestingDepth
}

/// Decodes a `Decodable` item from `String` using the supplied boundary.
///
/// let foo = try FormDataDecoder().decode(Foo.self, from: "...", boundary: "123")
///
/// - Parameters:
/// - decodable: Generic `Decodable` type.
/// - data: String to decode.
/// - boundary: Multipart boundary to used in the decoding.
/// - 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: D.Type, from data: String, boundary: String) throws -> D
where D: Decodable
{
Expand All @@ -17,11 +36,12 @@ public struct FormDataDecoder {
///
/// let foo = try FormDataDecoder().decode(Foo.self, from: data, boundary: "123")
///
/// - parameters:
/// - encodable: Generic `Decodable` type.
/// - boundary: Multipart boundary to used in the encoding.
/// - throws: Any errors decoding the model with `Codable` or parsing the data.
/// - returns: An instance of the decoded type `D`.
/// - Parameters:
/// - decodable: Generic `Decodable` type.
/// - data: Data to decode.
/// - boundary: Multipart boundary to used in the decoding.
/// - 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: D.Type, from data: [UInt8], boundary: String) throws -> D
where D: Decodable
{
Expand All @@ -45,199 +65,180 @@ public struct FormDataDecoder {
}

try parser.execute(data)
let multipart = FormDataDecoderContext(parts: parts)
let decoder = _FormDataDecoder(multipart: multipart, codingPath: [])
return try D(from: decoder)
let data = MultipartFormData(parts: parts, nestingDepth: nestingDepth)
return try data.decode(codingPath: [])
}
}

// MARK: Private

private final class FormDataDecoderContext {
var parts: [MultipartPart]
init(parts: [MultipartPart]) {
self.parts = parts
}

func decode<D>(_ decodable: D.Type, at codingPath: [CodingKey]) throws -> D where D: Decodable {
guard let convertible = D.self as? MultipartPartConvertible.Type else {
throw MultipartError.convertibleType(D.self)
}

let part: MultipartPart
switch codingPath.count {
case 1:
let name = codingPath[0].stringValue
guard let p = parts.firstPart(named: name) else {
throw MultipartError.missingPart(name)
}
part = p
case 2:
let name = codingPath[0].stringValue + "[]"
guard let offset = codingPath[1].intValue else {
throw MultipartError.nesting
}
guard let p = parts.allParts(named: name)[safe: offset] else {
throw MultipartError.missingPart("\(codingPath[1].stringValue)")
}
part = p
default:
throw MultipartError.nesting
}

guard let any = convertible.init(multipart: part) else {
throw MultipartError.convertiblePart(D.self, part)
}
return any as! D
}
}


private struct _FormDataDecoder: Decoder {
var codingPath: [CodingKey]
var userInfo: [CodingUserInfoKey: Any] {
return [:]
}
let multipart: FormDataDecoderContext
let codingPath: [CodingKey]
let data: MultipartFormData

init(multipart: FormDataDecoderContext, codingPath: [CodingKey]) {
self.multipart = multipart
self.codingPath = codingPath
}
var userInfo: [CodingUserInfoKey: Any] { [:] }

func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
return KeyedDecodingContainer(_FormDataKeyedDecoder<Key>(multipart: multipart, codingPath: codingPath))
try data.keyedContainer(codingPath: codingPath)
}

func unkeyedContainer() throws -> UnkeyedDecodingContainer {
return try _FormDataUnkeyedDecoder(multipart: multipart, codingPath: codingPath)
try data.unkeyedContainer(codingPath: codingPath)
}

func singleValueContainer() throws -> SingleValueDecodingContainer {
return _FormDataSingleValueDecoder(multipart: multipart, codingPath: codingPath)
try data.singleValueContainer(codingPath: codingPath)
}
}

private struct _FormDataSingleValueDecoder: SingleValueDecodingContainer {
var codingPath: [CodingKey]
let multipart: FormDataDecoderContext

init(multipart: FormDataDecoderContext, codingPath: [CodingKey]) {
self.multipart = multipart
self.codingPath = codingPath
}
let part: MultipartPart

func decodeNil() -> Bool {
return false
false
}

func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
return try multipart.decode(T.self, at: codingPath)
try part.decode(type, at: codingPath)
}
}

private struct _FormDataKeyedDecoder<K>: KeyedDecodingContainerProtocol where K: CodingKey {
var codingPath: [CodingKey]
let codingPath: [CodingKey]
var allKeys: [K] {
return multipart.parts
.compactMap { $0.name }
.compactMap { K(stringValue: $0) }
data.keys.compactMap(K.init(stringValue:))
}

let multipart: FormDataDecoderContext
let data: MultipartFormData.Keyed

init(multipart: FormDataDecoderContext, codingPath: [CodingKey]) {
self.multipart = multipart
self.codingPath = codingPath
func contains(_ key: K) -> Bool {
data.keys.contains(key.stringValue)
}

func contains(_ key: K) -> Bool {
return multipart.parts.contains { $0.name == key.stringValue }
func getValue(forKey key: K) throws -> MultipartFormData {
guard let value = data[key.stringValue] else {
throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, debugDescription: ""))
}
return value
}

func decodeNil(forKey key: K) throws -> Bool {
return false
false
}

func decode<T>(_ type: T.Type, forKey key: K) throws -> T where T : Decodable {
if T.self is MultipartPartConvertible.Type {
return try multipart.decode(T.self, at: codingPath + [key])
} else {
let decoder = _FormDataDecoder(multipart: multipart, codingPath: codingPath + [key])
return try T(from: decoder)
}
try getValue(forKey: key).decode(codingPath: codingPath + [key])
}

func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: K) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
return KeyedDecodingContainer(_FormDataKeyedDecoder<NestedKey>(multipart: multipart, codingPath: codingPath + [key]))
try getValue(forKey: key).keyedContainer(codingPath: codingPath + [key])
}

func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer {
return try _FormDataUnkeyedDecoder(multipart: multipart, codingPath: codingPath + [key])
try getValue(forKey: key).unkeyedContainer(codingPath: codingPath + [key])
}

func superDecoder() throws -> Decoder {
return _FormDataDecoder(multipart: multipart, codingPath: codingPath)
fatalError()
}

func superDecoder(forKey key: K) throws -> Decoder {
return _FormDataDecoder(multipart: multipart, codingPath: codingPath + [key])
fatalError()
}
}

private struct _FormDataUnkeyedDecoder: UnkeyedDecodingContainer {
var index: CodingKey { BasicCodingKey.index(currentIndex) }
var isAtEnd: Bool { currentIndex >= data.count }
var codingPath: [CodingKey]
var count: Int?
var isAtEnd: Bool {
return currentIndex >= count!
var count: Int? { data.count }
var currentIndex: Int = 0
var data: [MultipartFormData]

mutating func decodeNil() throws -> Bool {
false
}
var currentIndex: Int
var index: CodingKey {
return BasicCodingKey.index(self.currentIndex)

mutating func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
defer { currentIndex += 1 }
return try data[currentIndex].decode(codingPath: codingPath + [index])
}

let multipart: FormDataDecoderContext
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
try data[currentIndex].keyedContainer(codingPath: codingPath + [index])
}

init(multipart: FormDataDecoderContext, codingPath: [CodingKey]) throws {
self.multipart = multipart
self.codingPath = codingPath
func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
try data[currentIndex].unkeyedContainer(codingPath: codingPath)
}

let name: String
switch codingPath.count {
case 1: name = codingPath[0].stringValue
default:
throw MultipartError.nesting
}
let parts = multipart.parts.allParts(named: name + "[]")
self.count = parts.count
self.currentIndex = 0
func superDecoder() throws -> Decoder {
fatalError()
}
}

mutating func decodeNil() throws -> Bool {
return false
private extension MultipartFormData {
func keyedContainer<Key: CodingKey>(codingPath: [CodingKey]) throws -> KeyedDecodingContainer<Key> {
guard let dictionary = self.dictionary else {
throw DecodingError.typeMismatch(dataType, .init(codingPath: codingPath, debugDescription: "expected dictionary but encountered \(dataTypeDescription)"))
}
return KeyedDecodingContainer(_FormDataKeyedDecoder(codingPath: codingPath, data: dictionary))
}

mutating func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
defer { currentIndex += 1 }
if T.self is MultipartPartConvertible.Type {
return try multipart.decode(T.self, at: codingPath + [index])
} else {
let decoder = _FormDataDecoder(multipart: multipart, codingPath: codingPath + [index])
return try T(from: decoder)
func unkeyedContainer(codingPath: [CodingKey]) throws -> UnkeyedDecodingContainer {
guard let array = self.array else {
throw DecodingError.typeMismatch(dataType, .init(codingPath: codingPath, debugDescription: "expected array but encountered \(dataTypeDescription)"))
}
return _FormDataUnkeyedDecoder(codingPath: codingPath, data: array)
}

mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
return KeyedDecodingContainer(_FormDataKeyedDecoder<NestedKey>(multipart: multipart, codingPath: codingPath + [index]))
func singleValueContainer(codingPath: [CodingKey]) throws -> SingleValueDecodingContainer {
guard let part = self.part else {
throw DecodingError.typeMismatch(dataType, .init(codingPath: codingPath, debugDescription: "expected single value but encountered \(dataTypeDescription)"))
}
return _FormDataSingleValueDecoder(codingPath: codingPath, part: part)
}

mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
return try _FormDataUnkeyedDecoder(multipart: multipart, codingPath: codingPath + [index])
var dataTypeDescription: String {
switch self {
case .array: return "array"
case .keyed: return "dictionary"
case .single: return "single value"
}
}

mutating func superDecoder() throws -> Decoder {
return _FormDataDecoder(multipart: multipart, codingPath: codingPath + [index])
var dataType: Any.Type {
switch self {
case .array: return [MultipartFormData].self
case .keyed: return Keyed.self
case .single: return MultipartPart.self
}
}

func decode<T>(codingPath: [CodingKey]) throws -> T where T: Decodable {
guard let part = part else {
return try T(from: _FormDataDecoder(codingPath: codingPath, data: self))
}

return try part.decode(T.self, at: codingPath)
}
}

private extension MultipartPart {
func decode<T>(_ type: T.Type, at codingPath: [CodingKey]) throws -> T where T: Decodable {
guard
let Convertible = T.self as? MultipartPartConvertible.Type,
let decoded = Convertible.init(multipart: self) 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."#
)
)
}
return decoded
}
}
Loading

0 comments on commit 8c666b7

Please sign in to comment.