Skip to content

Commit

Permalink
SWIFT-1026 Extended JSON parsing performance improvements (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickfreed authored Jan 14, 2021
1 parent 6ababc1 commit 70c0ac0
Show file tree
Hide file tree
Showing 32 changed files with 487 additions and 224 deletions.
18 changes: 18 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@
"version": "8.1.1"
}
},
{
"package": "swift-extras-base64",
"repositoryURL": "https://github.com/swift-extras/swift-extras-base64",
"state": {
"branch": null,
"revision": "778e00dd7cc2b7970742f061cffc87dd570e6bfa",
"version": "0.5.0"
}
},
{
"package": "swift-extras-json",
"repositoryURL": "https://github.com/swift-extras/swift-extras-json",
"state": {
"branch": null,
"revision": "122b9454ef01bf89a4c190b8fd3717ddd0a2fbd0",
"version": "0.6.0"
}
},
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio",
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio", .upToNextMajor(from: "2.16.0")),
.package(url: "https://github.com/swift-extras/swift-extras-json", .upToNextMinor(from: "0.6.0")),
.package(url: "https://github.com/swift-extras/swift-extras-base64", .upToNextMinor(from: "0.5.0")),
.package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "8.0.0"))
],
targets: [
.target(name: "SwiftBSON", dependencies: ["NIO"]),
.target(name: "SwiftBSON", dependencies: ["NIO", "ExtrasJSON", "ExtrasBase64"]),
.testTarget(name: "SwiftBSONTests", dependencies: ["SwiftBSON", "Nimble"])
]
)
10 changes: 6 additions & 4 deletions Sources/SwiftBSON/Array+BSONValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import NIO

/// An extension of `Array` to represent the BSON array type.
extension Array: BSONValue where Element == BSON {
internal static let extJSONTypeWrapperKeys: [String] = []

/*
* Initializes an `Array` from ExtendedJSON.
*
Expand All @@ -18,22 +20,22 @@ extension Array: BSONValue where Element == BSON {
*/
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
// canonical and relaxed extended JSON
guard case let .array(a) = json else {
guard case let .array(a) = json.value else {
return nil
}
self = try a.enumerated().map { index, element in
try BSON(fromExtJSON: element, keyPath: keyPath + [String(index)])
try BSON(fromExtJSON: JSON(element), keyPath: keyPath + [String(index)])
}
}

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

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

internal static var bsonType: BSONType { .array }
Expand Down
5 changes: 4 additions & 1 deletion Sources/SwiftBSON/BSON.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ExtrasJSON
import Foundation

/// Enum representing a BSON value.
Expand Down Expand Up @@ -77,7 +78,9 @@ public enum BSON {
}
}

/// Initialize a `BSON` from ExtendedJSON
/// Initialize a `BSON` from ExtendedJSON.
/// This is not as performant as decoding via ExtendedJSONDecoder and should only be used scalar values.
///
/// Parameters:
/// - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for any `BSONValue`.
/// - `keyPath`: an array of `Strings`s containing the enclosing JSON keys of the current json being passed in.
Expand Down
34 changes: 19 additions & 15 deletions Sources/SwiftBSON/BSONBinary.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ExtrasBase64
import Foundation
import NIO

Expand Down Expand Up @@ -108,13 +109,14 @@ public struct BSONBinary: Equatable, Hashable {
/// - `BSONError.InvalidArgumentError` if the base64 `String` is invalid or if the provided data is
/// incompatible with the specified subtype.
public init(base64: String, subtype: Subtype) throws {
guard let dataObj = Data(base64Encoded: base64) else {
do {
let bytes = try base64.base64decoded()
try self.init(bytes: bytes, subtype: subtype)
} catch let error as ExtrasBase64.DecodingError {
throw BSONError.InvalidArgumentError(
message:
"failed to create Data object from invalid base64 string \(base64)"
message: "failed to create Data object from invalid base64 string \(base64): \(error)"
)
}
try self.init(data: dataObj, subtype: subtype)
}

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

extension BSONBinary: BSONValue {
internal static let extJSONTypeWrapperKeys: [String] = ["$binary", "$uuid"]

/*
* Initializes a `Binary` from ExtendedJSON.
*
Expand All @@ -158,16 +162,16 @@ extension BSONBinary: BSONValue {
* - `DecodingError` if `json` is a partial match or is malformed.
*/
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
if let uuidJSON = try json.unwrapObject(withKey: "$uuid", keyPath: keyPath) {
if let uuidJSON = try json.value.unwrapObject(withKey: "$uuid", keyPath: keyPath) {
guard let uuidString = uuidJSON.stringValue else {
throw DecodingError._extendedJSONError(
throw Swift.DecodingError._extendedJSONError(
keyPath: keyPath,
debugDescription: "Expected value for key $uuid \"\(uuidJSON)\" to be a string"
+ " but got some other value"
)
}
guard let uuid = UUID(uuidString: uuidString) else {
throw DecodingError._extendedJSONError(
throw Swift.DecodingError._extendedJSONError(
keyPath: keyPath,
debugDescription: "Invalid UUID string: \(uuidString)"
)
Expand All @@ -177,27 +181,27 @@ extension BSONBinary: BSONValue {
self = try BSONBinary(from: uuid)
return
} catch {
throw DecodingError._extendedJSONError(
throw Swift.DecodingError._extendedJSONError(
keyPath: keyPath,
debugDescription: error.localizedDescription
)
}
}

// canonical and relaxed extended JSON
guard let binary = try json.unwrapObject(withKey: "$binary", keyPath: keyPath) else {
guard let binary = try json.value.unwrapObject(withKey: "$binary", keyPath: keyPath) else {
return nil
}
guard
let (base64, subTypeInput) = try binary.unwrapObject(withKeys: "base64", "subType", keyPath: keyPath)
else {
throw DecodingError._extendedJSONError(
throw Swift.DecodingError._extendedJSONError(
keyPath: keyPath,
debugDescription: "Missing \"base64\" or \"subType\" in \(binary)"
)
}
guard let base64Str = base64.stringValue else {
throw DecodingError._extendedJSONError(
throw Swift.DecodingError._extendedJSONError(
keyPath: keyPath,
debugDescription: "Could not parse `base64` from \"\(base64)\", " +
"input must be a base64-encoded (with padding as =) payload as a string"
Expand All @@ -208,7 +212,7 @@ extension BSONBinary: BSONValue {
let subTypeInt = UInt8(subTypeStr, radix: 16),
let subType = Subtype(rawValue: subTypeInt)
else {
throw DecodingError._extendedJSONError(
throw Swift.DecodingError._extendedJSONError(
keyPath: keyPath,
debugDescription: "Could not parse `SubType` from \"\(subTypeInput)\", " +
"input must be a BSON binary type as a one- or two-character hex string"
Expand All @@ -217,7 +221,7 @@ extension BSONBinary: BSONValue {
do {
self = try BSONBinary(base64: base64Str, subtype: subType)
} catch {
throw DecodingError._extendedJSONError(
throw Swift.DecodingError._extendedJSONError(
keyPath: keyPath,
debugDescription: error.localizedDescription
)
Expand All @@ -233,8 +237,8 @@ extension BSONBinary: BSONValue {
internal func toCanonicalExtendedJSON() -> JSON {
[
"$binary": [
"base64": .string(Data(self.data.readableBytesView).base64EncodedString()),
"subType": .string(String(format: "%02x", self.subtype.rawValue))
"base64": JSON(.string(Data(self.data.readableBytesView).base64EncodedString())),
"subType": JSON(.string(String(format: "%02x", self.subtype.rawValue)))
]
]
}
Expand Down
18 changes: 11 additions & 7 deletions Sources/SwiftBSON/BSONCode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public struct BSONCodeWithScope: Equatable, Hashable {
}

extension BSONCode: BSONValue {
internal static let extJSONTypeWrapperKeys: [String] = ["$code"]

/*
* Initializes a `BSONCode` from ExtendedJSON.
*
Expand All @@ -43,7 +45,7 @@ extension BSONCode: BSONValue {
* - `DecodingError` if `json` is a partial match or is malformed.
*/
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
switch json {
switch json.value {
case let .object(obj):
// canonical and relaxed extended JSON
guard let value = obj["$code"] else {
Expand Down Expand Up @@ -78,7 +80,7 @@ extension BSONCode: BSONValue {

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

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

extension BSONCodeWithScope: BSONValue {
internal static let extJSONTypeWrapperKeys: [String] = ["$code", "$scope"]

/*
* Initializes a `BSONCode` from ExtendedJSON.
*
Expand All @@ -113,10 +117,10 @@ extension BSONCodeWithScope: BSONValue {
* - `DecodingError` if `json` is a partial match or is malformed.
*/
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
switch json {
switch json.value {
case .object:
// canonical and relaxed extended JSON
guard let (code, scope) = try json.unwrapObject(withKeys: "$code", "$scope", keyPath: keyPath) else {
guard let (code, scope) = try json.value.unwrapObject(withKeys: "$code", "$scope", keyPath: keyPath) else {
return nil
}
guard let codeStr = code.stringValue else {
Expand All @@ -126,7 +130,7 @@ extension BSONCodeWithScope: BSONValue {
" input must be a string."
)
}
guard let scopeDoc = try BSONDocument(fromExtJSON: scope, keyPath: keyPath + ["$scope"]) else {
guard let scopeDoc = try BSONDocument(fromExtJSON: JSON(scope), keyPath: keyPath + ["$scope"]) else {
throw DecodingError._extendedJSONError(
keyPath: keyPath,
debugDescription: "Could not parse scope from \"\(scope)\", input must be a Document."
Expand All @@ -140,12 +144,12 @@ extension BSONCodeWithScope: BSONValue {

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

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

internal static var bsonType: BSONType { .codeWithScope }
Expand Down
8 changes: 5 additions & 3 deletions Sources/SwiftBSON/BSONDBPointer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public struct BSONDBPointer: Equatable, Hashable {
}

extension BSONDBPointer: BSONValue {
internal static let extJSONTypeWrapperKeys: [String] = ["$dbPointer"]

/*
* Initializes a `BSONDBPointer` from ExtendedJSON.
*
Expand All @@ -32,7 +34,7 @@ extension BSONDBPointer: BSONValue {
*/
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
// canonical and relaxed extended JSON
guard let value = try json.unwrapObject(withKey: "$dbPointer", keyPath: keyPath) else {
guard let value = try json.value.unwrapObject(withKey: "$dbPointer", keyPath: keyPath) else {
return nil
}
guard let dbPointerObj = value.objectValue else {
Expand All @@ -54,7 +56,7 @@ extension BSONDBPointer: BSONValue {
}
guard
let refStr = ref.stringValue,
let oid = try BSONObjectID(fromExtJSON: id, keyPath: keyPath)
let oid = try BSONObjectID(fromExtJSON: JSON(id), keyPath: keyPath)
else {
throw DecodingError._extendedJSONError(
keyPath: keyPath,
Expand All @@ -75,7 +77,7 @@ extension BSONDBPointer: BSONValue {
internal func toCanonicalExtendedJSON() -> JSON {
[
"$dbPointer": [
"$ref": .string(self.ref),
"$ref": JSON(.string(self.ref)),
"$id": self.id.toCanonicalExtendedJSON()
]
]
Expand Down
6 changes: 4 additions & 2 deletions Sources/SwiftBSON/BSONDecimal128.swift
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,8 @@ public struct BSONDecimal128: Equatable, Hashable, CustomStringConvertible {
}

extension BSONDecimal128: BSONValue {
internal static let extJSONTypeWrapperKeys: [String] = ["$numberDecimal"]

/*
* Initializes a `Decimal128` from ExtendedJSON.
*
Expand All @@ -493,7 +495,7 @@ extension BSONDecimal128: BSONValue {
*/
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
// canonical and relaxed extended JSON
guard let value = try json.unwrapObject(withKey: "$numberDecimal", keyPath: keyPath) else {
guard let value = try json.value.unwrapObject(withKey: "$numberDecimal", keyPath: keyPath) else {
return nil
}
guard let str = value.stringValue else {
Expand All @@ -520,7 +522,7 @@ extension BSONDecimal128: BSONValue {

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

internal static var bsonType: BSONType { .decimal128 }
Expand Down
7 changes: 4 additions & 3 deletions Sources/SwiftBSON/BSONDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -380,15 +380,16 @@ extension _BSONDecoder {
case .base64:
let base64Str = try self.unboxCustom(value) { $0.stringValue }

guard let data = Data(base64Encoded: base64Str) else {
do {
return try Data(base64Str.base64decoded())
} catch {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: self.codingPath,
debugDescription: "Malformatted base64 encoded string. Got: \(value)"
debugDescription: "Malformatted base64 encoded string: \(error). Input string: \(value)"
)
)
}
return data
case let .custom(f):
self.storage.push(container: value)
defer { self.storage.popContainer() }
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftBSON/BSONDocument+Collection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ extension BSONDocument: Collection {
fatalError("Failed to advance iterator to position \(pos)")
}
}
guard let (k, v) = try? iter.nextThrowing() else {
guard let (k, v) = iter.next() else {
fatalError("Failed get current key and value at \(position)")
}
return (k, v)
Expand Down
Loading

0 comments on commit 70c0ac0

Please sign in to comment.