Skip to content

Commit 70c0ac0

Browse files
authored
SWIFT-1026 Extended JSON parsing performance improvements (#52)
1 parent 6ababc1 commit 70c0ac0

32 files changed

+487
-224
lines changed

Package.resolved

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ let package = Package(
1111
],
1212
dependencies: [
1313
.package(url: "https://github.com/apple/swift-nio", .upToNextMajor(from: "2.16.0")),
14+
.package(url: "https://github.com/swift-extras/swift-extras-json", .upToNextMinor(from: "0.6.0")),
15+
.package(url: "https://github.com/swift-extras/swift-extras-base64", .upToNextMinor(from: "0.5.0")),
1416
.package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "8.0.0"))
1517
],
1618
targets: [
17-
.target(name: "SwiftBSON", dependencies: ["NIO"]),
19+
.target(name: "SwiftBSON", dependencies: ["NIO", "ExtrasJSON", "ExtrasBase64"]),
1820
.testTarget(name: "SwiftBSONTests", dependencies: ["SwiftBSON", "Nimble"])
1921
]
2022
)

Sources/SwiftBSON/Array+BSONValue.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import NIO
22

33
/// An extension of `Array` to represent the BSON array type.
44
extension Array: BSONValue where Element == BSON {
5+
internal static let extJSONTypeWrapperKeys: [String] = []
6+
57
/*
68
* Initializes an `Array` from ExtendedJSON.
79
*
@@ -18,22 +20,22 @@ extension Array: BSONValue where Element == BSON {
1820
*/
1921
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
2022
// canonical and relaxed extended JSON
21-
guard case let .array(a) = json else {
23+
guard case let .array(a) = json.value else {
2224
return nil
2325
}
2426
self = try a.enumerated().map { index, element in
25-
try BSON(fromExtJSON: element, keyPath: keyPath + [String(index)])
27+
try BSON(fromExtJSON: JSON(element), keyPath: keyPath + [String(index)])
2628
}
2729
}
2830

2931
/// Converts this `BSONArray` to a corresponding `JSON` in relaxed extendedJSON format.
3032
internal func toRelaxedExtendedJSON() -> JSON {
31-
.array(self.map { $0.toRelaxedExtendedJSON() })
33+
JSON(.array(self.map { $0.toRelaxedExtendedJSON().value }))
3234
}
3335

3436
/// Converts this `BSONArray` to a corresponding `JSON` in canonical extendedJSON format.
3537
internal func toCanonicalExtendedJSON() -> JSON {
36-
.array(self.map { $0.toCanonicalExtendedJSON() })
38+
JSON(.array(self.map { $0.toCanonicalExtendedJSON().value }))
3739
}
3840

3941
internal static var bsonType: BSONType { .array }

Sources/SwiftBSON/BSON.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ExtrasJSON
12
import Foundation
23

34
/// Enum representing a BSON value.
@@ -77,7 +78,9 @@ public enum BSON {
7778
}
7879
}
7980

80-
/// Initialize a `BSON` from ExtendedJSON
81+
/// Initialize a `BSON` from ExtendedJSON.
82+
/// This is not as performant as decoding via ExtendedJSONDecoder and should only be used scalar values.
83+
///
8184
/// Parameters:
8285
/// - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for any `BSONValue`.
8386
/// - `keyPath`: an array of `Strings`s containing the enclosing JSON keys of the current json being passed in.

Sources/SwiftBSON/BSONBinary.swift

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ExtrasBase64
12
import Foundation
23
import NIO
34

@@ -108,13 +109,14 @@ public struct BSONBinary: Equatable, Hashable {
108109
/// - `BSONError.InvalidArgumentError` if the base64 `String` is invalid or if the provided data is
109110
/// incompatible with the specified subtype.
110111
public init(base64: String, subtype: Subtype) throws {
111-
guard let dataObj = Data(base64Encoded: base64) else {
112+
do {
113+
let bytes = try base64.base64decoded()
114+
try self.init(bytes: bytes, subtype: subtype)
115+
} catch let error as ExtrasBase64.DecodingError {
112116
throw BSONError.InvalidArgumentError(
113-
message:
114-
"failed to create Data object from invalid base64 string \(base64)"
117+
message: "failed to create Data object from invalid base64 string \(base64): \(error)"
115118
)
116119
}
117-
try self.init(data: dataObj, subtype: subtype)
118120
}
119121

120122
/// Converts this `BSONBinary` instance to a `UUID`.
@@ -143,6 +145,8 @@ public struct BSONBinary: Equatable, Hashable {
143145
}
144146

145147
extension BSONBinary: BSONValue {
148+
internal static let extJSONTypeWrapperKeys: [String] = ["$binary", "$uuid"]
149+
146150
/*
147151
* Initializes a `Binary` from ExtendedJSON.
148152
*
@@ -158,16 +162,16 @@ extension BSONBinary: BSONValue {
158162
* - `DecodingError` if `json` is a partial match or is malformed.
159163
*/
160164
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
161-
if let uuidJSON = try json.unwrapObject(withKey: "$uuid", keyPath: keyPath) {
165+
if let uuidJSON = try json.value.unwrapObject(withKey: "$uuid", keyPath: keyPath) {
162166
guard let uuidString = uuidJSON.stringValue else {
163-
throw DecodingError._extendedJSONError(
167+
throw Swift.DecodingError._extendedJSONError(
164168
keyPath: keyPath,
165169
debugDescription: "Expected value for key $uuid \"\(uuidJSON)\" to be a string"
166170
+ " but got some other value"
167171
)
168172
}
169173
guard let uuid = UUID(uuidString: uuidString) else {
170-
throw DecodingError._extendedJSONError(
174+
throw Swift.DecodingError._extendedJSONError(
171175
keyPath: keyPath,
172176
debugDescription: "Invalid UUID string: \(uuidString)"
173177
)
@@ -177,27 +181,27 @@ extension BSONBinary: BSONValue {
177181
self = try BSONBinary(from: uuid)
178182
return
179183
} catch {
180-
throw DecodingError._extendedJSONError(
184+
throw Swift.DecodingError._extendedJSONError(
181185
keyPath: keyPath,
182186
debugDescription: error.localizedDescription
183187
)
184188
}
185189
}
186190

187191
// canonical and relaxed extended JSON
188-
guard let binary = try json.unwrapObject(withKey: "$binary", keyPath: keyPath) else {
192+
guard let binary = try json.value.unwrapObject(withKey: "$binary", keyPath: keyPath) else {
189193
return nil
190194
}
191195
guard
192196
let (base64, subTypeInput) = try binary.unwrapObject(withKeys: "base64", "subType", keyPath: keyPath)
193197
else {
194-
throw DecodingError._extendedJSONError(
198+
throw Swift.DecodingError._extendedJSONError(
195199
keyPath: keyPath,
196200
debugDescription: "Missing \"base64\" or \"subType\" in \(binary)"
197201
)
198202
}
199203
guard let base64Str = base64.stringValue else {
200-
throw DecodingError._extendedJSONError(
204+
throw Swift.DecodingError._extendedJSONError(
201205
keyPath: keyPath,
202206
debugDescription: "Could not parse `base64` from \"\(base64)\", " +
203207
"input must be a base64-encoded (with padding as =) payload as a string"
@@ -208,7 +212,7 @@ extension BSONBinary: BSONValue {
208212
let subTypeInt = UInt8(subTypeStr, radix: 16),
209213
let subType = Subtype(rawValue: subTypeInt)
210214
else {
211-
throw DecodingError._extendedJSONError(
215+
throw Swift.DecodingError._extendedJSONError(
212216
keyPath: keyPath,
213217
debugDescription: "Could not parse `SubType` from \"\(subTypeInput)\", " +
214218
"input must be a BSON binary type as a one- or two-character hex string"
@@ -217,7 +221,7 @@ extension BSONBinary: BSONValue {
217221
do {
218222
self = try BSONBinary(base64: base64Str, subtype: subType)
219223
} catch {
220-
throw DecodingError._extendedJSONError(
224+
throw Swift.DecodingError._extendedJSONError(
221225
keyPath: keyPath,
222226
debugDescription: error.localizedDescription
223227
)
@@ -233,8 +237,8 @@ extension BSONBinary: BSONValue {
233237
internal func toCanonicalExtendedJSON() -> JSON {
234238
[
235239
"$binary": [
236-
"base64": .string(Data(self.data.readableBytesView).base64EncodedString()),
237-
"subType": .string(String(format: "%02x", self.subtype.rawValue))
240+
"base64": JSON(.string(Data(self.data.readableBytesView).base64EncodedString())),
241+
"subType": JSON(.string(String(format: "%02x", self.subtype.rawValue)))
238242
]
239243
]
240244
}

Sources/SwiftBSON/BSONCode.swift

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public struct BSONCodeWithScope: Equatable, Hashable {
2828
}
2929

3030
extension BSONCode: BSONValue {
31+
internal static let extJSONTypeWrapperKeys: [String] = ["$code"]
32+
3133
/*
3234
* Initializes a `BSONCode` from ExtendedJSON.
3335
*
@@ -43,7 +45,7 @@ extension BSONCode: BSONValue {
4345
* - `DecodingError` if `json` is a partial match or is malformed.
4446
*/
4547
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
46-
switch json {
48+
switch json.value {
4749
case let .object(obj):
4850
// canonical and relaxed extended JSON
4951
guard let value = obj["$code"] else {
@@ -78,7 +80,7 @@ extension BSONCode: BSONValue {
7880

7981
/// Converts this `BSONCode` to a corresponding `JSON` in canonical extendedJSON format.
8082
internal func toCanonicalExtendedJSON() -> JSON {
81-
["$code": .string(self.code)]
83+
["$code": JSON(.string(self.code))]
8284
}
8385

8486
internal static var bsonType: BSONType { .code }
@@ -98,6 +100,8 @@ extension BSONCode: BSONValue {
98100
}
99101

100102
extension BSONCodeWithScope: BSONValue {
103+
internal static let extJSONTypeWrapperKeys: [String] = ["$code", "$scope"]
104+
101105
/*
102106
* Initializes a `BSONCode` from ExtendedJSON.
103107
*
@@ -113,10 +117,10 @@ extension BSONCodeWithScope: BSONValue {
113117
* - `DecodingError` if `json` is a partial match or is malformed.
114118
*/
115119
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
116-
switch json {
120+
switch json.value {
117121
case .object:
118122
// canonical and relaxed extended JSON
119-
guard let (code, scope) = try json.unwrapObject(withKeys: "$code", "$scope", keyPath: keyPath) else {
123+
guard let (code, scope) = try json.value.unwrapObject(withKeys: "$code", "$scope", keyPath: keyPath) else {
120124
return nil
121125
}
122126
guard let codeStr = code.stringValue else {
@@ -126,7 +130,7 @@ extension BSONCodeWithScope: BSONValue {
126130
" input must be a string."
127131
)
128132
}
129-
guard let scopeDoc = try BSONDocument(fromExtJSON: scope, keyPath: keyPath + ["$scope"]) else {
133+
guard let scopeDoc = try BSONDocument(fromExtJSON: JSON(scope), keyPath: keyPath + ["$scope"]) else {
130134
throw DecodingError._extendedJSONError(
131135
keyPath: keyPath,
132136
debugDescription: "Could not parse scope from \"\(scope)\", input must be a Document."
@@ -140,12 +144,12 @@ extension BSONCodeWithScope: BSONValue {
140144

141145
/// Converts this `BSONCodeWithScope` to a corresponding `JSON` in relaxed extendedJSON format.
142146
internal func toRelaxedExtendedJSON() -> JSON {
143-
["$code": .string(self.code), "$scope": self.scope.toRelaxedExtendedJSON()]
147+
["$code": JSON(.string(self.code)), "$scope": self.scope.toRelaxedExtendedJSON()]
144148
}
145149

146150
/// Converts this `BSONCodeWithScope` to a corresponding `JSON` in canonical extendedJSON format.
147151
internal func toCanonicalExtendedJSON() -> JSON {
148-
["$code": .string(self.code), "$scope": self.scope.toCanonicalExtendedJSON()]
152+
["$code": JSON(.string(self.code)), "$scope": self.scope.toCanonicalExtendedJSON()]
149153
}
150154

151155
internal static var bsonType: BSONType { .codeWithScope }

Sources/SwiftBSON/BSONDBPointer.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public struct BSONDBPointer: Equatable, Hashable {
1616
}
1717

1818
extension BSONDBPointer: BSONValue {
19+
internal static let extJSONTypeWrapperKeys: [String] = ["$dbPointer"]
20+
1921
/*
2022
* Initializes a `BSONDBPointer` from ExtendedJSON.
2123
*
@@ -32,7 +34,7 @@ extension BSONDBPointer: BSONValue {
3234
*/
3335
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
3436
// canonical and relaxed extended JSON
35-
guard let value = try json.unwrapObject(withKey: "$dbPointer", keyPath: keyPath) else {
37+
guard let value = try json.value.unwrapObject(withKey: "$dbPointer", keyPath: keyPath) else {
3638
return nil
3739
}
3840
guard let dbPointerObj = value.objectValue else {
@@ -54,7 +56,7 @@ extension BSONDBPointer: BSONValue {
5456
}
5557
guard
5658
let refStr = ref.stringValue,
57-
let oid = try BSONObjectID(fromExtJSON: id, keyPath: keyPath)
59+
let oid = try BSONObjectID(fromExtJSON: JSON(id), keyPath: keyPath)
5860
else {
5961
throw DecodingError._extendedJSONError(
6062
keyPath: keyPath,
@@ -75,7 +77,7 @@ extension BSONDBPointer: BSONValue {
7577
internal func toCanonicalExtendedJSON() -> JSON {
7678
[
7779
"$dbPointer": [
78-
"$ref": .string(self.ref),
80+
"$ref": JSON(.string(self.ref)),
7981
"$id": self.id.toCanonicalExtendedJSON()
8082
]
8183
]

Sources/SwiftBSON/BSONDecimal128.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,8 @@ public struct BSONDecimal128: Equatable, Hashable, CustomStringConvertible {
477477
}
478478

479479
extension BSONDecimal128: BSONValue {
480+
internal static let extJSONTypeWrapperKeys: [String] = ["$numberDecimal"]
481+
480482
/*
481483
* Initializes a `Decimal128` from ExtendedJSON.
482484
*
@@ -493,7 +495,7 @@ extension BSONDecimal128: BSONValue {
493495
*/
494496
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
495497
// canonical and relaxed extended JSON
496-
guard let value = try json.unwrapObject(withKey: "$numberDecimal", keyPath: keyPath) else {
498+
guard let value = try json.value.unwrapObject(withKey: "$numberDecimal", keyPath: keyPath) else {
497499
return nil
498500
}
499501
guard let str = value.stringValue else {
@@ -520,7 +522,7 @@ extension BSONDecimal128: BSONValue {
520522

521523
/// Converts this `Decimal128` to a corresponding `JSON` in canonical extendedJSON format.
522524
internal func toCanonicalExtendedJSON() -> JSON {
523-
["$numberDecimal": .string(self.toString())]
525+
["$numberDecimal": JSON(.string(self.toString()))]
524526
}
525527

526528
internal static var bsonType: BSONType { .decimal128 }

Sources/SwiftBSON/BSONDecoder.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,15 +380,16 @@ extension _BSONDecoder {
380380
case .base64:
381381
let base64Str = try self.unboxCustom(value) { $0.stringValue }
382382

383-
guard let data = Data(base64Encoded: base64Str) else {
383+
do {
384+
return try Data(base64Str.base64decoded())
385+
} catch {
384386
throw DecodingError.dataCorrupted(
385387
DecodingError.Context(
386388
codingPath: self.codingPath,
387-
debugDescription: "Malformatted base64 encoded string. Got: \(value)"
389+
debugDescription: "Malformatted base64 encoded string: \(error). Input string: \(value)"
388390
)
389391
)
390392
}
391-
return data
392393
case let .custom(f):
393394
self.storage.push(container: value)
394395
defer { self.storage.popContainer() }

Sources/SwiftBSON/BSONDocument+Collection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ extension BSONDocument: Collection {
4545
fatalError("Failed to advance iterator to position \(pos)")
4646
}
4747
}
48-
guard let (k, v) = try? iter.nextThrowing() else {
48+
guard let (k, v) = iter.next() else {
4949
fatalError("Failed get current key and value at \(position)")
5050
}
5151
return (k, v)

0 commit comments

Comments
 (0)