Skip to content

Commit 56544ae

Browse files
authored
Allow for empty arrays and dictionaries, improve error messages (#692)
* Allow for empty arrays and dictionaries, improve error messages * Add public error * Add URLEncodedFormError.description, update array value in error
1 parent e839f7e commit 56544ae

File tree

3 files changed

+155
-29
lines changed

3 files changed

+155
-29
lines changed

Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift

+17-11
Original file line numberDiff line numberDiff line change
@@ -112,17 +112,25 @@ private class _URLEncodedFormDecoder: Decoder {
112112
}
113113

114114
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
115-
guard case .map(let map) = self.storage.topContainer else {
115+
switch self.storage.topContainer {
116+
case .map(let map):
117+
KeyedDecodingContainer(KDC(container: map, decoder: self))
118+
case .empty:
119+
KeyedDecodingContainer(KDC(container: .init(values: [:]), decoder: self))
120+
default:
116121
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Expected a dictionary"))
117122
}
118-
return KeyedDecodingContainer(KDC(container: map, decoder: self))
119123
}
120124

121125
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
122-
guard case .array(let array) = self.storage.topContainer else {
126+
switch self.storage.topContainer {
127+
case .array(let array):
128+
UKDC(container: array, decoder: self)
129+
case .empty:
130+
UKDC(container: .init(values: []), decoder: self)
131+
default:
123132
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Expected an array"))
124133
}
125-
return UKDC(container: array, decoder: self)
126134
}
127135

128136
func singleValueContainer() throws -> SingleValueDecodingContainer {
@@ -255,9 +263,7 @@ private class _URLEncodedFormDecoder: Decoder {
255263
self.decoder.codingPath.append(key)
256264
defer { self.decoder.codingPath.removeLast() }
257265

258-
guard let node = container.values[key.stringValue] else {
259-
throw DecodingError.keyNotFound(key, .init(codingPath: self.codingPath, debugDescription: ""))
260-
}
266+
let node = container.values[key.stringValue] ?? .empty
261267
return try self.decoder.unbox(node, as: T.self)
262268
}
263269

@@ -494,14 +500,14 @@ extension _URLEncodedFormDecoder: SingleValueDecodingContainer {
494500
extension _URLEncodedFormDecoder {
495501
func unboxNil(_ node: URLEncodedFormNode) throws -> Bool {
496502
guard case .leaf(let value) = node else {
497-
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Expect value not array of dictionary"))
503+
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Expect value not array or dictionary"))
498504
}
499505
return value == nil
500506
}
501507

502508
func unbox(_ node: URLEncodedFormNode, as type: Bool.Type) throws -> Bool {
503509
guard case .leaf(let value) = node else {
504-
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Expect value not array of dictionary"))
510+
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Expect value not array or dictionary"))
505511
}
506512
if let value2 = value {
507513
if let unboxValue = Bool(value2.value) {
@@ -516,7 +522,7 @@ extension _URLEncodedFormDecoder {
516522

517523
func unbox(_ node: URLEncodedFormNode, as type: String.Type) throws -> String {
518524
guard case .leaf(let value) = node else {
519-
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Expect value not array of dictionary"))
525+
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Expect value not array or dictionary"))
520526
}
521527
guard let value2 = value else {
522528
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Expected value not empty string"))
@@ -526,7 +532,7 @@ extension _URLEncodedFormDecoder {
526532

527533
func unbox(_ node: URLEncodedFormNode, as type: URL.Type) throws -> URL {
528534
guard case .leaf(let value) = node else {
529-
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Expect value not array of dictionary"))
535+
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Expect value not array or dictionary"))
530536
}
531537
guard let value2 = value else {
532538
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Expected value not empty string"))

Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift

+79-17
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,64 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
/// Error thrown from parsing URLEncoded forms
16+
public struct URLEncodedFormError: Error, CustomStringConvertible {
17+
public struct Code: Sendable, Equatable {
18+
fileprivate enum Internal: Equatable {
19+
case duplicateKeys
20+
case addingToInvalidType
21+
case failedToPercentDecode
22+
case corruptKeyValue
23+
case notSupported
24+
case invalidArrayIndex
25+
case unexpectedError
26+
}
27+
fileprivate let value: Internal
28+
29+
/// encoded form has duplicate keys in it
30+
public static var duplicateKeys: Self { .init(value: .duplicateKeys) }
31+
/// trying to add an array or dictionary value to something isnt an array of dictionary
32+
public static var addingToInvalidType: Self { .init(value: .addingToInvalidType) }
33+
/// failed to percent decode key or value
34+
public static var failedToPercentDecode: Self { .init(value: .failedToPercentDecode) }
35+
/// corrupt dictionary key in form data
36+
public static var corruptKeyValue: Self { .init(value: .corruptKeyValue) }
37+
/// Form structure not supported eg arrays of arrays
38+
public static var notSupported: Self { .init(value: .notSupported) }
39+
/// Array includes an invalid array index
40+
public static var invalidArrayIndex: Self { .init(value: .invalidArrayIndex) }
41+
/// Unexpected errpr
42+
public static var unexpectedError: Self { .init(value: .unexpectedError) }
43+
}
44+
45+
public let code: Code
46+
public let value: String
47+
48+
init(code: Code, value: String) {
49+
self.code = code
50+
self.value = value
51+
}
52+
53+
init(code: Code, value: Substring) {
54+
self.code = code
55+
self.value = .init(value)
56+
}
57+
}
58+
59+
extension URLEncodedFormError {
60+
public var description: String {
61+
switch self.code.value {
62+
case .duplicateKeys: "Found duplicate keys with name '\(self.value)'"
63+
case .addingToInvalidType: "Adding array or dictionary value to non array or dictionary value '\(self.value)'"
64+
case .failedToPercentDecode: "Failed to percent decode '\(self.value)'"
65+
case .corruptKeyValue: "Parsing dictionary key value failed '\(self.value)'"
66+
case .notSupported: "URLEncoded form structure not supported '\(self.value)'"
67+
case .invalidArrayIndex: "Invalid array index '\(self.value)'"
68+
case .unexpectedError:
69+
"Unexpected error with '\(self.value)' please add an issue at https://github.com/hummingbird-project/hummingbird/issues"
70+
}
71+
}
72+
}
1573
/// Internal representation of URL encoded form data used by both encode and decode
1674
enum URLEncodedFormNode: CustomStringConvertible, Equatable {
1775
/// holds a value
@@ -20,12 +78,8 @@ enum URLEncodedFormNode: CustomStringConvertible, Equatable {
2078
case map(Map)
2179
/// holds an array of nodes
2280
case array(Array)
23-
24-
enum Error: Swift.Error, Equatable {
25-
case failedToDecode(String? = nil)
26-
case notSupported
27-
case invalidArrayIndex(Int)
28-
}
81+
// empty node
82+
case empty
2983

3084
/// Initialize node from URL encoded form data
3185
/// - Parameter string: URL encoded form data
@@ -47,12 +101,14 @@ enum URLEncodedFormNode: CustomStringConvertible, Equatable {
47101
let before = element[..<equals].removingURLPercentEncoding()
48102
let afterEquals = element.index(after: equals)
49103
let after = element[afterEquals...].replacingOccurrences(of: "+", with: " ")
50-
guard let key = before else { throw Error.failedToDecode("Failed to percent decode \(element)") }
104+
guard let key = before else { throw URLEncodedFormError(code: .failedToPercentDecode, value: element[..<equals]) }
51105

52-
guard let keys = KeyParser.parse(key) else { throw Error.failedToDecode("Unexpected key value") }
53-
guard let value = NodeValue(percentEncoded: after) else { throw Error.failedToDecode("Failed to percent decode \(after)") }
106+
guard let keys = KeyParser.parse(key) else { throw URLEncodedFormError(code: .corruptKeyValue, value: key) }
107+
guard let value = NodeValue(percentEncoded: after) else {
108+
throw URLEncodedFormError(code: .failedToPercentDecode, value: after)
109+
}
54110

55-
try node.addValue(keys: keys[...], value: value)
111+
try node.addValue(keys: keys[...], value: value, key: key)
56112
}
57113
}
58114
return node
@@ -62,7 +118,7 @@ enum URLEncodedFormNode: CustomStringConvertible, Equatable {
62118
/// - Parameters:
63119
/// - keys: Array of key parser types (array or map)
64120
/// - value: value to add to leaf node
65-
private func addValue(keys: ArraySlice<KeyParser.KeyType>, value: NodeValue) throws {
121+
private func addValue(keys: ArraySlice<KeyParser.KeyType>, value: NodeValue, key: String) throws {
66122
/// function for create `URLEncodedFormNode` from `KeyParser.Key.Type`
67123
func createNode(from key: KeyParser.KeyType) -> URLEncodedFormNode {
68124
switch key {
@@ -81,31 +137,35 @@ enum URLEncodedFormNode: CustomStringConvertible, Equatable {
81137
case (.map(let map), .map(let key)):
82138
let key = String(key)
83139
if keys.count == 0 {
84-
guard map.values[key] == nil else { throw Error.failedToDecode() }
140+
guard map.values[key] == nil else { throw URLEncodedFormError(code: .duplicateKeys, value: key) }
85141
map.values[key] = .leaf(value)
86142
} else {
87143
if let node = map.values[key] {
88-
try node.addValue(keys: keys, value: value)
144+
try node.addValue(keys: keys, value: value, key: key)
89145
} else {
90146
let node = createNode(from: keys.first!)
91147
map.values[key] = node
92-
try node.addValue(keys: keys, value: value)
148+
try node.addValue(keys: keys, value: value, key: key)
93149
}
94150
}
95151
case (.array(let array), .array):
96152
if keys.count == 0 {
97153
array.values.append(.leaf(value))
98154
} else {
99155
// currently don't support arrays and maps inside arrays
100-
throw Error.notSupported
156+
throw URLEncodedFormError(code: .notSupported, value: key)
101157
}
102158
case (.array(let array), .arrayWithIndices(let index)):
103159
guard keys.count == 0, array.values.count == index else {
104-
throw Error.invalidArrayIndex(index)
160+
throw URLEncodedFormError(code: .invalidArrayIndex, value: "\(key)[\(index)]")
105161
}
106162
array.values.append(.leaf(value))
163+
case (_, .arrayWithIndices), (_, .array):
164+
throw URLEncodedFormError(code: .addingToInvalidType, value: key)
165+
case (_, .map):
166+
throw URLEncodedFormError(code: .addingToInvalidType, value: key)
107167
default:
108-
throw Error.failedToDecode()
168+
throw URLEncodedFormError(code: .unexpectedError, value: key)
109169
}
110170
}
111171

@@ -130,6 +190,8 @@ enum URLEncodedFormNode: CustomStringConvertible, Equatable {
130190
$0.value.encode("\(prefix)[\($0.key)]")
131191
}.joined(separator: "&")
132192
}
193+
case .empty:
194+
return ""
133195
}
134196
}
135197

Tests/HummingbirdTests/URLEncodedForm/URLDecoderTests.swift

+59-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,12 @@ final class URLDecodedFormDecoderTests: XCTestCase {
136136
// incorrect indices
137137
let query = "arr[0]=2&arr[2]=4"
138138
XCTAssertThrowsError(try decoder.decode(Test.self, from: query)) { error in
139-
XCTAssertEqual(error as? URLEncodedFormNode.Error, URLEncodedFormNode.Error.invalidArrayIndex(2))
139+
guard let error = try? XCTUnwrap(error as? URLEncodedFormError) else {
140+
XCTFail()
141+
return
142+
}
143+
XCTAssertEqual(error.code, .invalidArrayIndex)
144+
XCTAssertEqual(error.value, "arr[2]")
140145
}
141146
}
142147

@@ -280,4 +285,57 @@ final class URLDecodedFormDecoderTests: XCTestCase {
280285

281286
self.testForm(test, query: "site=https://hummingbird.codes")
282287
}
288+
289+
func testDecodingEmptyArrayAndMap() throws {
290+
struct ArrayDecoding: Decodable, Equatable {
291+
let array: [Int]
292+
let map: [String: Int]
293+
let a: Int
294+
}
295+
self.testForm(ArrayDecoding(array: [], map: [:], a: 3), query: "a=3")
296+
}
297+
298+
func testParsingErrors() throws {
299+
struct Input1: Decodable {}
300+
XCTAssertThrowsError(try URLEncodedFormDecoder().decode(Input1.self, from: "someField=1&someField=2")) { error in
301+
guard let error = try? XCTUnwrap(error as? URLEncodedFormError) else {
302+
XCTFail()
303+
return
304+
}
305+
XCTAssertEqual(error.code, .duplicateKeys)
306+
XCTAssertEqual(error.value, "someField")
307+
}
308+
XCTAssertThrowsError(try URLEncodedFormDecoder().decode(Input1.self, from: "someField[]=1&someField=2")) { error in
309+
guard let error = try? XCTUnwrap(error as? URLEncodedFormError) else {
310+
XCTFail()
311+
return
312+
}
313+
XCTAssertEqual(error.code, .duplicateKeys)
314+
XCTAssertEqual(error.value, "someField")
315+
}
316+
XCTAssertThrowsError(try URLEncodedFormDecoder().decode(Input1.self, from: "someField=1&someField[]=2")) { error in
317+
guard let error = try? XCTUnwrap(error as? URLEncodedFormError) else {
318+
XCTFail()
319+
return
320+
}
321+
XCTAssertEqual(error.code, .addingToInvalidType)
322+
XCTAssertEqual(error.value, "someField")
323+
}
324+
XCTAssertThrowsError(try URLEncodedFormDecoder().decode(Input1.self, from: "someField=1&someField[test]=2")) { error in
325+
guard let error = try? XCTUnwrap(error as? URLEncodedFormError) else {
326+
XCTFail()
327+
return
328+
}
329+
XCTAssertEqual(error.code, .addingToInvalidType)
330+
XCTAssertEqual(error.value, "someField")
331+
}
332+
XCTAssertThrowsError(try URLEncodedFormDecoder().decode(Input1.self, from: "someField[=2")) { error in
333+
guard let error = try? XCTUnwrap(error as? URLEncodedFormError) else {
334+
XCTFail()
335+
return
336+
}
337+
XCTAssertEqual(error.code, .corruptKeyValue)
338+
XCTAssertEqual(error.value, "someField[")
339+
}
340+
}
283341
}

0 commit comments

Comments
 (0)