Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SWIFT-1026 Extended JSON parsing performance improvements #52

Merged
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": "bf6706e1811e746cb204deaa921d8c7b4d0509e2",
"version": "0.4.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.4.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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does ExtrasBase64 define a conflicting DecodingError type? we might consider opening an issue about that, having a type name that conflicts with Foundation is not ideal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
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