From 41593229177e3f7117b001ed50f7c7af5bc68b45 Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Wed, 9 Dec 2020 15:22:39 -0500 Subject: [PATCH 01/12] json to bson improvements --- Package.resolved | 18 ++ Package.swift | 4 +- Sources/SwiftBSON/Array+BSONValue.swift | 10 +- Sources/SwiftBSON/BSON.swift | 5 +- Sources/SwiftBSON/BSONBinary.swift | 34 ++-- Sources/SwiftBSON/BSONCode.swift | 18 +- Sources/SwiftBSON/BSONDBPointer.swift | 8 +- Sources/SwiftBSON/BSONDecimal128.swift | 6 +- .../SwiftBSON/BSONDocument+Collection.swift | 2 +- Sources/SwiftBSON/BSONDocument.swift | 60 +++++-- Sources/SwiftBSON/BSONEncoder.swift | 2 + Sources/SwiftBSON/BSONNulls.swift | 18 +- Sources/SwiftBSON/BSONObjectID.swift | 6 +- Sources/SwiftBSON/BSONRegularExpression.swift | 8 +- Sources/SwiftBSON/BSONSymbol.swift | 6 +- Sources/SwiftBSON/BSONTimestamp.swift | 7 +- Sources/SwiftBSON/BSONValue.swift | 4 + Sources/SwiftBSON/Bool+BSONValue.swift | 6 +- Sources/SwiftBSON/Date+BSONValue.swift | 8 +- Sources/SwiftBSON/Double+BSONValue.swift | 19 ++- Sources/SwiftBSON/ExtendedJSONDecoder.swift | 159 +++++++++++++++++- Sources/SwiftBSON/ExtendedJSONEncoder.swift | 3 +- Sources/SwiftBSON/Integers+BSONValue.swift | 24 +-- Sources/SwiftBSON/JSON.swift | 103 ++++++------ Sources/SwiftBSON/String+BSONValue.swift | 6 +- Tests/SwiftBSONTests/BSONCorpusTests.swift | 1 - .../BSONDocument+SequenceTests.swift | 14 +- Tests/SwiftBSONTests/BSONTests.swift | 3 +- Tests/SwiftBSONTests/CommonTestUtils.swift | 7 +- .../ExtendedJSONConversionTests.swift | 57 ++++--- Tests/SwiftBSONTests/JSONTests.swift | 33 ++-- 31 files changed, 451 insertions(+), 208 deletions(-) diff --git a/Package.resolved b/Package.resolved index 8256ac8e..854877ff 100644 --- a/Package.resolved +++ b/Package.resolved @@ -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", diff --git a/Package.swift b/Package.swift index 3252edca..88228a2c 100644 --- a/Package.swift +++ b/Package.swift @@ -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"]) ] ) diff --git a/Sources/SwiftBSON/Array+BSONValue.swift b/Sources/SwiftBSON/Array+BSONValue.swift index f9b6a6a2..949d19db 100644 --- a/Sources/SwiftBSON/Array+BSONValue.swift +++ b/Sources/SwiftBSON/Array+BSONValue.swift @@ -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. * @@ -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 } diff --git a/Sources/SwiftBSON/BSON.swift b/Sources/SwiftBSON/BSON.swift index fa3833cd..de725379 100644 --- a/Sources/SwiftBSON/BSON.swift +++ b/Sources/SwiftBSON/BSON.swift @@ -1,3 +1,4 @@ +import ExtrasJSON import Foundation /// Enum representing a BSON value. @@ -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. diff --git a/Sources/SwiftBSON/BSONBinary.swift b/Sources/SwiftBSON/BSONBinary.swift index 879d693b..3c3864f5 100644 --- a/Sources/SwiftBSON/BSONBinary.swift +++ b/Sources/SwiftBSON/BSONBinary.swift @@ -1,3 +1,4 @@ +import ExtrasBase64 import Foundation import NIO @@ -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`. @@ -143,6 +145,8 @@ public struct BSONBinary: Equatable, Hashable { } extension BSONBinary: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = ["$binary", "$uuid"] + /* * Initializes a `Binary` from ExtendedJSON. * @@ -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)" ) @@ -177,7 +181,7 @@ extension BSONBinary: BSONValue { self = try BSONBinary(from: uuid) return } catch { - throw DecodingError._extendedJSONError( + throw Swift.DecodingError._extendedJSONError( keyPath: keyPath, debugDescription: error.localizedDescription ) @@ -185,19 +189,19 @@ extension BSONBinary: BSONValue { } // 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" @@ -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" @@ -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 ) @@ -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))) ] ] } diff --git a/Sources/SwiftBSON/BSONCode.swift b/Sources/SwiftBSON/BSONCode.swift index 440cb2f0..07f8596c 100644 --- a/Sources/SwiftBSON/BSONCode.swift +++ b/Sources/SwiftBSON/BSONCode.swift @@ -28,6 +28,8 @@ public struct BSONCodeWithScope: Equatable, Hashable { } extension BSONCode: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = ["$code"] + /* * Initializes a `BSONCode` from ExtendedJSON. * @@ -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 { @@ -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 } @@ -98,6 +100,8 @@ extension BSONCode: BSONValue { } extension BSONCodeWithScope: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = ["$code", "$scope"] + /* * Initializes a `BSONCode` from ExtendedJSON. * @@ -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 { @@ -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." @@ -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 } diff --git a/Sources/SwiftBSON/BSONDBPointer.swift b/Sources/SwiftBSON/BSONDBPointer.swift index ec268a61..87b1ca01 100644 --- a/Sources/SwiftBSON/BSONDBPointer.swift +++ b/Sources/SwiftBSON/BSONDBPointer.swift @@ -16,6 +16,8 @@ public struct BSONDBPointer: Equatable, Hashable { } extension BSONDBPointer: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = ["$dbPointer"] + /* * Initializes a `BSONDBPointer` from ExtendedJSON. * @@ -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 { @@ -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, @@ -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() ] ] diff --git a/Sources/SwiftBSON/BSONDecimal128.swift b/Sources/SwiftBSON/BSONDecimal128.swift index f4200cd8..82d94904 100644 --- a/Sources/SwiftBSON/BSONDecimal128.swift +++ b/Sources/SwiftBSON/BSONDecimal128.swift @@ -477,6 +477,8 @@ public struct BSONDecimal128: Equatable, Hashable, CustomStringConvertible { } extension BSONDecimal128: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = ["$numberDecimal"] + /* * Initializes a `Decimal128` from ExtendedJSON. * @@ -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 { @@ -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 } diff --git a/Sources/SwiftBSON/BSONDocument+Collection.swift b/Sources/SwiftBSON/BSONDocument+Collection.swift index 2cdbce4d..8d40d8d0 100644 --- a/Sources/SwiftBSON/BSONDocument+Collection.swift +++ b/Sources/SwiftBSON/BSONDocument+Collection.swift @@ -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) diff --git a/Sources/SwiftBSON/BSONDocument.swift b/Sources/SwiftBSON/BSONDocument.swift index 42fe2f49..de0beabc 100644 --- a/Sources/SwiftBSON/BSONDocument.swift +++ b/Sources/SwiftBSON/BSONDocument.swift @@ -1,3 +1,4 @@ +import ExtrasJSON import Foundation import NIO @@ -92,7 +93,7 @@ public struct BSONDocument { self = BSONDocument(fromUnsafeBSON: storage, keys: keys) } - private init(fromUnsafeBSON storage: BSONDocumentStorage, keys: Set) { + internal init(fromUnsafeBSON storage: BSONDocumentStorage, keys: Set) { self.storage = storage self.keySet = keys } @@ -329,7 +330,7 @@ public struct BSONDocument { /// Storage management for BSONDocuments. /// A wrapper around a ByteBuffer providing various BSONDocument-specific utilities. - private struct BSONDocumentStorage { + internal struct BSONDocumentStorage { internal var buffer: ByteBuffer /// Create BSONDocumentStorage from ByteBuffer. @@ -356,12 +357,41 @@ public struct BSONDocument { /// Appends element to underlying BSON bytes, returns the size of the element appended: type + key + value @discardableResult internal mutating func append(key: String, value: BSON) -> Int { let writer = self.buffer.writerIndex - self.buffer.writeInteger(value.bsonValue.bsonType.rawValue, as: UInt8.self) - self.buffer.writeCString(key) + self.appendElementHeader(key: key, bsonType: value.bsonValue.bsonType) value.bsonValue.write(to: &self.buffer) return self.buffer.writerIndex - writer } + /// Append the header (key and BSONType) for a given element. + @discardableResult internal mutating func appendElementHeader(key: String, bsonType: BSONType) -> Int { + let writer = self.buffer.writerIndex + self.buffer.writeInteger(bsonType.rawValue, as: UInt8.self) + self.buffer.writeCString(key) + return self.buffer.writerIndex - writer + } + + /// Build a document at the current position in the storage via the provided closure which returns + /// how many bytes it wrote. + /// + /// This may be used to build up a fresh document or a subdocument. + internal mutating func buildDocument(_ appendFunc: (inout Self) throws -> Int) throws -> Int { + var totalBytes = 0 + + // write length of document + let lengthIndex = self.buffer.writerIndex + totalBytes += self.buffer.writeInteger(0, endianness: .little, as: Int32.self) + + // write contents + totalBytes += try appendFunc(&self) + + // write null byte + totalBytes += self.buffer.writeInteger(0, as: UInt8.self) + + self.buffer.setInteger(Int32(totalBytes), at: lengthIndex, endianness: .little, as: Int32.self) + + return totalBytes + } + @discardableResult internal func validateAndRetrieveKeys() throws -> Set { // Pull apart the underlying binary into [KeyValuePair], should reveal issues @@ -447,8 +477,11 @@ extension BSONDocument: Equatable { } extension BSONDocument: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = [] + /* * Initializes a `BSONDocument` from ExtendedJSON. + * This is not as performant as ExtendedJSONDecoder.decode, so it should only be used for small documents. * * Parameters: * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for any `BSONDocument`. @@ -463,12 +496,12 @@ extension BSONDocument: BSONValue { */ internal init?(fromExtJSON json: JSON, keyPath: [String]) throws { // canonical and relaxed extended JSON - guard case let .object(obj) = json else { + guard case let .object(obj) = json.value else { return nil } var doc: [(String, BSON)] = [] for (key, val) in obj { - let bsonValue = try BSON(fromExtJSON: val, keyPath: keyPath + [key]) + let bsonValue = try BSON(fromExtJSON: JSON(val), keyPath: keyPath + [key]) doc.append((key, bsonValue)) } self = BSONDocument(keyValuePairs: doc) @@ -476,20 +509,20 @@ extension BSONDocument: BSONValue { /// Converts this `BSONDocument` to a corresponding `JSON` in relaxed extendedJSON format. internal func toRelaxedExtendedJSON() -> JSON { - var obj: [String: JSON] = [:] + var obj: [String: JSONValue] = [:] for (key, value) in self { - obj[key] = value.toRelaxedExtendedJSON() + obj[key] = value.toRelaxedExtendedJSON().value } - return .object(obj) + return JSON(.object(obj)) } /// Converts this `BSONDocument` to a corresponding `JSON` in canonical extendedJSON format. internal func toCanonicalExtendedJSON() -> JSON { - var obj: [String: JSON] = [:] + var obj: [String: JSONValue] = [:] for (key, value) in self { - obj[key] = value.toCanonicalExtendedJSON() + obj[key] = value.toCanonicalExtendedJSON().value } - return .object(obj) + return JSON(.object(obj)) } internal static var bsonType: BSONType { .document } @@ -509,8 +542,7 @@ extension BSONDocument: BSONValue { } internal func write(to buffer: inout ByteBuffer) { - var doc = ByteBuffer(self.storage.buffer.readableBytesView) - buffer.writeBuffer(&doc) + buffer.writeBytes(self.storage.buffer.readableBytesView) } } diff --git a/Sources/SwiftBSON/BSONEncoder.swift b/Sources/SwiftBSON/BSONEncoder.swift index e14bedb9..a66df4b8 100644 --- a/Sources/SwiftBSON/BSONEncoder.swift +++ b/Sources/SwiftBSON/BSONEncoder.swift @@ -753,6 +753,7 @@ extension _BSONEncoder: SingleValueEncodingContainer { /// it allows us to preserve Swift type information. private class MutableArray: BSONValue { fileprivate static var bsonType: BSONType { .array } + internal static let extJSONTypeWrapperKeys: [String] = [] fileprivate var bson: BSON { fatalError("MutableArray: BSONValue.bson should be unused") } @@ -817,6 +818,7 @@ private class MutableArray: BSONValue { /// for encoder storage purposes. We use this rather than NSMutableDictionary /// because it allows us to preserve Swift type information. private class MutableDictionary: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = [] fileprivate static var bsonType: BSONType { .document } fileprivate var bson: BSON { fatalError("MutableDictionary: BSONValue.bson should be unused") } diff --git a/Sources/SwiftBSON/BSONNulls.swift b/Sources/SwiftBSON/BSONNulls.swift index c63b6450..bb6dbe73 100644 --- a/Sources/SwiftBSON/BSONNulls.swift +++ b/Sources/SwiftBSON/BSONNulls.swift @@ -2,6 +2,8 @@ import NIO /// A struct to represent the BSON null type. internal struct BSONNull: BSONValue, Equatable { + internal static let extJSONTypeWrapperKeys: [String] = [] + /* * Initializes a `BSONNull` from ExtendedJSON. * @@ -15,7 +17,7 @@ internal struct BSONNull: BSONValue, Equatable { * */ internal init?(fromExtJSON json: JSON, keyPath _: [String]) { - switch json { + switch json.value { case .null: // canonical or relaxed extended JSON self = BSONNull() @@ -31,7 +33,7 @@ internal struct BSONNull: BSONValue, Equatable { /// Converts this `BSONNull` to a corresponding `JSON` in canonical extendedJSON format. internal func toCanonicalExtendedJSON() -> JSON { - .null + JSON(.null) } internal static var bsonType: BSONType { .null } @@ -52,6 +54,8 @@ internal struct BSONNull: BSONValue, Equatable { /// A struct to represent the BSON undefined type. internal struct BSONUndefined: BSONValue, Equatable { + internal static let extJSONTypeWrapperKeys: [String] = ["$undefined"] + /* * Initializes a `BSONUndefined` from ExtendedJSON. * @@ -68,7 +72,7 @@ internal struct BSONUndefined: BSONValue, Equatable { */ internal init?(fromExtJSON json: JSON, keyPath: [String]) throws { // canonical and relaxed extended JSON - guard let value = try json.unwrapObject(withKey: "$undefined", keyPath: keyPath) else { + guard let value = try json.value.unwrapObject(withKey: "$undefined", keyPath: keyPath) else { return nil } guard value.boolValue == true else { @@ -108,6 +112,8 @@ internal struct BSONUndefined: BSONValue, Equatable { /// A struct to represent the BSON MinKey type. internal struct BSONMinKey: BSONValue, Equatable { + internal static let extJSONTypeWrapperKeys: [String] = ["$minKey"] + /* * Initializes a `BSONMinKey` from ExtendedJSON. * @@ -124,7 +130,7 @@ internal struct BSONMinKey: BSONValue, Equatable { */ internal init?(fromExtJSON json: JSON, keyPath: [String]) throws { // canonical and relaxed extended JSON - guard let value = try json.unwrapObject(withKey: "$minKey", keyPath: keyPath) else { + guard let value = try json.value.unwrapObject(withKey: "$minKey", keyPath: keyPath) else { return nil } guard value.doubleValue == 1 else { @@ -164,6 +170,8 @@ internal struct BSONMinKey: BSONValue, Equatable { /// A struct to represent the BSON MinKey type. internal struct BSONMaxKey: BSONValue, Equatable { + internal static let extJSONTypeWrapperKeys: [String] = ["$maxKey"] + /* * Initializes a `BSONMaxKey` from ExtendedJSON. * @@ -180,7 +188,7 @@ internal struct BSONMaxKey: BSONValue, Equatable { */ internal init?(fromExtJSON json: JSON, keyPath: [String]) throws { // canonical and relaxed extended JSON - guard let value = try json.unwrapObject(withKey: "$maxKey", keyPath: keyPath) else { + guard let value = try json.value.unwrapObject(withKey: "$maxKey", keyPath: keyPath) else { return nil } guard value.doubleValue == 1 else { diff --git a/Sources/SwiftBSON/BSONObjectID.swift b/Sources/SwiftBSON/BSONObjectID.swift index df172176..d431cc0c 100644 --- a/Sources/SwiftBSON/BSONObjectID.swift +++ b/Sources/SwiftBSON/BSONObjectID.swift @@ -6,6 +6,8 @@ import NIOConcurrencyHelpers public struct BSONObjectID: Equatable, Hashable, CustomStringConvertible { internal static let LENGTH = 12 + internal static let extJSONTypeWrapperKeys: [String] = ["$oid"] + /// This `BSONObjectID`'s data represented as a `String`. public var hex: String { self.oid.reduce("") { $0 + String(format: "%02x", $1) } } @@ -80,7 +82,7 @@ extension BSONObjectID: BSONValue { */ internal init?(fromExtJSON json: JSON, keyPath: [String]) throws { // canonical and relaxed extended JSON - guard let value = try json.unwrapObject(withKey: "$oid", keyPath: keyPath) else { + guard let value = try json.value.unwrapObject(withKey: "$oid", keyPath: keyPath) else { return nil } guard let str = value.stringValue else { @@ -107,7 +109,7 @@ extension BSONObjectID: BSONValue { /// Converts this `BSONObjectID` to a corresponding `JSON` in canonical extendedJSON format. internal func toCanonicalExtendedJSON() -> JSON { - ["$oid": .string(self.hex)] + ["$oid": JSON(.string(self.hex))] } internal static var bsonType: BSONType { .objectID } diff --git a/Sources/SwiftBSON/BSONRegularExpression.swift b/Sources/SwiftBSON/BSONRegularExpression.swift index 49f2a561..72916942 100644 --- a/Sources/SwiftBSON/BSONRegularExpression.swift +++ b/Sources/SwiftBSON/BSONRegularExpression.swift @@ -64,6 +64,8 @@ public struct BSONRegularExpression: Equatable, Hashable { } extension BSONRegularExpression: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = ["$regularExpression"] + /* * Initializes a `BSONRegularExpression` from ExtendedJSON. * @@ -80,7 +82,7 @@ extension BSONRegularExpression: BSONValue { */ internal init?(fromExtJSON json: JSON, keyPath: [String]) throws { // canonical and relaxed extended JSON - guard let value = try json.unwrapObject(withKey: "$regularExpression", keyPath: keyPath) else { + guard let value = try json.value.unwrapObject(withKey: "$regularExpression", keyPath: keyPath) else { return nil } guard @@ -106,8 +108,8 @@ extension BSONRegularExpression: BSONValue { internal func toCanonicalExtendedJSON() -> JSON { [ "$regularExpression": [ - "pattern": .string(self.pattern), - "options": .string(self.options) + "pattern": JSON(.string(self.pattern)), + "options": JSON(.string(self.options)) ] ] } diff --git a/Sources/SwiftBSON/BSONSymbol.swift b/Sources/SwiftBSON/BSONSymbol.swift index dffb9b70..896949c6 100644 --- a/Sources/SwiftBSON/BSONSymbol.swift +++ b/Sources/SwiftBSON/BSONSymbol.swift @@ -3,6 +3,8 @@ import NIO /// A struct to represent the deprecated Symbol type. /// Symbols cannot be instantiated, but they can be read from existing documents that contain them. public struct BSONSymbol: BSONValue, CustomStringConvertible, Equatable, Hashable { + internal static let extJSONTypeWrapperKeys: [String] = ["$symbol"] + /* * Initializes a `Symbol` from ExtendedJSON. * @@ -18,7 +20,7 @@ public struct BSONSymbol: BSONValue, CustomStringConvertible, Equatable, Hashabl * - `nil` if the provided value is not an `Symbol`. */ internal init?(fromExtJSON json: JSON, keyPath: [String]) throws { - guard let value = try json.unwrapObject(withKey: "$symbol", keyPath: keyPath) else { + guard let value = try json.value.unwrapObject(withKey: "$symbol", keyPath: keyPath) else { return nil } guard let str = value.stringValue else { @@ -38,7 +40,7 @@ public struct BSONSymbol: BSONValue, CustomStringConvertible, Equatable, Hashabl /// Converts this `Symbol` to a corresponding `JSON` in canonical extendedJSON format. internal func toCanonicalExtendedJSON() -> JSON { - ["$symbol": .string(self.stringValue)] + ["$symbol": JSON(.string(self.stringValue))] } internal static var bsonType: BSONType { .symbol } diff --git a/Sources/SwiftBSON/BSONTimestamp.swift b/Sources/SwiftBSON/BSONTimestamp.swift index 766326cf..ae96a917 100644 --- a/Sources/SwiftBSON/BSONTimestamp.swift +++ b/Sources/SwiftBSON/BSONTimestamp.swift @@ -4,6 +4,7 @@ import NIO /// application development, you should use the BSON date type (represented in this library by `Date`.) /// - SeeAlso: https://docs.mongodb.com/manual/reference/bson-types/#timestamps public struct BSONTimestamp: BSONValue, Equatable, Hashable { + internal static let extJSONTypeWrapperKeys: [String] = ["$timestamp"] internal static var bsonType: BSONType { .timestamp } internal var bson: BSON { .timestamp(self) } @@ -40,7 +41,7 @@ public struct BSONTimestamp: BSONValue, Equatable, Hashable { */ internal init?(fromExtJSON json: JSON, keyPath: [String]) throws { // canonical and relaxed extended JSON - guard let value = try json.unwrapObject(withKey: "$timestamp", keyPath: keyPath) else { + guard let value = try json.value.unwrapObject(withKey: "$timestamp", keyPath: keyPath) else { return nil } guard let timestampObj = value.objectValue else { @@ -84,8 +85,8 @@ public struct BSONTimestamp: BSONValue, Equatable, Hashable { internal func toCanonicalExtendedJSON() -> JSON { [ "$timestamp": [ - "t": .number(Double(self.timestamp)), - "i": .number(Double(self.increment)) + "t": JSON(.number(String(self.timestamp))), + "i": JSON(.number(String(self.increment))) ] ] } diff --git a/Sources/SwiftBSON/BSONValue.swift b/Sources/SwiftBSON/BSONValue.swift index 12761773..3963e695 100644 --- a/Sources/SwiftBSON/BSONValue.swift +++ b/Sources/SwiftBSON/BSONValue.swift @@ -7,6 +7,10 @@ internal protocol BSONValue: Codable { /// A `BSON` corresponding to this `BSONValue`. var bson: BSON { get } + /// The `$`-prefixed keys that indicate an object is an extended JSON object wrapper + /// for this `BSONValue`. (e.g. for Int32, this value is ["$numberInt"]). + static var extJSONTypeWrapperKeys: [String] { get } + /// Initializes a corresponding `BSON` from the provided `ByteBuffer`, /// moving the buffer's readerIndex forward to the byte beyond the end /// of this value. diff --git a/Sources/SwiftBSON/Bool+BSONValue.swift b/Sources/SwiftBSON/Bool+BSONValue.swift index e3b278fd..e293ec8a 100644 --- a/Sources/SwiftBSON/Bool+BSONValue.swift +++ b/Sources/SwiftBSON/Bool+BSONValue.swift @@ -1,6 +1,8 @@ import NIO extension Bool: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = [] + /* * Initializes a `Bool` from ExtendedJSON. * @@ -13,7 +15,7 @@ extension Bool: BSONValue { * - `nil` if the provided value is not a `Bool`. */ internal init?(fromExtJSON json: JSON, keyPath _: [String]) { - switch json { + switch json.value { case let .bool(b): // canonical or relaxed extended JSON self = b @@ -29,7 +31,7 @@ extension Bool: BSONValue { /// Converts this `Bool` to a corresponding `JSON` in canonical extendedJSON format. internal func toCanonicalExtendedJSON() -> JSON { - .bool(self) + JSON(.bool(self)) } internal static var bsonType: BSONType { .bool } diff --git a/Sources/SwiftBSON/Date+BSONValue.swift b/Sources/SwiftBSON/Date+BSONValue.swift index 010225d7..78abf6bd 100644 --- a/Sources/SwiftBSON/Date+BSONValue.swift +++ b/Sources/SwiftBSON/Date+BSONValue.swift @@ -2,6 +2,8 @@ import Foundation import NIO extension Date: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = ["$date"] + /* * Initializes a `Date` from ExtendedJSON. * @@ -17,13 +19,13 @@ extension Date: BSONValue { * - `DecodingError` if `json` is a partial match or is malformed. */ internal init?(fromExtJSON json: JSON, keyPath: [String]) throws { - guard let value = try json.unwrapObject(withKey: "$date", keyPath: keyPath) else { + guard let value = try json.value.unwrapObject(withKey: "$date", keyPath: keyPath) else { return nil } switch value { case .object: // canonical extended JSON - guard let int = try Int64(fromExtJSON: value, keyPath: keyPath) else { + guard let int = try Int64(fromExtJSON: JSON(value), keyPath: keyPath) else { throw DecodingError._extendedJSONError( keyPath: keyPath, debugDescription: "Expected \(value) to be canonical extended JSON representing a " + @@ -69,7 +71,7 @@ extension Date: BSONValue { ? ExtendedJSONDecoder.extJSONDateFormatterSeconds : ExtendedJSONDecoder.extJSONDateFormatterMilliseconds let date = formatter.string(from: self) - return ["$date": .string(date)] + return ["$date": JSON(.string(date))] } else { return self.toCanonicalExtendedJSON() } diff --git a/Sources/SwiftBSON/Double+BSONValue.swift b/Sources/SwiftBSON/Double+BSONValue.swift index 8a4f599e..03fce383 100644 --- a/Sources/SwiftBSON/Double+BSONValue.swift +++ b/Sources/SwiftBSON/Double+BSONValue.swift @@ -1,6 +1,8 @@ import NIO extension Double: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = ["$numberDouble"] + /* * Initializes a `Double` from ExtendedJSON. * @@ -16,13 +18,20 @@ extension Double: 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 .number(n): // relaxed extended JSON - self = n + guard let num = Double(n) else { + throw DecodingError._extendedJSONError( + keyPath: keyPath, + debugDescription: "Could not parse `Double` from \"\(n)\", " + + "input must be a 64-bit signed floating point as a decimal string" + ) + } + self = num case .object: // canonical extended JSON - guard let value = try json.unwrapObject(withKey: "$numberDouble", keyPath: keyPath) else { + guard let value = try json.value.unwrapObject(withKey: "$numberDouble", keyPath: keyPath) else { return nil } guard @@ -59,13 +68,13 @@ extension Double: BSONValue { if self.isInfinite || self.isNaN { return self.toCanonicalExtendedJSON() } else { - return .number(self) + return JSON(.number(String(self))) } } /// Converts this `Double` to a corresponding `JSON` in canonical extendedJSON format. internal func toCanonicalExtendedJSON() -> JSON { - ["$numberDouble": .string(self.formatForExtendedJSON())] + ["$numberDouble": JSON(.string(self.formatForExtendedJSON()))] } internal static var bsonType: BSONType { .double } diff --git a/Sources/SwiftBSON/ExtendedJSONDecoder.swift b/Sources/SwiftBSON/ExtendedJSONDecoder.swift index afb7a29e..89849268 100644 --- a/Sources/SwiftBSON/ExtendedJSONDecoder.swift +++ b/Sources/SwiftBSON/ExtendedJSONDecoder.swift @@ -1,4 +1,6 @@ +import ExtrasJSON import Foundation + /// `ExtendedJSONDecoder` facilitates the decoding of ExtendedJSON into `Decodable` values. public class ExtendedJSONDecoder { internal static var extJSONDateFormatterSeconds: ISO8601DateFormatter = { @@ -13,6 +15,32 @@ public class ExtendedJSONDecoder { return formatter }() + /// A set of all the possible extenededJSON wrapper keys. + private static var wrapperKeySet: Set = { + Set(ExtendedJSONDecoder.wrapperKeyMap.keys) + }() + + /// A map from extended JSON wrapper keys (e.g. "$numberLong") to the BSON type(s) that they correspond to. + /// + /// Some types are associated with multiple wrapper keys (e.g. "$code" and "$scope" both map to + /// `BSONCodeWithScope`). Some wrapper keys are associated with multiple types (e.g. "$code" maps to both + /// `BSONCode` and `BSONCodeWithScope`). Attempt to decode each of the types returned from the map until one works + /// to find the proper decoding. + private static var wrapperKeyMap: [String: [BSONValue.Type]] = { + var map: [String: [BSONValue.Type]] = [:] + for t in BSON.allBSONTypes.values { + for k in t.extJSONTypeWrapperKeys { + if var existingList = map[k] { + existingList.append(t.self) + map[k] = existingList + } else { + map[k] = [t] + } + } + } + return map + }() + /// Contextual user-provided information for use during decoding. public var userInfo: [CodingUserInfoKey: Any] = [:] @@ -28,21 +56,138 @@ public class ExtendedJSONDecoder { /// - Returns: Decoded representation of the JSON input as an instance of `T`. /// - Throws: `DecodingError` if the JSON data is corrupt or if any value throws an error during decoding. public func decode(_: T.Type, from data: Data) throws -> T { - // Data --> JSON --> BSON --> T + // Data --> JSONValue --> BSON --> T // Takes in JSON as `Data` encoded with `.utf8` and runs it through a `JSONDecoder` to get an // instance of the `JSON` enum. - - // In earlier versions of Swift, JSONDecoder doesn't support decoding "fragments" at the top level, so we wrap - // the data in an array to guarantee it always decodes properly. - let wrappedData = "[".utf8 + data + "]".utf8 - let json = try JSONDecoder().decode([JSON].self, from: wrappedData)[0] + let json = try JSONParser().parse(bytes: data) // Then a `BSON` enum instance is created via the `JSON`. - let bson = try BSON(fromExtJSON: json, keyPath: []) + // let bson = try json.toBSON(keyPath: []) + let bson = try self.decodeBSONFromJSON(json, keyPath: []) + // let bson = BSON(fromExtJSON: json) // The `BSON` is then passed through a `BSONDecoder` where it is outputted as a `T` let bsonDecoder = BSONDecoder() bsonDecoder.userInfo = self.userInfo return try bsonDecoder.decode(T.self, fromBSON: bson) } + + /// Decode a `BSON` from the given extended JSON. + private func decodeBSONFromJSON(_ json: JSONValue, keyPath: [String]) throws -> BSON { + switch try self.decodeScalar(json, keyPath: keyPath) { + case let .scalar(s): + return s + case let .encodedArray(arr): + let bsonArr = try arr.enumerated().map { i, jsonValue in + try self.decodeBSONFromJSON(jsonValue, keyPath: keyPath + ["\(i)"]) + } + return .array(bsonArr) + case let .encodedObject(obj): + var storage = BSONDocument.BSONDocumentStorage() + _ = try self.appendObject(obj, to: &storage) + return .document(BSONDocument(fromUnsafeBSON: storage, keys: Set(obj.keys))) + } + } + + /// Decode and append the given extended JSON object to the provided BSONDocumentStorage. + private func appendObject( + _ object: [String: JSONValue], + to storage: inout BSONDocument.BSONDocumentStorage + ) throws -> Int { + try storage.buildDocument { storage in + var bytes = 0 + for (k, v) in object { + bytes += try self.appendElement(v, to: &storage, forKey: k) + } + return bytes + } + } + + /// Decode the given extended JSON value to BSON and append it to the provided storage. + private func appendElement( + _ value: JSONValue, + to storage: inout BSONDocument.BSONDocumentStorage, + forKey key: String + ) throws -> Int { + switch try self.decodeScalar(value, keyPath: []) { + case let .scalar(s): + return storage.append(key: key, value: s) + case let .encodedArray(arr): + var bytes = 0 + bytes += storage.appendElementHeader(key: key, bsonType: .array) + bytes += try storage.buildDocument { storage in + var bytes = 0 + for (i, v) in arr.enumerated() { + bytes += try self.appendElement(v, to: &storage, forKey: String(i)) + } + return bytes + } + return bytes + case let .encodedObject(obj): + var bytes = 0 + bytes += storage.appendElementHeader(key: key, bsonType: .document) + bytes += try self.appendObject(obj, to: &storage) + return bytes + } + } + + /// Attempt to decode a scalar value from either a JSON scalar or an extended JSON encoded scalar. + /// If the value is a regular document or an array, simply return it as-is for recursive processing. + internal func decodeScalar(_ json: JSONValue, keyPath: [String]) throws -> DecodeScalarResult { + switch json { + case let .string(s): + return .scalar(.string(s)) + case let .bool(b): + return .scalar(.bool(b)) + case let .number(numString): + if let int32 = Int32(numString) { + return .scalar(.int32(int32)) + } else if let int64 = Int64(numString) { + return .scalar(.int64(int64)) + } else if let double = Double(numString) { + return .scalar(.double(double)) + } else { + throw DecodingError._extendedJSONError( + keyPath: keyPath, + debugDescription: "Could not parse number \"\(numString)\"" + ) + } + case .null: + return .scalar(.null) + case let .object(obj): + if let (key, _) = obj.first, let bsonTypes = Self.wrapperKeyMap[key] { + for bsonType in bsonTypes { + guard let bsonValue = try bsonType.init(fromExtJSON: JSON(json), keyPath: keyPath + [key]) else { + continue + } + return .scalar(bsonValue.bson) + } + } + + /// Ensure extended JSON keys aren't interspersed with normal ones. + guard Self.wrapperKeySet.isDisjoint(with: obj.keys) else { + throw DecodingError._extendedJSONError( + keyPath: keyPath, + debugDescription: "Expected extended JSON wrapper object, but got extra keys: \(obj)" + ) + } + + return .encodedObject(obj) + case let .array(arr): + return .encodedArray(arr) + } + } + + /// The possible result of attempting to decode a BSON scalar value from a given extended JSON value. + /// Non-scalar values are preserved as-is. + internal enum DecodeScalarResult { + /// A BSON scalar that was successfully decoded from extended JSON. + case scalar(BSON) + + /// A non-wrapper object extended JSON object. + case encodedObject([String: JSONValue]) + + /// An array containing extended JSON values. + case encodedArray([JSONValue]) + } } diff --git a/Sources/SwiftBSON/ExtendedJSONEncoder.swift b/Sources/SwiftBSON/ExtendedJSONEncoder.swift index 359d0f26..eeefc3b7 100644 --- a/Sources/SwiftBSON/ExtendedJSONEncoder.swift +++ b/Sources/SwiftBSON/ExtendedJSONEncoder.swift @@ -1,3 +1,4 @@ +import ExtrasJSON import Foundation /// Facilitates the encoding of `Encodable` values into ExtendedJSON. @@ -57,7 +58,7 @@ public class ExtendedJSONEncoder { /// - Returns: Encoded representation of the `T` input as an instance of `Data` representing ExtendedJSON. /// - Throws: `EncodingError` if the value is corrupt or cannot be converted to valid ExtendedJSON. public func encode(_ value: T) throws -> Data { - // T --> BSON --> JSON --> Data + // T --> BSON --> JSONValue --> Data // Takes in any encodable type `T`, converts it to an instance of the `BSON` enum via the `BSONDecoder`. // The `BSON` is converted to an instance of the `JSON` enum via the `toRelaxedExtendedJSON` // or `toCanonicalExtendedJSON` methods on `BSONValue`s (depending on the `mode`). diff --git a/Sources/SwiftBSON/Integers+BSONValue.swift b/Sources/SwiftBSON/Integers+BSONValue.swift index 56bca8c7..438af9e2 100644 --- a/Sources/SwiftBSON/Integers+BSONValue.swift +++ b/Sources/SwiftBSON/Integers+BSONValue.swift @@ -1,6 +1,8 @@ import NIO extension Int32: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = ["$numberInt"] + /* * Initializes an `Int32` from ExtendedJSON. * @@ -16,16 +18,16 @@ extension Int32: 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 .number(n): // relaxed extended JSON - guard let int = Int32(exactly: n) else { + guard let int = Int32(n) else { return nil } self = int case .object: // canonical extended JSON - guard let value = try json.unwrapObject(withKey: "$numberInt", keyPath: keyPath) else { + guard let value = try json.value.unwrapObject(withKey: "$numberInt", keyPath: keyPath) else { return nil } guard @@ -46,12 +48,12 @@ extension Int32: BSONValue { /// Converts this `Int32` to a corresponding `JSON` in relaxed extendedJSON format. internal func toRelaxedExtendedJSON() -> JSON { - .number(Double(self)) + JSON(.number(String(self))) } /// Converts this `Int32` to a corresponding `JSON` in canonical extendedJSON format. internal func toCanonicalExtendedJSON() -> JSON { - ["$numberInt": .string(String(describing: self))] + ["$numberInt": JSON(.string(String(describing: self)))] } internal static var bsonType: BSONType { .int32 } @@ -71,6 +73,8 @@ extension Int32: BSONValue { } extension Int64: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = ["$numberLong"] + /* * Initializes an `Int64` from ExtendedJSON. * @@ -86,16 +90,16 @@ extension Int64: 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 .number(n): // relaxed extended JSON - guard let int = Int64(exactly: n) else { + guard let int = Int64(n) else { return nil } self = int case .object: // canonical extended JSON - guard let value = try json.unwrapObject(withKey: "$numberLong", keyPath: keyPath) else { + guard let value = try json.value.unwrapObject(withKey: "$numberLong", keyPath: keyPath) else { return nil } guard @@ -116,12 +120,12 @@ extension Int64: BSONValue { /// Converts this `Int64` to a corresponding `JSON` in relaxed extendedJSON format. internal func toRelaxedExtendedJSON() -> JSON { - .number(Double(self)) + JSON(.number(String(self))) } /// Converts this `Int64` to a corresponding `JSON` in canonical extendedJSON format. internal func toCanonicalExtendedJSON() -> JSON { - ["$numberLong": .string(String(describing: self))] + ["$numberLong": JSON(.string(String(describing: self)))] } internal static var bsonType: BSONType { .int64 } diff --git a/Sources/SwiftBSON/JSON.swift b/Sources/SwiftBSON/JSON.swift index c8c6fb89..f0034556 100644 --- a/Sources/SwiftBSON/JSON.swift +++ b/Sources/SwiftBSON/JSON.swift @@ -1,55 +1,29 @@ +import ExtrasJSON import Foundation -/// Enum representing a JSON value, used internally for modeling JSON -/// during extendedJSON parsing/generation. -internal enum JSON: Codable { - case number(Double) - case string(String) - case bool(Bool) - indirect case array([JSON]) - indirect case object([String: JSON]) - case null - - /// Initialize a `JSON` from a decoder. - /// Tries to decode into each of the JSON types one by one until one succeeds or - /// throws an error indicating that the input is not a valid `JSON` type. - internal init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let d = try? container.decode(Double.self) { - self = .number(d) - } else if let s = try? container.decode(String.self) { - self = .string(s) - } else if let b = try? container.decode(Bool.self) { - self = .bool(b) - } else if let a = try? container.decode([JSON].self) { - self = .array(a) - } else if let d = try? container.decode([String: JSON].self) { - self = .object(d) - } else if container.decodeNil() { - self = .null - } else { - throw DecodingError.dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, - debugDescription: "Not a valid JSON type" - )) - } +internal struct JSON { + internal let value: JSONValue + + internal init(_ value: JSONValue) { + self.value = value } +} +extension JSON: Encodable { /// Encode a `JSON` to a container by encoding the type of this `JSON` instance. internal func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - switch self { + switch self.value { case let .number(n): - try container.encode(n) + try container.encode(Double(n)) case let .string(s): try container.encode(s) case let .bool(b): try container.encode(b) case let .array(a): - try container.encode(a) + try container.encode(a.map(JSON.init)) case let .object(o): - try container.encode(o) + try container.encode(o.mapValues(JSON.init)) case .null: try container.encodeNil() } @@ -58,49 +32,53 @@ internal enum JSON: Codable { extension JSON: ExpressibleByFloatLiteral { internal init(floatLiteral value: Double) { - self = .number(value) + self.value = .number(String(value)) } } extension JSON: ExpressibleByIntegerLiteral { internal init(integerLiteral value: Int) { // The number `JSON` type is a Double, so we cast any integers to doubles. - self = .number(Double(value)) + self.value = .number(String(value)) } } extension JSON: ExpressibleByStringLiteral { internal init(stringLiteral value: String) { - self = .string(value) + self.value = .string(value) } } extension JSON: ExpressibleByBooleanLiteral { internal init(booleanLiteral value: Bool) { - self = .bool(value) + self.value = .bool(value) } } extension JSON: ExpressibleByArrayLiteral { internal init(arrayLiteral elements: JSON...) { - self = .array(elements) + self.value = .array(elements.map(\.value)) } } extension JSON: ExpressibleByDictionaryLiteral { internal init(dictionaryLiteral elements: (String, JSON)...) { - self = .object([String: JSON](uniqueKeysWithValues: elements)) + var map: [String: JSONValue] = [:] + for (k, v) in elements { + map[k] = v.value + } + self.value = .object(map) } } /// Value Getters -extension JSON { +extension JSONValue { /// If this `JSON` is a `.double`, return it as a `Double`. Otherwise, return nil. internal var doubleValue: Double? { guard case let .number(n) = self else { return nil } - return n + return Double(n) } /// If this `JSON` is a `.string`, return it as a `String`. Otherwise, return nil. @@ -120,7 +98,7 @@ extension JSON { } /// If this `JSON` is a `.array`, return it as a `[JSON]`. Otherwise, return nil. - internal var arrayValue: [JSON]? { + internal var arrayValue: [JSONValue]? { guard case let .array(a) = self else { return nil } @@ -128,7 +106,7 @@ extension JSON { } /// If this `JSON` is a `.object`, return it as a `[String: JSON]`. Otherwise, return nil. - internal var objectValue: [String: JSON]? { + internal var objectValue: [String: JSONValue]? { guard case let .object(o) = self else { return nil } @@ -137,7 +115,7 @@ extension JSON { } /// Helpers -extension JSON { +extension JSONValue { /// Helper function used in `BSONValue` initializers that take in extended JSON. /// If the current JSON is an object with only the specified key, return its value. /// @@ -149,8 +127,8 @@ extension JSON { /// - a JSON which is the value at the given `key` in `self` /// - or `nil` if `self` is not an `object` or does not contain the given `key` /// - /// - Throws: `DecodingError` if `self` has too many keys - internal func unwrapObject(withKey key: String, keyPath: [String]) throws -> JSON? { + /// - Throws: `DecodingError` if `self` includes the expected key along with other keys + internal func unwrapObject(withKey key: String, keyPath: [String]) throws -> JSONValue? { guard case let .object(obj) = self else { return nil } @@ -181,7 +159,11 @@ extension JSON { /// - or `nil` if `self` is not an `object` or does not contain the given keys /// /// - Throws: `DecodingError` if `self` has too many keys - internal func unwrapObject(withKeys key1: String, _ key2: String, keyPath: [String]) throws -> (JSON, JSON)? { + internal func unwrapObject( + withKeys key1: String, + _ key2: String, + keyPath: [String] + ) throws -> (JSONValue, JSONValue)? { guard case let .object(obj) = self else { return nil } @@ -201,4 +183,19 @@ extension JSON { } } -extension JSON: Equatable {} +extension JSON: Equatable { + internal static func == (lhs: JSON, rhs: JSON) -> Bool { + switch (lhs.value, rhs.value) { + case let (.number(lhsNum), .number(rhsNum)): + return Double(lhsNum) == Double(rhsNum) + case (_, .number), (.number, _): + return false + case let (.object(lhsObject), .object(rhsObject)): + return lhsObject.mapValues(JSON.init) == rhsObject.mapValues(JSON.init) + case let (.array(lhsArray), .array(rhsArray)): + return lhsArray.map(JSON.init) == rhsArray.map(JSON.init) + default: + return lhs.value == rhs.value + } + } +} diff --git a/Sources/SwiftBSON/String+BSONValue.swift b/Sources/SwiftBSON/String+BSONValue.swift index 9fc8a0d7..8a351b29 100644 --- a/Sources/SwiftBSON/String+BSONValue.swift +++ b/Sources/SwiftBSON/String+BSONValue.swift @@ -1,6 +1,8 @@ import NIO extension String: BSONValue { + internal static let extJSONTypeWrapperKeys: [String] = ["$maxKey"] + /* * Initializes a `String` from ExtendedJSON. * @@ -13,7 +15,7 @@ extension String: BSONValue { * - `nil` if the provided value is not an `String`. */ internal init?(fromExtJSON json: JSON, keyPath _: [String]) { - switch json { + switch json.value { case let .string(s): self = s default: @@ -28,7 +30,7 @@ extension String: BSONValue { /// Converts this `String` to a corresponding `JSON` in canonical extendedJSON format. internal func toCanonicalExtendedJSON() -> JSON { - .string(self) + JSON(.string(self)) } internal static var bsonType: BSONType { .string } diff --git a/Tests/SwiftBSONTests/BSONCorpusTests.swift b/Tests/SwiftBSONTests/BSONCorpusTests.swift index ac214b89..e07535ce 100644 --- a/Tests/SwiftBSONTests/BSONCorpusTests.swift +++ b/Tests/SwiftBSONTests/BSONCorpusTests.swift @@ -160,7 +160,6 @@ final class BSONCorpusTests: BSONTestCase { // BSONDocument -> Swift data type -> BSONDocument. // At the end, the new BSONDocument should be identical to the original one. // If not, our BSONDocument translation layer is lossy and/or buggy. - // TODO(SWIFT-867): Enable these lines when you can do subscript assignment let nativeFromDoc = docFromCB.toArray() let docFromNative = BSONDocument(fromArray: nativeFromDoc) expect(docFromNative.toByteString()).to(equal(cBData.toByteString())) diff --git a/Tests/SwiftBSONTests/BSONDocument+SequenceTests.swift b/Tests/SwiftBSONTests/BSONDocument+SequenceTests.swift index 2be976fe..21cf4915 100644 --- a/Tests/SwiftBSONTests/BSONDocument+SequenceTests.swift +++ b/Tests/SwiftBSONTests/BSONDocument+SequenceTests.swift @@ -4,7 +4,7 @@ import Nimble import XCTest final class Document_SequenceTests: BSONTestCase { - func testIterator() { + func testIterator() throws { let doc: BSONDocument = [ "string": "test string", "true": true, @@ -13,7 +13,7 @@ final class Document_SequenceTests: BSONTestCase { "int32": .int32(5), "int64": .int64(123), "double": .double(15), - // "decimal128": .decimal128(BSONDecimal128("1.2E+10")!), + "decimal128": .decimal128(try BSONDecimal128("1.2E+10")), "minkey": .minKey, "maxkey": .maxKey, "date": .datetime(Date(timeIntervalSince1970: 5000)), @@ -51,9 +51,9 @@ final class Document_SequenceTests: BSONTestCase { expect(doubleTup.key).to(equal("double")) expect(doubleTup.value).to(equal(15.0)) - // let decimalTup = iter.next()! - // expect(decimalTup.key).to(equal("decimal128")) - // expect(decimalTup.value).to(equal(.decimal128(try BSONDecimal128("1.2E+10")))) + let decimalTup = iter.next()! + expect(decimalTup.key).to(equal("decimal128")) + expect(decimalTup.value).to(equal(.decimal128(try BSONDecimal128("1.2E+10")))) let minTup = iter.next()! expect(minTup.key).to(equal("minkey")) @@ -76,12 +76,12 @@ final class Document_SequenceTests: BSONTestCase { // iterate via looping var expectedKeys = [ "string", "true", "false", "int", "int32", "int64", "double", - // "decimal128", + "decimal128", "minkey", "maxkey", "date", "timestamp" ] var expectedValues: [BSON] = [ "test string", true, false, 25, .int32(5), .int64(123), .double(15), - // .decimal128(try BSONDecimal128("1.2E+10")), + .decimal128(try BSONDecimal128("1.2E+10")), .minKey, .maxKey, .datetime(Date(timeIntervalSince1970: 5000)), .timestamp(BSONTimestamp(timestamp: 5, inc: 10)) ] diff --git a/Tests/SwiftBSONTests/BSONTests.swift b/Tests/SwiftBSONTests/BSONTests.swift index d29f04c0..75c8d4ca 100644 --- a/Tests/SwiftBSONTests/BSONTests.swift +++ b/Tests/SwiftBSONTests/BSONTests.swift @@ -1,3 +1,4 @@ +import ExtrasJSON import Foundation import Nimble import NIO @@ -51,7 +52,7 @@ public func retrieveSpecTestFiles( // TODO: update here to use BSONDecoder for more coverage let url = URL(fileURLWithPath: "\(path)/\(filename)") let data = try Data(contentsOf: url, options: .mappedIfSafe) - let jsonResult = try JSONDecoder().decode(T.self, from: data) + let jsonResult = try XJSONDecoder().decode(T.self, from: data) return (filename, jsonResult) } } diff --git a/Tests/SwiftBSONTests/CommonTestUtils.swift b/Tests/SwiftBSONTests/CommonTestUtils.swift index cfd378f7..b6a2cc01 100644 --- a/Tests/SwiftBSONTests/CommonTestUtils.swift +++ b/Tests/SwiftBSONTests/CommonTestUtils.swift @@ -1,3 +1,4 @@ +import ExtrasJSON import Foundation import Nimble @testable import SwiftBSON @@ -5,10 +6,8 @@ import XCTest /// Cleans and normalizes given JSON Data for comparison purposes public func clean(json: Data) throws -> JSON { - let jsonDecoder = JSONDecoder() do { - let jsonEnum = try jsonDecoder.decode(JSON.self, from: json) - return jsonEnum + return try JSON(JSONParser().parse(bytes: json)) } catch { fatalError("json should be decodable to jsonEnum") } @@ -29,7 +28,9 @@ public func cleanEqual(_ expectedValue: String) -> Predicate { } let cleanedActual = try clean(json: actualValue) let cleanedExpected = try clean(json: expectedValueData) + let matches = cleanedActual == cleanedExpected + return PredicateResult( status: PredicateStatus(bool: matches), message: .expectedCustomValueTo( diff --git a/Tests/SwiftBSONTests/ExtendedJSONConversionTests.swift b/Tests/SwiftBSONTests/ExtendedJSONConversionTests.swift index 16b0dd0d..375ee8ae 100644 --- a/Tests/SwiftBSONTests/ExtendedJSONConversionTests.swift +++ b/Tests/SwiftBSONTests/ExtendedJSONConversionTests.swift @@ -151,10 +151,10 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { let oid = "5F07445CFBBBBBBBBBFAAAAA" // Success case - let bson = try BSONObjectID(fromExtJSON: ["$oid": JSON.string(oid)], keyPath: []) + let bson = try BSONObjectID(fromExtJSON: ["$oid": JSON(.string(oid))], keyPath: []) expect(bson).to(equal(try BSONObjectID(oid))) - expect(bson?.toRelaxedExtendedJSON()).to(equal(["$oid": JSON.string(oid.lowercased())])) - expect(bson?.toCanonicalExtendedJSON()).to(equal(["$oid": JSON.string(oid.lowercased())])) + expect(bson?.toRelaxedExtendedJSON()).to(equal(["$oid": JSON(.string(oid.lowercased()))])) + expect(bson?.toCanonicalExtendedJSON()).to(equal(["$oid": JSON(.string(oid.lowercased()))])) // Nil cases expect(try BSONObjectID(fromExtJSON: ["random": "hello"], keyPath: [])).to(beNil()) @@ -165,7 +165,7 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { .to(throwError(errorType: DecodingError.self)) expect(try BSONObjectID(fromExtJSON: ["$oid": "hello"], keyPath: [])) .to(throwError(errorType: DecodingError.self)) - expect(try BSONObjectID(fromExtJSON: ["$oid": .string(oid), "extra": "hello"], keyPath: [])) + expect(try BSONObjectID(fromExtJSON: ["$oid": JSON(.string(oid)), "extra": "hello"], keyPath: [])) .to(throwError(errorType: DecodingError.self)) } @@ -191,13 +191,13 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { // Success cases let bson = try Int32(fromExtJSON: 5, keyPath: []) expect(bson).to(equal(5)) - expect(bson?.toRelaxedExtendedJSON()).to(equal(.number(5))) - expect(bson?.toCanonicalExtendedJSON()).to(equal(["$numberInt": .string("5")])) + expect(bson?.toRelaxedExtendedJSON()).to(equal(5)) + expect(bson?.toCanonicalExtendedJSON()).to(equal(["$numberInt": JSON(.string("5"))])) expect(try Int32(fromExtJSON: ["$numberInt": "5"], keyPath: [])).to(equal(5)) // Nil cases - expect(try Int32(fromExtJSON: .number(Double(Int32.max) + 1), keyPath: [])).to(beNil()) - expect(try Int32(fromExtJSON: .bool(true), keyPath: [])).to(beNil()) + expect(try Int32(fromExtJSON: JSON(.number(String(Int64(Int32.max) + 1))), keyPath: [])).to(beNil()) + expect(try Int32(fromExtJSON: true, keyPath: [])).to(beNil()) expect(try Int32(fromExtJSON: ["bad": "5"], keyPath: [])).to(beNil()) // Error cases @@ -205,7 +205,8 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { .to(throwError(errorType: DecodingError.self)) expect(try Int32(fromExtJSON: ["$numberInt": "5", "extra": true], keyPath: [])) .to(throwError(errorType: DecodingError.self)) - expect(try Int32(fromExtJSON: ["$numberInt": .string("\(Double(Int32.max) + 1)")], keyPath: ["key", "path"])) + expect( + try Int32(fromExtJSON: ["$numberInt": JSON(.string("\(Double(Int32.max) + 1)"))], keyPath: ["key", "path"])) .to(throwError(errorType: DecodingError.self)) } @@ -213,13 +214,13 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { // Success cases let bson = try Int64(fromExtJSON: 5, keyPath: []) expect(bson).to(equal(5)) - expect(bson?.toRelaxedExtendedJSON()).to(equal(.number(5))) - expect(bson?.toCanonicalExtendedJSON()).to(equal(["$numberLong": .string("5")])) + expect(bson?.toRelaxedExtendedJSON()).to(equal(5)) + expect(bson?.toCanonicalExtendedJSON()).to(equal(["$numberLong": "5"])) expect(try Int64(fromExtJSON: ["$numberLong": "5"], keyPath: [])).to(equal(5)) // Nil cases - expect(try Int64(fromExtJSON: .number(Double(Int64.max) + 1), keyPath: [])).to(beNil()) - expect(try Int64(fromExtJSON: .bool(true), keyPath: [])).to(beNil()) + expect(try Int64(fromExtJSON: JSON(.number(String(Double(Int64.max) + 1))), keyPath: [])).to(beNil()) + expect(try Int64(fromExtJSON: true, keyPath: [])).to(beNil()) expect(try Int64(fromExtJSON: ["bad": "5"], keyPath: [])).to(beNil()) // Error cases @@ -227,8 +228,10 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { .to(throwError(errorType: DecodingError.self)) expect(try Int64(fromExtJSON: ["$numberLong": "5", "extra": true], keyPath: [])) .to(throwError(errorType: DecodingError.self)) - expect(try Int64(fromExtJSON: ["$numberLong": .string("\(Double(Int64.max) + 1)")], keyPath: ["key", "path"])) - .to(throwError(errorType: DecodingError.self)) + expect(try Int64( + fromExtJSON: ["$numberLong": JSON(.string("\(Double(Int64.max) + 1)"))], + keyPath: ["key", "path"] + )).to(throwError(errorType: DecodingError.self)) } /// Tests the BSON Double [finite] and Double [non-finite] types. @@ -239,12 +242,12 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { expect(try Double(fromExtJSON: ["$numberDouble": "Infinity"], keyPath: [])).to(equal(Double.infinity)) expect(try Double(fromExtJSON: ["$numberDouble": "-Infinity"], keyPath: [])).to(equal(-Double.infinity)) expect(try Double(fromExtJSON: ["$numberDouble": "NaN"], keyPath: [])?.isNaN).to(beTrue()) - expect(Double("NaN")?.toCanonicalExtendedJSON()).to(equal(["$numberDouble": .string("NaN")])) - expect(Double(5.5).toCanonicalExtendedJSON()).to(equal(["$numberDouble": .string("5.5")])) - expect(Double(5.5).toRelaxedExtendedJSON()).to(equal(.number(5.5))) + expect(Double("NaN")?.toCanonicalExtendedJSON()).to(equal(["$numberDouble": "NaN"])) + expect(Double(5.5).toCanonicalExtendedJSON()).to(equal(["$numberDouble": "5.5"])) + expect(Double(5.5).toRelaxedExtendedJSON()).to(equal(5.5)) // Nil cases - expect(try Double(fromExtJSON: .bool(true), keyPath: [])).to(beNil()) + expect(try Double(fromExtJSON: true, keyPath: [])).to(beNil()) expect(try Double(fromExtJSON: ["bad": "5.5"], keyPath: [])).to(beNil()) // Error cases @@ -252,7 +255,7 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { .to(throwError(errorType: DecodingError.self)) expect(try Double(fromExtJSON: ["$numberDouble": "5.5", "extra": true], keyPath: [])) .to(throwError(errorType: DecodingError.self)) - expect(try Double(fromExtJSON: ["$numberDouble": .bool(true)], keyPath: ["key", "path"])) + expect(try Double(fromExtJSON: ["$numberDouble": true], keyPath: ["key", "path"])) .to(throwError(errorType: DecodingError.self)) } @@ -264,7 +267,7 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { .to(equal(["$numberDecimal": "0.020000000000000004"])) // Nil cases - expect(try BSONDecimal128(fromExtJSON: .bool(true), keyPath: [])).to(beNil()) + expect(try BSONDecimal128(fromExtJSON: true, keyPath: [])).to(beNil()) expect(try BSONDecimal128(fromExtJSON: ["bad": "5.5"], keyPath: [])).to(beNil()) // Error cases @@ -272,7 +275,7 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { .to(throwError(errorType: DecodingError.self)) expect(try BSONDecimal128(fromExtJSON: ["$numberDecimal": "5.5", "extra": true], keyPath: [])) .to(throwError(errorType: DecodingError.self)) - expect(try BSONDecimal128(fromExtJSON: ["$numberDecimal": .bool(true)], keyPath: ["key", "path"])) + expect(try BSONDecimal128(fromExtJSON: ["$numberDecimal": true], keyPath: ["key", "path"])) .to(throwError(errorType: DecodingError.self)) } @@ -427,7 +430,7 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { } func testDBPointer() throws { - let oid = JSON.object(["$oid": .string("5F07445CFBBBBBBBBBFAAAAA")]) + let oid: JSON = ["$oid": "5F07445CFBBBBBBBBBFAAAAA"] let objectId: BSONObjectID = try BSONObjectID("5F07445CFBBBBBBBBBFAAAAA") // Success case @@ -507,14 +510,14 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { func testUndefined() throws { // Success cases - expect(try BSONUndefined(fromExtJSON: ["$undefined": .bool(true)], keyPath: [])).to(equal(BSONUndefined())) + expect(try BSONUndefined(fromExtJSON: ["$undefined": true], keyPath: [])).to(equal(BSONUndefined())) // Nil cases expect(try BSONUndefined(fromExtJSON: "undefined", keyPath: [])).to(beNil()) expect(try BSONUndefined(fromExtJSON: ["bad": "5.5"], keyPath: [])).to(beNil()) // Error cases - expect(try BSONUndefined(fromExtJSON: ["$undefined": .bool(true), "extra": 1], keyPath: [])) + expect(try BSONUndefined(fromExtJSON: ["$undefined": true, "extra": 1], keyPath: [])) .to(throwError(errorType: DecodingError.self)) expect(try BSONUndefined(fromExtJSON: ["$undefined": 1], keyPath: [])) .to(throwError(errorType: DecodingError.self)) @@ -540,7 +543,7 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { func testBoolean() { // Success cases - expect(Bool(fromExtJSON: .bool(true), keyPath: [])).to(equal(true)) + expect(Bool(fromExtJSON: true, keyPath: [])).to(equal(true)) // Nil cases expect(Bool(fromExtJSON: 5.5, keyPath: [])).to(beNil()) @@ -549,7 +552,7 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { func testNull() { // Success cases - expect(BSONNull(fromExtJSON: .null, keyPath: [])).to(equal(BSONNull())) + expect(BSONNull(fromExtJSON: JSON(.null), keyPath: [])).to(equal(BSONNull())) // Nil cases expect(BSONNull(fromExtJSON: 5.5, keyPath: [])).to(beNil()) diff --git a/Tests/SwiftBSONTests/JSONTests.swift b/Tests/SwiftBSONTests/JSONTests.swift index 6b9b0543..465e66cd 100644 --- a/Tests/SwiftBSONTests/JSONTests.swift +++ b/Tests/SwiftBSONTests/JSONTests.swift @@ -1,3 +1,4 @@ +import ExtrasJSON import Foundation import Nimble import NIO @@ -5,68 +6,54 @@ import NIO import XCTest open class JSONTestCase: XCTestCase { - let encoder = JSONEncoder() - let decoder = JSONDecoder() + let encoder = XJSONEncoder() + let decoder = XJSONDecoder() func testInteger() throws { // Initializing a JSON with an int works, but it will be cast to a double. let intJSON: JSON = 12 - let encoded = try encoder.encode([intJSON]) + let encoded = Data(try encoder.encode([intJSON])) /* JSONEncoder currently cannot encode non-object/array top level values. To get around this, the generated JSON will need to be wrapped in an array and unwrapped again at the end as a workaround. This workaround can be removed when Swift 5.3 is the minimum supported version by the BSON library. */ expect(Double(String(data: encoded.dropFirst().dropLast(), encoding: .utf8)!)!) .to(beCloseTo(12)) - - let decoded = try decoder.decode([JSON].self, from: encoded)[0] - expect(decoded.doubleValue).to(beCloseTo(intJSON.doubleValue!)) } func testDouble() throws { let doubleJSON: JSON = 12.3 - let encoded = try encoder.encode([doubleJSON]) + let encoded = Data(try encoder.encode([doubleJSON])) expect(Double(String(data: encoded.dropFirst().dropLast(), encoding: .utf8)!)!) .to(beCloseTo(12.3)) - - let decoded = try decoder.decode([JSON].self, from: encoded)[0] - expect(decoded.doubleValue).to(beCloseTo(doubleJSON.doubleValue!)) } func testString() throws { let stringJSON: JSON = "I am a String" - let encoded = try encoder.encode([stringJSON]) + let encoded = Data(try encoder.encode([stringJSON])) expect(String(data: encoded.dropFirst().dropLast(), encoding: .utf8)) .to(equal("\"I am a String\"")) - let decoded = try decoder.decode([JSON].self, from: encoded)[0] - expect(decoded).to(equal(stringJSON)) } func testBool() throws { let boolJSON: JSON = true - let encoded = try encoder.encode([boolJSON]) + let encoded = Data(try encoder.encode([boolJSON])) expect(String(data: encoded.dropFirst().dropLast(), encoding: .utf8)) .to(equal("true")) - let decoded = try decoder.decode([JSON].self, from: encoded)[0] - expect(decoded).to(equal(boolJSON)) } func testArray() throws { let arrayJSON: JSON = ["I am a string in an array"] - let encoded = try encoder.encode(arrayJSON) - let decoded = try decoder.decode(JSON.self, from: encoded) + let encoded = Data(try encoder.encode(arrayJSON)) expect(String(data: encoded, encoding: .utf8)) .to(equal("[\"I am a string in an array\"]")) - expect(decoded).to(equal(arrayJSON)) } func testObject() throws { let objectJSON: JSON = ["Key": "Value"] - let encoded = try encoder.encode(objectJSON) - let decoded = try decoder.decode(JSON.self, from: encoded) + let encoded = Data(try encoder.encode(objectJSON)) expect(String(data: encoded, encoding: .utf8)) .to(equal("{\"Key\":\"Value\"}")) - expect(objectJSON.objectValue!["Key"]!.stringValue!).to(equal("Value")) - expect(decoded).to(equal(objectJSON)) + expect(objectJSON.value.objectValue!["Key"]!.stringValue!).to(equal("Value")) } } From a90596deea73d34d25e066ff26cfb44ea4eff3ff Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Fri, 8 Jan 2021 18:43:44 -0500 Subject: [PATCH 02/12] revert swift-extras-base64 --- Package.resolved | 9 --------- Package.swift | 3 +-- Sources/SwiftBSON/BSONBinary.swift | 24 +++++++++++------------- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/Package.resolved b/Package.resolved index 854877ff..6704c7d1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,15 +10,6 @@ "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", diff --git a/Package.swift b/Package.swift index 88228a2c..fa55fc5a 100644 --- a/Package.swift +++ b/Package.swift @@ -12,11 +12,10 @@ 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", "ExtrasJSON", "ExtrasBase64"]), + .target(name: "SwiftBSON", dependencies: ["NIO", "ExtrasJSON"]), .testTarget(name: "SwiftBSONTests", dependencies: ["SwiftBSON", "Nimble"]) ] ) diff --git a/Sources/SwiftBSON/BSONBinary.swift b/Sources/SwiftBSON/BSONBinary.swift index 3c3864f5..f6cec1eb 100644 --- a/Sources/SwiftBSON/BSONBinary.swift +++ b/Sources/SwiftBSON/BSONBinary.swift @@ -1,4 +1,3 @@ -import ExtrasBase64 import Foundation import NIO @@ -109,14 +108,13 @@ 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 { - do { - let bytes = try base64.base64decoded() - try self.init(bytes: bytes, subtype: subtype) - } catch let error as ExtrasBase64.DecodingError { + guard let dataObj = Data(base64Encoded: base64) else { throw BSONError.InvalidArgumentError( - message: "failed to create Data object from invalid base64 string \(base64): \(error)" + message: + "failed to create Data object from invalid base64 string \(base64)" ) } + try self.init(data: dataObj, subtype: subtype) } /// Converts this `BSONBinary` instance to a `UUID`. @@ -164,14 +162,14 @@ extension BSONBinary: BSONValue { internal init?(fromExtJSON json: JSON, keyPath: [String]) throws { if let uuidJSON = try json.value.unwrapObject(withKey: "$uuid", keyPath: keyPath) { guard let uuidString = uuidJSON.stringValue else { - throw Swift.DecodingError._extendedJSONError( + throw 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 Swift.DecodingError._extendedJSONError( + throw DecodingError._extendedJSONError( keyPath: keyPath, debugDescription: "Invalid UUID string: \(uuidString)" ) @@ -181,7 +179,7 @@ extension BSONBinary: BSONValue { self = try BSONBinary(from: uuid) return } catch { - throw Swift.DecodingError._extendedJSONError( + throw DecodingError._extendedJSONError( keyPath: keyPath, debugDescription: error.localizedDescription ) @@ -195,13 +193,13 @@ extension BSONBinary: BSONValue { guard let (base64, subTypeInput) = try binary.unwrapObject(withKeys: "base64", "subType", keyPath: keyPath) else { - throw Swift.DecodingError._extendedJSONError( + throw DecodingError._extendedJSONError( keyPath: keyPath, debugDescription: "Missing \"base64\" or \"subType\" in \(binary)" ) } guard let base64Str = base64.stringValue else { - throw Swift.DecodingError._extendedJSONError( + throw DecodingError._extendedJSONError( keyPath: keyPath, debugDescription: "Could not parse `base64` from \"\(base64)\", " + "input must be a base64-encoded (with padding as =) payload as a string" @@ -212,7 +210,7 @@ extension BSONBinary: BSONValue { let subTypeInt = UInt8(subTypeStr, radix: 16), let subType = Subtype(rawValue: subTypeInt) else { - throw Swift.DecodingError._extendedJSONError( + throw 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" @@ -221,7 +219,7 @@ extension BSONBinary: BSONValue { do { self = try BSONBinary(base64: base64Str, subtype: subType) } catch { - throw Swift.DecodingError._extendedJSONError( + throw DecodingError._extendedJSONError( keyPath: keyPath, debugDescription: error.localizedDescription ) From f3eb9a437f3e688fef567e6c639b0d6e2381ed74 Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Fri, 8 Jan 2021 20:01:59 -0500 Subject: [PATCH 03/12] fix keypath propagation in extjson decoder --- Sources/SwiftBSON/BSONDocument.swift | 2 +- Sources/SwiftBSON/ExtendedJSONDecoder.swift | 18 +++++++++-------- .../ExtendedJSONConversionTests.swift | 20 +++++++++++++++++++ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/Sources/SwiftBSON/BSONDocument.swift b/Sources/SwiftBSON/BSONDocument.swift index de0beabc..029034f1 100644 --- a/Sources/SwiftBSON/BSONDocument.swift +++ b/Sources/SwiftBSON/BSONDocument.swift @@ -374,7 +374,7 @@ public struct BSONDocument { /// how many bytes it wrote. /// /// This may be used to build up a fresh document or a subdocument. - internal mutating func buildDocument(_ appendFunc: (inout Self) throws -> Int) throws -> Int { + internal mutating func buildDocument(_ appendFunc: (inout Self) throws -> Int) rethrows -> Int { var totalBytes = 0 // write length of document diff --git a/Sources/SwiftBSON/ExtendedJSONDecoder.swift b/Sources/SwiftBSON/ExtendedJSONDecoder.swift index 89849268..892ba3f3 100644 --- a/Sources/SwiftBSON/ExtendedJSONDecoder.swift +++ b/Sources/SwiftBSON/ExtendedJSONDecoder.swift @@ -84,7 +84,7 @@ public class ExtendedJSONDecoder { return .array(bsonArr) case let .encodedObject(obj): var storage = BSONDocument.BSONDocumentStorage() - _ = try self.appendObject(obj, to: &storage) + _ = try self.appendObject(obj, to: &storage, keyPath: keyPath) return .document(BSONDocument(fromUnsafeBSON: storage, keys: Set(obj.keys))) } } @@ -92,12 +92,13 @@ public class ExtendedJSONDecoder { /// Decode and append the given extended JSON object to the provided BSONDocumentStorage. private func appendObject( _ object: [String: JSONValue], - to storage: inout BSONDocument.BSONDocumentStorage + to storage: inout BSONDocument.BSONDocumentStorage, + keyPath: [String] ) throws -> Int { try storage.buildDocument { storage in var bytes = 0 for (k, v) in object { - bytes += try self.appendElement(v, to: &storage, forKey: k) + bytes += try self.appendElement(v, to: &storage, forKey: k, keyPath: keyPath + [k]) } return bytes } @@ -107,9 +108,10 @@ public class ExtendedJSONDecoder { private func appendElement( _ value: JSONValue, to storage: inout BSONDocument.BSONDocumentStorage, - forKey key: String + forKey key: String, + keyPath: [String] ) throws -> Int { - switch try self.decodeScalar(value, keyPath: []) { + switch try self.decodeScalar(value, keyPath: keyPath) { case let .scalar(s): return storage.append(key: key, value: s) case let .encodedArray(arr): @@ -118,7 +120,7 @@ public class ExtendedJSONDecoder { bytes += try storage.buildDocument { storage in var bytes = 0 for (i, v) in arr.enumerated() { - bytes += try self.appendElement(v, to: &storage, forKey: String(i)) + bytes += try self.appendElement(v, to: &storage, forKey: String(i), keyPath: keyPath + [String(i)]) } return bytes } @@ -126,7 +128,7 @@ public class ExtendedJSONDecoder { case let .encodedObject(obj): var bytes = 0 bytes += storage.appendElementHeader(key: key, bsonType: .document) - bytes += try self.appendObject(obj, to: &storage) + bytes += try self.appendObject(obj, to: &storage, keyPath: keyPath) return bytes } } @@ -157,7 +159,7 @@ public class ExtendedJSONDecoder { case let .object(obj): if let (key, _) = obj.first, let bsonTypes = Self.wrapperKeyMap[key] { for bsonType in bsonTypes { - guard let bsonValue = try bsonType.init(fromExtJSON: JSON(json), keyPath: keyPath + [key]) else { + guard let bsonValue = try bsonType.init(fromExtJSON: JSON(json), keyPath: keyPath) else { continue } return .scalar(bsonValue.bson) diff --git a/Tests/SwiftBSONTests/ExtendedJSONConversionTests.swift b/Tests/SwiftBSONTests/ExtendedJSONConversionTests.swift index 375ee8ae..1d595892 100644 --- a/Tests/SwiftBSONTests/ExtendedJSONConversionTests.swift +++ b/Tests/SwiftBSONTests/ExtendedJSONConversionTests.swift @@ -41,6 +41,26 @@ open class ExtendedJSONConversionTestCase: BSONTestCase { expect(decoded).to(equal(test)) } + func testExtendedJSONDecoderErrorKeyPath() throws { + let badExtJSON = "{ \"mydoc\": [ true, { \"bar\": 12, \"foo\": { \"$numberInt\": 3, \"extra\": true } } ] }" + let decoder = ExtendedJSONDecoder() + let result = Result { + try decoder.decode(BSON.self, from: badExtJSON.data(using: .utf8)!) + } + + guard case let .failure(error) = result else { + XCTFail("expected decode to fail, but succeeded: \(result)") + return + } + + guard case let .dataCorrupted(context) = error as? DecodingError else { + XCTFail("expected DecodingError, got: \(error)") + return + } + + expect(context.debugDescription).to(contain("mydoc.1.foo")) + } + func testExtendedJSONDecodingWithUserInfo() throws { struct Foo: Decodable, Equatable { let val: BSON From 6bf3d9d92aff0dbcdc1c3a5d2bc7eb204f25d646 Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Mon, 11 Jan 2021 12:23:18 -0500 Subject: [PATCH 04/12] reintroduce swift-extras-base64 --- Package.resolved | 9 +++++++++ Package.swift | 3 ++- Sources/SwiftBSON/BSONBinary.swift | 24 +++++++++++++----------- Sources/SwiftBSON/BSONDecoder.swift | 7 ++++--- Sources/SwiftBSON/BSONEncoder.swift | 3 ++- 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/Package.resolved b/Package.resolved index 6704c7d1..bc093f1b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,6 +10,15 @@ "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", diff --git a/Package.swift b/Package.swift index fa55fc5a..a1d2924b 100644 --- a/Package.swift +++ b/Package.swift @@ -12,10 +12,11 @@ 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", "ExtrasJSON"]), + .target(name: "SwiftBSON", dependencies: ["NIO", "ExtrasJSON", "ExtrasBase64"]), .testTarget(name: "SwiftBSONTests", dependencies: ["SwiftBSON", "Nimble"]) ] ) diff --git a/Sources/SwiftBSON/BSONBinary.swift b/Sources/SwiftBSON/BSONBinary.swift index f6cec1eb..3c3864f5 100644 --- a/Sources/SwiftBSON/BSONBinary.swift +++ b/Sources/SwiftBSON/BSONBinary.swift @@ -1,3 +1,4 @@ +import ExtrasBase64 import Foundation import NIO @@ -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`. @@ -162,14 +164,14 @@ extension BSONBinary: BSONValue { internal init?(fromExtJSON json: JSON, keyPath: [String]) throws { 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)" ) @@ -179,7 +181,7 @@ extension BSONBinary: BSONValue { self = try BSONBinary(from: uuid) return } catch { - throw DecodingError._extendedJSONError( + throw Swift.DecodingError._extendedJSONError( keyPath: keyPath, debugDescription: error.localizedDescription ) @@ -193,13 +195,13 @@ extension BSONBinary: BSONValue { 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" @@ -210,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" @@ -219,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 ) diff --git a/Sources/SwiftBSON/BSONDecoder.swift b/Sources/SwiftBSON/BSONDecoder.swift index 74fb9250..bac8eb92 100644 --- a/Sources/SwiftBSON/BSONDecoder.swift +++ b/Sources/SwiftBSON/BSONDecoder.swift @@ -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() } diff --git a/Sources/SwiftBSON/BSONEncoder.swift b/Sources/SwiftBSON/BSONEncoder.swift index a66df4b8..53383e38 100644 --- a/Sources/SwiftBSON/BSONEncoder.swift +++ b/Sources/SwiftBSON/BSONEncoder.swift @@ -1,3 +1,4 @@ +import ExtrasBase64 import Foundation import NIO @@ -483,7 +484,7 @@ extension _BSONEncoder { case .binary: return try BSONBinary(data: data, subtype: .generic) case .base64: - return data.base64EncodedString() + return String(base64Encoding: data) case let .custom(f): return try self.handleCustomStrategy(encodeFunc: f, forValue: data) } From 3ba2024f9a67e2f629f4a397ab28fa8ed430d008 Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Tue, 12 Jan 2021 14:32:44 -0500 Subject: [PATCH 05/12] update buildDocument documentation --- Sources/SwiftBSON/BSONDocument.swift | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftBSON/BSONDocument.swift b/Sources/SwiftBSON/BSONDocument.swift index 029034f1..e6aa0213 100644 --- a/Sources/SwiftBSON/BSONDocument.swift +++ b/Sources/SwiftBSON/BSONDocument.swift @@ -370,19 +370,25 @@ public struct BSONDocument { return self.buffer.writerIndex - writer } - /// Build a document at the current position in the storage via the provided closure which returns - /// how many bytes it wrote. + /// Build a document at the current position in the storage via the provided closure which appends + /// the elements of the document and returns how many bytes it wrote in total. This method will append the + /// required metadata surrounding the document as necessary (length, null byte). + /// + /// If this method is used to build a subdocument, the caller is responsible for updating + /// the length of the containing document based on this method's return value. If this method was invoked + /// recursively from `buildDocument`, such updating will happen automatically if the returned byte count + /// is propagated. /// /// This may be used to build up a fresh document or a subdocument. - internal mutating func buildDocument(_ appendFunc: (inout Self) throws -> Int) rethrows -> Int { + internal mutating func buildDocument(_ appendElementsFunc: (inout Self) throws -> Int) rethrows -> Int { var totalBytes = 0 - // write length of document + // write placeholder length of document let lengthIndex = self.buffer.writerIndex totalBytes += self.buffer.writeInteger(0, endianness: .little, as: Int32.self) // write contents - totalBytes += try appendFunc(&self) + totalBytes += try appendElementsFunc(&self) // write null byte totalBytes += self.buffer.writeInteger(0, as: UInt8.self) From 522b6e289042cebb7ed523f040c1a4a2204fe569 Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Tue, 12 Jan 2021 15:04:59 -0500 Subject: [PATCH 06/12] fix typo --- Sources/SwiftBSON/ExtendedJSONDecoder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftBSON/ExtendedJSONDecoder.swift b/Sources/SwiftBSON/ExtendedJSONDecoder.swift index 892ba3f3..32af11b8 100644 --- a/Sources/SwiftBSON/ExtendedJSONDecoder.swift +++ b/Sources/SwiftBSON/ExtendedJSONDecoder.swift @@ -15,7 +15,7 @@ public class ExtendedJSONDecoder { return formatter }() - /// A set of all the possible extenededJSON wrapper keys. + /// A set of all the possible extendedJSON wrapper keys. private static var wrapperKeySet: Set = { Set(ExtendedJSONDecoder.wrapperKeyMap.keys) }() From 0a2d1bf115177497062a62d434a3ccda67bbcc42 Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Tue, 12 Jan 2021 15:10:42 -0500 Subject: [PATCH 07/12] avoid CoW --- Sources/SwiftBSON/ExtendedJSONDecoder.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Sources/SwiftBSON/ExtendedJSONDecoder.swift b/Sources/SwiftBSON/ExtendedJSONDecoder.swift index 32af11b8..76736a90 100644 --- a/Sources/SwiftBSON/ExtendedJSONDecoder.swift +++ b/Sources/SwiftBSON/ExtendedJSONDecoder.swift @@ -30,12 +30,7 @@ public class ExtendedJSONDecoder { var map: [String: [BSONValue.Type]] = [:] for t in BSON.allBSONTypes.values { for k in t.extJSONTypeWrapperKeys { - if var existingList = map[k] { - existingList.append(t.self) - map[k] = existingList - } else { - map[k] = [t] - } + map[k, default: []].append(t.self) } } return map From 53613af3b183cab2e81ec5de549c368047e2fa27 Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Tue, 12 Jan 2021 15:14:43 -0500 Subject: [PATCH 08/12] clean up decode --- Sources/SwiftBSON/ExtendedJSONDecoder.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftBSON/ExtendedJSONDecoder.swift b/Sources/SwiftBSON/ExtendedJSONDecoder.swift index 76736a90..db98462b 100644 --- a/Sources/SwiftBSON/ExtendedJSONDecoder.swift +++ b/Sources/SwiftBSON/ExtendedJSONDecoder.swift @@ -52,14 +52,12 @@ public class ExtendedJSONDecoder { /// - Throws: `DecodingError` if the JSON data is corrupt or if any value throws an error during decoding. public func decode(_: T.Type, from data: Data) throws -> T { // Data --> JSONValue --> BSON --> T - // Takes in JSON as `Data` encoded with `.utf8` and runs it through a `JSONDecoder` to get an - // instance of the `JSON` enum. + // Takes in JSON as `Data` encoded with `.utf8` and runs it through ExtrasJSON's parser to get an + // instance of the `JSONValue` enum. let json = try JSONParser().parse(bytes: data) - // Then a `BSON` enum instance is created via the `JSON`. - // let bson = try json.toBSON(keyPath: []) + // Then a `BSON` enum instance is decoded from the `JSONValue`. let bson = try self.decodeBSONFromJSON(json, keyPath: []) - // let bson = BSON(fromExtJSON: json) // The `BSON` is then passed through a `BSONDecoder` where it is outputted as a `T` let bsonDecoder = BSONDecoder() From 9e6c2d5f5ce7979e6d61383e621fb38eda921915 Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Tue, 12 Jan 2021 15:19:50 -0500 Subject: [PATCH 09/12] document return values --- Sources/SwiftBSON/ExtendedJSONDecoder.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftBSON/ExtendedJSONDecoder.swift b/Sources/SwiftBSON/ExtendedJSONDecoder.swift index db98462b..8c1c5e46 100644 --- a/Sources/SwiftBSON/ExtendedJSONDecoder.swift +++ b/Sources/SwiftBSON/ExtendedJSONDecoder.swift @@ -82,7 +82,8 @@ public class ExtendedJSONDecoder { } } - /// Decode and append the given extended JSON object to the provided BSONDocumentStorage. + /// Decode and append the given extended JSON object to the provided BSONDocumentStorage, returning the number of + /// bytes written to the storage. private func appendObject( _ object: [String: JSONValue], to storage: inout BSONDocument.BSONDocumentStorage, @@ -97,7 +98,8 @@ public class ExtendedJSONDecoder { } } - /// Decode the given extended JSON value to BSON and append it to the provided storage. + /// Decode the given extended JSON value to BSON and append it to the provided storage, returning the number of + /// bytes written to the storage. private func appendElement( _ value: JSONValue, to storage: inout BSONDocument.BSONDocumentStorage, From 17f864a36ee8d9b7f84889c31d257e05adc052d3 Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Tue, 12 Jan 2021 16:35:28 -0500 Subject: [PATCH 10/12] remove outdated comment --- Sources/SwiftBSON/JSON.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/SwiftBSON/JSON.swift b/Sources/SwiftBSON/JSON.swift index f0034556..b1dd4d8f 100644 --- a/Sources/SwiftBSON/JSON.swift +++ b/Sources/SwiftBSON/JSON.swift @@ -38,7 +38,6 @@ extension JSON: ExpressibleByFloatLiteral { extension JSON: ExpressibleByIntegerLiteral { internal init(integerLiteral value: Int) { - // The number `JSON` type is a Double, so we cast any integers to doubles. self.value = .number(String(value)) } } From 5659fefa8fa88b88f59e6af523ed3e2e581d30ba Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Tue, 12 Jan 2021 16:37:24 -0500 Subject: [PATCH 11/12] fix string extjson wrapper key --- Sources/SwiftBSON/String+BSONValue.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftBSON/String+BSONValue.swift b/Sources/SwiftBSON/String+BSONValue.swift index 8a351b29..b65e76a2 100644 --- a/Sources/SwiftBSON/String+BSONValue.swift +++ b/Sources/SwiftBSON/String+BSONValue.swift @@ -1,7 +1,7 @@ import NIO extension String: BSONValue { - internal static let extJSONTypeWrapperKeys: [String] = ["$maxKey"] + internal static let extJSONTypeWrapperKeys: [String] = [] /* * Initializes a `String` from ExtendedJSON. From 23cfe16ff8bac69f2a75414f897a6a3d5aab3e77 Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Tue, 12 Jan 2021 16:41:58 -0500 Subject: [PATCH 12/12] remove JSON fragment workaround --- Tests/SwiftBSONTests/JSONTests.swift | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/Tests/SwiftBSONTests/JSONTests.swift b/Tests/SwiftBSONTests/JSONTests.swift index 465e66cd..9ad27a88 100644 --- a/Tests/SwiftBSONTests/JSONTests.swift +++ b/Tests/SwiftBSONTests/JSONTests.swift @@ -12,33 +12,29 @@ open class JSONTestCase: XCTestCase { func testInteger() throws { // Initializing a JSON with an int works, but it will be cast to a double. let intJSON: JSON = 12 - let encoded = Data(try encoder.encode([intJSON])) - /* JSONEncoder currently cannot encode non-object/array top level values. - To get around this, the generated JSON will need to be wrapped in an array - and unwrapped again at the end as a workaround. - This workaround can be removed when Swift 5.3 is the minimum supported version by the BSON library. */ - expect(Double(String(data: encoded.dropFirst().dropLast(), encoding: .utf8)!)!) + let encoded = Data(try encoder.encode(intJSON)) + expect(Double(String(data: encoded, encoding: .utf8)!)!) .to(beCloseTo(12)) } func testDouble() throws { let doubleJSON: JSON = 12.3 - let encoded = Data(try encoder.encode([doubleJSON])) - expect(Double(String(data: encoded.dropFirst().dropLast(), encoding: .utf8)!)!) + let encoded = Data(try encoder.encode(doubleJSON)) + expect(Double(String(data: encoded, encoding: .utf8)!)!) .to(beCloseTo(12.3)) } func testString() throws { let stringJSON: JSON = "I am a String" - let encoded = Data(try encoder.encode([stringJSON])) - expect(String(data: encoded.dropFirst().dropLast(), encoding: .utf8)) + let encoded = Data(try encoder.encode(stringJSON)) + expect(String(data: encoded, encoding: .utf8)) .to(equal("\"I am a String\"")) } func testBool() throws { let boolJSON: JSON = true - let encoded = Data(try encoder.encode([boolJSON])) - expect(String(data: encoded.dropFirst().dropLast(), encoding: .utf8)) + let encoded = Data(try encoder.encode(boolJSON)) + expect(String(data: encoded, encoding: .utf8)) .to(equal("true")) }