diff --git a/Sources/MongoSwift/BSON/AnyBSONValue.swift b/Sources/MongoSwift/BSON/AnyBSONValue.swift deleted file mode 100644 index 27c3e55f2..000000000 --- a/Sources/MongoSwift/BSON/AnyBSONValue.swift +++ /dev/null @@ -1,194 +0,0 @@ -import Foundation - -/// A struct wrapping a `BSONValue` type that allows for encoding/ -/// decoding `BSONValue`s of unknown type. - -public struct AnyBSONValue: Codable, Equatable, Hashable { - // swiftlint:disable:next cyclomatic_complexity - public func hash(into hasher: inout Hasher) { - hasher.combine(self.value.bsonType) - - switch self.value { - case let v as Date: - hasher.combine(v) - case let v as Binary: - hasher.combine(v) - case let arr as [BSONValue]: - let mapped = arr.map { AnyBSONValue($0) } - hasher.combine(mapped) - case let v as String: - hasher.combine(v) - case let v as ObjectId: - hasher.combine(v) - case let v as Bool: - hasher.combine(v) - case let v as RegularExpression: - hasher.combine(v) - case let v as CodeWithScope: - hasher.combine(v) - case let v as Int: - hasher.combine(v) - case let v as Int32: - hasher.combine(v) - case let v as Int64: - hasher.combine(v) - case let v as Double: - hasher.combine(v) - case let v as Decimal128: - hasher.combine(v) - case let v as MinKey: - hasher.combine(v) - case let v as MaxKey: - hasher.combine(v) - case let v as Document: - hasher.combine(v) - case let v as BSONNull: - hasher.combine(v) - case let v as Timestamp: - hasher.combine(v) - case let v as BSONUndefined: - hasher.combine(v) - case let v as DBPointer: - hasher.combine(v) - default: - hasher.combine("\(self.value)") - } - } - - /// The `BSONValue` wrapped by this struct. - public let value: BSONValue - - /// Initializes a new `AnyBSONValue` wrapping the provided `BSONValue`. - public init(_ value: BSONValue) { - self.value = value - } - - public func encode(to encoder: Encoder) throws { - // short-circuit in the `BSONEncoder` case - if let bsonEncoder = encoder as? _BSONEncoder { - // Need to handle `Date`s and `UUID`s separately to respect the encoding strategy choices. - if let date = self.value as? Date { - try bsonEncoder.encode(date) - } else if let uuid = self.value as? UUID { - try bsonEncoder.encode(uuid) - } else { - bsonEncoder.storage.containers.append(self.value) - } - return - } - - // in this case, we need to wrap each value in an - // `AnyBSONValue`, before we encode, because `[BSONValue]` - // is not considered `Encodable` - if let arr = self.value as? [BSONValue] { - let mapped = arr.map { AnyBSONValue($0) } - try mapped.encode(to: encoder) - } else { - if let c = self.value as? Codable { - try c.encode(to: encoder) - } else { - throw EncodingError.invalidValue( - self.value, - EncodingError.Context(codingPath: [], - debugDescription: "Encountered a non-Codable value while encoding \(self)")) - } - } - } - - public static func == (lhs: AnyBSONValue, rhs: AnyBSONValue) -> Bool { - return lhs.value.bsonEquals(rhs.value) - } - - /** - * Initializes a new `AnyBSONValue` from a `Decoder`. - * - * Caveats for usage with `Decoder`s other than MongoSwift's `BSONDecoder` - - * 1) This method does *not* support initializing an `AnyBSONValue` wrapping - * a `Date`. This is because, in non-BSON formats, `Date`s are encoded - * as other types such as `Double` or `String`. We have no way of knowing - * which type is the intended one when decoding to a `Document`, as `Document`s - * can contain any `BSONValue` type, so for simplicity we always go with a - * `Double` or a `String` over a `Date`. - * 2) Numeric values will be attempted to be decoded in the following - * order of types: `Int`, `Int32`, `Int64`, `Double`. The first one - * that can successfully represent the value with no loss of precision will - * be used. - * - * - Throws: - * - `DecodingError` if a `BSONValue` could not be decoded from the given decoder (which is not a `BSONDecoder`). - * - `DecodingError` if a BSON datetime is encountered but a non-default date decoding strategy was set on the - * decoder (which is a `BSONDecoder`). - */ - // swiftlint:disable:next cyclomatic_complexity - public init(from decoder: Decoder) throws { - // short-circuit in the `BSONDecoder` case - if let bsonDecoder = decoder as? _BSONDecoder { - if bsonDecoder.storage.topContainer is Date { - guard case .bsonDateTime = bsonDecoder.options.dateDecodingStrategy else { - throw DecodingError.typeMismatch( - AnyBSONValue.self, - DecodingError.Context( - codingPath: bsonDecoder.codingPath, - debugDescription: "Got a BSON datetime but was expecting another format. To " + - "decode from BSON datetimes, use the default .bsonDateTime " + - "DateDecodingStrategy." - ) - ) - } - } - self.value = bsonDecoder.storage.topContainer - return - } - - let container = try decoder.singleValueContainer() - - // since we aren't sure which BSON type this is, just try decoding - // to each of them and go with the first one that succeeds - if container.decodeNil() { - self.value = BSONNull() - } else if let value = try? container.decode(String.self) { - self.value = value - } else if let value = try? container.decode(Binary.self) { - self.value = value - } else if let value = try? container.decode(ObjectId.self) { - self.value = value - } else if let value = try? container.decode(Bool.self) { - self.value = value - } else if let value = try? container.decode(RegularExpression.self) { - self.value = value - } else if let value = try? container.decode(CodeWithScope.self) { - self.value = value - } else if let value = try? container.decode(Int.self) { - self.value = value - } else if let value = try? container.decode(Int32.self) { - self.value = value - } else if let value = try? container.decode(Int64.self) { - self.value = value - } else if let value = try? container.decode(Double.self) { - self.value = value - } else if let value = try? container.decode(Decimal128.self) { - self.value = value - } else if let value = try? container.decode(MinKey.self) { - self.value = value - } else if let value = try? container.decode(MaxKey.self) { - self.value = value - } else if let value = try? container.decode([AnyBSONValue].self) { - self.value = value.map { $0.value } - } else if let value = try? container.decode(Document.self) { - self.value = value - } else if let value = try? container.decode(Timestamp.self) { - self.value = value - } else if let value = try? container.decode(BSONUndefined.self) { - self.value = value - } else if let value = try? container.decode(DBPointer.self) { - self.value = value - } else { - throw DecodingError.typeMismatch( - AnyBSONValue.self, - DecodingError.Context( - codingPath: decoder.codingPath, - debugDescription: "Encountered a value that could not be decoded to any BSON type") - ) - } - } -} diff --git a/Sources/MongoSwift/BSON/BSON.swift b/Sources/MongoSwift/BSON/BSON.swift index 430e67742..9eb926cd4 100644 --- a/Sources/MongoSwift/BSON/BSON.swift +++ b/Sources/MongoSwift/BSON/BSON.swift @@ -74,14 +74,14 @@ public enum BSON { /// Initialize a `BSON` from an integer. On 64-bit systems, this will result in an `.int64`. On 32-bit systems, /// this will result in an `.int32`. public init(_ int: Int) { - if Int.bsonType == .int32 { + if MemoryLayout.size == 4 { self = .int32(Int32(int)) } else { self = .int64(Int64(int)) } } - /// Gets the `BSONType` of this `BSON`. + /// Get the `BSONType` of this `BSON`. public var type: BSONType { return self.bsonValue.bsonType } @@ -403,8 +403,7 @@ extension BSON: ExpressibleByIntegerLiteral { extension BSON: ExpressibleByDictionaryLiteral { public init(dictionaryLiteral elements: (String, BSON)...) { - // TODO SWIFT-630: Implement this. - self = .document(Document()) + self = .document(Document(keyValuePairs: elements)) } } @@ -418,4 +417,31 @@ extension BSON: Equatable {} extension BSON: Hashable {} -// TODO SWIFT-629: Implement Codable conformance +extension BSON: Codable { + public init(from decoder: Decoder) throws { + if let bsonDecoder = decoder as? _BSONDecoder { + // This path only taken if a BSON is directly decoded at the top-level. Otherwise execution will never reach + // this point. + self = try bsonDecoder.decodeBSON() + } else { + // This path is taken no matter what when a non-BSONDecoder is used. + for bsonType in BSON.allBSONTypes { + if let value = try? bsonType.init(from: decoder) { + self = value.bson + } + } + + throw DecodingError.typeMismatch( + BSON.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Encountered a value that could not be decoded to any BSON type") + ) + } + } + + public func encode(to encoder: Encoder) throws { + // This is only reached when a non-BSON encoder is used. + try self.bsonValue.encode(to: encoder) + } +} diff --git a/Sources/MongoSwift/BSON/BSONDecoder.swift b/Sources/MongoSwift/BSON/BSONDecoder.swift index 76e9179fe..7272e7b8e 100644 --- a/Sources/MongoSwift/BSON/BSONDecoder.swift +++ b/Sources/MongoSwift/BSON/BSONDecoder.swift @@ -140,7 +140,7 @@ public class BSONDecoder { if let doc = document as? T { return doc } - let _decoder = _BSONDecoder(referencing: document, options: self.options) + let _decoder = _BSONDecoder(referencing: .document(document), options: self.options) return try type.init(from: _decoder) } @@ -240,7 +240,7 @@ internal class _BSONDecoder: Decoder { } /// Initializes `self` with the given top-level container and options. - fileprivate init(referencing container: BSONValue, + fileprivate init(referencing container: BSON, at codingPath: [CodingKey] = [], options: BSONDecoder._Options) { self.storage = _BSONDecodingStorage() @@ -251,10 +251,10 @@ internal class _BSONDecoder: Decoder { // Returns the data stored in this decoder as represented in a container keyed by the given key type. public func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { - guard let topContainer = self.storage.topContainer as? Document else { + guard let topContainer = self.storage.topContainer.documentValue else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: Document.self, - reality: self.storage.topContainer) + reality: self.storage.topContainer.bsonValue) } let container = _BSONKeyedDecodingContainer(referencing: self, wrapping: topContainer) @@ -268,10 +268,12 @@ internal class _BSONDecoder: Decoder { // Returns the data stored in this decoder in a container appropriate for holding values with no keys. public func unkeyedContainer() throws -> UnkeyedDecodingContainer { - guard let arr = self.storage.topContainer as? [BSONValue] else { - throw DecodingError._typeMismatch(at: self.codingPath, - expectation: [BSONValue].self, - reality: self.storage.topContainer) + guard let arr = self.storage.topContainer.arrayValue else { + throw DecodingError._typeMismatch( + at: self.codingPath, + expectation: [BSON].self, + reality: self.storage.topContainer.bsonValue + ) } return _BSONUnkeyedDecodingContainer(referencing: self, wrapping: arr) @@ -281,7 +283,7 @@ internal class _BSONDecoder: Decoder { // Storage for a _BSONDecoder. internal struct _BSONDecodingStorage { /// The container stack, consisting of `BSONValue`s. - fileprivate private(set) var containers: [BSONValue] = [] + fileprivate private(set) var containers: [BSON] = [] /// Initializes `self` with no containers. fileprivate init() {} @@ -290,7 +292,7 @@ internal struct _BSONDecodingStorage { fileprivate var count: Int { return self.containers.count } /// The container at the top of the stack. - internal var topContainer: BSONValue { + internal var topContainer: BSON { guard !self.containers.isEmpty else { fatalError("Empty container stack.") } @@ -299,7 +301,7 @@ internal struct _BSONDecodingStorage { } /// Adds a new container to the stack. - fileprivate mutating func push(container: BSONValue) { + fileprivate mutating func push(container: BSON) { self.containers.append(container) } @@ -314,39 +316,58 @@ internal struct _BSONDecodingStorage { /// Extend _BSONDecoder to add methods for "unboxing" values as various types. extension _BSONDecoder { - fileprivate func unboxBSONValue(_ value: BSONValue, as type: T.Type) throws -> T { + /// Unbox a type using the provided closure. + fileprivate func unboxCustom(_ value: BSON, f: (BSON) -> T?) throws -> T { // We throw in the case of BSONNull because nulls should be requested through decodeNil(). - guard !(value is BSONNull) else { + guard value != .null else { throw DecodingError.valueNotFound( - type, - DecodingError.Context(codingPath: self.codingPath, - debugDescription: "Expected a non-null type.")) + T.self, + DecodingError.Context(codingPath: self.codingPath, + debugDescription: "Expected a non-null type.")) } - guard let typed = value as? T else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) + guard let typed = f(value) else { + throw DecodingError._typeMismatch(at: self.codingPath, expectation: T.self, reality: value.bsonValue) } return typed } - fileprivate func unboxNumber(_ value: BSONValue, as type: T.Type) throws -> T { + /// Attempt to unbox a type that conforms to `BSONValue`. + fileprivate func unboxBSONValue(_ value: BSON, as type: T.Type) throws -> T { + // We throw in the case of BSONNull because nulls should be requested through decodeNil(). + guard value != .null else { + throw DecodingError.valueNotFound( + type, + DecodingError.Context(codingPath: self.codingPath, + debugDescription: "Expected a non-null type.")) + } + + guard let typed = value.bsonValue as? T else { + throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value.bsonValue) + } + return typed + } + + /// Attempt to unbox a type that conforms to `CodableNumber`. + fileprivate func unboxNumber(_ value: BSON, as type: T.Type) throws -> T { guard let primitive = T(from: value) else { - throw DecodingError._numberMismatch(at: self.codingPath, expectation: type, reality: value) + throw DecodingError._numberMismatch(at: self.codingPath, expectation: type, reality: value.bsonValue) } return primitive } - fileprivate func unboxData(_ value: BSONValue) throws -> Data { + /// Attempt to unbox a `Data` according to the set `DataDecodingStrategy`. + fileprivate func unboxData(_ value: BSON) throws -> Data { switch self.options.dataDecodingStrategy { case .deferredToData: self.storage.push(container: value) defer { self.storage.popContainer() } return try Data(from: self) case .binary: - let binary = try self.unboxBSONValue(value, as: Binary.self) + let binary = try self.unboxCustom(value) { $0.binaryValue } return binary.data case.base64: - let base64Str = try self.unboxBSONValue(value, as: String.self) + let base64Str = try self.unboxCustom(value) { $0.stringValue } guard let data = Data(base64Encoded: base64Str) else { throw DecodingError.dataCorrupted( @@ -363,11 +384,11 @@ extension _BSONDecoder { } } - /// Private helper function used specifically for decoding dates. - fileprivate func unboxDate(_ value: BSONValue) throws -> Date { + /// Attempt to unbox a `Data` according to the set `DateDecodingStrategy`. + fileprivate func unboxDate(_ value: BSON) throws -> Date { switch self.options.dateDecodingStrategy { case .bsonDateTime: - let date = try self.unboxBSONValue(value, as: Date.self) + let date = try self.unboxCustom(value) { $0.dateValue } return date case .deferredToDate: self.storage.push(container: value) @@ -383,7 +404,7 @@ extension _BSONDecoder { guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { fatalError("ISO8601DateFormatter is unavailable on this platform.") } - let isoString = try self.unboxBSONValue(value, as: String.self) + let isoString = try self.unboxCustom(value) { $0.stringValue } guard let date = BSONDecoder.iso8601Formatter.date(from: isoString) else { throw DecodingError.dataCorrupted( DecodingError.Context( @@ -399,7 +420,7 @@ extension _BSONDecoder { defer { self.storage.popContainer() } return try f(self) case .formatted(let formatter): - let dateString = try self.unboxBSONValue(value, as: String.self) + let dateString = try self.unboxCustom(value) { $0.stringValue } guard let date = formatter.date(from: dateString) else { throw DecodingError.dataCorrupted( DecodingError.Context( @@ -413,15 +434,15 @@ extension _BSONDecoder { } } - /// Private helper used specifically for decoding UUIDs. - fileprivate func unboxUUID(_ value: BSONValue) throws -> UUID { + /// Attempt to unbox a `Data` according to the set `UUIDDecodingStrategy`. + fileprivate func unboxUUID(_ value: BSON) throws -> UUID { switch self.options.uuidDecodingStrategy { case .deferredToUUID: self.storage.push(container: value) defer { self.storage.popContainer() } return try UUID(from: self) case .binary: - let binary = try self.unboxBSONValue(value, as: Binary.self) + let binary = try self.unboxCustom(value) { $0.binaryValue } do { return try UUID(from: binary) } catch { @@ -435,7 +456,7 @@ extension _BSONDecoder { } } - fileprivate func unbox(_ value: BSONValue, as type: T.Type) throws -> T { + fileprivate func unbox(_ value: BSON, as type: T.Type) throws -> T { // swiftlint:disable force_cast switch type { case is Date.Type: @@ -447,6 +468,15 @@ extension _BSONDecoder { case is Data.Type: // We know T is a Data and unboxData returns a Data or throws, so this cast will always work. return try unboxData(value) as! T + case is BSON.Type: + switch value { + case .datetime: + // We know T is a BSON so this cast will always work. + return try BSON.datetime(unboxDate(value)) as! T + default: + // We know T is a BSON so this cast will always work. + return value as! T + } default: break } @@ -454,7 +484,7 @@ extension _BSONDecoder { // if the data is already stored as the correct type in the document, then we can short-circuit // and just return the typed value here - if let val = value as? T { + if let val = value.bsonValue as? T { return val } @@ -501,7 +531,7 @@ private struct _BSONKeyedDecodingContainer: KeyedDecodingContainer /// Private helper function to check for a value in self.container. Returns the value stored /// under `key`, or throws an error if the value is not found. - private func getValue(forKey key: Key) throws -> BSONValue { + private func getValue(forKey key: Key) throws -> BSON { guard let entry = try self.container.getValue(for: key.stringValue) else { throw DecodingError.keyNotFound( key, @@ -515,7 +545,7 @@ private struct _BSONKeyedDecodingContainer: KeyedDecodingContainer private func decodeBSONType(_ type: T.Type, forKey key: Key) throws -> T { let entry = try getValue(forKey: key) return try self.decoder.with(pushedKey: key) { - try decoder.unboxBSONValue(entry, as: type) + try decoder.unboxBSONValue(entry, as: type) } } @@ -552,11 +582,11 @@ private struct _BSONKeyedDecodingContainer: KeyedDecodingContainer DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Key \(_errorDescription(of: key)) not found.")) } - return try self.container.getValue(for: key.stringValue) is BSONNull + return try self.container.getValue(for: key.stringValue) == .null } // swiftlint:disable line_length - public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { return try decodeBSONType(type, forKey: key) } + public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { return try self.decodeBSONType(Bool.self, forKey: key) } public func decode(_ type: Int.Type, forKey key: Key) throws -> Int { return try decodeNumber(type, forKey: key) } public func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { return try decodeNumber(type, forKey: key) } public func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { return try decodeNumber(type, forKey: key) } @@ -569,7 +599,7 @@ private struct _BSONKeyedDecodingContainer: KeyedDecodingContainer public func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { return try decodeNumber(type, forKey: key) } public func decode(_ type: Float.Type, forKey key: Key) throws -> Float { return try decodeNumber(type, forKey: key) } public func decode(_ type: Double.Type, forKey key: Key) throws -> Double { return try decodeNumber(type, forKey: key) } - public func decode(_ type: String.Type, forKey key: Key) throws -> String { return try decodeBSONType(type, forKey: key) } + public func decode(_ type: String.Type, forKey key: Key) throws -> String { return try self.decodeBSONType(String.self, forKey: key) } // swiftlint:enable line_length /// Returns the data stored for the given key as represented in a container keyed by the given key type. @@ -578,8 +608,10 @@ private struct _BSONKeyedDecodingContainer: KeyedDecodingContainer return try self.decoder.with(pushedKey: key) { let value = try getValue(forKey: key) - guard let doc = value as? Document else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: Document.self, reality: value) + guard let doc = value.documentValue else { + throw DecodingError._typeMismatch(at: self.codingPath, + expectation: Document.self, + reality: value.bsonValue) } let container = _BSONKeyedDecodingContainer(referencing: self.decoder, wrapping: doc) @@ -592,8 +624,10 @@ private struct _BSONKeyedDecodingContainer: KeyedDecodingContainer return try self.decoder.with(pushedKey: key) { let value = try getValue(forKey: key) - guard let array = value as? [BSONValue] else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: [BSONValue].self, reality: value) + guard let array = value.arrayValue else { + throw DecodingError._typeMismatch(at: self.codingPath, + expectation: [BSON].self, + reality: value.bsonValue) } return _BSONUnkeyedDecodingContainer(referencing: self.decoder, wrapping: array) @@ -611,7 +645,9 @@ private struct _BSONKeyedDecodingContainer: KeyedDecodingContainer ) ) } - return _BSONDecoder(referencing: value, at: self.decoder.codingPath, options: self.decoder.options) + return _BSONDecoder(referencing: value, + at: self.decoder.codingPath, + options: self.decoder.options) } } @@ -631,7 +667,7 @@ private struct _BSONUnkeyedDecodingContainer: UnkeyedDecodingContainer { private let decoder: _BSONDecoder /// A reference to the container we're reading from. - private let container: [BSONValue] + private let container: [BSON] /// The path of coding keys taken to get to this point in decoding. public private(set) var codingPath: [CodingKey] @@ -640,7 +676,7 @@ private struct _BSONUnkeyedDecodingContainer: UnkeyedDecodingContainer { public private(set) var currentIndex: Int /// Initializes `self` by referencing the given decoder and container. - fileprivate init(referencing decoder: _BSONDecoder, wrapping container: [BSONValue]) { + fileprivate init(referencing decoder: _BSONDecoder, wrapping container: [BSON]) { self.decoder = decoder self.container = container self.codingPath = decoder.codingPath @@ -660,7 +696,7 @@ private struct _BSONUnkeyedDecodingContainer: UnkeyedDecodingContainer { private func checkAtEnd() throws { guard !self.isAtEnd else { throw DecodingError.valueNotFound( - BSONValue.self, + BSON.self, DecodingError.Context(codingPath: self.decoder.codingPath + [_BSONKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) } @@ -707,7 +743,7 @@ private struct _BSONUnkeyedDecodingContainer: UnkeyedDecodingContainer { public mutating func decodeNil() throws -> Bool { try self.checkAtEnd() - if self.container[self.currentIndex] is BSONNull { + if self.container[self.currentIndex] == .null { self.currentIndex += 1 return true } @@ -746,7 +782,7 @@ private struct _BSONUnkeyedDecodingContainer: UnkeyedDecodingContainer { public mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { return try self.decoder.with(pushedKey: _BSONKey(index: self.currentIndex)) { try self.checkAtEnd() - let array = try self.decodeBSONType([BSONValue].self) + let array = try self.decodeBSONType([BSON].self) self.currentIndex += 1 return _BSONUnkeyedDecodingContainer(referencing: self.decoder, wrapping: array) } @@ -775,6 +811,14 @@ extension _BSONDecoder: SingleValueDecodingContainer { } } + /// Internal method used to decode directly to a `BSON`. + internal func decodeBSON() throws -> BSON { + if self.decodeNil() { + return .null + } + return try self.unbox(self.storage.topContainer, as: BSON.self) + } + /// Decode a BSONValue type from this container. private func decodeBSONType(_ type: T.Type) throws -> T { try expectNonNull(T.self) @@ -794,7 +838,7 @@ extension _BSONDecoder: SingleValueDecodingContainer { } /// Decode a null value from this container. - public func decodeNil() -> Bool { return self.storage.topContainer is BSONNull } + public func decodeNil() -> Bool { return self.storage.topContainer == .null } /// Decode all the required types from this container using the helpers defined above. public func decode(_ type: Bool.Type) throws -> Bool { return try decodeBSONType(type) } diff --git a/Sources/MongoSwift/BSON/BSONEncoder.swift b/Sources/MongoSwift/BSON/BSONEncoder.swift index a44128212..bdf0a98cf 100644 --- a/Sources/MongoSwift/BSON/BSONEncoder.swift +++ b/Sources/MongoSwift/BSON/BSONEncoder.swift @@ -140,15 +140,10 @@ public class BSONEncoder { */ public func encode(_ value: T) throws -> Document { // if the value being encoded is already a `Document` we're done - switch value { - case let doc as Document: + if let doc = value as? Document { + return doc + } else if let bson = value as? BSON, let doc = bson.documentValue { return doc - case let abv as AnyBSONValue: - if let doc = abv.value as? Document { - return doc - } - default: - break } let encoder = _BSONEncoder(options: self.options) @@ -474,6 +469,12 @@ extension _BSONEncoder { switch value { case let date as Date: return try boxDate(date) + case let bson as BSON: + if case let .datetime(d) = bson { + return try boxDate(d) + } else { + return bson.bsonValue + } case let uuid as UUID: return try boxUUID(uuid) case let data as Data: @@ -482,15 +483,11 @@ extension _BSONEncoder { break } - // if it's already a `BSONValue`, just return it, unless if it is an - // array. technically `[Any]` is a `BSONValue`, but we can only use this - // short-circuiting if all the elements are actually BSONValues. - if let bsonValue = value as? BSONValue, !(bsonValue is [Any]) { + // if it's already a `BSONValue`, just return it. + if let bsonValue = value as? BSONValue { return bsonValue - } - - if let bsonArray = value as? [BSONValue] { - return bsonArray + } else if let bsonArray = value as? [BSONValue] { + return bsonArray.map { $0.bson } } // The value should request a container from the _BSONEncoder. @@ -534,7 +531,9 @@ private struct _BSONKeyedEncodingContainer: KeyedEncodingContainer public mutating func encodeNil(forKey key: Key) throws { self.container[key.stringValue] = BSONNull() } public mutating func encode(_ value: Bool, forKey key: Key) throws { self.container[key.stringValue] = value } - public mutating func encode(_ value: Int, forKey key: Key) throws { self.container[key.stringValue] = value } + public mutating func encode(_ value: Int, forKey key: Key) throws { + self.container[key.stringValue] = BSON(value).bsonValue + } public mutating func encode(_ value: Int8, forKey key: Key) throws { try self.encodeNumber(value, forKey: key) } public mutating func encode(_ value: Int16, forKey key: Key) throws { try self.encodeNumber(value, forKey: key) } public mutating func encode(_ value: Int32, forKey key: Key) throws { self.container[key.stringValue] = value } @@ -617,7 +616,7 @@ private struct _BSONUnkeyedEncodingContainer: UnkeyedEncodingContainer { public mutating func encodeNil() throws { self.container.add(BSONNull()) } public mutating func encode(_ value: Bool) throws { self.container.add(value) } - public mutating func encode(_ value: Int) throws { self.container.add(value) } + public mutating func encode(_ value: Int) throws { self.container.add(BSON(value).bsonValue) } public mutating func encode(_ value: Int8) throws { try self.encodeNumber(value) } public mutating func encode(_ value: Int16) throws { try self.encodeNumber(value) } public mutating func encode(_ value: Int32) throws { self.container.add(value) } @@ -686,7 +685,7 @@ extension _BSONEncoder: SingleValueEncodingContainer { } public func encode(_ value: Bool) throws { try self.encodeBSONType(value) } - public func encode(_ value: Int) throws { try self.encodeBSONType(value) } + public func encode(_ value: Int) throws { try self.encodeNumber(value) } public func encode(_ value: Int8) throws { try self.encodeNumber(value) } public func encode(_ value: Int16) throws { try self.encodeNumber(value) } public func encode(_ value: Int32) throws { try self.encodeBSONType(value) } @@ -720,54 +719,54 @@ extension _BSONEncoder: SingleValueEncodingContainer { /// encoder storage purposes. We use this rather than NSMutableArray because /// it allows us to preserve Swift type information. private class MutableArray: BSONValue { - var bsonType: BSONType { return .array } + fileprivate static var bsonType: BSONType { return .array } + + fileprivate var bson: BSON { return .array(self.array.map { $0.bson }) } - var array = [BSONValue]() + fileprivate var array = [BSONValue]() fileprivate func add(_ value: BSONValue) { array.append(value) } - var count: Int { return array.count } + fileprivate var count: Int { return array.count } - func insert(_ value: BSONValue, at index: Int) { + fileprivate func insert(_ value: BSONValue, at index: Int) { self.array.insert(value, at: index) } - func encode(to storage: DocumentStorage, forKey key: String) throws { - try self.array.encode(to: storage, forKey: key) - } - - init() {} + fileprivate init() {} /// methods required by the BSONValue protocol that we don't actually need/use. MutableArray /// is just a BSONValue to simplify usage alongside true BSONValues within the encoder. - public static func from(iterator iter: DocumentIterator) -> Self { + fileprivate func encode(to storage: DocumentStorage, forKey key: String) throws { + fatalError("`MutableArray` is not meant to be encoded to a `DocumentStorage`") + } + internal static func from(iterator iter: DocumentIterator) -> BSON { fatalError("`MutableArray` is not meant to be initialized from a `DocumentIterator`") } - func encode(to encoder: Encoder) throws { + fileprivate func encode(to encoder: Encoder) throws { fatalError("`MutableArray` is not meant to be encoded with an `Encoder`") } required convenience init(from decoder: Decoder) throws { fatalError("`MutableArray` is not meant to be initialized from a `Decoder`") } - - func bsonEquals(_ other: BSONValue?) -> Bool { - return self.array.bsonEquals(other) - } } /// A private class wrapping a Swift dictionary so we can pass it by reference /// for encoder storage purposes. We use this rather than NSMutableDictionary /// because it allows us to preserve Swift type information. private class MutableDictionary: BSONValue { - var bsonType: BSONType { return .document } + fileprivate static var bsonType: BSONType { return .document } + + // This is unused + fileprivate var bson: BSON { return .document(self.asDocument()) } // rather than using a dictionary, do this so we preserve key orders - var keys = [String]() - var values = [BSONValue]() + fileprivate var keys = [String]() + fileprivate var values = [BSONValue]() - subscript(key: String) -> BSONValue? { + fileprivate subscript(key: String) -> BSONValue? { get { guard let index = keys.firstIndex(of: key) else { return nil @@ -789,36 +788,29 @@ private class MutableDictionary: BSONValue { } /// Converts self to a `Document` with equivalent key-value pairs. - func asDocument() -> Document { + fileprivate func asDocument() -> Document { var doc = Document() for i in 0 ..< keys.count { - doc[keys[i]] = values[i] + doc[keys[i]] = values[i].bson } return doc } - func encode(to storage: DocumentStorage, forKey key: String) throws { - try self.asDocument().encode(to: storage, forKey: key) - } - - init() {} - - func bsonEquals(_ other: BSONValue?) -> Bool { - guard let otherDict = other as? MutableDictionary else { - return false - } - return otherDict.keys == self.keys && otherDict.values.bsonEquals(self.values) - } + fileprivate init() {} /// methods required by the BSONValue protocol that we don't actually need/use. MutableDictionary /// is just a BSONValue to simplify usage alongside true BSONValues within the encoder. - public static func from(iterator iter: DocumentIterator) -> Self { + fileprivate func encode(to storage: DocumentStorage, forKey key: String) throws { + fatalError("`MutableDictionary` is not meant to be encoded to a `DocumentStorage`") + } + + internal static func from(iterator iter: DocumentIterator) -> BSON { fatalError("`MutableDictionary` is not meant to be initialized from a `DocumentIterator`") } - func encode(to encoder: Encoder) throws { + fileprivate func encode(to encoder: Encoder) throws { fatalError("`MutableDictionary` is not meant to be encoded with an `Encoder`") } - required convenience init(from decoder: Decoder) throws { + fileprivate required convenience init(from decoder: Decoder) throws { fatalError("`MutableDictionary` is not meant to be initialized from a `Decoder`") } } diff --git a/Sources/MongoSwift/BSON/BSONValue.swift b/Sources/MongoSwift/BSON/BSONValue.swift index 92c6fa759..8c5a29b24 100644 --- a/Sources/MongoSwift/BSON/BSONValue.swift +++ b/Sources/MongoSwift/BSON/BSONValue.swift @@ -4,56 +4,59 @@ import Foundation /// The possible types of BSON values and their corresponding integer values. public enum BSONType: UInt32 { /// An invalid type - case invalid = 0x00, + case invalid = 0x00 /// 64-bit binary floating point - double = 0x01, + case double = 0x01 /// UTF-8 string - string = 0x02, + case string = 0x02 /// BSON document - document = 0x03, + case document = 0x03 /// Array - array = 0x04, + case array = 0x04 /// Binary data - binary = 0x05, + case binary = 0x05 /// Undefined value - deprecated - undefined = 0x06, + case undefined = 0x06 /// A MongoDB ObjectId. /// - SeeAlso: https://docs.mongodb.com/manual/reference/method/ObjectId/ - objectId = 0x07, + case objectId = 0x07 /// A boolean - bool = 0x08, + case bool = 0x08 /// UTC datetime, stored as UTC milliseconds since the Unix epoch - datetime = 0x09, + case datetime = 0x09 /// Null value - null = 0x0a, + case null = 0x0a /// A regular expression - regex = 0x0b, + case regex = 0x0b /// A database pointer - deprecated - dbPointer = 0x0c, + case dbPointer = 0x0c /// Javascript code - code = 0x0d, + case code = 0x0d /// A symbol - deprecated - symbol = 0x0e, + case symbol = 0x0e /// JavaScript code w/ scope - codeWithScope = 0x0f, + case codeWithScope = 0x0f /// 32-bit integer - int32 = 0x10, + case int32 = 0x10 /// Special internal type used by MongoDB replication and sharding - timestamp = 0x11, + case timestamp = 0x11 /// 64-bit integer - int64 = 0x12, + case int64 = 0x12 /// 128-bit decimal floating point - decimal128 = 0x13, + case decimal128 = 0x13 /// Special type which compares lower than all other possible BSON element values - minKey = 0xff, + case minKey = 0xff /// Special type which compares higher than all other possible BSON element values - maxKey = 0x7f + case maxKey = 0x7f } /// A protocol all types representing `BSONType`s must implement. -public protocol BSONValue { +internal protocol BSONValue: Codable { /// The `BSONType` of this value. - var bsonType: BSONType { get } + static var bsonType: BSONType { get } + + /// A corresponding `BSON` to this `BSONValue`. + var bson: BSON { get } /** * Given the `DocumentStorage` backing a `Document`, appends this `BSONValue` to the end. @@ -69,20 +72,6 @@ public protocol BSONValue { */ func encode(to storage: DocumentStorage, forKey key: String) throws - /** - * Function to test equality with another `BSONValue`. This function tests for exact BSON equality. - * This means that differing types with equivalent value are not equivalent. - * - * e.g. - * 4.0 (Double) != 4 (Int) - * - * - Parameters: - * - other: The right-hand-side `BSONValue` to compare. - * - * - Returns: `true` if `self` is equal to `rhs`, `false` otherwise. - */ - func bsonEquals(_ other: BSONValue?) -> Bool - /** * Given a `DocumentIterator` known to have a next value of this type, * initializes the value. @@ -90,98 +79,29 @@ public protocol BSONValue { * - Throws: `UserError.logicError` if the current type of the `DocumentIterator` does not correspond to the * associated type of this `BSONValue`. */ - static func from(iterator iter: DocumentIterator) throws -> Self + static func from(iterator iter: DocumentIterator) throws -> BSON } -extension BSONValue where Self: Equatable { - /// Default implementation of `bsonEquals` for `BSONValue`s that conform to `Equatable`. - public func bsonEquals(_ other: BSONValue?) -> Bool { - guard let otherAsSelf = other as? Self else { - return false - } - return self == otherAsSelf +extension BSONValue { + internal var bsonType: BSONType { + return type(of: self).bsonType } } -/// A protocol that numeric `BSONValue`s should conform to. It provides functionality for converting to BSON's native -/// number types. -public protocol BSONNumber: BSONValue { - /// Create an `Int` from this `BSONNumber`. - /// This will return nil if the conversion cannot result in an exact representation. - var intValue: Int? { get } - - /// Create an `Int32` from this `BSONNumber`. - /// This will return nil if the conversion cannot result in an exact representation. - var int32Value: Int32? { get } - - /// Create an `Int64` from this `BSONNumber`. - /// This will return nil if the conversion cannot result in an exact representation. - var int64Value: Int64? { get } - - /// Create a `Double` from this `BSONNumber`. - /// This will return nil if the conversion cannot result in an exact representation. - var doubleValue: Double? { get } - - /// Create a `Decimal128` from this `BSONNumber`. - /// This will return nil if the conversion cannot result in an exact representation. - var decimal128Value: Decimal128? { get } -} - -/// Default conformance to `BSONNumber` for `BinaryInteger`s. -extension BSONNumber where Self: BinaryInteger { - /// Create an `Int` from this `BinaryInteger`. - /// This will return nil if the conversion cannot result in an exact representation. - public var intValue: Int? { return Int(exactly: self) } - - /// Create an `Int32` from this `BinaryInteger`. - /// This will return nil if the conversion cannot result in an exact representation. - public var int32Value: Int32? { return Int32(exactly: self) } - - /// Create an `Int64` from this `BinaryInteger`. - /// This will return nil if the conversion cannot result in an exact representation. - public var int64Value: Int64? { return Int64(exactly: self) } - - /// Create a `Double` from this `BinaryInteger`. - /// This will return nil if the conversion cannot result in an exact representation. - public var doubleValue: Double? { return Double(exactly: self) } -} - -/// Default conformance to `BSONNumber` for `BinaryFloatingPoint`s. -extension BSONNumber where Self: BinaryFloatingPoint { - /// Create an `Int` from this `BinaryFloatingPoint`. - /// This will return nil if the conversion cannot result in an exact representation. - public var intValue: Int? { return Int(exactly: self) } - - /// Create an `Int32` from this `BinaryFloatingPoint`. - /// This will return nil if the conversion cannot result in an exact representation. - public var int32Value: Int32? { return Int32(exactly: self) } - - /// Create an `Int64` from this `BinaryFloatingPoint`. - /// This will return nil if the conversion cannot result in an exact representation. - public var int64Value: Int64? { return Int64(exactly: self) } - - /// Create a `Double` from this `BinaryFloatingPoint`. - /// This will return nil if the conversion cannot result in an exact representation. - public var doubleValue: Double? { return Double(self) } -} - -/// Default implementation of `Decimal128` conversions for all `Numeric`s. -extension BSONNumber where Self: Numeric { - /// Create a `Decimal128` from this `Numeric`. - /// This will return nil if the conversion cannot result in an exact representation. - public var decimal128Value: Decimal128? { return Decimal128(String(describing: self)) } -} - /// An extension of `Array` to represent the BSON array type. -extension Array: BSONValue { - public var bsonType: BSONType { return .array } +extension Array: BSONValue where Element == BSON { + internal static var bsonType: BSONType { return .array } + + internal var bson: BSON { + return .array(self) + } - public static func from(iterator iter: DocumentIterator) throws -> Array { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .array else { throw wrongIterTypeError(iter, expected: Array.self) } - return try iter.withBSONIterPointer { iterPtr in + return .array(try iter.withBSONIterPointer { iterPtr in var length: UInt32 = 0 let array = UnsafeMutablePointer?>.allocate(capacity: 1) defer { @@ -197,26 +117,14 @@ extension Array: BSONValue { } let arrDoc = Document(stealing: arrayData) - - guard let arr = arrDoc.values as? Array else { - fatalError("Failed to cast values for document \(arrDoc) to array") - } - - return arr - } + return arrDoc.values + }) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { var arr = Document() for (i, v) in self.enumerated() { - guard let val = v as? BSONValue else { - throw UserError.logicError( - message: "Cannot encode a non-BSONValue array element: \(String(describing: v)) " - + "with type: \(type(of: v)) " - + "at index: \(i)" - ) - } - try arr.setValue(for: String(i), to: val) + try arr.setValue(for: String(i), to: v) } guard bson_append_array(storage._bson, key, Int32(key.utf8.count), arr._bson) else { @@ -224,23 +132,30 @@ extension Array: BSONValue { } } - public func bsonEquals(_ other: BSONValue?) -> Bool { - guard let otherArr = other as? [BSONValue], let selfArr = self as? [BSONValue] else { - return false + /// Attempts to map this `[BSON]` to a `[T]`, where `T` is a `BSONValue`. + internal func asArrayOf(_ type: T.Type) -> [T]? { + var result: [T] = [] + for element in self { + guard let bsonValue = element.bsonValue as? T else { + return nil + } + result.append(bsonValue) } - return self.count == otherArr.count && zip(selfArr, otherArr).allSatisfy { lhs, rhs in lhs.bsonEquals(rhs) } + return result } } /// A struct to represent the BSON null type. -public struct BSONNull: BSONValue, Codable, Equatable { - public var bsonType: BSONType { return .null } +internal struct BSONNull: BSONValue, Codable, Equatable { + internal static var bsonType: BSONType { return .null } + + internal var bson: BSON { return .null } - public static func from(iterator iter: DocumentIterator) throws -> BSONNull { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .null else { throw wrongIterTypeError(iter, expected: BSONNull.self) } - return BSONNull() + return .null } /// Initializes a new `BSONNull` instance. @@ -254,7 +169,7 @@ public struct BSONNull: BSONValue, Codable, Equatable { throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_null(storage._bson, key, Int32(key.utf8.count)) else { throw bsonTooLargeError(value: self, forKey: key) } @@ -270,7 +185,9 @@ extension BSONNull: Hashable { /// A struct to represent the BSON Binary type. public struct Binary: BSONValue, Equatable, Codable, Hashable { - public var bsonType: BSONType { return .binary } + internal static var bsonType: BSONType { return .binary } + + internal var bson: BSON { return .binary(self) } /// The binary data. public let data: Data @@ -359,7 +276,7 @@ public struct Binary: BSONValue, Equatable, Codable, Hashable { throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { let subtype = bson_subtype_t(UInt32(self.subtype)) let length = self.data.count let byteArray = [UInt8](self.data) @@ -368,12 +285,12 @@ public struct Binary: BSONValue, Equatable, Codable, Hashable { } } - public static func from(iterator iter: DocumentIterator) throws -> Binary { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .binary else { throw wrongIterTypeError(iter, expected: Binary.self) } - return try iter.withBSONIterPointer { iterPtr in + return .binary(try iter.withBSONIterPointer { iterPtr in var subtype = bson_subtype_t(rawValue: 0) var length: UInt32 = 0 let dataPointer = UnsafeMutablePointer?>.allocate(capacity: 1) @@ -390,34 +307,38 @@ public struct Binary: BSONValue, Equatable, Codable, Hashable { let dataObj = Data(bytes: data, count: Int(length)) return try self.init(data: dataObj, subtype: UInt8(subtype.rawValue)) - } + }) } } /// An extension of `Bool` to represent the BSON Boolean type. extension Bool: BSONValue { - public var bsonType: BSONType { return .bool } + internal static var bsonType: BSONType { return .bool } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal var bson: BSON { return .bool(self) } + + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_bool(storage._bson, key, Int32(key.utf8.count), self) else { throw bsonTooLargeError(value: self, forKey: key) } } - public static func from(iterator iter: DocumentIterator) throws -> Bool { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .bool else { throw wrongIterTypeError(iter, expected: Bool.self) } - return iter.withBSONIterPointer { iterPtr in + return .bool(iter.withBSONIterPointer { iterPtr in self.init(bson_iter_bool(iterPtr)) - } + }) } } /// An extension of `Date` to represent the BSON Datetime type. Supports millisecond level precision. extension Date: BSONValue { - public var bsonType: BSONType { return .datetime } + internal static var bsonType: BSONType { return .datetime } + + internal var bson: BSON { return .datetime(self) } /// Initializes a new `Date` representing the instance `msSinceEpoch` milliseconds /// since the Unix epoch. @@ -428,27 +349,29 @@ extension Date: BSONValue { /// The number of milliseconds after the Unix epoch that this `Date` occurs. public var msSinceEpoch: Int64 { return Int64((self.timeIntervalSince1970 * 1000.0).rounded()) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_date_time(storage._bson, key, Int32(key.utf8.count), self.msSinceEpoch) else { throw bsonTooLargeError(value: self, forKey: key) } } - public static func from(iterator iter: DocumentIterator) throws -> Date { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .datetime else { throw wrongIterTypeError(iter, expected: Date.self) } - return iter.withBSONIterPointer { iterPtr in + return .datetime(iter.withBSONIterPointer { iterPtr in self.init(msSinceEpoch: bson_iter_date_time(iterPtr)) - } + }) } } /// A struct to represent the deprecated DBPointer type. /// DBPointers cannot be instantiated, but they can be read from existing documents that contain them. public struct DBPointer: BSONValue, Codable, Equatable, Hashable { - public var bsonType: BSONType { return .dbPointer } + internal static var bsonType: BSONType { return .dbPointer } + + internal var bson: BSON { return .dbPointer(self) } /// Destination namespace of the pointer. public let ref: String @@ -469,7 +392,7 @@ public struct DBPointer: BSONValue, Codable, Equatable, Hashable { throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { try withUnsafePointer(to: id.oid) { oidPtr in guard bson_append_dbpointer(storage._bson, key, Int32(key.utf8.count), self.ref, oidPtr) else { throw bsonTooLargeError(value: self, forKey: key) @@ -477,7 +400,7 @@ public struct DBPointer: BSONValue, Codable, Equatable, Hashable { } } - public static func from(iterator iter: DocumentIterator) throws -> DBPointer { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { return try iter.withBSONIterPointer { iterPtr in var length: UInt32 = 0 let collectionPP = UnsafeMutablePointer?>.allocate(capacity: 1) @@ -498,14 +421,16 @@ public struct DBPointer: BSONValue, Codable, Equatable, Hashable { throw wrongIterTypeError(iter, expected: DBPointer.self) } - return DBPointer(ref: String(cString: collectionP), id: ObjectId(bsonOid: oidP.pointee)) + return .dbPointer(DBPointer(ref: String(cString: collectionP), id: ObjectId(bsonOid: oidP.pointee))) } } } /// A struct to represent the BSON Decimal128 type. -public struct Decimal128: BSONNumber, Equatable, Codable, CustomStringConvertible { - public var bsonType: BSONType { return .decimal128 } +public struct Decimal128: BSONValue, Equatable, Codable, CustomStringConvertible { + internal static var bsonType: BSONType { return .decimal128 } + + internal var bson: BSON { return .decimal128(self) } public var description: String { var str = Data(count: Int(BSON_DECIMAL128_STRING)) @@ -543,7 +468,7 @@ public struct Decimal128: BSONNumber, Equatable, Codable, CustomStringConvertibl throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { try withUnsafePointer(to: self.decimal128) { ptr in guard bson_append_decimal128(storage._bson, key, Int32(key.utf8.count), ptr) else { throw bsonTooLargeError(value: self, forKey: key) @@ -567,15 +492,15 @@ public struct Decimal128: BSONNumber, Equatable, Codable, CustomStringConvertibl return lhs.decimal128.low == rhs.decimal128.low && lhs.decimal128.high == rhs.decimal128.high } - public static func from(iterator iter: DocumentIterator) throws -> Decimal128 { - return try iter.withBSONIterPointer { iterPtr in + internal static func from(iterator iter: DocumentIterator) throws -> BSON { + return .decimal128(try iter.withBSONIterPointer { iterPtr in var value = bson_decimal128_t() guard bson_iter_decimal128(iterPtr, &value) else { throw wrongIterTypeError(iter, expected: Decimal128.self) } return Decimal128(bsonDecimal: value) - } + }) } } @@ -586,223 +511,90 @@ extension Decimal128: Hashable { } } -/// Extension of `Decimal128` to add `BSONNumber` conformance. -/// TODO: implement the missing converters (SWIFT-367) -extension Decimal128 { - /// Create an `Int` from this `Decimal128`. - /// Note: this function is not implemented yet and will always return nil. - public var intValue: Int? { return nil } - - /// Create an `Int32` from this `Decimal128`. - /// Note: this function is not implemented yet and will always return nil. - public var int32Value: Int32? { return nil } - - /// Create an `Int64` from this `Decimal128`. - /// Note: this function is not implemented yet and will always return nil. - public var int64Value: Int64? { return nil } - - /// Create a `Double` from this `Decimal128`. - /// Note: this function is not implemented yet and will always return nil. - public var doubleValue: Double? { return nil } - - /// Returns this `Decimal128`. - /// This is implemented as part of `BSONNumber` conformance. - public var decimal128Value: Decimal128? { return self } -} - /// An extension of `Double` to represent the BSON Double type. -extension Double: BSONNumber { - public var bsonType: BSONType { return .double } +extension Double: BSONValue { + internal static var bsonType: BSONType { return .double } + + internal var bson: BSON { return .double(self) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_double(storage._bson, key, Int32(key.utf8.count), self) else { throw bsonTooLargeError(value: self, forKey: key) } } - public static func from(iterator iter: DocumentIterator) throws -> Double { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .double else { throw wrongIterTypeError(iter, expected: Double.self) } - return iter.withBSONIterPointer { iterPtr in + return .double(iter.withBSONIterPointer { iterPtr in self.init(bson_iter_double(iterPtr)) - } - } -} - -/// An extension of `Int` to represent the BSON Int32 or Int64 type. -/// On 64-bit systems, `Int` corresponds to a BSON Int64. On 32-bit systems, it corresponds to a BSON Int32. -extension Int: BSONNumber { - /// `Int` corresponds to a BSON int32 or int64 depending upon whether the compilation system is 32 or 64 bit. - /// Use MemoryLayout instead of Int.bitWidth to avoid a compiler warning. - /// See: https://forums.swift.org/t/how-can-i-condition-on-the-size-of-int/9080/4 - internal static var bsonType: BSONType { - return MemoryLayout.size == 4 ? .int32 : .int64 - } - - public var bsonType: BSONType { return Int.bsonType } - - // Return this `Int` as an `Int32` on 32-bit systems or an `Int64` on 64-bit systems - internal var typedValue: BSONNumber { - if self.bsonType == .int64 { - return Int64(self) - } - return Int32(self) - } - - public func encode(to storage: DocumentStorage, forKey key: String) throws { - try self.typedValue.encode(to: storage, forKey: key) - } - - public func bsonEquals(_ other: BSONValue?) -> Bool { - guard let other = other, other.bsonType == self.bsonType else { - return false - } - - if let otherInt = other as? Int { - return self == otherInt - } - - switch (self.typedValue, other) { - case let (self32 as Int32, other32 as Int32): - return self32 == other32 - case let (self64 as Int64, other64 as Int64): - return self64 == other64 - default: - return false - } - } - - public static func from(iterator iter: DocumentIterator) throws -> Int { - var val: Int? - if Int.bsonType == .int64 { - val = Int(exactly: try Int64.from(iterator: iter)) - } else { - val = Int(exactly: try Int32.from(iterator: iter)) - } - - guard let out = val else { - // This should not occur - throw RuntimeError.internalError(message: "Couldn't read `Int` from Document") - } - return out + }) } } /// An extension of `Int32` to represent the BSON Int32 type. -extension Int32: BSONNumber { - public var bsonType: BSONType { return .int32 } +extension Int32: BSONValue { + internal static var bsonType: BSONType { return .int32 } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal var bson: BSON { return .int32(self) } + + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_int32(storage._bson, key, Int32(key.utf8.count), self) else { throw bsonTooLargeError(value: self, forKey: key) } } - public static func from(iterator iter: DocumentIterator) throws -> Int32 { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .int32 else { throw wrongIterTypeError(iter, expected: Int32.self) } - return iter.withBSONIterPointer { iterPtr in + return .int32(iter.withBSONIterPointer { iterPtr in self.init(bson_iter_int32(iterPtr)) - } - } - - public func bsonEquals(_ other: BSONValue?) -> Bool { - if let other32 = other as? Int32 { - return self == other32 - } else if let otherInt = other as? Int { - return self == otherInt.typedValue as? Int32 - } - return false + }) } } /// An extension of `Int64` to represent the BSON Int64 type. -extension Int64: BSONNumber { - public var bsonType: BSONType { return .int64 } +extension Int64: BSONValue { + internal static var bsonType: BSONType { return .int64 } + + internal var bson: BSON { return .int64(self) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_int64(storage._bson, key, Int32(key.utf8.count), self) else { throw bsonTooLargeError(value: self, forKey: key) } } - public static func from(iterator iter: DocumentIterator) throws -> Int64 { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .int64 else { throw wrongIterTypeError(iter, expected: Int64.self) } - return iter.withBSONIterPointer { iterPtr in + return .int64(iter.withBSONIterPointer { iterPtr in self.init(bson_iter_int64(iterPtr)) - } - } - - public func bsonEquals(_ other: BSONValue?) -> Bool { - if let other64 = other as? Int64 { - return self == other64 - } else if let otherInt = other as? Int { - return self == otherInt.typedValue as? Int64 - } - return false + }) } } -/// A struct to represent the BSON Code type. -public struct Code: BSONValue, Equatable, Codable, Hashable { - public var bsonType: BSONType { return .code } +/// A struct to represent BSON CodeWithScope. +public struct CodeWithScope: BSONValue, Equatable, Codable, Hashable { + internal static var bsonType: BSONType { return .codeWithScope } - internal var bson: BSON { return .code(self) } + internal var bson: BSON { return .codeWithScope(self) } /// A string containing Javascript code. public let code: String - /// Initializes a `CodeWithScope` with an optional scope value. - public init(code: String) { - self.code = code - } - - public init(from decoder: Decoder) throws { - throw getDecodingError(type: Code.self, decoder: decoder) - } - - public func encode(to: Encoder) throws { - throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) - } - - public func encode(to storage: DocumentStorage, forKey key: String) throws { - guard bson_append_code(storage._bson, key, Int32(key.utf8.count), self.code) else { - throw bsonTooLargeError(value: self, forKey: key) - } - } - - public static func from(iterator iter: DocumentIterator) throws -> Code { - return try iter.withBSONIterPointer { iterPtr in - guard iter.currentType == .code else { - throw wrongIterTypeError(iter, expected: Code.self) - } - let code = String(cString: bson_iter_code(iterPtr, nil)) - return self.init(code: code) - } - } -} - -/// A struct to represent the BSON Code and CodeWithScope types. -public struct CodeWithScope: BSONValue, Equatable, Codable, Hashable { - /// A string containing Javascript code. - public let code: String /// An optional scope `Document` containing a mapping of identifiers to values, /// representing the context in which `code` should be evaluated. - public let scope: Document? - - public var bsonType: BSONType { - return self.scope == nil ? .code : .codeWithScope - } + public let scope: Document /// Initializes a `CodeWithScope` with an optional scope value. - public init(code: String, scope: Document? = nil) { + public init(code: String, scope: Document) { self.code = code self.scope = scope } @@ -815,26 +607,15 @@ public struct CodeWithScope: BSONValue, Equatable, Codable, Hashable { throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { - if let s = self.scope { - guard bson_append_code_with_scope(storage._bson, key, Int32(key.utf8.count), self.code, s._bson) else { - throw bsonTooLargeError(value: self, forKey: key) - } - } else { - guard bson_append_code(storage._bson, key, Int32(key.utf8.count), self.code) else { - throw bsonTooLargeError(value: self, forKey: key) - } + internal func encode(to storage: DocumentStorage, forKey key: String) throws { + guard bson_append_code_with_scope(storage._bson, key, Int32(key.utf8.count), self.code, self.scope._bson) else { + throw bsonTooLargeError(value: self, forKey: key) } } - public static func from(iterator iter: DocumentIterator) throws -> CodeWithScope { - return try iter.withBSONIterPointer { iterPtr in + internal static func from(iterator iter: DocumentIterator) throws -> BSON { + return .codeWithScope(try iter.withBSONIterPointer { iterPtr in var length: UInt32 = 0 - if iter.currentType.rawValue == BSONType.code.rawValue { - let code = String(cString: bson_iter_code(iterPtr, &length)) - return self.init(code: code) - } - guard iter.currentType == .codeWithScope else { throw wrongIterTypeError(iter, expected: CodeWithScope.self) } @@ -853,17 +634,56 @@ public struct CodeWithScope: BSONValue, Equatable, Codable, Hashable { let scopeDoc = Document(stealing: scopeData) return self.init(code: code, scope: scopeDoc) + }) + } +} + +/// A struct to represent the BSON Code type. +public struct Code: BSONValue, Equatable, Codable, Hashable { + internal static var bsonType: BSONType { return .code } + + internal var bson: BSON { return .code(self) } + + /// A string containing Javascript code. + public let code: String + + /// Initializes a `CodeWithScope` with an optional scope value. + public init(code: String) { + self.code = code + } + + public init(from decoder: Decoder) throws { + throw getDecodingError(type: CodeWithScope.self, decoder: decoder) + } + + public func encode(to: Encoder) throws { + throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) + } + + internal func encode(to storage: DocumentStorage, forKey key: String) throws { + guard bson_append_code(storage._bson, key, Int32(key.utf8.count), self.code) else { + throw bsonTooLargeError(value: self, forKey: key) } } + + internal static func from(iterator iter: DocumentIterator) throws -> BSON { + return .code(try iter.withBSONIterPointer { iterPtr in + guard iter.currentType == .code else { + throw wrongIterTypeError(iter, expected: Code.self) + } + let code = String(cString: bson_iter_code(iterPtr, nil)) + return self.init(code: code) + }) + } } /// A struct to represent the BSON MaxKey type. -public struct MaxKey: BSONValue, Equatable, Codable, Hashable { - private var maxKey = 1 +internal struct MaxKey: BSONValue, Equatable, Codable, Hashable { + internal var bson: BSON { return .maxKey } - public var bsonType: BSONType { return .maxKey } + internal static var bsonType: BSONType { return .maxKey } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_maxkey(storage._bson, key, Int32(key.utf8.count)) else { throw bsonTooLargeError(value: self, forKey: key) } @@ -880,21 +700,21 @@ public struct MaxKey: BSONValue, Equatable, Codable, Hashable { throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) } - public static func from(iterator iter: DocumentIterator) throws -> MaxKey { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .maxKey else { throw wrongIterTypeError(iter, expected: MaxKey.self) } - return MaxKey() + return .maxKey } } /// A struct to represent the BSON MinKey type. -public struct MinKey: BSONValue, Equatable, Codable, Hashable { - private var minKey = 1 +internal struct MinKey: BSONValue, Equatable, Codable, Hashable { + internal var bson: BSON { return .minKey } - public var bsonType: BSONType { return .minKey } + internal static var bsonType: BSONType { return .minKey } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_minkey(storage._bson, key, Int32(key.utf8.count)) else { throw bsonTooLargeError(value: self, forKey: key) } @@ -911,17 +731,19 @@ public struct MinKey: BSONValue, Equatable, Codable, Hashable { throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) } - public static func from(iterator iter: DocumentIterator) throws -> MinKey { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .minKey else { throw wrongIterTypeError(iter, expected: MinKey.self) } - return MinKey() + return .minKey } } /// A struct to represent the BSON ObjectId type. public struct ObjectId: BSONValue, Equatable, CustomStringConvertible, Codable { - public var bsonType: BSONType { return .objectId } + internal var bson: BSON { return .objectId(self) } + + internal static var bsonType: BSONType { return .objectId } /// This `ObjectId`'s data represented as a `String`. public var hex: String { @@ -975,7 +797,7 @@ public struct ObjectId: BSONValue, Equatable, CustomStringConvertible, Codable { throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { // encode the bson_oid_t to the bson_t try withUnsafePointer(to: self.oid) { oidPtr in guard bson_append_oid(storage._bson, key, Int32(key.utf8.count), oidPtr) else { @@ -984,13 +806,13 @@ public struct ObjectId: BSONValue, Equatable, CustomStringConvertible, Codable { } } - public static func from(iterator iter: DocumentIterator) throws -> ObjectId { - return try iter.withBSONIterPointer { iterPtr in + internal static func from(iterator iter: DocumentIterator) throws -> BSON { + return .objectId(try iter.withBSONIterPointer { iterPtr in guard let oid = bson_iter_oid(iterPtr) else { throw wrongIterTypeError(iter, expected: ObjectId.self) } return self.init(bsonOid: oid.pointee) - } + }) } public static func == (lhs: ObjectId, rhs: ObjectId) -> Bool { @@ -1078,7 +900,9 @@ extension NSRegularExpression { /// A struct to represent a BSON regular expression. public struct RegularExpression: BSONValue, Equatable, Codable, Hashable { - public var bsonType: BSONType { return .regex } + internal static var bsonType: BSONType { return .regex } + + internal var bson: BSON { return .regex(self) } /// The pattern for this regular expression. public let pattern: String @@ -1106,14 +930,14 @@ public struct RegularExpression: BSONValue, Equatable, Codable, Hashable { throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_regex(storage._bson, key, Int32(key.utf8.count), self.pattern, self.options) else { throw bsonTooLargeError(value: self, forKey: key) } } - public static func from(iterator iter: DocumentIterator) throws -> RegularExpression { - return try iter.withBSONIterPointer { iterPtr in + internal static func from(iterator iter: DocumentIterator) throws -> BSON { + return .regex(try iter.withBSONIterPointer { iterPtr in let options = UnsafeMutablePointer?>.allocate(capacity: 1) defer { options.deinitialize(count: 1) @@ -1131,15 +955,17 @@ public struct RegularExpression: BSONValue, Equatable, Codable, Hashable { let optionsString = String(cString: stringOptions) return self.init(pattern: patternString, options: optionsString) - } + }) } } /// An extension of String to represent the BSON string type. extension String: BSONValue { - public var bsonType: BSONType { return .string } + internal static var bsonType: BSONType { return .string } + + internal var bson: BSON { return .string(self) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_utf8(storage._bson, key, Int32(key.utf8.count), self, Int32(self.utf8.count)) else { throw bsonTooLargeError(value: self, forKey: key) } @@ -1151,8 +977,8 @@ extension String: BSONValue { self.init(data: buffer, encoding: .utf8) } - public static func from(iterator iter: DocumentIterator) throws -> String { - return try iter.withBSONIterPointer { iterPtr in + internal static func from(iterator iter: DocumentIterator) throws -> BSON { + return .string(try iter.withBSONIterPointer { iterPtr in var length: UInt32 = 0 guard iter.currentType == .string, let strValue = bson_iter_utf8(iterPtr, &length) else { throw wrongIterTypeError(iter, expected: String.self) @@ -1168,14 +994,16 @@ extension String: BSONValue { } return out - } + }) } } /// 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 Symbol: BSONValue, CustomStringConvertible, Codable, Equatable, Hashable { - public var bsonType: BSONType { return .symbol } + internal static var bsonType: BSONType { return .symbol } + + internal var bson: BSON { return .symbol(self) } public var description: String { return stringValue @@ -1196,7 +1024,7 @@ public struct Symbol: BSONValue, CustomStringConvertible, Codable, Equatable, Ha throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_symbol( storage._bson, key, @@ -1207,8 +1035,8 @@ public struct Symbol: BSONValue, CustomStringConvertible, Codable, Equatable, Ha } } - public static func from(iterator iter: DocumentIterator) throws -> Symbol { - return try iter.withBSONIterPointer { iterPtr in + internal static func from(iterator iter: DocumentIterator) throws -> BSON { + return .symbol(try iter.withBSONIterPointer { iterPtr in var length: UInt32 = 0 guard iter.currentType == .symbol, let cStr = bson_iter_symbol(iterPtr, &length) else { throw wrongIterTypeError(iter, expected: Symbol.self) @@ -1219,13 +1047,15 @@ public struct Symbol: BSONValue, CustomStringConvertible, Codable, Equatable, Ha } return Symbol(strValue) - } + }) } } /// A struct to represent the BSON Timestamp type. public struct Timestamp: BSONValue, Equatable, Codable, Hashable { - public var bsonType: BSONType { return .timestamp } + internal static var bsonType: BSONType { return .timestamp } + + internal var bson: BSON { return .timestamp(self) } /// A timestamp representing seconds since the Unix epoch. public let timestamp: UInt32 @@ -1253,31 +1083,33 @@ public struct Timestamp: BSONValue, Equatable, Codable, Hashable { throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_timestamp(storage._bson, key, Int32(key.utf8.count), self.timestamp, self.increment) else { throw bsonTooLargeError(value: self, forKey: key) } } - public static func from(iterator iter: DocumentIterator) throws -> Timestamp { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .timestamp else { throw wrongIterTypeError(iter, expected: Timestamp.self) } - return iter.withBSONIterPointer { iterPtr in + return .timestamp(iter.withBSONIterPointer { iterPtr in var t: UInt32 = 0 var i: UInt32 = 0 bson_iter_timestamp(iterPtr, &t, &i) return self.init(timestamp: t, inc: i) - } + }) } } /// A struct to represent the deprecated Undefined type. /// Undefined instances cannot be created, but they can be read from existing documents that contain them. -public struct BSONUndefined: BSONValue, Equatable, Codable { - public var bsonType: BSONType { return .undefined } +internal struct BSONUndefined: BSONValue, Equatable, Codable { + internal static var bsonType: BSONType { return .undefined } + + internal var bson: BSON { return .undefined } internal init() {} @@ -1289,17 +1121,17 @@ public struct BSONUndefined: BSONValue, Equatable, Codable { throw bsonEncodingUnsupportedError(value: self, at: to.codingPath) } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_undefined(storage._bson, key, Int32(key.utf8.count)) else { throw bsonTooLargeError(value: self, forKey: key) } } - public static func from(iterator iter: DocumentIterator) throws -> BSONUndefined { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .undefined else { throw wrongIterTypeError(iter, expected: BSONUndefined.self) } - return BSONUndefined() + return .undefined } } @@ -1310,46 +1142,6 @@ extension BSONUndefined: Hashable { } } -/** - * A helper function to test equality between two `BSONValue`s. This function tests for exact BSON equality. - * This means that differing types with equivalent value are not equivalent. - * - * e.g. - * 4.0 (Double) != 4 (Int) - * - * NOTE: This function will always return `false` if it is used with two arrays that are not of the type `[BSONValue]`, - * because only arrays composed of solely `BSONValue`s are valid BSON arrays. - * - * * - Parameters: - * - lhs: The left-hand-side `BSONValue` to compare. - * - rhs: The right-hand-side `BSONValue` to compare. - * - * - Returns: `true` if `lhs` is equal to `rhs`, `false` otherwise. - */ -@available(*, deprecated, message: "Use lhs.bsonEquals(rhs) instead") -public func bsonEquals(_ lhs: BSONValue, _ rhs: BSONValue) -> Bool { - return lhs.bsonEquals(rhs) -} - -/** - * A helper function to test equality between two BSONValue?s. See bsonEquals for BSONValues (non-optional) for more - * information. - * - * * - Parameters: - * - lhs: The left-hand-side BSONValue? to compare. - * - rhs: The right-hand-side BSONValue? to compare. - * - * - Returns: True if lhs is equal to rhs, false otherwise. - */ -@available(*, deprecated, message: "use lhs?.bsonEquals(rhs) instead") -public func bsonEquals(_ lhs: BSONValue?, _ rhs: BSONValue?) -> Bool { - guard let left = lhs, let right = rhs else { - return lhs == nil && rhs == nil - } - - return bsonEquals(left, right) -} - /// Error thrown when a BSONValue type introduced by the driver (e.g. ObjectId) is encoded not using BSONEncoder private func bsonEncodingUnsupportedError(value: T, at codingPath: [CodingKey]) -> EncodingError { let description = "Encoding \(T.self) BSONValue type with a non-BSONEncoder is currently unsupported" @@ -1406,7 +1198,7 @@ internal func getDecodingError(type: T.Type, decoder: Decoder) -> return DecodingError._typeMismatch( at: decoder.codingPath, expectation: T.self, - reality: bsonDecoder.storage.topContainer + reality: bsonDecoder.storage.topContainer.bsonValue ) } diff --git a/Sources/MongoSwift/BSON/CodableNumber.swift b/Sources/MongoSwift/BSON/CodableNumber.swift index c10f15e31..de83d84be 100644 --- a/Sources/MongoSwift/BSON/CodableNumber.swift +++ b/Sources/MongoSwift/BSON/CodableNumber.swift @@ -4,7 +4,7 @@ import Foundation internal protocol CodableNumber { /// Attempts to initialize this type from an analogous `BSONValue`. Returns `nil` /// the `from` value cannot be accurately represented as this type. - init?(from value: BSONValue) + init?(from value: BSON) /// Initializer for creating from `Int`, `Int32`, `Int64` init?(exactly source: T) @@ -18,24 +18,19 @@ internal protocol CodableNumber { } extension CodableNumber { - internal init?(from value: BSONValue) { + internal init?(from value: BSON) { switch value { - case let v as Int: - if let exact = Self(exactly: v) { - self = exact - return - } - case let v as Int32: + case let .int32(v): if let exact = Self(exactly: v) { self = exact return } - case let v as Int64: + case let .int64(v): if let exact = Self(exactly: v) { self = exact return } - case let v as Double: + case let .double(v): if let exact = Self(exactly: v) { self = exact return @@ -54,7 +49,11 @@ extension CodableNumber { } } -extension Int: CodableNumber {} +extension Int: CodableNumber { + internal var bsonValue: BSONValue? { + return BSON(self).bsonValue + } +} extension Int32: CodableNumber {} extension Int64: CodableNumber {} diff --git a/Sources/MongoSwift/BSON/Document+Codable.swift b/Sources/MongoSwift/BSON/Document+Codable.swift index a660eb55f..655e07a41 100644 --- a/Sources/MongoSwift/BSON/Document+Codable.swift +++ b/Sources/MongoSwift/BSON/Document+Codable.swift @@ -16,11 +16,11 @@ extension Document: Codable { for (k, v) in self { // swiftlint:disable:next force_unwrapping let key = _BSONKey(stringValue: k)! // the initializer never actually returns nil. - if v is BSONNull { + switch v { + case .null: try container.encodeNil(forKey: key) - } else { - let val = AnyBSONValue(v) - try container.encode(val, forKey: key) + default: + try container.encode(v, forKey: key) } } } @@ -34,8 +34,8 @@ extension Document: Codable { // return the container `Document` if let bsonDecoder = decoder as? _BSONDecoder { let topContainer = bsonDecoder.storage.topContainer - guard let doc = topContainer as? Document else { - throw DecodingError._typeMismatch(at: [], expectation: Document.self, reality: topContainer) + guard let doc = topContainer.documentValue else { + throw DecodingError._typeMismatch(at: [], expectation: Document.self, reality: topContainer.bsonValue) } self = doc return @@ -48,9 +48,9 @@ extension Document: Codable { for key in container.allKeys { let k = key.stringValue if try container.decodeNil(forKey: key) { - try output.setValue(for: k, to: BSONNull()) + try output.setValue(for: k, to: .null) } else { - let val = try container.decode(AnyBSONValue.self, forKey: key).value + let val = try container.decode(BSON.self, forKey: key) try output.setValue(for: k, to: val) } } diff --git a/Sources/MongoSwift/BSON/Document+Sequence.swift b/Sources/MongoSwift/BSON/Document+Sequence.swift index 5917fd801..91b0625f6 100644 --- a/Sources/MongoSwift/BSON/Document+Sequence.swift +++ b/Sources/MongoSwift/BSON/Document+Sequence.swift @@ -11,7 +11,7 @@ import mongoc /// ``` extension Document: Sequence { /// The element type of a document: a tuple containing an individual key-value pair. - public typealias KeyValuePair = (key: String, value: BSONValue) + public typealias KeyValuePair = (key: String, value: BSON) // Since a `Document` is a recursive structure, we want to enforce the use of it as a subsequence of itself. // instead of something like `Slice`. @@ -40,7 +40,7 @@ extension Document: Sequence { * * - Throws: An error if `transform` throws an error. */ - public func mapValues(_ transform: (BSONValue) throws -> BSONValue) rethrows -> Document { + public func mapValues(_ transform: (BSON) throws -> BSON) rethrows -> Document { var output = Document() for (k, v) in self { output[k] = try transform(v) diff --git a/Sources/MongoSwift/BSON/Document.swift b/Sources/MongoSwift/BSON/Document.swift index 316e81422..81162596e 100644 --- a/Sources/MongoSwift/BSON/Document.swift +++ b/Sources/MongoSwift/BSON/Document.swift @@ -113,7 +113,7 @@ extension Document { * * - Returns: a new `Document` */ - internal init(_ elements: [BSONValue]) { + internal init(_ elements: [BSON]) { self._storage = DocumentStorage() for (i, elt) in elements.enumerated() { do { @@ -135,10 +135,12 @@ extension Document { * - `RuntimeError.internalError` if the `DocumentStorage` would exceed the maximum size by encoding this * key-value pair. */ - internal mutating func setValue(for key: String, to newValue: BSONValue, checkForKey: Bool = true) throws { + internal mutating func setValue(for key: String, to newValue: BSON, checkForKey: Bool = true) throws { + let newBSONValue = newValue.bsonValue + // if the key already exists in the `Document`, we need to replace it if checkForKey, let existingType = DocumentIterator(forDocument: self, advancedTo: key)?.currentType { - let newBSONType = newValue.bsonType + let newBSONType = newBSONValue.bsonType let sameTypes = newBSONType == existingType // if the new type is the same and it's a type with no custom data, no-op @@ -147,7 +149,7 @@ extension Document { } // if the new type is the same and it's a fixed length type, we can overwrite - if let ov = newValue as? Overwritable, ov.bsonType == existingType { + if let ov = newBSONValue as? Overwritable, ov.bsonType == existingType { self.copyStorageIfRequired() // key is guaranteed present so initialization will succeed. // swiftlint:disable:next force_unwrapping @@ -172,7 +174,7 @@ extension Document { // otherwise, it's a new key } else { self.copyStorageIfRequired() - try newValue.encode(to: self._storage, forKey: key) + try newBSONValue.encode(to: self._storage, forKey: key) } } @@ -180,7 +182,7 @@ extension Document { /// `Document`. /// /// - Throws: `RuntimeError.internalError` if the BSON buffer is too small (< 5 bytes). - internal func getValue(for key: String) throws -> BSONValue? { + internal func getValue(for key: String) throws -> BSON? { guard let iter = DocumentIterator(forDocument: self) else { throw RuntimeError.internalError(message: "BSON buffer is unexpectedly too small (< 5 bytes)") } @@ -211,7 +213,7 @@ extension Document { * */ internal func get(_ key: String) throws -> T { - guard let value = try self.getValue(for: key) as? T else { + guard let value = try self.getValue(for: key)?.bsonValue as? T else { throw RuntimeError.internalError(message: "Could not cast value for key \(key) to type \(T.self)") } return value @@ -255,7 +257,7 @@ extension Document { return self } - var idDoc: Document = ["_id": ObjectId()] + var idDoc: Document = ["_id": .objectId(ObjectId())] try idDoc.merge(self) return idDoc } @@ -269,7 +271,7 @@ extension Document { } /// Returns a `[BSONValue]` containing the values stored in this `Document`. - public var values: [BSONValue] { + public var values: [BSON] { return self.makeIterator().values } @@ -323,6 +325,22 @@ extension Document { self._storage = DocumentStorage() } + internal init(keyValuePairs: [(String, BSON)]) { + // make sure all keys are unique + guard Set(keyValuePairs.map { $0.0 }).count == keyValuePairs.count else { + fatalError("Dictionary literal \(keyValuePairs) contains duplicate keys") + } + + self._storage = DocumentStorage() + for (key, value) in keyValuePairs { + do { + try self.setValue(for: key, to: value, checkForKey: false) + } catch { + fatalError("Error setting key \(key) to value \(String(describing: value)): \(error)") + } + } + } + /** * Constructs a new `Document` from the provided JSON text. * @@ -389,7 +407,7 @@ extension Document { * A nil return suggests that the subscripted key does not exist in the `Document`. A true BSON null is returned as * a `BSONNull`. */ - public subscript(key: String) -> BSONValue? { + public subscript(key: String) -> BSON? { // TODO: This `get` method _should_ guarantee constant-time O(1) access, and it is possible to make it do so. // This criticism also applies to indexed-based subscripting via `Int`. // See SWIFT-250. @@ -418,7 +436,7 @@ extension Document { * print(d["a", default: "foo"]) // prints "foo" * ``` */ - public subscript(key: String, default defaultValue: @autoclosure () -> BSONValue) -> BSONValue { + public subscript(key: String, default defaultValue: @autoclosure () -> BSON) -> BSON { return self[key] ?? defaultValue() } @@ -436,7 +454,7 @@ extension Document { * Only available in Swift 4.2+. */ @available(swift 4.2) - public subscript(dynamicMember member: String) -> BSONValue? { + public subscript(dynamicMember member: String) -> BSON? { get { return self[member] } @@ -448,20 +466,22 @@ extension Document { /// An extension of `Document` to make it a `BSONValue`. extension Document: BSONValue { - public var bsonType: BSONType { return .document } + internal static var bsonType: BSONType { return .document } - public func encode(to storage: DocumentStorage, forKey key: String) throws { + internal var bson: BSON { return .document(self) } + + internal func encode(to storage: DocumentStorage, forKey key: String) throws { guard bson_append_document(storage._bson, key, Int32(key.utf8.count), self._bson) else { throw bsonTooLargeError(value: self, forKey: key) } } - public static func from(iterator iter: DocumentIterator) throws -> Document { + internal static func from(iterator iter: DocumentIterator) throws -> BSON { guard iter.currentType == .document else { throw wrongIterTypeError(iter, expected: Document.self) } - return try iter.withBSONIterPointer { iterPtr in + return .document(try iter.withBSONIterPointer { iterPtr in var length: UInt32 = 0 let document = UnsafeMutablePointer?>.allocate(capacity: 1) defer { @@ -476,7 +496,7 @@ extension Document: BSONValue { } return self.init(stealing: docData) - } + }) } } @@ -508,20 +528,8 @@ extension Document: ExpressibleByDictionaryLiteral { * * - Returns: a new `Document` */ - public init(dictionaryLiteral keyValuePairs: (String, BSONValue)...) { - // make sure all keys are unique - guard Set(keyValuePairs.map { $0.0 }).count == keyValuePairs.count else { - fatalError("Dictionary literal \(keyValuePairs) contains duplicate keys") - } - - self._storage = DocumentStorage() - for (key, value) in keyValuePairs { - do { - try self.setValue(for: key, to: value, checkForKey: false) - } catch { - fatalError("Error setting key \(key) to value \(String(describing: value)): \(error)") - } - } + public init(dictionaryLiteral keyValuePairs: (String, BSON)...) { + self.init(keyValuePairs: keyValuePairs) } } @@ -545,7 +553,7 @@ extension Document: ExpressibleByArrayLiteral { * * - Returns: a new `Document` */ - public init(arrayLiteral elements: BSONValue...) { + public init(arrayLiteral elements: BSON...) { self.init(elements) } } diff --git a/Sources/MongoSwift/BSON/DocumentIterator.swift b/Sources/MongoSwift/BSON/DocumentIterator.swift index 04c22afa0..4a01c73fd 100644 --- a/Sources/MongoSwift/BSON/DocumentIterator.swift +++ b/Sources/MongoSwift/BSON/DocumentIterator.swift @@ -70,7 +70,7 @@ public class DocumentIterator: IteratorProtocol { } /// Returns the current value. Assumes the iterator is in a valid position. - internal var currentValue: BSONValue { + internal var currentValue: BSON { do { return try self.safeCurrentValue() } catch { // Since properties cannot throw, we need to catch and raise a fatalError. @@ -95,8 +95,8 @@ public class DocumentIterator: IteratorProtocol { /// Returns the values from the iterator's current position to the end. The iterator /// will be exhausted after this property is accessed. - internal var values: [BSONValue] { - var values = [BSONValue]() + internal var values: [BSON] { + var values = [BSON]() while self.advance() { values.append(self.currentValue) } return values } @@ -105,7 +105,7 @@ public class DocumentIterator: IteratorProtocol { /// /// - Throws: /// - `RuntimeError.internalError` if the current value of this `DocumentIterator` cannot be decoded to BSON. - internal func safeCurrentValue() throws -> BSONValue { + internal func safeCurrentValue() throws -> BSON { guard let bsonType = DocumentIterator.bsonTypeMap[currentType] else { throw RuntimeError.internalError( message: "Unknown BSONType for iterator's current value with type: \(currentType)" @@ -159,8 +159,9 @@ public class DocumentIterator: IteratorProtocol { * - `UserError.logicError` if the new value is a `Decimal128` or `ObjectId` and is improperly formatted. */ internal func overwriteCurrentValue(with newValue: Overwritable) throws { - guard newValue.bsonType == self.currentType else { - fatalError("Expected \(newValue) to have BSON type \(self.currentType), but has type \(newValue.bsonType)") + let newValueType = type(of: newValue).bsonType + guard newValueType == self.currentType else { + fatalError("Expected \(newValue) to have BSON type \(self.currentType), but has type \(newValueType)") } try newValue.writeToCurrentPosition(of: self) } @@ -193,19 +194,19 @@ public class DocumentIterator: IteratorProtocol { .double: Double.self, .string: String.self, .document: Document.self, - .array: [BSONValue].self, + .array: [BSON].self, .binary: Binary.self, .objectId: ObjectId.self, .bool: Bool.self, .datetime: Date.self, .regex: RegularExpression.self, .dbPointer: DBPointer.self, - .code: CodeWithScope.self, + .code: Code.self, .symbol: Symbol.self, .codeWithScope: CodeWithScope.self, - .int32: Int.bsonType == .int32 ? Int.self : Int32.self, + .int32: Int32.self, .timestamp: Timestamp.self, - .int64: Int.bsonType == .int64 ? Int.self : Int64.self, + .int64: Int64.self, .decimal128: Decimal128.self, .minKey: MinKey.self, .maxKey: MaxKey.self, diff --git a/Sources/MongoSwift/BSON/Overwritable.swift b/Sources/MongoSwift/BSON/Overwritable.swift index 315a15d4d..c878a4907 100644 --- a/Sources/MongoSwift/BSON/Overwritable.swift +++ b/Sources/MongoSwift/BSON/Overwritable.swift @@ -19,19 +19,6 @@ extension Bool: Overwritable { } } -extension Int: Overwritable { - internal func writeToCurrentPosition(of iter: DocumentIterator) throws { - switch self.typedValue { - case let int32 as Int32: - return int32.writeToCurrentPosition(of: iter) - case let int64 as Int64: - return int64.writeToCurrentPosition(of: iter) - default: - throw RuntimeError.internalError(message: "`Int` value \(self) could not be encoded as `Int32` or `Int64`") - } - } -} - extension Int32: Overwritable { internal func writeToCurrentPosition(of iter: DocumentIterator) { iter.withMutableBSONIterPointer { iterPtr in bson_iter_overwrite_int32(iterPtr, self) } diff --git a/Sources/MongoSwift/MongoCollection+BulkWrite.swift b/Sources/MongoSwift/MongoCollection+BulkWrite.swift index 88f88e26a..d4386c1f5 100644 --- a/Sources/MongoSwift/MongoCollection+BulkWrite.swift +++ b/Sources/MongoSwift/MongoCollection+BulkWrite.swift @@ -70,10 +70,10 @@ public enum WriteModel { /// Adds this model to the provided `mongoc_bulk_t`, using the provided encoder for encoding options and /// `CollectionType` values if needed. If this is an `insertOne`, returns the `_id` field of the inserted /// document; otherwise, returns nil. - fileprivate func addToBulkWrite(_ bulk: OpaquePointer, encoder: BSONEncoder) throws -> BSONValue? { + fileprivate func addToBulkWrite(_ bulk: OpaquePointer, encoder: BSONEncoder) throws -> BSON? { var error = bson_error_t() let success: Bool - var res: BSONValue? + var res: BSON? switch self { case let .deleteOne(filter, options): let opts = try encoder.encode(options) @@ -188,7 +188,7 @@ internal struct BulkWriteOperation: Operation { var reply = Document() var error = bson_error_t() let opts = try encodeOptions(options: options, session: session) - var insertedIds: [Int: BSONValue] = [:] + var insertedIds: [Int: BSON] = [:] let (serverId, isAcknowledged): (UInt32, Bool) = try self.collection.withMongocCollection(from: connection) { collPtr in @@ -269,7 +269,7 @@ public struct BulkWriteResult: Decodable { public let insertedCount: Int /// Map of the index of the operation to the id of the inserted document. - public let insertedIds: [Int: BSONValue] + public let insertedIds: [Int: BSON] /// Number of documents matched for update. public let matchedCount: Int @@ -281,7 +281,7 @@ public struct BulkWriteResult: Decodable { public let upsertedCount: Int /// Map of the index of the operation to the id of the upserted document. - public let upsertedIds: [Int: BSONValue] + public let upsertedIds: [Int: BSON] private enum CodingKeys: CodingKey { case deletedCount, insertedCount, insertedIds, matchedCount, modifiedCount, upsertedCount, upsertedIds @@ -301,15 +301,11 @@ public struct BulkWriteResult: Decodable { self.matchedCount = try container.decodeIfPresent(Int.self, forKey: .matchedCount) ?? 0 self.modifiedCount = try container.decodeIfPresent(Int.self, forKey: .modifiedCount) ?? 0 - let insertedIds = - (try container.decodeIfPresent([Int: AnyBSONValue].self, forKey: .insertedIds) ?? [:]) - .mapValues { $0.value } + let insertedIds = try container.decodeIfPresent([Int: BSON].self, forKey: .insertedIds) ?? [:] self.insertedIds = insertedIds self.insertedCount = try container.decodeIfPresent(Int.self, forKey: .insertedCount) ?? insertedIds.count - let upsertedIds = - (try container.decodeIfPresent([Int: AnyBSONValue].self, forKey: .upsertedIds) ?? [:]) - .mapValues { $0.value } + let upsertedIds = try container.decodeIfPresent([Int: BSON].self, forKey: .upsertedIds) ?? [:] self.upsertedIds = upsertedIds self.upsertedCount = try container.decodeIfPresent(Int.self, forKey: .upsertedCount) ?? upsertedIds.count } @@ -330,23 +326,23 @@ public struct BulkWriteResult: Decodable { * - Throws: * - `RuntimeError.internalError` if an unexpected error occurs reading the reply from the server. */ - fileprivate init(reply: Document, insertedIds: [Int: BSONValue]) throws { - // These values are converted to Int via BSONNumber because they're returned from libmongoc as BSON int32s, - // which are retrieved from documents as Ints on 32-bit systems and Int32s on 64-bit ones. To retrieve them in a - // cross-platform manner, we must convert them this way. Also, regardless of how they are stored in the - // we want to use them as Ints. - self.deletedCount = (try reply.getValue(for: "nRemoved") as? BSONNumber)?.intValue ?? 0 - self.insertedCount = (try reply.getValue(for: "nInserted") as? BSONNumber)?.intValue ?? 0 + fileprivate init(reply: Document, insertedIds: [Int: BSON]) throws { + self.deletedCount = try reply.getValue(for: "nRemoved")?.asInt() ?? 0 + self.insertedCount = try reply.getValue(for: "nInserted")?.asInt() ?? 0 self.insertedIds = insertedIds - self.matchedCount = (try reply.getValue(for: "nMatched") as? BSONNumber)?.intValue ?? 0 - self.modifiedCount = (try reply.getValue(for: "nModified") as? BSONNumber)?.intValue ?? 0 - self.upsertedCount = (try reply.getValue(for: "nUpserted") as? BSONNumber)?.intValue ?? 0 + self.matchedCount = try reply.getValue(for: "nMatched")?.asInt() ?? 0 + self.modifiedCount = try reply.getValue(for: "nModified")?.asInt() ?? 0 + self.upsertedCount = try reply.getValue(for: "nUpserted")?.asInt() ?? 0 - var upsertedIds = [Int: BSONValue]() + var upsertedIds = [Int: BSON]() + + if let upserted = try reply.getValue(for: "upserted")?.arrayValue { + guard let upserted = upserted.asArrayOf(Document.self) else { + throw RuntimeError.internalError(message: "\"upserted\" array did not contain only documents") + } - if let upserted = try reply.getValue(for: "upserted") as? [Document] { for upsert in upserted { - guard let index = (try upsert.getValue(for: "index") as? BSONNumber)?.intValue else { + guard let index = try upsert.getValue(for: "index")?.asInt() else { throw RuntimeError.internalError(message: "Could not cast upserted index to `Int`") } upsertedIds[index] = upsert["_id"] @@ -360,11 +356,11 @@ public struct BulkWriteResult: Decodable { internal init( deletedCount: Int? = nil, insertedCount: Int? = nil, - insertedIds: [Int: BSONValue]? = nil, + insertedIds: [Int: BSON]? = nil, matchedCount: Int? = nil, modifiedCount: Int? = nil, upsertedCount: Int? = nil, - upsertedIds: [Int: BSONValue]? = nil) { + upsertedIds: [Int: BSON]? = nil) { self.deletedCount = deletedCount ?? 0 self.insertedCount = insertedCount ?? 0 self.insertedIds = insertedIds ?? [:] diff --git a/Sources/MongoSwift/MongoCollection+Indexes.swift b/Sources/MongoSwift/MongoCollection+Indexes.swift index 68ae1f830..8ac4d6777 100644 --- a/Sources/MongoSwift/MongoCollection+Indexes.swift +++ b/Sources/MongoSwift/MongoCollection+Indexes.swift @@ -16,7 +16,7 @@ public struct IndexModel: Codable { /// Gets the default name for this index. internal var defaultName: String { - return self.keys.map { k, v in "\(k)_\(v)" }.joined(separator: "_") + return self.keys.map { k, v in "\(k)_\(v.bsonValue)" }.joined(separator: "_") } // Encode own data as well as nested options data @@ -254,7 +254,7 @@ extension SyncMongoCollection { throw UserError.invalidArgumentError(message: "Invalid index name '*'; use dropIndexes() to drop all indexes") } - return try _dropIndexes(index: name, options: options, session: session) + return try _dropIndexes(index: .string(name), options: options, session: session) } /** @@ -278,7 +278,7 @@ extension SyncMongoCollection { public func dropIndex(_ keys: Document, options: DropIndexOptions? = nil, session: SyncClientSession? = nil) throws -> Document { - return try _dropIndexes(index: keys, options: options, session: session) + return try _dropIndexes(index: .document(keys), options: options, session: session) } /** @@ -302,7 +302,7 @@ extension SyncMongoCollection { public func dropIndex(_ model: IndexModel, options: DropIndexOptions? = nil, session: SyncClientSession? = nil) throws -> Document { - return try _dropIndexes(index: model.keys, options: options, session: session) + return try _dropIndexes(index: .document(model.keys), options: options, session: session) } /** @@ -328,7 +328,7 @@ extension SyncMongoCollection { /// Internal helper to drop an index. `index` must either be an index specification document or a /// string index name. - private func _dropIndexes(index: BSONValue, + private func _dropIndexes(index: BSON, options: DropIndexOptions?, session: SyncClientSession?) throws -> Document { let operation = DropIndexesOperation(collection: self, index: index, options: options) diff --git a/Sources/MongoSwift/MongoCollection+Read.swift b/Sources/MongoSwift/MongoCollection+Read.swift index 85aad585d..dac0724c8 100644 --- a/Sources/MongoSwift/MongoCollection+Read.swift +++ b/Sources/MongoSwift/MongoCollection+Read.swift @@ -55,7 +55,7 @@ extension SyncMongoCollection { session: SyncClientSession? = nil) throws -> SyncMongoCursor { let opts = try encodeOptions(options: options, session: session) let rp = options?.readPreference?._readPreference - let pipeline: Document = ["pipeline": pipeline] + let pipeline: Document = ["pipeline": .array(pipeline.map { .document($0) })] return try SyncMongoCursor(client: self._client, decoder: self.decoder, session: session) { conn in self.withMongocCollection(from: conn) { collPtr in @@ -146,7 +146,7 @@ extension SyncMongoCollection { public func distinct(fieldName: String, filter: Document = [:], options: DistinctOptions? = nil, - session: SyncClientSession? = nil) throws -> [BSONValue] { + session: SyncClientSession? = nil) throws -> [BSON] { let operation = DistinctOperation(collection: self, fieldName: fieldName, filter: filter, options: options) return try self._client.executeOperation(operation, session: session) } diff --git a/Sources/MongoSwift/MongoCollection+Write.swift b/Sources/MongoSwift/MongoCollection+Write.swift index c9c766b80..cb1aaa40c 100644 --- a/Sources/MongoSwift/MongoCollection+Write.swift +++ b/Sources/MongoSwift/MongoCollection+Write.swift @@ -334,13 +334,9 @@ public struct DeleteOptions: Codable, BulkWriteOptionsConvertible { /// The result of an `insertOne` command on a `MongoCollection` or a `SyncMongoCollection`. public struct InsertOneResult: Decodable { - private enum CodingKeys: String, CodingKey { - case insertedId - } - /// The identifier that was inserted. If the document doesn't have an identifier, this value /// will be generated and added to the document before insertion. - public let insertedId: BSONValue + public let insertedId: BSON internal init?(from result: BulkWriteResult?) throws { guard let result = result else { @@ -351,12 +347,6 @@ public struct InsertOneResult: Decodable { } self.insertedId = id } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let abv = try container.decode(AnyBSONValue.self, forKey: .insertedId) - self.insertedId = abv.value - } } /// The result of a multi-document insert operation on a `MongoCollection` or a `SyncMongoCollection`. @@ -365,7 +355,7 @@ public struct InsertManyResult { public let insertedCount: Int /// Map of the index of the document in `values` to the value of its ID - public let insertedIds: [Int: BSONValue] + public let insertedIds: [Int: BSON] /// Internal initializer used for converting from a `BulkWriteResult` optional to an `InsertManyResult` optional. internal init?(from result: BulkWriteResult?) { @@ -399,7 +389,7 @@ public struct UpdateResult: Decodable { public let modifiedCount: Int /// The identifier of the inserted document if an upsert took place. - public let upsertedId: BSONValue? + public let upsertedId: BSON? /// The number of documents that were upserted. public let upsertedCount: Int @@ -424,13 +414,4 @@ public struct UpdateResult: Decodable { private enum CodingKeys: String, CodingKey { case matchedCount, modifiedCount, upsertedId, upsertedCount } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.matchedCount = try container.decode(Int.self, forKey: .matchedCount) - self.modifiedCount = try container.decode(Int.self, forKey: .modifiedCount) - let id = try container.decodeIfPresent(AnyBSONValue.self, forKey: .upsertedId) - self.upsertedId = id?.value - self.upsertedCount = try container.decode(Int.self, forKey: .upsertedCount) - } } diff --git a/Sources/MongoSwift/MongoError.swift b/Sources/MongoSwift/MongoError.swift index 73bb74915..ca255286a 100644 --- a/Sources/MongoSwift/MongoError.swift +++ b/Sources/MongoSwift/MongoError.swift @@ -202,8 +202,8 @@ private func parseMongocError(_ error: bson_error_t, reply: Document?) -> MongoE let code = mongoc_error_code_t(rawValue: error.code) let message = toErrorString(error) - let errorLabels = reply?["errorLabels"] as? [String] - let codeName = reply?["codeName"] as? String ?? "" + let errorLabels = reply?["errorLabels"]?.arrayValue?.asArrayOf(String.self) + let codeName = reply?["codeName"]?.stringValue ?? "" switch (domain, code) { case (MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE): @@ -241,18 +241,19 @@ private func parseMongocError(_ error: bson_error_t, reply: Document?) -> MongoE /// Internal function used to get an appropriate error from a libmongoc error and/or a server reply to a command. internal func extractMongoError(error bsonError: bson_error_t, reply: Document? = nil) -> MongoError { // if the reply is nil or writeErrors or writeConcernErrors aren't present, then this is likely a commandError. - guard let serverReply = reply, - !(serverReply["writeErrors"] as? [BSONValue] ?? []).isEmpty || - !(serverReply["writeConcernErrors"] as? [BSONValue] ?? []).isEmpty else { + guard let serverReply: Document = reply, + !(serverReply["writeErrors"]?.arrayValue ?? []).isEmpty || + !(serverReply["writeConcernErrors"]?.arrayValue ?? []).isEmpty else { return parseMongocError(bsonError, reply: reply) } - let fallback = RuntimeError.internalError(message: "Got error from the server but couldn't parse it. " + - "Message: \(toErrorString(bsonError))") + let fallback = RuntimeError.internalError( + message: "Got error from the server but couldn't parse it. Message: \(toErrorString(bsonError))") do { var writeError: WriteError? - if let writeErrors = serverReply["writeErrors"] as? [Document], !writeErrors.isEmpty { + if let writeErrors = serverReply["writeErrors"]?.arrayValue?.compactMap({ $0.documentValue }), + !writeErrors.isEmpty { writeError = try BSONDecoder().decode(WriteError.self, from: writeErrors[0]) } let wcError = try extractWriteConcernError(from: serverReply) @@ -264,7 +265,7 @@ internal func extractMongoError(error bsonError: bson_error_t, reply: Document? return ServerError.writeError( writeError: writeError, writeConcernError: wcError, - errorLabels: serverReply["errorLabels"] as? [String] + errorLabels: serverReply["errorLabels"]?.arrayValue?.asArrayOf(String.self) ) } catch { return fallback @@ -283,7 +284,7 @@ internal func extractBulkWriteError(for op: BulkWriteOperation, do { var bulkWriteErrors: [BulkWriteError] = [] - if let writeErrors = reply["writeErrors"] as? [Document] { + if let writeErrors = reply["writeErrors"]?.arrayValue?.compactMap({ $0.documentValue }) { bulkWriteErrors = try writeErrors.map { try BSONDecoder().decode(BulkWriteError.self, from: $0) } } @@ -292,33 +293,32 @@ internal func extractBulkWriteError(for op: BulkWriteOperation, if let result = partialResult { let ordered = op.options?.ordered ?? true - // remove the unsuccessful inserts/upserts from the insertedIds/upsertedIds maps - let filterFailures = { (map: [Int: BSONValue], nSucceeded: Int) -> [Int: BSONValue] in - guard nSucceeded > 0 else { - return [:] - } - + // remove the unsuccessful inserts from the insertedIds map + let filteredIds: [Int: BSON] + if result.insertedCount == 0 { + filteredIds = [:] + } else { if ordered { // remove all after the last index that succeeded - let maxIndex = map.keys.sorted()[nSucceeded - 1] - return map.filter { $0.key <= maxIndex } + let maxIndex = result.insertedIds.keys.sorted()[result.insertedCount - 1] + filteredIds = result.insertedIds.filter { $0.key <= maxIndex } } else { // if unordered, just remove those that have write errors associated with them - let errs = bulkWriteErrors.map { $0.index } - return map.filter { !errs.contains($0.key) } + let errs = Set(bulkWriteErrors.map { $0.index }) + filteredIds = result.insertedIds.filter { !errs.contains($0.key) } } } errResult = BulkWriteResult( deletedCount: result.deletedCount, insertedCount: result.insertedCount, - insertedIds: filterFailures(result.insertedIds, result.insertedCount), + insertedIds: filteredIds, matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, upsertedCount: result.upsertedCount, - upsertedIds: filterFailures(result.upsertedIds, result.upsertedCount) + upsertedIds: result.upsertedIds ) } - // extract any other error that might have ocurred outside of the write/write concern errors. (e.g. connection) + // extract any other error that might have occurred outside of the write/write concern errors. (e.g. connection) var other: Error? = parseMongocError(error, reply: reply) // in the absence of other errors, libmongoc will simply populate the mongoc_error_t with the error code of the @@ -338,7 +338,7 @@ internal func extractBulkWriteError(for op: BulkWriteOperation, writeConcernError: try extractWriteConcernError(from: reply), otherError: other, result: errResult, - errorLabels: reply["errorLabels"] as? [String] + errorLabels: reply["errorLabels"]?.arrayValue?.asArrayOf(String.self) ) } catch { return fallback @@ -347,7 +347,8 @@ internal func extractBulkWriteError(for op: BulkWriteOperation, /// Extracts a `WriteConcernError` from a server reply. private func extractWriteConcernError(from reply: Document) throws -> WriteConcernError? { - guard let writeConcernErrors = reply["writeConcernErrors"] as? [Document], !writeConcernErrors.isEmpty else { + guard let writeConcernErrors = reply["writeConcernErrors"]?.arrayValue?.compactMap({ $0.documentValue }), + !writeConcernErrors.isEmpty else { return nil } return try BSONDecoder().decode(WriteConcernError.self, from: writeConcernErrors[0]) diff --git a/Sources/MongoSwift/Operations/CreateIndexesOperation.swift b/Sources/MongoSwift/Operations/CreateIndexesOperation.swift index 4c0ad655b..6ade5aab9 100644 --- a/Sources/MongoSwift/Operations/CreateIndexesOperation.swift +++ b/Sources/MongoSwift/Operations/CreateIndexesOperation.swift @@ -28,16 +28,16 @@ internal struct CreateIndexesOperation: Operation { } internal func execute(using connection: Connection, session: SyncClientSession?) throws -> [String] { - var indexData = [Document]() + var indexData = [BSON]() for index in self.models { var indexDoc = try self.collection.encoder.encode(index) if indexDoc["name"] == nil { - indexDoc["name"] = index.defaultName + indexDoc["name"] = .string(index.defaultName) } - indexData.append(indexDoc) + indexData.append(.document(indexDoc)) } - let command: Document = ["createIndexes": self.collection.name, "indexes": indexData] + let command: Document = ["createIndexes": .string(self.collection.name), "indexes": .array(indexData)] let opts = try encodeOptions(options: options, session: session) diff --git a/Sources/MongoSwift/Operations/DistinctOperation.swift b/Sources/MongoSwift/Operations/DistinctOperation.swift index d78530f91..e91c0a84d 100644 --- a/Sources/MongoSwift/Operations/DistinctOperation.swift +++ b/Sources/MongoSwift/Operations/DistinctOperation.swift @@ -46,11 +46,11 @@ internal struct DistinctOperation: Operation { self.options = options } - internal func execute(using connection: Connection, session: SyncClientSession?) throws -> [BSONValue] { + internal func execute(using connection: Connection, session: SyncClientSession?) throws -> [BSON] { let command: Document = [ - "distinct": self.collection.name, - "key": self.fieldName, - "query": self.filter + "distinct": .string(self.collection.name), + "key": .string(self.fieldName), + "query": .document(self.filter) ] let opts = try encodeOptions(options: self.options, session: session) @@ -66,7 +66,7 @@ internal struct DistinctOperation: Operation { throw extractMongoError(error: error, reply: reply) } - guard let values = try reply.getValue(for: "values") as? [BSONValue] else { + guard let values = try reply.getValue(for: "values")?.arrayValue else { throw RuntimeError.internalError(message: "expected server reply \(reply) to contain an array of distinct values") } diff --git a/Sources/MongoSwift/Operations/DropCollectionOperation.swift b/Sources/MongoSwift/Operations/DropCollectionOperation.swift index 3d9b8254c..f94547f1a 100644 --- a/Sources/MongoSwift/Operations/DropCollectionOperation.swift +++ b/Sources/MongoSwift/Operations/DropCollectionOperation.swift @@ -11,7 +11,7 @@ internal struct DropCollectionOperation: Operation { } internal func execute(using connection: Connection, session: SyncClientSession?) throws { - let command: Document = ["drop": self.collection.name] + let command: Document = ["drop": .string(self.collection.name)] let opts = try encodeOptions(options: options, session: session) var reply = Document() diff --git a/Sources/MongoSwift/Operations/DropIndexesOperation.swift b/Sources/MongoSwift/Operations/DropIndexesOperation.swift index 94ba68733..be592b870 100644 --- a/Sources/MongoSwift/Operations/DropIndexesOperation.swift +++ b/Sources/MongoSwift/Operations/DropIndexesOperation.swift @@ -18,17 +18,17 @@ public struct DropIndexOptions: Encodable { /// An operation corresponding to a "dropIndexes" command. internal struct DropIndexesOperation: Operation { private let collection: SyncMongoCollection - private let index: BSONValue + private let index: BSON private let options: DropIndexOptions? - internal init(collection: SyncMongoCollection, index: BSONValue, options: DropIndexOptions?) { + internal init(collection: SyncMongoCollection, index: BSON, options: DropIndexOptions?) { self.collection = collection self.index = index self.options = options } internal func execute(using connection: Connection, session: SyncClientSession?) throws -> Document { - let command: Document = ["dropIndexes": self.collection.name, "index": self.index] + let command: Document = ["dropIndexes": .string(self.collection.name), "index": self.index] let opts = try encodeOptions(options: self.options, session: session) var reply = Document() var error = bson_error_t() diff --git a/Sources/MongoSwift/Operations/FindAndModifyOperation.swift b/Sources/MongoSwift/Operations/FindAndModifyOperation.swift index 48ba87024..5c605662d 100644 --- a/Sources/MongoSwift/Operations/FindAndModifyOperation.swift +++ b/Sources/MongoSwift/Operations/FindAndModifyOperation.swift @@ -70,8 +70,10 @@ internal class FindAndModifyOptions { // build an "extra" document of fields without their own setters var extra = Document() - if let filters = arrayFilters { try extra.setValue(for: "arrayFilters", to: filters) } - if let coll = collation { try extra.setValue(for: "collation", to: coll) } + if let filters = arrayFilters { + try extra.setValue(for: "arrayFilters", to: .array(filters.map { .document($0) })) + } + if let coll = collation { try extra.setValue(for: "collation", to: .document(coll)) } // note: mongoc_find_and_modify_opts_set_max_time_ms() takes in a // uint32_t, but it should be a positive 64-bit integer, so we @@ -80,12 +82,12 @@ internal class FindAndModifyOptions { guard maxTime > 0 else { throw UserError.invalidArgumentError(message: "maxTimeMS must be positive, but got value \(maxTime)") } - try extra.setValue(for: "maxTimeMS", to: maxTime) + try extra.setValue(for: "maxTimeMS", to: .int64(maxTime)) } if let wc = writeConcern { do { - try extra.setValue(for: "writeConcern", to: try BSONEncoder().encode(wc)) + try extra.setValue(for: "writeConcern", to: .document(try BSONEncoder().encode(wc))) } catch { throw RuntimeError.internalError(message: "Error encoding WriteConcern \(wc): \(error)") } @@ -156,7 +158,7 @@ internal struct FindAndModifyOperation: Operation { throw extractMongoError(error: error, reply: reply) } - guard let value = try reply.getValue(for: "value") as? Document else { + guard let value = try reply.getValue(for: "value")?.documentValue else { return nil } diff --git a/Sources/MongoSwift/Operations/ListCollectionsOperation.swift b/Sources/MongoSwift/Operations/ListCollectionsOperation.swift index db020f75c..d3b83895a 100644 --- a/Sources/MongoSwift/Operations/ListCollectionsOperation.swift +++ b/Sources/MongoSwift/Operations/ListCollectionsOperation.swift @@ -106,9 +106,9 @@ internal struct ListCollectionsOperation: Operation { internal func execute(using connection: Connection, session: SyncClientSession?) throws -> ListCollectionsResults { var opts = try encodeOptions(options: self.options, session: session) ?? Document() - opts["nameOnly"] = self.nameOnly + opts["nameOnly"] = .bool(self.nameOnly) if let filterDoc = self.filter { - opts["filter"] = filterDoc + opts["filter"] = .document(filterDoc) // If `listCollectionNames` is called with a non-name filter key, change server-side nameOnly flag to false. if self.nameOnly && filterDoc.keys.contains { $0 != "name" } { @@ -130,7 +130,7 @@ internal struct ListCollectionsOperation: Operation { session: session, initializer: initializer) return try .names(cursor.map { - guard let name = $0["name"] as? String else { + guard let name = $0["name"]?.stringValue else { throw RuntimeError.internalError(message: "Invalid server response: collection has no name") } return name diff --git a/Sources/MongoSwift/Operations/ListDatabasesOperation.swift b/Sources/MongoSwift/Operations/ListDatabasesOperation.swift index 61d901cc9..1a0e7025d 100644 --- a/Sources/MongoSwift/Operations/ListDatabasesOperation.swift +++ b/Sources/MongoSwift/Operations/ListDatabasesOperation.swift @@ -44,10 +44,10 @@ internal struct ListDatabasesOperation: Operation { let readPref = ReadPreference(.primary) var cmd: Document = ["listDatabases": 1] if let filter = self.filter { - cmd["filter"] = filter + cmd["filter"] = .document(filter) } if let nameOnly = self.nameOnly { - cmd["nameOnly"] = nameOnly + cmd["nameOnly"] = .bool(nameOnly) } let opts = try encodeOptions(options: nil as Document?, session: session) @@ -68,12 +68,18 @@ internal struct ListDatabasesOperation: Operation { throw extractMongoError(error: error, reply: reply) } - guard let databases = reply["databases"] as? [Document] else { + guard let databases = reply["databases"]?.arrayValue?.asArrayOf(Document.self) else { throw RuntimeError.internalError(message: "Invalid server response: \(reply)") } if self.nameOnly ?? false { - return .names(databases.map { $0["name"] as? String ?? "" }) + let names: [String] = try databases.map { + guard let name = $0["name"]?.stringValue else { + throw RuntimeError.internalError(message: "Server response missing names: \(reply)") + } + return name + } + return .names(names) } return try .specs(databases.map { try self.client.decoder.decode(DatabaseSpecification.self, from: $0) }) diff --git a/Sources/MongoSwift/Operations/NextOperation.swift b/Sources/MongoSwift/Operations/NextOperation.swift index bc7ee77e5..6c5c9f929 100644 --- a/Sources/MongoSwift/Operations/NextOperation.swift +++ b/Sources/MongoSwift/Operations/NextOperation.swift @@ -66,7 +66,7 @@ internal struct NextOperation: Operation { return try cursor.decoder.decode(T.self, from: doc) case let .changeStream(changeStream): // Update the resumeToken with the `_id` field from the document. - guard let resumeToken = doc["_id"] as? Document else { + guard let resumeToken = doc["_id"]?.documentValue else { throw RuntimeError.internalError(message: "_id field is missing from the change stream document.") } changeStream.resumeToken = ResumeToken(resumeToken) diff --git a/Sources/MongoSwift/Operations/WatchOperation.swift b/Sources/MongoSwift/Operations/WatchOperation.swift index 3365efca6..e5c950ec2 100644 --- a/Sources/MongoSwift/Operations/WatchOperation.swift +++ b/Sources/MongoSwift/Operations/WatchOperation.swift @@ -15,7 +15,7 @@ internal enum ChangeStreamTarget { /// An operation corresponding to a "watch" command on either a client, database, or collection. internal struct WatchOperation: Operation { private let target: ChangeStreamTarget - private let pipeline: [Document] + private let pipeline: BSON private let options: ChangeStreamOptions? internal let connectionStrategy: ConnectionStrategy @@ -24,7 +24,7 @@ internal struct WatchOperation(values) - - expect(Set(valuesSet.map { abv in abv.hashValue }).count).to(equal(values.count)) - expect(valuesSet.count).to(equal(values.count)) - expect(values).to(contain(Array(valuesSet))) - - let abv1 = AnyBSONValue(Int32(1)) - let abv2 = AnyBSONValue(Int64(1)) - let abv3 = AnyBSONValue(Int32(5)) - - var map: [AnyBSONValue: Int] = [abv1: 1, abv2: 2] - - expect(map[abv1]).to(equal(1)) - expect(map[abv2]).to(equal(2)) - - map[abv1] = 4 - map[abv2] = 3 - map[abv3] = 5 - - expect(map[abv1]).to(equal(4)) - expect(map[abv2]).to(equal(3)) - expect(map[abv3]).to(equal(5)) - - let str = AnyBSONValue("world") - let doc = AnyBSONValue(["value": str.value] as Document) - let json = AnyBSONValue((doc.value as! Document).extendedJSON) - - map[str] = 12 - map[doc] = 13 - map[json] = 14 - - expect(map[str]).to(equal(12)) - expect(map[doc]).to(equal(13)) - expect(map[json]).to(equal(14)) - - expect(Set([str.hashValue, doc.hashValue, json.hashValue]).count).to(equal(3)) - } - struct BSONNumberTestCase { let int: Int? let double: Double? diff --git a/Tests/MongoSwiftTests/ChangeStreamTests.swift b/Tests/MongoSwiftTests/ChangeStreamTests.swift index b5f015967..2937fc36c 100644 --- a/Tests/MongoSwiftTests/ChangeStreamTests.swift +++ b/Tests/MongoSwiftTests/ChangeStreamTests.swift @@ -314,7 +314,7 @@ final class ChangeStreamTests: MongoSwiftTestCase { let changeStream = try coll.watch(options: ChangeStreamOptions(maxAwaitTimeMS: ChangeStreamTests.MAX_AWAIT_TIME)) for x in 0..<5 { - try coll.insertOne(["x": x]) + try coll.insertOne(["x": BSON(x)]) } expect(changeStream.resumeToken).to(beNil()) @@ -344,9 +344,9 @@ final class ChangeStreamTests: MongoSwiftTestCase { } try withTestNamespace { client, _, coll in - let changeStream = try coll.watch([["$project": ["_id": false] as Document]]) + let changeStream = try coll.watch([["$project": ["_id": false]]]) for x in 0..<5 { - try coll.insertOne(["x": x]) + try coll.insertOne(["x": BSON(x)]) } if try client.maxWireVersion() >= 8 { @@ -383,9 +383,9 @@ final class ChangeStreamTests: MongoSwiftTestCase { let options = ChangeStreamOptions(batchSize: 123, fullDocument: .updateLookup, maxAwaitTimeMS: ChangeStreamTests.MAX_AWAIT_TIME) - let changeStream = try coll.watch([["$match": ["fullDocument.x": 2] as Document]], options: options) + let changeStream = try coll.watch([["$match": ["fullDocument.x": 2]]], options: options) for x in 0..<5 { - try coll.insertOne(["x": x]) + try coll.insertOne(["x": .int64(Int64(x))]) } // notMaster error @@ -402,13 +402,13 @@ final class ChangeStreamTests: MongoSwiftTestCase { let originalCommand = (events[0] as? CommandStartedEvent)!.command let resumeCommand = (events[1] as? CommandStartedEvent)!.command - let originalPipeline = originalCommand["pipeline"]! as! [Document] - let resumePipeline = resumeCommand["pipeline"]! as! [Document] + let originalPipeline = originalCommand["pipeline"]!.arrayValue!.compactMap { $0.documentValue } + let resumePipeline = resumeCommand["pipeline"]!.arrayValue!.compactMap { $0.documentValue } // verify the $changeStream stage is identical except for resume options. let filteredStreamStage = { (pipeline: [Document]) -> Document in let stage = pipeline[0] - let streamDoc = stage["$changeStream"] as? Document + let streamDoc = stage["$changeStream"]?.documentValue expect(streamDoc).toNot(beNil()) return streamDoc!.filter { $0.key != "resumeAfter" } } @@ -418,7 +418,7 @@ final class ChangeStreamTests: MongoSwiftTestCase { expect(resumePipeline[1...]).to(equal(originalPipeline[1...])) // verify the cursor options were preserved. - expect(resumeCommand["cursor"]).to(bsonEqual(originalCommand["cursor"])) + expect(resumeCommand["cursor"]).to(equal(originalCommand["cursor"])) } /** @@ -588,9 +588,9 @@ final class ChangeStreamTests: MongoSwiftTestCase { expect(try changeStream?.nextOrError()).toNot(throwError()) // kill the underlying cursor to trigger a resume. - let reply = (aggEvent[0] as! CommandSucceededEvent).reply["cursor"] as! Document - let cursorId = (reply["id"] as! BSONNumber).intValue! - try client.db("admin").runCommand(["killCursors": self.getCollectionName(), "cursors": [cursorId]]) + let reply = (aggEvent[0] as! CommandSucceededEvent).reply["cursor"]!.documentValue! + let cursorId = reply["id"]! + try client.db("admin").runCommand(["killCursors": .string(self.getCollectionName()), "cursors": [cursorId]]) // Make the killCursors command fail as part of the resume process. let failPoint = FailPoint.failCommand(failCommands: ["killCursors"], mode: .times(1), errorCode: 10107) @@ -675,7 +675,7 @@ final class ChangeStreamTests: MongoSwiftTestCase { let changeStream = try coll.watch(options: ChangeStreamOptions(maxAwaitTimeMS: ChangeStreamTests.MAX_AWAIT_TIME)) for i in 0..<5 { - try coll.insertOne(["x": i]) + try coll.insertOne(["x": BSON(i)]) } for _ in 0..<3 { _ = try changeStream.nextOrError() @@ -806,7 +806,7 @@ final class ChangeStreamTests: MongoSwiftTestCase { // expect the resumeToken to be updated to the _id field of the most recently accessed document expect(changeStream.resumeToken).to(equal(change1?._id)) - try coll.updateOne(filter: ["x": 1], update: ["$set": ["x": 2] as Document]) + try coll.updateOne(filter: ["x": 1], update: ["$set": ["x": 2]]) let change2 = changeStream.next() // expect the change stream to contain a change document for the `update` operation expect(change2).toNot(beNil()) @@ -837,7 +837,7 @@ final class ChangeStreamTests: MongoSwiftTestCase { defer { try? db.drop() } let coll = try db.createCollection(self.getCollectionName(suffix: "1")) let options = ChangeStreamOptions(fullDocument: .updateLookup, maxAwaitTimeMS: ChangeStreamTests.MAX_AWAIT_TIME) - let pipeline: [Document] = [["$match": ["fullDocument.a": 1] as Document]] + let pipeline: [Document] = [["$match": ["fullDocument.a": 1]]] let changeStream = try coll.watch(pipeline, options: options) let doc1: Document = ["_id": 1, "a": 1] diff --git a/Tests/MongoSwiftTests/ClientSessionTests.swift b/Tests/MongoSwiftTests/ClientSessionTests.swift index fbe339c0b..c7990ce8c 100644 --- a/Tests/MongoSwiftTests/ClientSessionTests.swift +++ b/Tests/MongoSwiftTests/ClientSessionTests.swift @@ -110,11 +110,11 @@ final class ClientSessionTests: MongoSwiftTestCase { let sessionC: SyncClientSession = try client.startSession() expect(sessionC.active).to(beTrue()) - expect(sessionC.id).to(bsonEqual(idB)) + expect(sessionC.id).to(equal(idB)) let sessionD: SyncClientSession = try client.startSession() expect(sessionD.active).to(beTrue()) - expect(sessionD.id).to(bsonEqual(idA)) + expect(sessionD.id).to(equal(idA)) // test via explicitly ending sessionC.end() @@ -124,17 +124,17 @@ final class ClientSessionTests: MongoSwiftTestCase { // test via withSession try client.withSession { session in - expect(session.id).to(bsonEqual(idA)) + expect(session.id).to(equal(idA)) } try client.withSession { session in - expect(session.id).to(bsonEqual(idA)) + expect(session.id).to(equal(idA)) } try client.withSession { session in - expect(session.id).to(bsonEqual(idA)) + expect(session.id).to(equal(idA)) try client.withSession { nestedSession in - expect(nestedSession.id).to(bsonEqual(idB)) + expect(nestedSession.id).to(equal(idB)) } } } @@ -153,11 +153,11 @@ final class ClientSessionTests: MongoSwiftTestCase { expect(event.command["lsid"]).toNot(beNil(), description: op.name) if !seenExplicit { - expect(event.command["lsid"]).to(bsonEqual(session.id), description: op.name) + expect(event.command["lsid"]).to(equal(.document(session.id)), description: op.name) seenExplicit = true } else { expect(seenImplicit).to(beFalse()) - expect(event.command["lsid"]).toNot(bsonEqual(session.id), description: op.name) + expect(event.command["lsid"]).toNot(equal(.document(session.id)), description: op.name) seenImplicit = true } } @@ -231,7 +231,7 @@ final class ClientSessionTests: MongoSwiftTestCase { let database = client.db(type(of: self).testDatabase) let collection1 = database.collection(self.getCollectionName()) - try (1...3).forEach { try collection1.insertOne(["x": $0]) } + try (1...3).forEach { try collection1.insertOne(["x": BSON($0)]) } let cursor = try collection.find(session: session2) expect(cursor.next()).toNot(beNil()) @@ -249,10 +249,10 @@ final class ClientSessionTests: MongoSwiftTestCase { let session = try client.startSession() for x in 1...3 { - try collection.insertOne(["x": x]) + try collection.insertOne(["x": BSON(x)]) } - var id: Document? + var id: BSON? var seenFind = false var seenGetMore = false @@ -265,21 +265,21 @@ final class ClientSessionTests: MongoSwiftTestCase { if event.command["find"] != nil { seenFind = true if let id = id { - expect(id).to(bsonEqual(event.command["lsid"])) + expect(id).to(equal(event.command["lsid"])) } else { expect(event.command["lsid"]).toNot(beNil()) - id = event.command["lsid"] as? Document + id = event.command["lsid"] } } else if event.command["getMore"] != nil { seenGetMore = true expect(id).toNot(beNil()) expect(event.command["lsid"]).toNot(beNil()) - expect(event.command["lsid"]).to(bsonEqual(id)) + expect(event.command["lsid"]).to(equal(id)) } } // explicit - id = session.id + id = .document(session.id) seenFind = false seenGetMore = false let cursor = try collection.find(options: FindOptions(batchSize: 2), session: session) @@ -321,9 +321,11 @@ final class ClientSessionTests: MongoSwiftTestCase { try client.withSession { session in let date = Date() expect(session.clusterTime).to(beNil()) - let newTime: Document = ["clusterTime": Timestamp(timestamp: Int(date.timeIntervalSince1970), inc: 100)] + let newTime: Document = [ + "clusterTime": .timestamp(Timestamp(timestamp: Int(date.timeIntervalSince1970), inc: 100)) + ] session.advanceClusterTime(to: newTime) - expect(session.clusterTime).to(bsonEqual(newTime)) + expect(session.clusterTime).to(equal(newTime)) } } @@ -359,14 +361,14 @@ final class ClientSessionTests: MongoSwiftTestCase { guard let event = notif.userInfo?["event"] as? CommandSucceededEvent else { return } - replyOpTime = event.reply["operationTime"] as? Timestamp + replyOpTime = event.reply["operationTime"]?.timestampValue } defer { center.removeObserver(replyObserver) } _ = try collection.find(session: session).next() expect(seenCommands).to(beTrue()) expect(replyOpTime).toNot(beNil()) - expect(replyOpTime).to(bsonEqual(session.operationTime)) + expect(replyOpTime).to(equal(session.operationTime)) } // Causal consistency spec test 3: the first read/write on a session should update the operationTime of a @@ -391,9 +393,9 @@ final class ClientSessionTests: MongoSwiftTestCase { guard let event = notif.userInfo?["event"] as? CommandStartedEvent else { return } - let readConcern = event.command["readConcern"] as? Document + let readConcern = event.command["readConcern"]?.documentValue expect(readConcern).toNot(beNil(), description: op.name) - expect(readConcern!["afterClusterTime"]).to(bsonEqual(opTime), description: op.name) + expect(readConcern!["afterClusterTime"]?.timestampValue).to(equal(opTime), description: op.name) expect(readConcern!["level"]).to(beNil(), description: op.name) seenCommand = true } @@ -416,8 +418,8 @@ final class ClientSessionTests: MongoSwiftTestCase { guard let event = notif.userInfo?["event"] as? CommandStartedEvent else { return } - expect((event.command["readConcern"] as? Document)?["afterClusterTime"]) - .to(bsonEqual(opTime), description: op.name) + expect(event.command["readConcern"]?.documentValue?["afterClusterTime"]?.timestampValue) + .to(equal(opTime), description: op.name) seenCommand = true } defer { center.removeObserver(observer) } @@ -435,11 +437,11 @@ final class ClientSessionTests: MongoSwiftTestCase { guard let event = notif.userInfo?["event"] as? CommandStartedEvent else { return } - expect((event.command["readConcern"] as? Document)?["afterClusterTime"]).to(beNil()) + expect(event.command["readConcern"]?.documentValue?["afterClusterTime"]).to(beNil()) seenCommand = true } defer { center.removeObserver(observer) } - _ = try collection.aggregate([["$match": ["x": 1] as Document]], session: session).next() + _ = try collection.aggregate([["$match": ["x": 1]]], session: session).next() expect(seenCommand).to(beTrue()) } @@ -456,10 +458,10 @@ final class ClientSessionTests: MongoSwiftTestCase { guard let event = notif.userInfo?["event"] as? CommandStartedEvent else { return } - let readConcern = event.command["readConcern"] as? Document + let readConcern = event.command["readConcern"]?.documentValue expect(readConcern).toNot(beNil()) - expect(readConcern!["afterClusterTime"]).to(bsonEqual(opTime)) - expect(readConcern!["level"]).to(bsonEqual("snapshot")) + expect(readConcern!["afterClusterTime"]?.timestampValue).to(equal(opTime)) + expect(readConcern!["level"]).to(equal("snapshot")) seenCommand = true } defer { center.removeObserver(observer) } @@ -508,7 +510,7 @@ final class ClientSessionTests: MongoSwiftTestCase { guard let event = notif.userInfo?["event"] as? CommandStartedEvent else { return } - expect((event.command["readConcern"] as? Document)?["afterClusterTime"]).to(beNil()) + expect(event.command["readConcern"]?.documentValue?["afterClusterTime"]).to(beNil()) seenCommand = true } defer { center.removeObserver(observer) } @@ -556,7 +558,7 @@ final class ClientSessionTests: MongoSwiftTestCase { guard let event = notif.userInfo?["event"] as? CommandStartedEvent else { return } - expect((event.command["readConcern"] as? Document)?["afterClusterTime"]).to(beNil()) + expect(event.command["readConcern"]?.documentValue?["afterClusterTime"]).to(beNil()) seenCommand = true } defer { center.removeObserver(startObserver) } diff --git a/Tests/MongoSwiftTests/CodecTests.swift b/Tests/MongoSwiftTests/CodecTests.swift index 25bc32c60..215fbef9b 100644 --- a/Tests/MongoSwiftTests/CodecTests.swift +++ b/Tests/MongoSwiftTests/CodecTests.swift @@ -10,7 +10,7 @@ final class CodecTests: MongoSwiftTestCase { static let dataCorruptedErr = DecodingError.dataCorrupted( DecodingError.Context(codingPath: [], debugDescription: "dummy error")) - struct TestClass: Encodable { + struct TestStruct: Encodable { let val1 = "a" let val2 = 0 let val3 = [[1, 2], [3, 4]] @@ -51,11 +51,11 @@ final class CodecTests: MongoSwiftTestCase { "val1": "a", "val2": 0, "val3": [[1, 2], [3, 4]], - "val4": ["x": 1, "y": 2] as Document, - "val5": [["x": 1, "y": 2] as Document] + "val4": ["x": 1, "y": 2], + "val5": [["x": 1, "y": 2]] ] - expect(try encoder.encode(TestClass())).to(equal(expected)) + expect(try encoder.encode(TestStruct())).to(equal(expected)) // a basic struct let basic1 = BasicStruct(int: 1, string: "hello") @@ -68,19 +68,19 @@ final class CodecTests: MongoSwiftTestCase { let basic2Doc: Document = ["int": 2, "string": "hi"] let nestedStruct = NestedStruct(s1: basic1, s2: basic2) - let nestedStructDoc: Document = ["s1": basic1Doc, "s2": basic2Doc] + let nestedStructDoc: Document = ["s1": .document(basic1Doc), "s2": .document(basic2Doc)] expect(try encoder.encode(nestedStruct)).to(equal(nestedStructDoc)) expect(try decoder.decode(NestedStruct.self, from: nestedStructDoc)).to(equal(nestedStruct)) // a struct storing two nested structs in an array let nestedArray = NestedArray(array: [basic1, basic2]) - let nestedArrayDoc: Document = ["array": [basic1Doc, basic2Doc]] + let nestedArrayDoc: Document = ["array": [.document(basic1Doc), .document(basic2Doc)]] expect(try encoder.encode(nestedArray)).to(equal(nestedArrayDoc)) expect(try decoder.decode(NestedArray.self, from: nestedArrayDoc)).to(equal(nestedArray)) // one more level of nesting let nestedNested = NestedNestedStruct(s: nestedStruct) - let nestedNestedDoc: Document = ["s": nestedStructDoc] + let nestedNestedDoc: Document = ["s": .document(nestedStructDoc)] expect(try encoder.encode(nestedNested)).to(equal(nestedNestedDoc)) expect(try decoder.decode(NestedNestedStruct.self, from: nestedNestedDoc)).to(equal(nestedNested)) } @@ -107,7 +107,7 @@ final class CodecTests: MongoSwiftTestCase { expect(try decoder.decode(OptionalsStruct.self, from: s2Doc1)).to(equal(s2)) // test with key in doc explicitly set to BSONNull - let s2Doc2: Document = ["int": BSONNull(), "bool": true, "string": "hi"] + let s2Doc2: Document = ["int": .null, "bool": true, "string": "hi"] expect(try decoder.decode(OptionalsStruct.self, from: s2Doc2)).to(equal(s2)) } @@ -152,18 +152,18 @@ final class CodecTests: MongoSwiftTestCase { let int32 = Int32(42) // all should be stored as Int32s, except the float should be stored as a double let doc1: Document = [ - "int8": int32, "int16": int32, "uint8": int32, "uint16": int32, - "uint32": int32, "uint64": int32, "uint": int32, "float": 42.0 + "int8": .int32(int32), "int16": .int32(int32), "uint8": .int32(int32), "uint16": .int32(int32), + "uint32": .int32(int32), "uint64": .int32(int32), "uint": .int32(int32), "float": 42.0 ] expect(try encoder.encode(s1)).to(equal(doc1)) // check that a UInt32 too large for an Int32 gets converted to Int64 - expect(try encoder.encode(Numbers(uint32: 4294967295))).to(equal(["uint32": Int64(4294967295)])) + expect(try encoder.encode(Numbers(uint32: 4294967295))).to(equal(["uint32": .int64(4294967295)])) // check that UInt, UInt64 too large for an Int32 gets converted to Int64 - expect(try encoder.encode(Numbers(uint64: 4294967295))).to(equal(["uint64": Int64(4294967295)])) - expect(try encoder.encode(Numbers(uint: 4294967295))).to(equal(["uint": Int64(4294967295)])) + expect(try encoder.encode(Numbers(uint64: 4294967295))).to(equal(["uint64": .int64(4294967295)])) + expect(try encoder.encode(Numbers(uint: 4294967295))).to(equal(["uint": .int64(4294967295)])) // check that UInt, UInt64 too large for an Int64 gets converted to Double expect(try encoder.encode(Numbers(uint64: UInt64(Int64.max) + 1))).to(equal(["uint64": 9223372036854775808.0])) @@ -200,7 +200,7 @@ final class CodecTests: MongoSwiftTestCase { // store all values as Int64s and decode them to their requested types. var doc2 = Document() for k in Numbers.keys { - doc2[k] = Int64(42) + doc2[k] = .int64(42) } let res2 = try decoder.decode(Numbers.self, from: doc2) @@ -209,23 +209,23 @@ final class CodecTests: MongoSwiftTestCase { // store all values as Doubles and decode them to their requested types var doc3 = Document() for k in Numbers.keys { - doc3[k] = Double(42) + doc3[k] = .double(42) } let res3 = try decoder.decode(Numbers.self, from: doc3) expect(res3).to(equal(s)) // test for each type that we fail gracefully when values cannot be represented because they are out of bounds - expect(try decoder.decode(Numbers.self, from: ["int8": Int(Int8.max) + 1])) + expect(try decoder.decode(Numbers.self, from: ["int8": .int64(Int64(Int8.max) + 1)])) .to(throwError(CodecTests.typeMismatchErr)) - expect(try decoder.decode(Numbers.self, from: ["int16": Int(Int16.max) + 1])) + expect(try decoder.decode(Numbers.self, from: ["int16": .int64(Int64(Int16.max) + 1)])) .to(throwError(CodecTests.typeMismatchErr)) expect(try decoder.decode(Numbers.self, from: ["uint8": -1])).to(throwError(CodecTests.typeMismatchErr)) expect(try decoder.decode(Numbers.self, from: [ "uint16": -1])).to(throwError(CodecTests.typeMismatchErr)) expect(try decoder.decode(Numbers.self, from: ["uint32": -1])).to(throwError(CodecTests.typeMismatchErr)) expect(try decoder.decode(Numbers.self, from: ["uint64": -1])).to(throwError(CodecTests.typeMismatchErr)) expect(try decoder.decode(Numbers.self, from: ["uint": -1])).to(throwError(CodecTests.typeMismatchErr)) - expect(try decoder.decode(Numbers.self, from: ["float": Double.greatestFiniteMagnitude])) + expect(try decoder.decode(Numbers.self, from: ["float": .double(Double.greatestFiniteMagnitude)])) .to(throwError(CodecTests.typeMismatchErr)) } @@ -243,22 +243,22 @@ final class CodecTests: MongoSwiftTestCase { // the struct we expect to get back let s = BSONNumbers(int: 42, int32: 42, int64: 42, double: 42) expect(try encoder.encode(s)).to(equal([ - "int": Int(42), - "int32": Int32(42), - "int64": Int64(42), - "double": Double(42) + "int": 42, + "int32": .int32(42), + "int64": .int64(42), + "double": .double(42) ])) // store all values as Int32s and decode them to their requested types - let doc1: Document = ["int": Int32(42), "int32": Int32(42), "int64": Int32(42), "double": Int32(42)] + let doc1: Document = ["int": .int32(42), "int32": .int32(42), "int64": .int32(42), "double": .int32(42)] expect(try decoder.decode(BSONNumbers.self, from: doc1)).to(equal(s)) // store all values as Int64s and decode them to their requested types - let doc2: Document = ["int": Int64(42), "int32": Int64(42), "int64": Int64(42), "double": Int64(42)] + let doc2: Document = ["int": .int64(42), "int32": .int64(42), "int64": .int64(42), "double": .int64(42)] expect(try decoder.decode(BSONNumbers.self, from: doc2)).to(equal(s)) // store all values as Doubles and decode them to their requested types - let doc3: Document = ["int": Double(42), "int32": Double(42), "int64": Double(42), "double": Double(42)] + let doc3: Document = ["int": 42.0, "int32": 42.0, "int64": 42.0, "double": 42.0] expect(try decoder.decode(BSONNumbers.self, from: doc3)).to(equal(s)) } @@ -266,13 +266,13 @@ final class CodecTests: MongoSwiftTestCase { let double: Double let string: String let doc: Document - let arr: [Int] + let arr: [BSON] let binary: Binary let oid: ObjectId let bool: Bool let date: Date - let code: CodeWithScope - let int: Int + let code: Code + let codeWithScope: CodeWithScope let ts: Timestamp let int32: Int32 let int64: Int64 @@ -290,13 +290,13 @@ final class CodecTests: MongoSwiftTestCase { double: Double(2), string: "hi", doc: ["x": 1], - arr: [1, 2], + arr: [.int32(1), .int32(2)], binary: try Binary(base64: "//8=", subtype: .generic), oid: ObjectId("507f1f77bcf86cd799439011")!, bool: true, date: Date(timeIntervalSinceReferenceDate: 5000), - code: CodeWithScope(code: "hi", scope: ["x": 1]), - int: 1, + code: Code(code: "hi"), + codeWithScope: CodeWithScope(code: "hi", scope: ["x": .int64(1)]), ts: Timestamp(timestamp: 1, inc: 2), int32: 5, int64: 6, @@ -313,27 +313,27 @@ final class CodecTests: MongoSwiftTestCase { // Manually construct a document from this instance for comparision with encoder output. public func toDocument() -> Document { return [ - "double": self.double, - "string": self.string, - "doc": self.doc, - "arr": self.arr, - "binary": self.binary, - "oid": self.oid, - "bool": self.bool, - "date": self.date, - "code": self.code, - "int": self.int, - "ts": self.ts, - "int32": self.int32, - "int64": self.int64, - "dec": self.dec, - "minkey": self.minkey, - "maxkey": self.maxkey, - "regex": self.regex, - "symbol": self.symbol, - "undefined": self.undefined, - "dbpointer": self.dbpointer, - "null": self.null + "double": .double(self.double), + "string": .string(self.string), + "doc": .document(self.doc), + "arr": .array(self.arr), + "binary": .binary(self.binary), + "oid": .objectId(self.oid), + "bool": .bool(self.bool), + "date": .datetime(self.date), + "code": .code(self.code), + "codeWithScope": .codeWithScope(self.codeWithScope), + "ts": .timestamp(self.ts), + "int32": .int32(self.int32), + "int64": .int64(self.int64), + "dec": .decimal128(self.dec), + "minkey": .minKey, + "maxkey": .maxKey, + "regex": .regex(self.regex), + "symbol": .symbol(self.symbol), + "undefined": .undefined, + "dbpointer": .dbPointer(self.dbpointer), + "null": .null ] } } @@ -363,7 +363,8 @@ final class CodecTests: MongoSwiftTestCase { "oid" : { "$oid" : "507f1f77bcf86cd799439011" }, "bool" : true, "date" : { "$date" : "2001-01-01T01:23:20Z" }, - "code" : { "$code" : "hi", "$scope" : { "x" : { "$numberLong": "1" } } }, + "code" : { "$code" : "hi" }, + "codeWithScope" : { "$code" : "hi", "$scope" : { "x" : { "$numberLong": "1" } } }, "int" : 1, "ts" : { "$timestamp" : { "t" : 1, "i" : 2 } }, "int32" : 5, @@ -411,13 +412,21 @@ final class CodecTests: MongoSwiftTestCase { from: "{\"$binary\" : {\"base64\": \"//8=\", \"subType\" : \"00\"}}") ).to(equal(binary)) + expect(try decoder.decode(Code.self, + from: "{\"$code\": \"hi\" }")).to(equal(Code(code: "hi"))) + let code = Code(code: "hi") + expect(try decoder.decode(Code.self, + from: "{\"$code\": \"hi\", \"$scope\": {\"x\" : { \"$numberLong\": \"1\" }} }") + ).to(throwError()) + expect(try decoder.decode(Code.self, from: "{\"$code\": \"hi\" }")).to(equal(code)) + expect(try decoder.decode(CodeWithScope.self, - from: "{\"$code\": \"hi\" }")).to(equal(CodeWithScope(code: "hi"))) + from: "{\"$code\": \"hi\" }")).to(throwError()) let cws = CodeWithScope(code: "hi", scope: ["x": 1]) expect(try decoder.decode(CodeWithScope.self, from: "{\"$code\": \"hi\", \"$scope\": {\"x\" : { \"$numberLong\": \"1\" }} }") ).to(equal(cws)) - expect(try decoder.decode(Document.self, from: "{\"x\": 1}")).to(equal(["x": Int32(1)])) + expect(try decoder.decode(Document.self, from: "{\"x\": 1}")).to(equal(["x": .int32(1)])) let ts = Timestamp(timestamp: 1, inc: 2) expect(try decoder.decode(Timestamp.self, from: "{ \"$timestamp\" : { \"t\" : 1, \"i\" : 2 } }")).to(equal(ts)) @@ -487,254 +496,223 @@ final class CodecTests: MongoSwiftTestCase { expect(try encoder.encode(values2)).to(equal([["int": 1, "string": "hello"], nil])) } - struct AnyBSONStruct: Codable { - let x: AnyBSONValue + struct AnyBSONStruct: Codable, Equatable { + let x: BSON - init(_ x: BSONValue) { - self.x = AnyBSONValue(x) + init(_ x: BSON) { + self.x = x } } - // test encoding/decoding AnyBSONValues with BSONEncoder and Decoder - func testAnyBSONValueIsBSONCodable() throws { + // test encoding/decoding BSONs with BSONEncoder and Decoder + func testBSONIsBSONCodable() throws { let encoder = BSONEncoder() let decoder = BSONDecoder() // standalone document let doc: Document = ["y": 1] - expect(try encoder.encode(AnyBSONValue(doc))).to(equal(doc)) - expect(try decoder.decode(AnyBSONValue.self, from: doc).value).to(bsonEqual(doc)) - expect(try decoder.decode(AnyBSONValue.self, from: doc.canonicalExtendedJSON).value).to(bsonEqual(doc)) + let bsonDoc = BSON.document(doc) + expect(try encoder.encode(bsonDoc)).to(equal(doc)) + expect(try decoder.decode(BSON.self, from: doc)).to(equal(bsonDoc)) + expect(try decoder.decode(BSON.self, from: doc.canonicalExtendedJSON)).to(equal(bsonDoc)) // doc wrapped in a struct - let wrappedDoc: Document = ["x": doc] - expect(try encoder.encode(AnyBSONStruct(doc))).to(equal(wrappedDoc)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedDoc).x.value).to(bsonEqual(doc)) + let wrappedDoc: Document = ["x": bsonDoc] + expect(try encoder.encode(AnyBSONStruct(bsonDoc))).to(equal(wrappedDoc)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedDoc).x).to(equal(bsonDoc)) expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedDoc.canonicalExtendedJSON).x.value).to(bsonEqual(doc)) + from: wrappedDoc.canonicalExtendedJSON).x).to(equal(bsonDoc)) // values wrapped in an `AnyBSONStruct` - let double = 42.0 - expect(try decoder.decode(AnyBSONValue.self, - from: "{\"$numberDouble\": \"42\"}").value).to(bsonEqual(double)) + let double: BSON = 42.0 + expect(try decoder.decode(BSON.self, from: "{\"$numberDouble\": \"42\"}")).to(equal(double)) let wrappedDouble: Document = ["x": double] expect(try encoder.encode(AnyBSONStruct(double))).to(equal(wrappedDouble)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedDouble).x.value).to(bsonEqual(double)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedDouble.canonicalExtendedJSON).x.value).to(bsonEqual(double)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedDouble).x).to(equal(double)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedDouble.canonicalExtendedJSON).x).to(equal(double)) // string - let string = "hi" - expect(try decoder.decode(AnyBSONValue.self, from: "\"hi\"").value).to(bsonEqual(string)) + let string: BSON = "hi" + expect(try decoder.decode(BSON.self, from: "\"hi\"")).to(equal(string)) let wrappedString: Document = ["x": string] expect(try encoder.encode(AnyBSONStruct(string))).to(equal(wrappedString)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedString).x.value).to(bsonEqual(string)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedString.canonicalExtendedJSON).x.value).to(bsonEqual(string)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedString).x).to(equal(string)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedString.canonicalExtendedJSON).x).to(equal(string)) // array - let array: [BSONValue] = [1, 2, "hello"] + let array: BSON = [1, 2, "hello"] let decodedArray = try decoder.decode( - AnyBSONValue.self, + BSON.self, from: "[{\"$numberLong\": \"1\"}, {\"$numberLong\": \"2\"}, \"hello\"]" - ).value as? [BSONValue] - expect(decodedArray?[0]).to(bsonEqual(1)) - expect(decodedArray?[1]).to(bsonEqual(2)) - expect(decodedArray?[2]).to(bsonEqual("hello")) + ).arrayValue + expect(decodedArray).toNot(beNil()) + expect(decodedArray?[0]).to(equal(1)) + expect(decodedArray?[1]).to(equal(2)) + expect(decodedArray?[2]).to(equal("hello")) let wrappedArray: Document = ["x": array] expect(try encoder.encode(AnyBSONStruct(array))).to(equal(wrappedArray)) - let decodedWrapped = try decoder.decode(AnyBSONStruct.self, from: wrappedArray).x.value as? [BSONValue] - expect(decodedWrapped?[0]).to(bsonEqual(1)) - expect(decodedWrapped?[1]).to(bsonEqual(2)) - expect(decodedWrapped?[2]).to(bsonEqual("hello")) - - // an array with a non-BSONValue - let arrWithNonBSONValue: [Any?] = [1, "hi", BSONNull(), Int16(4)] - expect(try arrWithNonBSONValue.encode(to: DocumentStorage(), forKey: "arrWithNonBSONValue")) - .to(throwError(UserError.logicError(message: ""))) + let decodedWrapped = try decoder.decode(AnyBSONStruct.self, from: wrappedArray).x.arrayValue + expect(decodedWrapped?[0]).to(equal(1)) + expect(decodedWrapped?[1]).to(equal(2)) + expect(decodedWrapped?[2]).to(equal("hello")) // binary - let binary = try Binary(base64: "//8=", subtype: .generic) + let binary = BSON.binary(try Binary(base64: "//8=", subtype: .generic)) expect( - try decoder.decode(AnyBSONValue.self, - from: "{\"$binary\" : {\"base64\": \"//8=\", \"subType\" : \"00\"}}").value as? Binary + try decoder.decode(BSON.self, + from: "{\"$binary\" : {\"base64\": \"//8=\", \"subType\" : \"00\"}}") ).to(equal(binary)) let wrappedBinary: Document = ["x": binary] expect(try encoder.encode(AnyBSONStruct(binary))).to(equal(wrappedBinary)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedBinary).x.value).to(bsonEqual(binary)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedBinary).x).to(equal(binary)) expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedBinary.canonicalExtendedJSON).x.value).to(bsonEqual(binary)) + from: wrappedBinary.canonicalExtendedJSON).x).to(equal(binary)) // objectid let oid = ObjectId() + let bsonOid = BSON.objectId(oid) - expect(try decoder.decode(AnyBSONValue.self, - from: "{\"$oid\": \"\(oid.hex)\"}").value).to(bsonEqual(oid)) + expect(try decoder.decode(BSON.self, from: "{\"$oid\": \"\(oid.hex)\"}")).to(equal(bsonOid)) - let wrappedOid: Document = ["x": oid] - expect(try encoder.encode(AnyBSONStruct(oid))).to(equal(wrappedOid)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedOid).x.value).to(bsonEqual(oid)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedOid.canonicalExtendedJSON).x.value).to(bsonEqual(oid)) + let wrappedOid: Document = ["x": bsonOid] + expect(try encoder.encode(AnyBSONStruct(bsonOid))).to(equal(wrappedOid)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedOid).x).to(equal(bsonOid)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedOid.canonicalExtendedJSON).x).to(equal(bsonOid)) // bool - let bool = true + let bool: BSON = true - expect(try decoder.decode(AnyBSONValue.self, from: "true").value).to(bsonEqual(bool)) + expect(try decoder.decode(BSON.self, from: "true")).to(equal(bool)) let wrappedBool: Document = ["x": bool] expect(try encoder.encode(AnyBSONStruct(bool))).to(equal(wrappedBool)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedBool).x.value).to(bsonEqual(bool)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedBool.canonicalExtendedJSON).x.value).to(bsonEqual(bool)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedBool).x).to(equal(bool)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedBool.canonicalExtendedJSON).x).to(equal(bool)) // date - let date = Date(timeIntervalSince1970: 5000) + let date = BSON.datetime(Date(timeIntervalSince1970: 5000)) - expect( - try decoder.decode(AnyBSONValue.self, - from: "{ \"$date\" : { \"$numberLong\" : \"5000000\" } }").value as? Date - ).to(equal(date)) + expect(try decoder.decode(BSON.self, from: "{ \"$date\" : { \"$numberLong\" : \"5000000\" } }")).to(equal(date)) let wrappedDate: Document = ["x": date] expect(try encoder.encode(AnyBSONStruct(date))).to(equal(wrappedDate)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedDate).x.value).to(bsonEqual(date)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedDate.canonicalExtendedJSON).x.value).to(bsonEqual(date)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedDate).x).to(equal(date)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedDate.canonicalExtendedJSON).x).to(equal(date)) let dateEncoder = BSONEncoder() dateEncoder.dateEncodingStrategy = .millisecondsSince1970 - expect(try dateEncoder.encode(AnyBSONStruct(date))).to(bsonEqual(["x": date.msSinceEpoch] as Document)) + expect(try dateEncoder.encode(AnyBSONStruct(date))).to(equal(["x": 5000000])) let dateDecoder = BSONDecoder() dateDecoder.dateDecodingStrategy = .millisecondsSince1970 - expect(try dateDecoder.decode(AnyBSONStruct.self, from: wrappedDate)) - .to(throwError(CodecTests.typeMismatchErr)) + expect(try dateDecoder.decode(AnyBSONStruct.self, from: wrappedDate)).to(throwError(CodecTests.typeMismatchErr)) // regex - let regex = RegularExpression(pattern: "abc", options: "imx") + let regex = BSON.regex(RegularExpression(pattern: "abc", options: "imx")) - expect(try decoder.decode(AnyBSONValue.self, + expect(try decoder.decode(BSON.self, from: "{ \"$regularExpression\" : { \"pattern\" : \"abc\", \"options\" : \"imx\" } }") - .value).to(bsonEqual(regex)) + ).to(equal(regex)) let wrappedRegex: Document = ["x": regex] expect(try encoder.encode(AnyBSONStruct(regex))).to(equal(wrappedRegex)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedRegex).x.value).to(bsonEqual(regex)) - expect( - try decoder.decode(AnyBSONStruct.self, - from: wrappedRegex.canonicalExtendedJSON).x.value as? RegularExpression - ).to(equal(regex)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedRegex).x).to(equal(regex)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedRegex.canonicalExtendedJSON).x).to(equal(regex)) // codewithscope - let code = CodeWithScope(code: "console.log(x);", scope: ["x": 1]) + let code = BSON.codeWithScope(CodeWithScope(code: "console.log(x);", scope: ["x": 1])) expect( - try decoder.decode(AnyBSONValue.self, + try decoder.decode(BSON.self, from: "{ \"$code\" : \"console.log(x);\", " - + "\"$scope\" : { \"x\" : { \"$numberLong\" : \"1\" } } }").value as? CodeWithScope + + "\"$scope\" : { \"x\" : { \"$numberLong\" : \"1\" } } }") ).to(equal(code)) let wrappedCode: Document = ["x": code] expect(try encoder.encode(AnyBSONStruct(code))).to(equal(wrappedCode)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedCode).x.value).to(bsonEqual(code)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedCode.canonicalExtendedJSON).x.value).to(bsonEqual(code)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedCode).x).to(equal(code)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedCode.canonicalExtendedJSON).x).to(equal(code)) // int32 - let int32 = Int32(5) + let int32 = BSON.int32(5) - expect(try decoder.decode(AnyBSONValue.self, from: "{ \"$numberInt\" : \"5\" }").value).to(bsonEqual(int32)) + expect(try decoder.decode(BSON.self, from: "{ \"$numberInt\" : \"5\" }")).to(equal(int32)) let wrappedInt32: Document = ["x": int32] expect(try encoder.encode(AnyBSONStruct(int32))).to(equal(wrappedInt32)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedInt32).x.value).to(bsonEqual(int32)) - expect(try decoder.decode( - AnyBSONStruct.self, - from: wrappedInt32.canonicalExtendedJSON).x.value).to(bsonEqual(int32) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedInt32).x).to(equal(int32)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedInt32.canonicalExtendedJSON).x).to(equal(int32) ) // int - let int = 5 + let int: BSON = 5 - expect(try decoder.decode(AnyBSONValue.self, from: "{ \"$numberLong\" : \"5\" }").value).to(bsonEqual(int)) + expect(try decoder.decode(BSON.self, from: "{ \"$numberLong\" : \"5\" }")).to(equal(int)) let wrappedInt: Document = ["x": int] expect(try encoder.encode(AnyBSONStruct(int))).to(equal(wrappedInt)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedInt).x.value).to(bsonEqual(int)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedInt.canonicalExtendedJSON).x.value).to(bsonEqual(int)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedInt).x).to(equal(int)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedInt.canonicalExtendedJSON).x).to(equal(int)) // int64 - let int64 = Int64(5) + let int64 = BSON.int64(5) - expect(try decoder.decode(AnyBSONValue.self, from: "{\"$numberLong\":\"5\"}").value).to(bsonEqual(int64)) + expect(try decoder.decode(BSON.self, from: "{\"$numberLong\":\"5\"}")).to(equal(int64)) let wrappedInt64: Document = ["x": int64] - expect(try encoder.encode(AnyBSONStruct(Int64(5)))).to(equal(wrappedInt64)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedInt64).x.value).to(bsonEqual(int64)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedInt64.canonicalExtendedJSON).x.value).to(bsonEqual(int64)) + expect(try encoder.encode(AnyBSONStruct(int64))).to(equal(wrappedInt64)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedInt64).x).to(equal(int64)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedInt64.canonicalExtendedJSON).x).to(equal(int64)) // decimal128 - let decimal = Decimal128("1.2E+10")! + let decimal = BSON.decimal128(Decimal128("1.2E+10")!) - expect( - try decoder.decode(AnyBSONValue.self, from: "{ \"$numberDecimal\" : \"1.2E+10\" }").value as? Decimal128 - ).to(equal(decimal)) + expect(try decoder.decode(BSON.self, from: "{ \"$numberDecimal\" : \"1.2E+10\" }")).to(equal(decimal)) let wrappedDecimal: Document = ["x": decimal] expect(try encoder.encode(AnyBSONStruct(decimal))).to(equal(wrappedDecimal)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedDecimal).x.value).to(bsonEqual(decimal)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedDecimal.canonicalExtendedJSON).x.value).to(bsonEqual(decimal)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedDecimal).x).to(equal(decimal)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedDecimal.canonicalExtendedJSON).x).to(equal(decimal)) // maxkey - let maxKey = MaxKey() + let maxKey = BSON.maxKey - expect(try decoder.decode(AnyBSONValue.self, from: "{ \"$maxKey\" : 1 }").value).to(bsonEqual(maxKey)) + expect(try decoder.decode(BSON.self, from: "{ \"$maxKey\" : 1 }")).to(equal(maxKey)) let wrappedMaxKey: Document = ["x": maxKey] expect(try encoder.encode(AnyBSONStruct(maxKey))).to(equal(wrappedMaxKey)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedMaxKey).x.value).to(bsonEqual(maxKey)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedMaxKey.canonicalExtendedJSON).x.value).to(bsonEqual(maxKey)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedMaxKey).x).to(equal(maxKey)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedMaxKey.canonicalExtendedJSON).x).to(equal(maxKey)) // minkey - let minKey = MinKey() + let minKey = BSON.minKey - expect(try decoder.decode(AnyBSONValue.self, from: "{ \"$minKey\" : 1 }").value).to(bsonEqual(minKey)) + expect(try decoder.decode(BSON.self, from: "{ \"$minKey\" : 1 }")).to(equal(minKey)) let wrappedMinKey: Document = ["x": minKey] expect(try encoder.encode(AnyBSONStruct(minKey))).to(equal(wrappedMinKey)) - expect(try decoder.decode(AnyBSONStruct.self, from: wrappedMinKey).x.value).to(bsonEqual(minKey)) - expect(try decoder.decode(AnyBSONStruct.self, - from: wrappedMinKey.canonicalExtendedJSON).x.value).to(bsonEqual(minKey)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedMinKey).x).to(equal(minKey)) + expect(try decoder.decode(AnyBSONStruct.self, from: wrappedMinKey.canonicalExtendedJSON).x).to(equal(minKey)) // BSONNull - expect( - try decoder.decode(AnyBSONStruct.self, from: ["x": BSONNull()]).x - ).to(equal(AnyBSONValue(BSONNull()))) - - expect(try encoder.encode(AnyBSONStruct(BSONNull()))).to(equal(["x": BSONNull()])) + expect(try decoder.decode(AnyBSONStruct.self, from: ["x": .null]).x).to(equal(BSON.null)) + expect(try encoder.encode(AnyBSONStruct(.null))).to(equal(["x": .null])) } fileprivate struct IncorrectTopLevelEncode: Encodable { - let x: AnyBSONValue + let x: BSON // An empty encode here is incorrect. func encode(to encoder: Encoder) throws {} - init(_ x: BSONValue) { - self.x = AnyBSONValue(x) + init(_ x: BSON) { + self.x = x } } @@ -751,7 +729,7 @@ final class CodecTests: MongoSwiftTestCase { try container.encode(x, forKey: .x) } - init(_ x: BSONValue) { + init(_ x: BSON) { self.x = IncorrectTopLevelEncode(x) } } @@ -761,8 +739,8 @@ final class CodecTests: MongoSwiftTestCase { // A top-level `encode()` problem should throw an error, but any such issues deeper in the recursion should not. // These tests are to ensure that we handle incorrect encode() implementations in the same way as JSONEncoder. - expect(try encoder.encode(IncorrectTopLevelEncode(BSONNull()))).to(throwError(CodecTests.invalidValueErr)) - expect(try encoder.encode(CorrectTopLevelEncode(BSONNull()))).to(equal(["x": Document()])) + expect(try encoder.encode(IncorrectTopLevelEncode(.null))).to(throwError(CodecTests.invalidValueErr)) + expect(try encoder.encode(CorrectTopLevelEncode(.null))).to(equal(["x": [:]])) } // test encoding options structs that have non-standard CodingKeys diff --git a/Tests/MongoSwiftTests/CommandMonitoringTests.swift b/Tests/MongoSwiftTests/CommandMonitoringTests.swift index 468dd58dc..26602de69 100644 --- a/Tests/MongoSwiftTests/CommandMonitoringTests.swift +++ b/Tests/MongoSwiftTests/CommandMonitoringTests.swift @@ -143,7 +143,7 @@ private struct CMTest: Decodable { // swiftlint:disable cyclomatic_complexity func doOperation(withCollection collection: SyncMongoCollection) throws { // TODO SWIFT-31: use readPreferences for commands if provided - let filter: Document = self.op.args["filter"] as? Document ?? [:] + let filter: Document = self.op.args["filter"]?.documentValue ?? [:] switch self.op.name { case "count": @@ -154,29 +154,29 @@ private struct CMTest: Decodable { _ = try? collection.deleteOne(filter) case "find": - let modifiers = self.op.args["modifiers"] as? Document + let modifiers = self.op.args["modifiers"]?.documentValue var hint: Hint? - if let hintDoc = modifiers?["$hint"] as? Document { + if let hintDoc = modifiers?["$hint"]?.documentValue { hint = .indexSpec(hintDoc) } - let options = FindOptions(batchSize: (self.op.args["batchSize"] as? BSONNumber)?.int32Value, - comment: modifiers?["$comment"] as? String, + let options = FindOptions(batchSize: self.op.args["batchSize"]?.asInt32(), + comment: modifiers?["$comment"]?.stringValue, hint: hint, - limit: (self.op.args["limit"] as? BSONNumber)?.int64Value, - max: modifiers?["$max"] as? Document, - maxTimeMS: (modifiers?["$maxTimeMS"] as? BSONNumber)?.int64Value, - min: modifiers?["$min"] as? Document, - returnKey: modifiers?["$returnKey"] as? Bool, - showRecordId: modifiers?["$showDiskLoc"] as? Bool, - skip: (self.op.args["skip"] as? BSONNumber)?.int64Value, - sort: self.op.args["sort"] as? Document) + limit: self.op.args["limit"]?.asInt64(), + max: modifiers?["$max"]?.documentValue, + maxTimeMS: modifiers?["$maxTimeMS"]?.asInt64(), + min: modifiers?["$min"]?.documentValue, + returnKey: modifiers?["$returnKey"]?.boolValue, + showRecordId: modifiers?["$showDiskLoc"]?.boolValue, + skip: self.op.args["skip"]?.asInt64(), + sort: self.op.args["sort"]?.documentValue) // we have to iterate the cursor to make the command execute for _ in try! collection.find(filter, options: options) {} case "insertMany": - let documents: [Document] = try self.op.args.get("documents") - let options = InsertManyOptions(ordered: self.op.args["ordered"] as? Bool) + let documents = (self.op.args["documents"]?.arrayValue?.compactMap { $0.documentValue })! + let options = InsertManyOptions(ordered: self.op.args["ordered"]?.boolValue) _ = try? collection.insertMany(documents, options: options) case "insertOne": @@ -189,7 +189,7 @@ private struct CMTest: Decodable { case "updateOne": let update: Document = try self.op.args.get("update") - let options = UpdateOptions(upsert: self.op.args["upsert"] as? Bool) + let options = UpdateOptions(upsert: self.op.args["upsert"]?.boolValue) _ = try? collection.updateOne(filter: filter, update: update, options: options) default: @@ -211,15 +211,15 @@ private protocol ExpectationType { private func makeExpectation(_ document: Document) throws -> ExpectationType { let decoder = BSONDecoder() - if let doc = document["command_started_event"] as? Document { + if let doc = document["command_started_event"]?.documentValue { return try decoder.decode(CommandStartedExpectation.self, from: doc) } - if let doc = document["command_succeeded_event"] as? Document { + if let doc = document["command_succeeded_event"]?.documentValue { return try decoder.decode(CommandSucceededExpectation.self, from: doc) } - if let doc = document["command_failed_event"] as? Document { + if let doc = document["command_failed_event"]?.documentValue { return try decoder.decode(CommandFailedExpectation.self, from: doc) } @@ -250,10 +250,10 @@ private struct CommandStartedExpectation: ExpectationType, Decodable { // if it's a getMore, we can't directly compare the results if commandName == "getMore" { // verify that the getMore ID matches the stored cursor ID for this test - expect(event.command["getMore"]).to(bsonEqual(testContext["cursorId"] as? BSONNumber)) + expect(event.command["getMore"]).to(equal(testContext["cursorId"] as? BSON)) // compare collection and batchSize fields - expect(event.command["collection"]).to(bsonEqual(self.command["collection"])) - expect(event.command["batchSize"]).to(bsonEqual(self.command["batchSize"])) + expect(event.command["collection"]).to(equal(self.command["collection"])) + expect(event.command["batchSize"]).to(equal(self.command["batchSize"])) } else { // remove fields from the command we received that are not in the expected // command, and reorder them, so we can do a direct comparison of the documents @@ -271,29 +271,36 @@ private func normalizeCommand(_ input: Document) -> Document { // from the expected command unless if it is set to true, because none of the // tests explicitly provide upsert: false or multi: false, yet they // are in the expected commands anyway. - if ["upsert", "multi"].contains(k), let bV = v as? Bool { + if ["upsert", "multi"].contains(k), let bV = v.boolValue { if bV { output[k] = true } else { continue } // The tests don't explicitly store maxTimeMS as an Int64, so libmongoc // parses it as an Int32 which we convert to Int. convert to Int64 here because we - /// (as per the crud spec) use an Int64 for maxTimeMS and send that to + // (as per the crud spec) use an Int64 for maxTimeMS and send that to // the server in our actual commands. - } else if k == "maxTimeMS", let iV = (v as? BSONNumber)?.int64Value { - output[k] = iV + } else if k == "maxTimeMS", let iV = v.asInt64() { + output[k] = .int64(iV) // The expected batch sizes are always Int64s, however, find command // events actually have Int32 batch sizes... (as the spec says...) // but getMores have Int64s. so only convert if it's a find command... - } else if k == "batchSize", let iV = v as? BSONNumber { - if input["find"] != nil { output[k] = iV.int32Value! } else { output[k] = v } + } else if k == "batchSize" && input["find"] != nil { + output[k] = .int32(v.asInt32()!) // recursively normalize if it's a document - } else if let docVal = v as? Document { - output[k] = normalizeCommand(docVal) + } else if let docVal = v.documentValue { + output[k] = .document(normalizeCommand(docVal)) // recursively normalize each element if it's an array - } else if let arrVal = v as? [Document] { - output[k] = arrVal.map { normalizeCommand($0) } + } else if case let .array(arrVal) = v { + output[k] = .array(arrVal.map { + switch $0 { + case let .document(d): + return .document(normalizeCommand(d)) + default: + return $0 + } + }) // just copy the value over as is } else { @@ -325,8 +332,8 @@ private struct CommandSucceededExpectation: ExpectationType, Decodable { let commandName: String var reply: Document { return normalizeExpectedReply(originalReply) } - var writeErrors: [Document]? { return originalReply["writeErrors"] as? [Document] } - var cursor: Document? { return originalReply["cursor"] as? Document } + var writeErrors: [Document]? { return originalReply["writeErrors"]?.arrayValue?.compactMap { $0.documentValue } } + var cursor: Document? { return originalReply["cursor"]?.documentValue } enum CodingKeys: String, CodingKey { case commandName = "command_name", originalReply = "reply" @@ -344,7 +351,7 @@ private struct CommandSucceededExpectation: ExpectationType, Decodable { expect(event.commandName).to(equal(self.commandName)) // compare writeErrors, if any - let receivedWriteErrs = event.reply["writeErrors"] as? [Document] + let receivedWriteErrs = event.reply["writeErrors"]?.arrayValue?.compactMap { $0.documentValue } if let expectedErrs = self.writeErrors { expect(receivedWriteErrs).toNot(beNil()) checkWriteErrors(expected: expectedErrs, actual: receivedWriteErrs!) @@ -352,17 +359,17 @@ private struct CommandSucceededExpectation: ExpectationType, Decodable { expect(receivedWriteErrs).to(beNil()) } - let receivedCursor = event.reply["cursor"] as? Document + let receivedCursor = event.reply["cursor"]?.documentValue if let expectedCursor = self.cursor { // if the received cursor has an ID, and the expected ID is not 0, compare cursor IDs - if let id = receivedCursor!["id"] as? BSONNumber, (expectedCursor["id"] as? BSONNumber)?.intValue != 0 { - let storedId = testContext["cursorId"] as? BSONNumber + if let id = receivedCursor!["id"], expectedCursor["id"]?.asInt() != 0 { + let storedId = testContext["cursorId"] as? BSON // if we aren't already storing a cursor ID for this test, add one if storedId == nil { testContext["cursorId"] = id // otherwise, verify that this ID matches the stored one } else { - expect(storedId).to(bsonEqual(id)) + expect(storedId).to(equal(id)) } } compareCursors(expected: expectedCursor, actual: receivedCursor!) @@ -378,9 +385,9 @@ private struct CommandSucceededExpectation: ExpectationType, Decodable { expect(expected.count).to(equal(actual.count)) for err in actual { // check each error code exists and is > 0 - expect((err["code"] as? BSONNumber)?.intValue).to(beGreaterThan(0)) + expect(err["code"]?.asInt()).to(beGreaterThan(0)) // check each error msg exists and has length > 0 - expect(err["errmsg"] as? String).toNot(beEmpty()) + expect(err["errmsg"]?.stringValue).toNot(beEmpty()) } } @@ -388,11 +395,11 @@ private struct CommandSucceededExpectation: ExpectationType, Decodable { /// (handled in `compare` because we need the test context). func compareCursors(expected: Document, actual: Document) { let ordered = rearrangeDoc(actual, toLookLike: expected) - expect(ordered["ns"]).to(bsonEqual(expected["ns"])) - if let firstBatch = expected["firstBatch"] as? [Document] { - expect(ordered["firstBatch"]).to(bsonEqual(firstBatch)) - } else if let nextBatch = expected["nextBatch"] as? [Document] { - expect(ordered["nextBatch"]).to(bsonEqual(nextBatch)) + expect(ordered["ns"]).to(equal(expected["ns"])) + if let firstBatch = expected["firstBatch"] { + expect(ordered["firstBatch"]).to(equal(firstBatch)) + } else if let nextBatch = expected["nextBatch"] { + expect(ordered["nextBatch"]).to(equal(nextBatch)) } } } @@ -409,8 +416,8 @@ private func normalizeExpectedReply(_ input: Document) -> Document { continue // The server sends back doubles, but the JSON test files // contain integer statuses (see SPEC-1050.) - } else if k == "ok", let dV = (v as? BSONNumber)?.doubleValue { - output[k] = dV + } else if k == "ok", let dV = v.asDouble() { + output[k] = .double(dV) // just copy the value over as is } else { output[k] = v diff --git a/Tests/MongoSwiftTests/CrudTests.swift b/Tests/MongoSwiftTests/CrudTests.swift index 3f8405aa7..1fc1697d6 100644 --- a/Tests/MongoSwiftTests/CrudTests.swift +++ b/Tests/MongoSwiftTests/CrudTests.swift @@ -109,23 +109,23 @@ private class CrudTest { let operationName: String let args: Document let error: Bool? - let result: BSONValue? + let result: BSON? let collection: Document? - var arrayFilters: [Document]? { return self.args["arrayFilters"] as? [Document] } - var batchSize: Int32? { return (self.args["batchSize"] as? BSONNumber)?.int32Value } - var collation: Document? { return self.args["collation"] as? Document } - var sort: Document? { return self.args["sort"] as? Document } - var skip: Int64? { return (self.args["skip"] as? BSONNumber)?.int64Value } - var limit: Int64? { return (self.args["limit"] as? BSONNumber)?.int64Value } - var projection: Document? { return self.args["projection"] as? Document } + var arrayFilters: [Document]? { return self.args["arrayFilters"]?.arrayValue?.compactMap { $0.documentValue } } + var batchSize: Int32? { return self.args["batchSize"]?.int32Value } + var collation: Document? { return self.args["collation"]?.documentValue } + var sort: Document? { return self.args["sort"]?.documentValue } + var skip: Int64? { return self.args["skip"]?.asInt64() } + var limit: Int64? { return self.args["limit"]?.asInt64() } + var projection: Document? { return self.args["projection"]?.documentValue } var returnDoc: ReturnDocument? { - if let ret = self.args["returnDocument"] as? String { + if let ret = self.args["returnDocument"]?.stringValue { return ret == "After" ? .after : .before } return nil } - var upsert: Bool? { return self.args["upsert"] as? Bool } + var upsert: Bool? { return self.args["upsert"]?.boolValue } /// Initializes a new `CrudTest` from a `Document`. required init(_ test: Document) throws { @@ -134,9 +134,9 @@ private class CrudTest { self.operationName = try operation.get("name") self.args = try operation.get("arguments") let outcome: Document = try test.get("outcome") - self.error = outcome["error"] as? Bool + self.error = outcome["error"]?.boolValue self.result = outcome["result"] - self.collection = outcome["collection"] as? Document + self.collection = outcome["collection"]?.documentValue } // Subclasses should implement `execute` according to the particular operation(s) they are for. @@ -151,23 +151,23 @@ private class CrudTest { } // if a name is not specified, check the current collection var collToCheck = coll - if let name = collection["name"] as? String { + if let name = collection["name"]?.stringValue { collToCheck = db.collection(name) } - expect(Array(try collToCheck.find([:]))).to(equal(try collection.get("data"))) + expect(BSON.array(try collToCheck.find([:]).map { .document($0) })).to(equal(collection["data"])) } // Given an `UpdateResult`, verify that it matches the expected results in this `CrudTest`. // Meant for use by subclasses whose operations return `UpdateResult`s, such as `UpdateTest` // and `ReplaceOneTest`. func verifyUpdateResult(_ result: UpdateResult?) throws { - let expected = try BSONDecoder().decode(UpdateResult.self, from: self.result as! Document) + let expected = try BSONDecoder().decode(UpdateResult.self, from: self.result!.documentValue!) expect(result?.matchedCount).to(equal(expected.matchedCount)) expect(result?.modifiedCount).to(equal(expected.modifiedCount)) expect(result?.upsertedCount).to(equal(expected.upsertedCount)) - if let upsertedId = result?.upsertedId as? BSONNumber { - expect(upsertedId).to(bsonEqual(expected.upsertedId)) + if let upsertedId = result?.upsertedId { + expect(upsertedId).to(equal(expected.upsertedId)) } else { expect(expected.upsertedId).to(beNil()) } @@ -180,10 +180,10 @@ private class CrudTest { return } - if self.result is BSONNull { + if self.result == .null { expect(result).to(beNil()) } else { - expect(result).to(equal(self.result as? Document)) + expect(result).to(equal(self.result?.documentValue)) } } } @@ -191,7 +191,7 @@ private class CrudTest { /// A class for executing `aggregate` tests private class AggregateTest: CrudTest { override func execute(usingCollection coll: SyncMongoCollection) throws { - let pipeline: [Document] = try self.args.get("pipeline") + let pipeline = self.args["pipeline"]!.arrayValue!.compactMap { $0.documentValue } let options = AggregateOptions(batchSize: self.batchSize, collation: self.collation) let cursor = try coll.aggregate(pipeline, options: options) if self.collection != nil { @@ -202,16 +202,16 @@ private class AggregateTest: CrudTest { expect(cursor.next()).to(beNil()) } else { // if not $out, verify that the cursor contains the expected documents. - expect(Array(cursor)).to(equal(self.result as? [Document])) + expect(BSON.array(cursor.map { .document($0) })).to(equal(self.result)) } } } private class BulkWriteTest: CrudTest { override func execute(usingCollection coll: SyncMongoCollection) throws { - let requestDocuments: [Document] = try self.args.get("requests") + let requestDocuments: [Document] = self.args["requests"]!.arrayValue!.compactMap { $0.documentValue } let requests = try requestDocuments.map { try BSONDecoder().decode(WriteModel.self, from: $0) } - let options = try BSONDecoder().decode(BulkWriteOptions.self, from: self.args["options"] as? Document ?? [:]) + let options = try BSONDecoder().decode(BulkWriteOptions.self, from: self.args["options"]?.documentValue ?? [:]) let expectError = self.error ?? false do { @@ -227,7 +227,7 @@ private class BulkWriteTest: CrudTest { } } - private static func prepareIds(_ ids: [Int: BSONValue]) -> Document { + private static func prepareIds(_ ids: [Int: BSON]) -> Document { var document = Document() // Dictionaries are unsorted. Sort before comparing with expected map @@ -239,29 +239,29 @@ private class BulkWriteTest: CrudTest { } private func verifyBulkWriteResult(_ result: BulkWriteResult) { - guard let expected = self.result as? Document else { + guard let expected = self.result?.documentValue else { return } - if let expectedDeletedCount = expected["deletedCount"] as? BSONNumber { - expect(result.deletedCount).to(equal(expectedDeletedCount.intValue)) + if let expectedDeletedCount = expected["deletedCount"]?.asInt() { + expect(result.deletedCount).to(equal(expectedDeletedCount)) } - if let expectedInsertedCount = expected["insertedCount"] as? BSONNumber { - expect(result.insertedCount).to(equal(expectedInsertedCount.intValue)) + if let expectedInsertedCount = expected["insertedCount"]?.asInt() { + expect(result.insertedCount).to(equal(expectedInsertedCount)) } - if let expectedInsertedIds = expected["insertedIds"] as? Document { + if let expectedInsertedIds = expected["insertedIds"]?.documentValue { expect(BulkWriteTest.prepareIds(result.insertedIds)).to(equal(expectedInsertedIds)) } - if let expectedMatchedCount = expected["matchedCount"] as? BSONNumber { - expect(result.matchedCount).to(equal(expectedMatchedCount.intValue)) + if let expectedMatchedCount = expected["matchedCount"]?.asInt() { + expect(result.matchedCount).to(equal(expectedMatchedCount)) } - if let expectedModifiedCount = expected["modifiedCount"] as? BSONNumber { - expect(result.modifiedCount).to(equal(expectedModifiedCount.intValue)) + if let expectedModifiedCount = expected["modifiedCount"]?.asInt() { + expect(result.modifiedCount).to(equal(expectedModifiedCount)) } - if let expectedUpsertedCount = expected["upsertedCount"] as? BSONNumber { - expect(result.upsertedCount).to(equal(expectedUpsertedCount.intValue)) + if let expectedUpsertedCount = expected["upsertedCount"]?.asInt() { + expect(result.upsertedCount).to(equal(expectedUpsertedCount)) } - if let expectedUpsertedIds = expected["upsertedIds"] as? Document { + if let expectedUpsertedIds = expected["upsertedIds"]?.documentValue { expect(BulkWriteTest.prepareIds(result.upsertedIds)).to(equal(expectedUpsertedIds)) } } @@ -273,7 +273,7 @@ private class CountTest: CrudTest { let filter: Document = try self.args.get("filter") let options = CountOptions(collation: self.collation, limit: self.limit, skip: self.skip) let result = try coll.count(filter, options: options) - expect(result).to(equal((self.result as? BSONNumber)?.intValue)) + expect(result).to(equal(self.result?.asInt())) } } @@ -288,21 +288,21 @@ private class DeleteTest: CrudTest { } else { result = try coll.deleteMany(filter, options: options) } - let expected = self.result as? Document + let expected = self.result?.documentValue // the only value in a DeleteResult is `deletedCount` - expect(result?.deletedCount).to(equal((expected?["deletedCount"] as? BSONNumber)?.intValue)) + expect(result?.deletedCount).to(equal(expected?["deletedCount"]?.asInt())) } } /// A class for executing `distinct` tests private class DistinctTest: CrudTest { override func execute(usingCollection coll: SyncMongoCollection) throws { - let filter = self.args["filter"] as? Document + let filter = self.args["filter"]?.documentValue let fieldName: String = try self.args.get("fieldName") let options = DistinctOptions(collation: self.collation) // rather than casting to all the possible BSON types, just wrap the arrays in documents to compare them let resultDoc: Document = [ - "result": try coll.distinct(fieldName: fieldName, filter: filter ?? [:], options: options) + "result": .array(try coll.distinct(fieldName: fieldName, filter: filter ?? [:], options: options)) ] if let result = self.result { let expectedDoc: Document = ["result": result] @@ -320,8 +320,8 @@ private class FindTest: CrudTest { limit: self.limit, skip: self.skip, sort: self.sort) - let result = try Array(coll.find(filter, options: options)) - expect(result).to(equal(self.result as? [Document])) + let result = BSON.array(try coll.find(filter, options: options).map { .document($0) }) + expect(result).to(equal(self.result)) } } @@ -374,8 +374,8 @@ private class FindOneAndUpdateTest: CrudTest { /// A class for executing `insertMany` tests private class InsertManyTest: CrudTest { override func execute(usingCollection coll: SyncMongoCollection) throws { - let documents: [Document] = try self.args.get("documents") - let options = InsertManyTest.parseInsertManyOptions(self.args["options"] as? Document) + let documents = self.args["documents"]!.arrayValue!.compactMap { $0.documentValue } + let options = InsertManyTest.parseInsertManyOptions(self.args["options"]?.documentValue) let expectError = self.error ?? false do { @@ -396,12 +396,12 @@ private class InsertManyTest: CrudTest { return nil } - let ordered = options["ordered"] as? Bool + let ordered = options["ordered"]?.boolValue return InsertManyOptions(ordered: ordered) } - private static func prepareIds(_ ids: [Int: BSONValue]) -> Document { + private static func prepareIds(_ ids: [Int: BSON]) -> Document { var document = Document() // Dictionaries are unsorted. Sort before comparing with expected map @@ -413,14 +413,14 @@ private class InsertManyTest: CrudTest { } private func verifyInsertManyResult(_ result: InsertManyResult) { - guard let expected = self.result as? Document else { + guard let expected = self.result?.documentValue else { return } - if let expectedInsertedCount = expected["insertedCount"] as? BSONNumber { - expect(result.insertedCount).to(equal(expectedInsertedCount.intValue)) + if let expectedInsertedCount = expected["insertedCount"]?.asInt() { + expect(result.insertedCount).to(equal(expectedInsertedCount)) } - if let expectedInsertedIds = expected["insertedIds"] as? Document { + if let expectedInsertedIds = expected["insertedIds"]?.documentValue { expect(InsertManyTest.prepareIds(result.insertedIds)).to(equal(expectedInsertedIds)) } } @@ -431,7 +431,7 @@ private class InsertOneTest: CrudTest { override func execute(usingCollection coll: SyncMongoCollection) throws { let doc: Document = try self.args.get("document") let result = try coll.insertOne(doc) - expect(doc["_id"]).to(bsonEqual(result?.insertedId)) + expect(doc["_id"]).to(equal(result?.insertedId)) } } diff --git a/Tests/MongoSwiftTests/Document+CollectionTests.swift b/Tests/MongoSwiftTests/Document+CollectionTests.swift index cf91ada35..57171338c 100644 --- a/Tests/MongoSwiftTests/Document+CollectionTests.swift +++ b/Tests/MongoSwiftTests/Document+CollectionTests.swift @@ -17,7 +17,7 @@ final class Document_CollectionTests: MongoSwiftTestCase { expect(doc.endIndex).to(equal(doc.count)) expect(doc.index(after: doc.index(after: doc.startIndex))).to(equal(doc.endIndex)) expect(doc[1].key).to(equal("b")) - expect(doc[1].value).to(bsonEqual(4)) + expect(doc[1].value).to(equal(4)) // doc.indices expect(doc.indices.count).to(equal(doc.count)) @@ -28,7 +28,7 @@ final class Document_CollectionTests: MongoSwiftTestCase { // doc.first let firstElem = doc[doc.startIndex] expect(doc.first?.key).to(equal(firstElem.key)) - expect(doc.first?.value).to(bsonEqual(firstElem.value)) + expect(doc.first?.value).to(equal(firstElem.value)) // doc.distance expect(doc.distance(from: doc.startIndex, to: doc.endIndex)).to(equal(doc.count)) @@ -44,8 +44,8 @@ final class Document_CollectionTests: MongoSwiftTestCase { expect(doc.index(doc.startIndex, offsetBy: 2, limitedBy: doc.endIndex)).to(equal(doc.endIndex)) expect(doc.index(doc.startIndex, offsetBy: 99, limitedBy: 1)).to(beNil()) - // firstIndex(where:); This line is commented out because Travis currently builds on 9.4, but this needs 10+ - // expect(doc.firstIndex { $0.key == "a" && bsonEquals($0.value, 3) }).to(equal(doc.startIndex)) + // firstIndex(where:) + expect(doc.firstIndex { $0.key == "a" && $0.value == 3 }).to(equal(doc.startIndex)) } func testMutators() throws { @@ -54,7 +54,7 @@ final class Document_CollectionTests: MongoSwiftTestCase { // doc.removeFirst let firstElem = doc.removeFirst() expect(firstElem.key).to(equal("a")) - expect(firstElem.value).to(bsonEqual(3)) + expect(firstElem.value).to(equal(3)) expect(doc).to(equal(["b": 2, "c": 5, "d": 4])) // doc.removeFirst(k:) @@ -64,7 +64,7 @@ final class Document_CollectionTests: MongoSwiftTestCase { // doc.popFirst let lastElem = doc.popFirst() expect(lastElem?.key).to(equal("d")) - expect(lastElem?.value).to(bsonEqual(4)) + expect(lastElem?.value).to(equal(4)) expect(doc).to(equal([:])) // doc.merge diff --git a/Tests/MongoSwiftTests/Document+SequenceTests.swift b/Tests/MongoSwiftTests/Document+SequenceTests.swift index 724c09900..18c772c5b 100644 --- a/Tests/MongoSwiftTests/Document+SequenceTests.swift +++ b/Tests/MongoSwiftTests/Document+SequenceTests.swift @@ -10,13 +10,14 @@ final class Document_SequenceTests: MongoSwiftTestCase { "true": true, "false": false, "int": 25, - "int32": Int32(5), - "double": Double(15), - "decimal128": Decimal128("1.2E+10")!, - "minkey": MinKey(), - "maxkey": MaxKey(), - "date": Date(timeIntervalSince1970: 5000), - "timestamp": Timestamp(timestamp: 5, inc: 10) + "int32": .int32(5), + "int64": .int64(123), + "double": .double(15), + "decimal128": .decimal128(Decimal128("1.2E+10")!), + "minkey": .minKey, + "maxkey": .maxKey, + "date": .datetime(Date(timeIntervalSince1970: 5000)), + "timestamp": .timestamp(Timestamp(timestamp: 5, inc: 10)) ] // create and use iter manually @@ -24,86 +25,92 @@ final class Document_SequenceTests: MongoSwiftTestCase { let stringTup = iter.next()! expect(stringTup.key).to(equal("string")) - expect(stringTup.value).to(bsonEqual("test string")) + expect(stringTup.value).to(equal("test string")) let trueTup = iter.next()! expect(trueTup.key).to(equal("true")) - expect(trueTup.value).to(bsonEqual(true)) + expect(trueTup.value).to(equal(true)) let falseTup = iter.next()! expect(falseTup.key).to(equal("false")) - expect(falseTup.value).to(bsonEqual(false)) + expect(falseTup.value).to(equal(false)) let intTup = iter.next()! expect(intTup.key).to(equal("int")) - expect(intTup.value).to(bsonEqual(25)) + expect(intTup.value).to(equal(25)) let int32Tup = iter.next()! expect(int32Tup.key).to(equal("int32")) - expect(int32Tup.value).to(bsonEqual(Int32(5))) + expect(int32Tup.value).to(equal(.int32(5))) + + let int64Tup = iter.next()! + expect(int64Tup.key).to(equal("int64")) + expect(int64Tup.value).to(equal(.int64(123))) let doubleTup = iter.next()! expect(doubleTup.key).to(equal("double")) - expect(doubleTup.value).to(bsonEqual(15.0)) + expect(doubleTup.value).to(equal(15.0)) let decimalTup = iter.next()! expect(decimalTup.key).to(equal("decimal128")) - expect(decimalTup.value).to(bsonEqual(Decimal128("1.2E+10")!)) + expect(decimalTup.value).to(equal(.decimal128(Decimal128("1.2E+10")!))) let minTup = iter.next()! expect(minTup.key).to(equal("minkey")) - expect(minTup.value).to(bsonEqual(MinKey())) + expect(minTup.value).to(equal(.minKey)) let maxTup = iter.next()! expect(maxTup.key).to(equal("maxkey")) - expect(maxTup.value).to(bsonEqual(MaxKey())) + expect(maxTup.value).to(equal(.maxKey)) let dateTup = iter.next()! expect(dateTup.key).to(equal("date")) - expect(dateTup.value).to(bsonEqual(Date(timeIntervalSince1970: 5000))) + expect(dateTup.value).to(equal(.datetime(Date(timeIntervalSince1970: 5000)))) let timeTup = iter.next()! expect(timeTup.key).to(equal("timestamp")) - expect(timeTup.value).to(bsonEqual(Timestamp(timestamp: 5, inc: 10))) + expect(timeTup.value).to(equal(.timestamp(Timestamp(timestamp: 5, inc: 10)))) expect(iter.next()).to(beNil()) // iterate via looping var expectedKeys = [ - "string", "true", "false", "int", "int32", "double", + "string", "true", "false", "int", "int32", "int64", "double", "decimal128", "minkey", "maxkey", "date", "timestamp" ] + var expectedValues: [BSON] = [ + "test string", true, false, 25, .int32(5), .int64(123), .double(15), + .decimal128(Decimal128("1.2E+10")!), .minKey, .maxKey, .datetime(Date(timeIntervalSince1970: 5000)), + .timestamp(Timestamp(timestamp: 5, inc: 10)) + ] for (k, v) in doc { expect(k).to(equal(expectedKeys.removeFirst())) - // we can't compare `BSONValue`s for equality, nor can we cast v - // to a dynamically determined equatable type, so just verify - // it's a `BSONValue` anyway - expect(v).to(beAKindOf(BSONValue.self)) + expect(v).to(equal(expectedValues.removeFirst())) } } func testMapFilter() throws { - let doc1: Document = ["a": 1, "b": BSONNull(), "c": 3, "d": 4, "e": BSONNull()] - expect(doc1.mapValues { $0 is BSONNull ? 1 : $0 }).to(equal(["a": 1, "b": 1, "c": 3, "d": 4, "e": 1])) + let doc1: Document = ["a": 1, "b": .null, "c": 3, "d": 4, "e": .null] + expect(doc1.mapValues { $0 == .null ? 1 : $0 }).to(equal(["a": 1, "b": 1, "c": 3, "d": 4, "e": 1])) let output1 = doc1.mapValues { val in - if let int = val as? Int { - return int + 1 + if let int = val.asInt() { + return BSON(integerLiteral: int + 1) } return val } - expect(output1).to(equal(["a": 2, "b": BSONNull(), "c": 4, "d": 5, "e": BSONNull()])) - expect(doc1.filter { !($0.value is BSONNull) }).to(equal(["a": 1, "c": 3, "d": 4])) + expect(output1).to(equal(["a": 2, "b": .null, "c": 4, "d": 5, "e": .null])) + expect(doc1.filter { !($0.value == .null) }).to(equal(["a": 1, "c": 3, "d": 4])) - let doc2: Document = ["a": 1, "b": "hello", "c": [1, 2] as [Int]] - expect(doc2.filter { $0.value is String }).to(equal(["b": "hello"])) + let doc2: Document = ["a": 1, "b": "hello", "c": [1, 2]] + expect(doc2.filter { $0.value.stringValue != nil }).to(equal(["b": "hello"])) let output2 = doc2.mapValues { val in switch val { - case let val as Int: - return val + 1 - case let val as String: - return val + " there" - case let val as [Int]: - return val.reduce(0, +) + case let .int64(val): + return .int64(val + 1) + case let .string(val): + return .string(val + " there") + case .array: + return BSON(integerLiteral: val.arrayValue!.compactMap { $0.asInt() }.reduce(0, +)) default: return val } @@ -117,13 +124,13 @@ final class Document_SequenceTests: MongoSwiftTestCase { // shared docs for subsequence tests let emptyDoc = Document() let smallDoc: Document = ["x": 1] - let doc: Document = ["a": 1, "b": "hi", "c": [1, 2] as [Int], "d": false, "e": BSONNull(), "f": MinKey(), "g": 10] + let doc: Document = ["a": 1, "b": "hi", "c": [1, 2], "d": false, "e": .null, "f": .minKey, "g": 10] // shared predicates for subsequence tests - func isInt(_ pair: Document.KeyValuePair) -> Bool { return pair.value is Int } - func isNotNil(_ pair: Document.KeyValuePair) -> Bool { return !(pair.value is BSONNull) } + func isInt(_ pair: Document.KeyValuePair) -> Bool { return pair.value.asInt() != nil } + func isNotNil(_ pair: Document.KeyValuePair) -> Bool { return pair.value != .null } func is10(_ pair: Document.KeyValuePair) -> Bool { - if let int = pair.value as? Int { + if let int = pair.value.asInt() { return int == 10 } return false @@ -142,14 +149,14 @@ final class Document_SequenceTests: MongoSwiftTestCase { expect(self.doc.dropFirst()).to(equal( [ "b": "hi", - "c": [1, 2] as [Int], + "c": [1, 2], "d": false, - "e": BSONNull(), - "f": MinKey(), + "e": .null, + "f": .minKey, "g": 10 ] )) - expect(self.doc.dropFirst(4)).to(equal(["e": BSONNull(), "f": MinKey(), "g": 10])) + expect(self.doc.dropFirst(4)).to(equal(["e": .null, "f": .minKey, "g": 10])) expect(self.doc.dropFirst(7)).to(equal([:])) expect(self.doc.dropFirst(8)).to(equal([:])) } @@ -166,12 +173,12 @@ final class Document_SequenceTests: MongoSwiftTestCase { expect(self.doc.dropLast()).to(equal([ "a": 1, "b": "hi", - "c": [1, 2] as [Int], + "c": [1, 2], "d": false, - "e": BSONNull(), - "f": MinKey() + "e": .null, + "f": .minKey ])) - expect(self.doc.dropLast(4)).to(equal(["a": 1, "b": "hi", "c": [1, 2] as [Int]])) + expect(self.doc.dropLast(4)).to(equal(["a": 1, "b": "hi", "c": [1, 2]])) expect(self.doc.dropLast(7)).to(equal([:])) expect(self.doc.dropLast(8)).to(equal([:])) } @@ -181,16 +188,16 @@ final class Document_SequenceTests: MongoSwiftTestCase { expect(self.smallDoc.drop(while: self.isInt)).to(equal([:])) expect(self.doc.drop(while: self.isInt)).to(equal([ "b": "hi", - "c": [1, 2] as [Int], + "c": [1, 2], "d": false, - "e": BSONNull(), - "f": MinKey(), + "e": .null, + "f": .minKey, "g": 10 ])) expect(self.emptyDoc.drop(while: self.isNotNil)).to(equal([:])) expect(self.smallDoc.drop(while: self.isNotNil)).to(equal([:])) - expect(self.doc.drop(while: self.isNotNil)).to(equal(["e": BSONNull(), "f": MinKey(), "g": 10])) + expect(self.doc.drop(while: self.isNotNil)).to(equal(["e": .null, "f": .minKey, "g": 10])) expect(self.emptyDoc.drop(while: self.isNot10)).to(equal([:])) expect(self.smallDoc.drop(while: self.isNot10)).to(equal([:])) @@ -212,7 +219,7 @@ final class Document_SequenceTests: MongoSwiftTestCase { expect(self.doc.prefix(0)).to(equal([:])) expect(self.doc.prefix(1)).to(equal(["a": 1])) expect(self.doc.prefix(2)).to(equal(["a": 1, "b": "hi"])) - expect(self.doc.prefix(4)).to(equal(["a": 1, "b": "hi", "c": [1, 2] as [Int], "d": false])) + expect(self.doc.prefix(4)).to(equal(["a": 1, "b": "hi", "c": [1, 2], "d": false])) expect(self.doc.prefix(7)).to(equal(doc)) expect(self.doc.prefix(8)).to(equal(doc)) } @@ -224,7 +231,7 @@ final class Document_SequenceTests: MongoSwiftTestCase { expect(self.emptyDoc.prefix(while: self.isNotNil)).to(equal([:])) expect(self.smallDoc.prefix(while: self.isNotNil)).to(equal(smallDoc)) - expect(self.doc.prefix(while: self.isNotNil)).to(equal(["a": 1, "b": "hi", "c": [1, 2] as [Int], "d": false])) + expect(self.doc.prefix(while: self.isNotNil)).to(equal(["a": 1, "b": "hi", "c": [1, 2], "d": false])) expect(self.emptyDoc.prefix(while: self.isNot10)).to(equal([:])) expect(self.smallDoc.prefix(while: self.isNot10)).to(equal(smallDoc)) @@ -232,10 +239,10 @@ final class Document_SequenceTests: MongoSwiftTestCase { [ "a": 1, "b": "hi", - "c": [1, 2] as [Int], + "c": [1, 2], "d": false, - "e": BSONNull(), - "f": MinKey() + "e": .null, + "f": .minKey ] )) @@ -256,8 +263,8 @@ final class Document_SequenceTests: MongoSwiftTestCase { expect(self.doc.suffix(0)).to(equal([])) expect(self.doc.suffix(1)).to(equal(["g": 10])) - expect(self.doc.suffix(2)).to(equal(["f": MinKey(), "g": 10])) - expect(self.doc.suffix(4)).to(equal(["d": false, "e": BSONNull(), "f": MinKey(), "g": 10])) + expect(self.doc.suffix(2)).to(equal(["f": .minKey, "g": 10])) + expect(self.doc.suffix(4)).to(equal(["d": false, "e": .null, "f": .minKey, "g": 10])) expect(self.doc.suffix(7)).to(equal(doc)) expect(self.doc.suffix(8)).to(equal(doc)) } @@ -269,10 +276,10 @@ final class Document_SequenceTests: MongoSwiftTestCase { [ [ "b": "hi", - "c": [1, 2] as [Int], + "c": [1, 2], "d": false, - "e": BSONNull(), - "f": MinKey() + "e": .null, + "f": .minKey ] ] )) @@ -282,13 +289,13 @@ final class Document_SequenceTests: MongoSwiftTestCase { expect(self.doc.split(omittingEmptySubsequences: false, whereSeparator: self.isInt)).to(equal( [ [:], - ["b": "hi", "c": [1, 2] as [Int], "d": false, "e": BSONNull(), "f": MinKey()], + ["b": "hi", "c": [1, 2], "d": false, "e": .null, "f": .minKey], [:] ] )) expect(self.doc.split(maxSplits: 1, omittingEmptySubsequences: false, whereSeparator: self.isInt)) - .to(equal([[:], ["b": "hi", "c": [1, 2] as [Int], "d": false, "e": BSONNull(), "f": MinKey(), "g": 10]])) + .to(equal([[:], ["b": "hi", "c": [1, 2], "d": false, "e": .null, "f": .minKey, "g": 10]])) } func testIsEmpty() throws { diff --git a/Tests/MongoSwiftTests/DocumentTests.swift b/Tests/MongoSwiftTests/DocumentTests.swift index c11f42cac..f130b5e37 100644 --- a/Tests/MongoSwiftTests/DocumentTests.swift +++ b/Tests/MongoSwiftTests/DocumentTests.swift @@ -30,8 +30,12 @@ extension Data { struct DocElem { let key: String - let value: BSONValue - let type: BSONType + let value: SwiftBSON +} + +enum SwiftBSON { + case document([DocElem]) + case other(BSON) } /// Extension of Document to allow conversion to and from arrays @@ -40,20 +44,21 @@ extension Document { self.init() for elem in array { - if let subdoc = elem.value as? [DocElem], elem.type == .document { - self[elem.key] = Document(fromArray: subdoc) - } else { - self[elem.key] = elem.value + switch elem.value { + case let .document(els): + self[elem.key] = .document(Document(fromArray: els)) + case let .other(b): + self[elem.key] = b } } } internal func toArray() -> [DocElem] { return self.map { kvp in - if let subdoc = kvp.value as? Document { - return DocElem(key: kvp.key, value: subdoc.toArray(), type: .document) + if let subdoc = kvp.value.documentValue { + return DocElem(key: kvp.key, value: .document(subdoc.toArray())) } - return DocElem(key: kvp.key, value: kvp.value, type: kvp.value.bsonType) + return DocElem(key: kvp.key, value: .other(kvp.value)) } } } @@ -65,23 +70,23 @@ final class DocumentTests: MongoSwiftTestCase { "true": true, "false": false, "int": 25, - "int32": Int32(5), - "int64": Int64(10), - "double": Double(15), - "decimal128": Decimal128("1.2E+10")!, - "minkey": MinKey(), - "maxkey": MaxKey(), - "date": Date(timeIntervalSince1970: 500.004), - "timestamp": Timestamp(timestamp: 5, inc: 10), - "nestedarray": [[1, 2], [Int32(3), Int32(4)]] as [[Int32]], - "nesteddoc": ["a": 1, "b": 2, "c": false, "d": [3, 4]] as Document, - "oid": ObjectId("507f1f77bcf86cd799439011")!, - "regex": RegularExpression(pattern: "^abc", options: "imx"), + "int32": .int32(5), + "int64": .int64(10), + "double": .double(15), + "decimal128": .decimal128(Decimal128("1.2E+10")!), + "minkey": .minKey, + "maxkey": .maxKey, + "date": .datetime(Date(timeIntervalSince1970: 500.004)), + "timestamp": .timestamp(Timestamp(timestamp: 5, inc: 10)), + "nestedarray": [[1, 2], [.int32(3), .int32(4)]], + "nesteddoc": ["a": 1, "b": 2, "c": false, "d": [3, 4]], + "oid": .objectId(ObjectId("507f1f77bcf86cd799439011")!), + "regex": .regex(RegularExpression(pattern: "^abc", options: "imx")), "array1": [1, 2], "array2": ["string1", "string2"], - "null": BSONNull(), - "code": CodeWithScope(code: "console.log('hi');"), - "codewscope": CodeWithScope(code: "console.log(x);", scope: ["x": 2]) + "null": .null, + "code": .code(Code(code: "console.log('hi');")), + "codewscope": .codeWithScope(CodeWithScope(code: "console.log(x);", scope: ["x": 2])) ] func testDocument() throws { @@ -102,14 +107,14 @@ final class DocumentTests: MongoSwiftTestCase { // can't handle all the keys being declared together let binaryData: Document = [ - "binary0": try Binary(data: testData, subtype: .generic), - "binary1": try Binary(data: testData, subtype: .function), - "binary2": try Binary(data: testData, subtype: .binaryDeprecated), - "binary3": try Binary(data: uuidData, subtype: .uuidDeprecated), - "binary4": try Binary(data: uuidData, subtype: .uuid), - "binary5": try Binary(data: testData, subtype: .md5), - "binary6": try Binary(data: testData, subtype: .userDefined), - "binary7": try Binary(data: testData, subtype: 200) + "binary0": .binary(try Binary(data: testData, subtype: .generic)), + "binary1": .binary(try Binary(data: testData, subtype: .function)), + "binary2": .binary(try Binary(data: testData, subtype: .binaryDeprecated)), + "binary3": .binary(try Binary(data: uuidData, subtype: .uuidDeprecated)), + "binary4": .binary(try Binary(data: uuidData, subtype: .uuid)), + "binary5": .binary(try Binary(data: testData, subtype: .md5)), + "binary6": .binary(try Binary(data: testData, subtype: .userDefined)), + "binary7": .binary(try Binary(data: testData, subtype: 200)) ] try doc.merge(binaryData) @@ -128,96 +133,96 @@ final class DocumentTests: MongoSwiftTestCase { expect(doc.count).to(equal(expectedKeys.count)) expect(doc.keys).to(equal(expectedKeys)) - expect(doc["string"]).to(bsonEqual("test string")) - expect(doc["true"]).to(bsonEqual(true)) - expect(doc["false"]).to(bsonEqual(false)) - expect(doc["int"]).to(bsonEqual(25)) - expect(doc["int32"]).to(bsonEqual(Int32(5))) - expect(doc["int64"]).to(bsonEqual(Int64(10))) - expect(doc["double"]).to(bsonEqual(15.0)) - expect(doc["decimal128"]).to(bsonEqual(Decimal128("1.2E+10")!)) - expect(doc["minkey"]).to(bsonEqual(MinKey())) - expect(doc["maxkey"]).to(bsonEqual(MaxKey())) - expect(doc["date"]).to(bsonEqual(Date(timeIntervalSince1970: 500.004))) - expect(doc["timestamp"]).to(bsonEqual(Timestamp(timestamp: 5, inc: 10))) - expect(doc["oid"]).to(bsonEqual(ObjectId("507f1f77bcf86cd799439011")!)) - - let regex = doc["regex"] as? RegularExpression + expect(doc["string"]).to(equal("test string")) + expect(doc["true"]).to(equal(true)) + expect(doc["false"]).to(equal(false)) + expect(doc["int"]).to(equal(25)) + expect(doc["int32"]).to(equal(.int32(5))) + expect(doc["int64"]).to(equal(.int64(10))) + expect(doc["double"]).to(equal(15.0)) + expect(doc["decimal128"]).to(equal(.decimal128(Decimal128("1.2E+10")!))) + expect(doc["minkey"]).to(equal(.minKey)) + expect(doc["maxkey"]).to(equal(.maxKey)) + expect(doc["date"]).to(equal(.datetime(Date(timeIntervalSince1970: 500.004)))) + expect(doc["timestamp"]).to(equal(.timestamp(Timestamp(timestamp: 5, inc: 10)))) + expect(doc["oid"]).to(equal(.objectId(ObjectId("507f1f77bcf86cd799439011")!))) + + let regex = doc["regex"]?.regexValue expect(regex).to(equal(RegularExpression(pattern: "^abc", options: "imx"))) expect(try NSRegularExpression(from: regex!)).to(equal(try NSRegularExpression( pattern: "^abc", options: NSRegularExpression.optionsFromString("imx") ))) - expect(doc["array1"]).to(bsonEqual([1, 2])) - expect(doc["array2"]).to(bsonEqual(["string1", "string2"])) - expect(doc["null"]).to(bsonEqual(BSONNull())) + expect(doc["array1"]).to(equal([1, 2])) + expect(doc["array2"]).to(equal(["string1", "string2"])) + expect(doc["null"]).to(equal(.null)) - let code = doc["code"] as? CodeWithScope + let code = doc["code"]?.codeValue expect(code?.code).to(equal("console.log('hi');")) - expect(code?.scope).to(beNil()) - let codewscope = doc["codewscope"] as? CodeWithScope + let codewscope = doc["codewscope"]?.codeWithScopeValue expect(codewscope?.code).to(equal("console.log(x);")) expect(codewscope?.scope).to(equal(["x": 2])) - expect(doc["binary0"]).to(bsonEqual(try Binary(data: testData, subtype: .generic))) - expect(doc["binary1"]).to(bsonEqual(try Binary(data: testData, subtype: .function))) - expect(doc["binary2"]).to(bsonEqual(try Binary(data: testData, subtype: .binaryDeprecated))) - expect(doc["binary3"]).to(bsonEqual(try Binary(data: uuidData, subtype: .uuidDeprecated))) - expect(doc["binary4"]).to(bsonEqual(try Binary(data: uuidData, subtype: .uuid))) - expect(doc["binary5"]).to(bsonEqual(try Binary(data: testData, subtype: .md5))) - expect(doc["binary6"]).to(bsonEqual(try Binary(data: testData, subtype: .userDefined))) - expect(doc["binary7"]).to(bsonEqual(try Binary(data: testData, subtype: 200))) + expect(doc["binary0"]).to(equal(.binary(try Binary(data: testData, subtype: .generic)))) + expect(doc["binary1"]).to(equal(.binary(try Binary(data: testData, subtype: .function)))) + expect(doc["binary2"]).to(equal(.binary(try Binary(data: testData, subtype: .binaryDeprecated)))) + expect(doc["binary3"]).to(equal(.binary(try Binary(data: uuidData, subtype: .uuidDeprecated)))) + expect(doc["binary4"]).to(equal(.binary(try Binary(data: uuidData, subtype: .uuid)))) + expect(doc["binary5"]).to(equal(.binary(try Binary(data: testData, subtype: .md5)))) + expect(doc["binary6"]).to(equal(.binary(try Binary(data: testData, subtype: .userDefined)))) + expect(doc["binary7"]).to(equal(.binary(try Binary(data: testData, subtype: 200)))) - let nestedArray = doc["nestedarray"] as? [[Int32]] + let nestedArray = doc["nestedarray"]?.arrayValue?.compactMap { $0.arrayValue?.compactMap { $0.asInt() } } expect(nestedArray?[0]).to(equal([1, 2])) expect(nestedArray?[1]).to(equal([3, 4])) - expect(doc["nesteddoc"]).to(bsonEqual(["a": 1, "b": 2, "c": false, "d": [3, 4]] as Document)) + expect(doc["nesteddoc"]).to(equal(["a": 1, "b": 2, "c": false, "d": [3, 4]])) } func testDocumentDynamicMemberLookup() throws { // Test reading various types - expect(DocumentTests.testDoc.string).to(bsonEqual("test string")) - expect(DocumentTests.testDoc.true).to(bsonEqual(true)) - expect(DocumentTests.testDoc.false).to(bsonEqual(false)) - expect(DocumentTests.testDoc.int).to(bsonEqual(25)) - expect(DocumentTests.testDoc.int32).to(bsonEqual(Int32(5))) - expect(DocumentTests.testDoc.int64).to(bsonEqual(Int64(10))) - expect(DocumentTests.testDoc.double).to(bsonEqual(15.0)) - expect(DocumentTests.testDoc.decimal128).to(bsonEqual(Decimal128("1.2E+10")!)) - expect(DocumentTests.testDoc.minkey).to(bsonEqual(MinKey())) - expect(DocumentTests.testDoc.maxkey).to(bsonEqual(MaxKey())) - expect(DocumentTests.testDoc.date).to(bsonEqual(Date(timeIntervalSince1970: 500.004))) - expect(DocumentTests.testDoc.timestamp).to(bsonEqual(Timestamp(timestamp: 5, inc: 10))) - expect(DocumentTests.testDoc.oid).to(bsonEqual(ObjectId("507f1f77bcf86cd799439011")!)) - - let codewscope = DocumentTests.testDoc.codewscope as? CodeWithScope + expect(DocumentTests.testDoc.string).to(equal("test string")) + expect(DocumentTests.testDoc.true).to(equal(true)) + expect(DocumentTests.testDoc.false).to(equal(false)) + expect(DocumentTests.testDoc.int).to(equal(25)) + expect(DocumentTests.testDoc.int32).to(equal(.int32(5))) + expect(DocumentTests.testDoc.int64).to(equal(.int64(10))) + expect(DocumentTests.testDoc.double).to(equal(15.0)) + expect(DocumentTests.testDoc.decimal128).to(equal(.decimal128(Decimal128("1.2E+10")!))) + expect(DocumentTests.testDoc.minkey).to(equal(.minKey)) + expect(DocumentTests.testDoc.maxkey).to(equal(.maxKey)) + expect(DocumentTests.testDoc.date).to(equal(.datetime(Date(timeIntervalSince1970: 500.004)))) + expect(DocumentTests.testDoc.timestamp).to(equal(.timestamp(Timestamp(timestamp: 5, inc: 10)))) + expect(DocumentTests.testDoc.oid).to(equal(.objectId(ObjectId("507f1f77bcf86cd799439011")!))) + + let codewscope = DocumentTests.testDoc.codewscope?.codeWithScopeValue expect(codewscope?.code).to(equal("console.log(x);")) expect(codewscope?.scope).to(equal(["x": 2])) - let code = DocumentTests.testDoc.code as? CodeWithScope + let code = DocumentTests.testDoc.code?.codeValue expect(code?.code).to(equal("console.log('hi');")) - expect(code?.scope).to(beNil()) - expect(DocumentTests.testDoc.array1).to(bsonEqual([1, 2])) - expect(DocumentTests.testDoc.array2).to(bsonEqual(["string1", "string2"])) - expect(DocumentTests.testDoc.null).to(bsonEqual(BSONNull())) + expect(DocumentTests.testDoc.array1).to(equal([1, 2])) + expect(DocumentTests.testDoc.array2).to(equal(["string1", "string2"])) + expect(DocumentTests.testDoc.null).to(equal(.null)) - let regex = DocumentTests.testDoc.regex as? RegularExpression + let regex = DocumentTests.testDoc.regex?.regexValue expect(regex).to(equal(RegularExpression(pattern: "^abc", options: "imx"))) expect(try NSRegularExpression(from: regex!)).to(equal(try NSRegularExpression( pattern: "^abc", options: NSRegularExpression.optionsFromString("imx") ))) - let nestedArray = DocumentTests.testDoc.nestedarray as? [[Int32]] - expect(nestedArray?[0]).to(bsonEqual([Int32(1), Int32(2)])) - expect(nestedArray?[1]).to(bsonEqual([Int32(3), Int32(4)])) + let nestedArray = DocumentTests.testDoc.nestedarray?.arrayValue?.compactMap { + $0.arrayValue?.compactMap { $0.asInt() } + } + expect(nestedArray?[0]).to(equal([1, 2])) + expect(nestedArray?[1]).to(equal([3, 4])) - expect(DocumentTests.testDoc.nesteddoc).to(bsonEqual(["a": 1, "b": 2, "c": false, "d": [3, 4]] as Document)) - expect((DocumentTests.testDoc.nesteddoc as? Document)?.a).to(bsonEqual(1)) + expect(DocumentTests.testDoc.nesteddoc).to(equal(["a": 1, "b": 2, "c": false, "d": [3, 4]])) + expect(DocumentTests.testDoc.nesteddoc?.documentValue?.a).to(equal(1)) // Test assignment var doc = Document() @@ -225,39 +230,39 @@ final class DocumentTests: MongoSwiftTestCase { doc.a = 1 doc.b = "b" - doc.c = subdoc + doc.c = .document(subdoc) - expect(doc.a).to(bsonEqual(1)) - expect(doc.b).to(bsonEqual("b")) - expect(doc.c).to(bsonEqual(subdoc)) + expect(doc.a).to(equal(1)) + expect(doc.b).to(equal("b")) + expect(doc.c).to(equal(.document(subdoc))) doc.a = 2 doc.b = "different" - expect(doc.a).to(bsonEqual(2)) - expect(doc.b).to(bsonEqual("different")) + expect(doc.a).to(equal(2)) + expect(doc.b).to(equal("different")) } func testDocumentFromArray() { - let doc1: Document = ["foo", MinKey(), BSONNull()] + let doc1: Document = ["foo", .minKey, .null] expect(doc1.keys).to(equal(["0", "1", "2"])) - expect(doc1["0"]).to(bsonEqual("foo")) - expect(doc1["1"]).to(bsonEqual(MinKey())) - expect(doc1["2"]).to(bsonEqual(BSONNull())) + expect(doc1["0"]).to(equal("foo")) + expect(doc1["1"]).to(equal(.minKey)) + expect(doc1["2"]).to(equal(.null)) - let elements: [BSONValue] = ["foo", MinKey(), BSONNull()] + let elements: [BSON] = ["foo", .minKey, .null] let doc2 = Document(elements) expect(doc2.keys).to(equal(["0", "1", "2"])) - expect(doc2["0"]).to(bsonEqual("foo")) - expect(doc2["1"]).to(bsonEqual(MinKey())) - expect(doc2["2"]).to(bsonEqual(BSONNull())) + expect(doc2["0"]).to(equal("foo")) + expect(doc2["1"]).to(equal(.minKey)) + expect(doc2["2"]).to(equal(.null)) } func testEquatable() { expect(["hi": true, "hello": "hi", "cat": 2] as Document) - .to(equal(["hi": true, "hello": "hi", "cat": 2] as Document)) + .to(equal(["hi": true, "hello": "hi", "cat": 2] as Document)) } func testRawBSON() throws { @@ -270,7 +275,7 @@ final class DocumentTests: MongoSwiftTestCase { let doc1: Document = ["a": 1] var doc2 = doc1 doc2["b"] = 2 - expect(doc2["b"]).to(bsonEqual(2)) + expect(doc2["b"]).to(equal(2)) expect(doc1["b"]).to(beNil()) expect(doc1).toNot(equal(doc2)) } @@ -284,20 +289,20 @@ final class DocumentTests: MongoSwiftTestCase { let int32max_add1 = Int64(Int32.max) + Int64(1) let doc: Document = [ - "int32min": Int(Int32.min), - "int32max": Int(Int32.max), - "int32min-1": Int(int32min_sub1), - "int32max+1": Int(int32max_add1), - "int64min": Int(Int64.min), - "int64max": Int(Int64.max) + "int32min": BSON(Int(Int32.min)), + "int32max": BSON(Int(Int32.max)), + "int32min-1": BSON(Int(int32min_sub1)), + "int32max+1": BSON(Int(int32max_add1)), + "int64min": BSON(Int(Int64.min)), + "int64max": BSON(Int(Int64.max)) ] - expect(doc["int32min"]).to(bsonEqual(Int(Int32.min))) - expect(doc["int32max"]).to(bsonEqual(Int(Int32.max))) - expect(doc["int32min-1"]).to(bsonEqual(int32min_sub1)) - expect(doc["int32max+1"]).to(bsonEqual(int32max_add1)) - expect(doc["int64min"]).to(bsonEqual(Int64.min)) - expect(doc["int64max"]).to(bsonEqual(Int64.max)) + expect(doc["int32min"]).to(equal(.int64(Int64(Int32.min)))) + expect(doc["int32max"]).to(equal(.int64(Int64(Int32.max)))) + expect(doc["int32min-1"]).to(equal(.int64(int32min_sub1))) + expect(doc["int32max+1"]).to(equal(.int64(int32max_add1))) + expect(doc["int64min"]).to(equal(.int64(Int64.min))) + expect(doc["int64max"]).to(equal(.int64(Int64.max))) } // swiftlint:disable:next cyclomatic_complexity @@ -439,32 +444,32 @@ final class DocumentTests: MongoSwiftTestCase { } func testNilInNestedArray() throws { - let arr1: [BSONValue] = ["a", "b", "c", BSONNull()] - let arr2: [BSONValue] = ["d", "e", BSONNull(), "f"] + let arr1: BSON = ["a", "b", "c", .null] + let arr2: BSON = ["d", "e", .null, "f"] let doc = ["a1": arr1, "a2": arr2] - expect(doc["a1"]).to(bsonEqual(arr1)) - expect(doc["a2"]).to(bsonEqual(arr2)) + expect(doc["a1"]).to(equal(arr1)) + expect(doc["a2"]).to(equal(arr2)) } // exclude Int64 value on 32-bit platforms static let overwritables: Document = [ "double": 2.5, - "int32": Int32(32), - "int64": Int64.max, + "int32": .int32(32), + "int64": .int64(Int64.max), "bool": false, - "decimal": Decimal128("1.2E+10")!, - "oid": ObjectId(), - "timestamp": Timestamp(timestamp: 1, inc: 2), - "datetime": Date(msSinceEpoch: 1000) + "decimal": .decimal128(Decimal128("1.2E+10")!), + "oid": .objectId(ObjectId()), + "timestamp": .timestamp(Timestamp(timestamp: 1, inc: 2)), + "datetime": .datetime(Date(msSinceEpoch: 1000)) ] static let nonOverwritables: Document = [ "string": "hello", - "nil": BSONNull(), - "doc": ["x": 1] as Document, - "arr": [1, 2] as [Int] + "nil": .null, + "doc": ["x": 1], + "arr": [1, 2] ] // test replacing `Overwritable` types with values of their own type @@ -476,8 +481,8 @@ final class DocumentTests: MongoSwiftTestCase { let pointer = doc._bson // overwrite int32 with int32 - doc["int32"] = Int32(15) - expect(doc["int32"]).to(bsonEqual(Int32(15))) + doc["int32"] = .int32(15) + expect(doc["int32"]).to(equal(.int32(15))) expect(doc._bson).to(equal(pointer)) doc["bool"] = true @@ -486,32 +491,32 @@ final class DocumentTests: MongoSwiftTestCase { doc["double"] = 3.0 expect(doc._bson).to(equal(pointer)) - doc["decimal"] = Decimal128("100")! + doc["decimal"] = .decimal128(Decimal128("100")!) expect(doc._bson).to(equal(pointer)) // overwrite int64 with int64 - doc["int64"] = Int64.min + doc["int64"] = .int64(Int64.min) expect(doc._bson).to(equal(pointer)) let newOid = ObjectId() - doc["oid"] = newOid + doc["oid"] = .objectId(newOid) expect(doc._bson).to(equal(pointer)) - doc["timestamp"] = Timestamp(timestamp: 5, inc: 10) + doc["timestamp"] = .timestamp(Timestamp(timestamp: 5, inc: 10)) expect(doc._bson).to(equal(pointer)) - doc["datetime"] = Date(msSinceEpoch: 2000) + doc["datetime"] = .datetime(Date(msSinceEpoch: 2000)) expect(doc._bson).to(equal(pointer)) expect(doc).to(equal([ "double": 3.0, - "int32": Int32(15), - "int64": Int64.min, + "int32": .int32(15), + "int64": .int64(Int64.min), "bool": true, - "decimal": Decimal128("100")!, - "oid": newOid, - "timestamp": Timestamp(timestamp: 5, inc: 10), - "datetime": Date(msSinceEpoch: 2000) + "decimal": .decimal128(Decimal128("100")!), + "oid": .objectId(newOid), + "timestamp": .timestamp(Timestamp(timestamp: 5, inc: 10)), + "datetime": .datetime(Date(msSinceEpoch: 2000)) ])) // return early as we will to use an Int requiring > 32 bits after this @@ -520,24 +525,24 @@ final class DocumentTests: MongoSwiftTestCase { } let bigInt = Int(Int32.max) + 1 - doc["int64"] = bigInt + doc["int64"] = BSON(integerLiteral: bigInt) expect(doc._bson).to(equal(pointer)) // final values expect(doc).to(equal([ "double": 3.0, - "int32": Int32(15), - "int64": bigInt, + "int32": .int32(15), + "int64": BSON(integerLiteral: bigInt), "bool": true, - "decimal": Decimal128("100")!, - "oid": newOid, - "timestamp": Timestamp(timestamp: 5, inc: 10), - "datetime": Date(msSinceEpoch: 2000) + "decimal": .decimal128(Decimal128("100")!), + "oid": .objectId(newOid), + "timestamp": .timestamp(Timestamp(timestamp: 5, inc: 10)), + "datetime": .datetime(Date(msSinceEpoch: 2000)) ])) // should not be able to overwrite an int32 with an int on a 64-bit system doc["int32"] = 20 - expect(doc["int32"]).to(bsonEqual(20)) + expect(doc["int32"]).to(equal(.int64(20))) expect(doc._bson).toNot(equal(pointer)) } @@ -552,7 +557,7 @@ final class DocumentTests: MongoSwiftTestCase { // save these to compare to at the end let newDoc: Document = ["y": 1] - let newPairs: [(String, BSONValue)] = [("string", "hi"), ("doc", newDoc), ("arr", [3, 4])] + let newPairs: [(String, BSON)] = [("string", "hi"), ("doc", .document(newDoc)), ("arr", [3, 4])] newPairs.forEach { k, v in doc[k] = v @@ -561,7 +566,7 @@ final class DocumentTests: MongoSwiftTestCase { pointer = doc._bson } - expect(doc).to(equal(["string": "hi", "nil": BSONNull(), "doc": newDoc, "arr": [3, 4] as [Int]])) + expect(doc).to(equal(["string": "hi", "nil": .null, "doc": .document(newDoc), "arr": [3, 4]])) } // test replacing both overwritable and nonoverwritable values with values of different types @@ -573,15 +578,15 @@ final class DocumentTests: MongoSwiftTestCase { var overwritablePointer = overwritableDoc._bson let newOid = ObjectId() - let overwritablePairs: [(String, BSONValue)] = [ - ("double", Int(10)), + let overwritablePairs: [(String, BSON)] = [ + ("double", BSON(10)), ("int32", "hi"), - ("int64", Decimal128("1.0")!), + ("int64", .decimal128(Decimal128("1.0")!)), ("bool", [1, 2, 3]), ("decimal", 100), ("oid", 25.5), - ("timestamp", newOid), - ("datetime", Timestamp(timestamp: 1, inc: 2)) + ("timestamp", .objectId(newOid)), + ("datetime", .timestamp(Timestamp(timestamp: 1, inc: 2))) ] overwritablePairs.forEach { k, v in @@ -591,14 +596,14 @@ final class DocumentTests: MongoSwiftTestCase { } expect(overwritableDoc).to(equal([ - "double": 10, + "double": BSON(10), "int32": "hi", - "int64": Decimal128("1.0")!, - "bool": [1, 2, 3] as [Int], + "int64": .decimal128(Decimal128("1.0")!), + "bool": [1, 2, 3], "decimal": 100, "oid": 25.5, - "timestamp": newOid, - "datetime": Timestamp(timestamp: 1, inc: 2) + "timestamp": .objectId(newOid), + "datetime": .timestamp(Timestamp(timestamp: 1, inc: 2)) ])) // make a deep copy so we start off with uniquely referenced storage @@ -607,7 +612,7 @@ final class DocumentTests: MongoSwiftTestCase { // save a reference to original bson_t so we can verify it changes var nonOverwritablePointer = nonOverwritableDoc._bson - let nonOverwritablePairs: [(String, BSONValue)] = [("string", 1), ("nil", "hello"), ("doc", "hi"), ("arr", 5)] + let nonOverwritablePairs: [(String, BSON)] = [("string", 1), ("nil", "hello"), ("doc", "hi"), ("arr", 5)] nonOverwritablePairs.forEach { k, v in nonOverwritableDoc[k] = v @@ -624,7 +629,7 @@ final class DocumentTests: MongoSwiftTestCase { var overwritablePointer = overwritableDoc._bson ["double", "int32", "int64", "bool", "decimal", "oid", "timestamp", "datetime"].forEach { - overwritableDoc[$0] = BSONNull() + overwritableDoc[$0] = .null // the storage should change every time expect(overwritableDoc._bson).toNot(equal(overwritablePointer)) overwritablePointer = overwritableDoc._bson @@ -634,24 +639,24 @@ final class DocumentTests: MongoSwiftTestCase { var nonOverwritablePointer = nonOverwritableDoc._bson ["string", "doc", "arr"].forEach { - nonOverwritableDoc[$0] = BSONNull() + nonOverwritableDoc[$0] = .null // the storage should change every time expect(nonOverwritableDoc._bson).toNot(equal(nonOverwritablePointer)) nonOverwritablePointer = nonOverwritableDoc._bson } expect(nonOverwritableDoc).to( - equal(["string": BSONNull(), "nil": BSONNull(), "doc": BSONNull(), "arr": BSONNull()])) + equal(["string": .null, "nil": .null, "doc": .null, "arr": .null])) } // Test types where replacing them with an instance of their own type is a no-op func testReplaceValueNoop() throws { - var noops: Document = ["null": BSONNull(), "maxkey": MaxKey(), "minkey": MinKey()] + var noops: Document = ["null": .null, "maxkey": .maxKey, "minkey": .minKey] var pointer = noops._bson // replace values with own types. these should all be no-ops - let newPairs1: [(String, BSONValue)] = [("null", BSONNull()), ("maxkey", MaxKey()), ("minkey", MinKey())] + let newPairs1: [(String, BSON)] = [("null", .null), ("maxkey", .maxKey), ("minkey", .minKey)] newPairs1.forEach { k, v in noops[k] = v @@ -660,10 +665,10 @@ final class DocumentTests: MongoSwiftTestCase { } // we should still have exactly the same document we started with - expect(noops).to(equal(["null": BSONNull(), "maxkey": MaxKey(), "minkey": MinKey()])) + expect(noops).to(equal(["null": .null, "maxkey": .maxKey, "minkey": .minKey])) // now try replacing them with values of different types that do require replacing storage - let newPairs2: [(String, BSONValue)] = [("null", 5), ("maxkey", "hi"), ("minkey", false)] + let newPairs2: [(String, BSON)] = [("null", 5), ("maxkey", "hi"), ("minkey", false)] newPairs2.forEach { k, v in noops[k] = v @@ -676,13 +681,13 @@ final class DocumentTests: MongoSwiftTestCase { } func testDocumentDictionarySimilarity() throws { - var doc: Document = ["hello": "world", "swift": 4.2, "null": BSONNull(), "remove_me": "please"] - let dict: [String: BSONValue] = ["hello": "world", "swift": 4.2, "null": BSONNull(), "remove_me": "please"] + var doc: Document = ["hello": "world", "swift": 4.2, "null": .null, "remove_me": "please"] + let dict: [String: BSON] = ["hello": "world", "swift": 4.2, "null": .null, "remove_me": "please"] - expect(doc["hello"]).to(bsonEqual(dict["hello"])) - expect(doc["swift"]).to(bsonEqual(dict["swift"])) + expect(doc["hello"]).to(equal(dict["hello"])) + expect(doc["swift"]).to(equal(dict["swift"])) expect(doc["nonexistent key"]).to(beNil()) - expect(doc["null"]).to(bsonEqual(dict["null"])) + expect(doc["null"]).to(equal(dict["null"])) doc["remove_me"] = nil @@ -694,29 +699,29 @@ final class DocumentTests: MongoSwiftTestCase { let doc: Document = ["hello": "world"] let floatVal = 18.2 let stringVal = "this is a string" - expect(doc["DNE", default: floatVal]).to(bsonEqual(floatVal)) - expect(doc["hello", default: floatVal]).to(bsonEqual(doc["hello"])) - expect(doc["DNE", default: stringVal]).to(bsonEqual(stringVal)) - expect(doc["DNE", default: BSONNull()]).to(bsonEqual(BSONNull())) - expect(doc["autoclosure test", default: floatVal * floatVal]).to(bsonEqual(floatVal * floatVal)) - expect(doc["autoclosure test", default: "\(stringVal) and \(floatVal)" + stringVal]) - .to(bsonEqual("\(stringVal) and \(floatVal)" + stringVal)) + expect(doc["DNE", default: .double(floatVal)]).to(equal(.double(floatVal))) + expect(doc["hello", default: .double(floatVal)]).to(equal(doc["hello"])) + expect(doc["DNE", default: .string(stringVal)]).to(equal(.string(stringVal))) + expect(doc["DNE", default: .null]).to(equal(.null)) + expect(doc["autoclosure test", default: .double(floatVal * floatVal)]).to(equal(.double(floatVal * floatVal))) + expect(doc["autoclosure test", default: .string("\(stringVal) and \(floatVal)" + stringVal)]) + .to(equal(.string("\(stringVal) and \(floatVal)" + stringVal))) } func testMultibyteCharacterStrings() throws { let str = String(repeating: "🇧🇷", count: 10) - var doc: Document = ["first": str] - expect(doc["first"] as? String).to(equal(str)) + var doc: Document = ["first": .string(str)] + expect(doc["first"]).to(equal(.string(str))) let doc1: Document = [str: "second"] - expect(doc1[str] as? String).to(equal("second")) + expect(doc1[str]).to(equal("second")) let abt = try CodecTests.AllBSONTypes.factory() Mirror(reflecting: abt).children.forEach { child in let value = child.value as! BSONValue - doc[str] = value - expect(doc[str]).to(bsonEqual(value)) + doc[str] = value.bson + expect(doc[str]).to(equal(value.bson)) } } @@ -732,15 +737,15 @@ final class DocumentTests: MongoSwiftTestCase { let encoder = BSONEncoder() let defaultEncoding = try encoder.encode(uuidStruct) - expect(defaultEncoding["uuid"] as? Binary).to(equal(binary)) + expect(defaultEncoding["uuid"]).to(equal(.binary(binary))) encoder.uuidEncodingStrategy = .binary let binaryEncoding = try encoder.encode(uuidStruct) - expect(binaryEncoding["uuid"] as? Binary).to(equal(binary)) + expect(binaryEncoding["uuid"]).to(equal(.binary(binary))) encoder.uuidEncodingStrategy = .deferredToUUID let deferred = try encoder.encode(uuidStruct) - expect(deferred["uuid"] as? String).to(equal(uuid.uuidString)) + expect(deferred["uuid"]).to(equal(.string(uuid.uuidString))) } func testUUIDDecodingStrategies() throws { @@ -751,7 +756,7 @@ final class DocumentTests: MongoSwiftTestCase { // UUID default decoder expects a string decoder.uuidDecodingStrategy = .deferredToUUID - let stringDoc: Document = ["uuid": uuid.description] + let stringDoc: Document = ["uuid": .string(uuid.description)] let badString: Document = ["uuid": "hello"] let deferredStruct = try decoder.decode(UUIDWrapper.self, from: stringDoc) expect(deferredStruct.uuid).to(equal(uuid)) @@ -765,11 +770,11 @@ final class DocumentTests: MongoSwiftTestCase { uuidt.8, uuidt.9, uuidt.10, uuidt.11, uuidt.12, uuidt.13, uuidt.14, uuidt.15 ]) - let binaryDoc: Document = ["uuid": try Binary(data: bytes, subtype: .uuid)] + let binaryDoc: Document = ["uuid": .binary(try Binary(data: bytes, subtype: .uuid))] let binaryStruct = try decoder.decode(UUIDWrapper.self, from: binaryDoc) expect(binaryStruct.uuid).to(equal(uuid)) - let badBinary: Document = ["uuid": try Binary(data: bytes, subtype: .generic)] + let badBinary: Document = ["uuid": .binary(try Binary(data: bytes, subtype: .generic))] expect(try decoder.decode(UUIDWrapper.self, from: badBinary)).to(throwError(CodecTests.dataCorruptedErr)) expect(try decoder.decode(UUIDWrapper.self, from: stringDoc)).to(throwError(CodecTests.typeMismatchErr)) @@ -786,24 +791,24 @@ final class DocumentTests: MongoSwiftTestCase { let encoder = BSONEncoder() let defaultEncoding = try encoder.encode(dateStruct) - expect(defaultEncoding["date"] as? Date).to(equal(date)) + expect(defaultEncoding["date"]).to(equal(.datetime(date))) encoder.dateEncodingStrategy = .bsonDateTime let bsonDate = try encoder.encode(dateStruct) - expect(bsonDate["date"] as? Date).to(equal(date)) + expect(bsonDate["date"]).to(equal(.datetime(date))) encoder.dateEncodingStrategy = .secondsSince1970 let secondsSince1970 = try encoder.encode(dateStruct) - expect(secondsSince1970["date"] as? TimeInterval).to(equal(date.timeIntervalSince1970)) + expect(secondsSince1970["date"]).to(equal(.double(date.timeIntervalSince1970))) encoder.dateEncodingStrategy = .millisecondsSince1970 let millisecondsSince1970 = try encoder.encode(dateStruct) - expect(millisecondsSince1970["date"]).to(bsonEqual(date.msSinceEpoch)) + expect(millisecondsSince1970["date"]).to(equal(.int64(date.msSinceEpoch))) if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { encoder.dateEncodingStrategy = .iso8601 let iso = try encoder.encode(dateStruct) - expect(iso["date"] as? String).to(equal(BSONDecoder.iso8601Formatter.string(from: date))) + expect(iso["date"]).to(equal(.string(BSONDecoder.iso8601Formatter.string(from: date)))) } let formatter = DateFormatter() @@ -812,18 +817,18 @@ final class DocumentTests: MongoSwiftTestCase { encoder.dateEncodingStrategy = .formatted(formatter) let formatted = try encoder.encode(dateStruct) - expect(formatted["date"] as? String).to(equal(formatter.string(from: date))) + expect(formatted["date"]).to(equal(.string(formatter.string(from: date)))) encoder.dateEncodingStrategy = .deferredToDate let deferred = try encoder.encode(dateStruct) - expect(deferred["date"] as? TimeInterval).to(equal(date.timeIntervalSinceReferenceDate)) + expect(deferred["date"]).to(equal(.double(date.timeIntervalSinceReferenceDate))) encoder.dateEncodingStrategy = .custom({ d, e in var container = e.singleValueContainer() try container.encode(Int64(d.timeIntervalSince1970 + 12)) }) let custom = try encoder.encode(dateStruct) - expect(custom["date"]).to(bsonEqual(Int64(date.timeIntervalSince1970 + 12))) + expect(custom["date"]).to(equal(.int64(Int64(date.timeIntervalSince1970 + 12)))) let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short @@ -837,8 +842,11 @@ final class DocumentTests: MongoSwiftTestCase { } }) let customArr = try encoder.encode(noSecondsDate) - expect(dateFormatter.date(from: (customArr["date"] as! [String]).joined(separator: "/"))) - .to(equal(noSecondsDate.date)) + expect(dateFormatter.date(from: (customArr["date"]? + .arrayValue? + .compactMap { $0.stringValue } + .joined(separator: "/"))!) + ).to(equal(noSecondsDate.date)) enum DateKeys: String, CodingKey { case month, day, year @@ -852,11 +860,11 @@ final class DocumentTests: MongoSwiftTestCase { try container.encode(components[2], forKey: .year) }) let customDoc = try encoder.encode(noSecondsDate) - expect(customDoc["date"] as? Document).to(bsonEqual(["month": "1", "day": "2", "year": "19"] as Document)) + expect(customDoc["date"]).to(equal(["month": "1", "day": "2", "year": "19"])) encoder.dateEncodingStrategy = .custom({ _, _ in }) let customNoop = try encoder.encode(noSecondsDate) - expect(customNoop["date"] as? Document).to(bsonEqual([:] as Document)) + expect(customNoop["date"]).to(equal([:])) } func testDateDecodingStrategies() throws { @@ -865,7 +873,7 @@ final class DocumentTests: MongoSwiftTestCase { let date = Date(timeIntervalSince1970: 125.0) // Default is .bsonDateTime - let bsonDate: Document = ["date": date] + let bsonDate: Document = ["date": .datetime(date)] let defaultStruct = try decoder.decode(DateWrapper.self, from: bsonDate) expect(defaultStruct.date).to(equal(date)) @@ -874,21 +882,21 @@ final class DocumentTests: MongoSwiftTestCase { expect(bsonDateStruct.date).to(equal(date)) decoder.dateDecodingStrategy = .millisecondsSince1970 - let msInt64: Document = ["date": date.msSinceEpoch] + let msInt64: Document = ["date": .int64(date.msSinceEpoch)] let msInt64Struct = try decoder.decode(DateWrapper.self, from: msInt64) expect(msInt64Struct.date).to(equal(date)) expect(try BSONDecoder().decode(DateWrapper.self, from: msInt64)).to(throwError(CodecTests.typeMismatchErr)) - let msDouble: Document = ["date": Double(date.msSinceEpoch)] + let msDouble: Document = ["date": .double(Double(date.msSinceEpoch))] let msDoubleStruct = try decoder.decode(DateWrapper.self, from: msDouble) expect(msDoubleStruct.date).to(equal(date)) decoder.dateDecodingStrategy = .secondsSince1970 - let sDouble: Document = ["date": date.timeIntervalSince1970] + let sDouble: Document = ["date": .double(date.timeIntervalSince1970)] let sDoubleStruct = try decoder.decode(DateWrapper.self, from: sDouble) expect(sDoubleStruct.date).to(equal(date)) - let sInt64: Document = ["date": Int64(date.timeIntervalSince1970)] + let sInt64: Document = ["date": .double(date.timeIntervalSince1970)] let sInt64Struct = try decoder.decode(DateWrapper.self, from: sInt64) expect(sInt64Struct.date).to(equal(date)) @@ -898,7 +906,7 @@ final class DocumentTests: MongoSwiftTestCase { formatter.locale = Locale(identifier: "en_US") decoder.dateDecodingStrategy = .formatted(formatter) - let formatted: Document = ["date": formatter.string(from: date)] + let formatted: Document = ["date": .string(formatter.string(from: date))] let badlyFormatted: Document = ["date": "this is not a date"] let formattedStruct = try decoder.decode(DateWrapper.self, from: formatted) expect(formattedStruct.date).to(equal(date)) @@ -907,7 +915,7 @@ final class DocumentTests: MongoSwiftTestCase { if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { decoder.dateDecodingStrategy = .iso8601 - let isoDoc: Document = ["date": BSONDecoder.iso8601Formatter.string(from: date)] + let isoDoc: Document = ["date": .string(BSONDecoder.iso8601Formatter.string(from: date))] let isoStruct = try decoder.decode(DateWrapper.self, from: isoDoc) expect(isoStruct.date).to(equal(date)) expect(try decoder.decode(DateWrapper.self, from: formatted)).to(throwError(CodecTests.dataCorruptedErr)) @@ -916,7 +924,7 @@ final class DocumentTests: MongoSwiftTestCase { } decoder.dateDecodingStrategy = .custom({ decode in try Date(from: decode) }) - let customDoc: Document = ["date": date.timeIntervalSinceReferenceDate] + let customDoc: Document = ["date": .double(date.timeIntervalSinceReferenceDate)] let customStruct = try decoder.decode(DateWrapper.self, from: customDoc) expect(customStruct.date).to(equal(date)) expect(try decoder.decode(DateWrapper.self, from: badlyFormatted)).to(throwError(CodecTests.typeMismatchErr)) @@ -941,21 +949,21 @@ final class DocumentTests: MongoSwiftTestCase { let dataStruct = DataWrapper(data: data) let defaultDoc = try encoder.encode(dataStruct) - expect(defaultDoc["data"] as? Binary).to(equal(binaryData)) + expect(defaultDoc["data"]?.binaryValue).to(equal(binaryData)) let roundTripDefault = try decoder.decode(DataWrapper.self, from: defaultDoc) expect(roundTripDefault.data).to(equal(data)) encoder.dataEncodingStrategy = .binary decoder.dataDecodingStrategy = .binary let binaryDoc = try encoder.encode(dataStruct) - expect(binaryDoc["data"] as? Binary).to(bsonEqual(binaryData)) + expect(binaryDoc["data"]?.binaryValue).to(equal(binaryData)) let roundTripBinary = try decoder.decode(DataWrapper.self, from: binaryDoc) expect(roundTripBinary.data).to(equal(data)) encoder.dataEncodingStrategy = .deferredToData decoder.dataDecodingStrategy = .deferredToData let deferredDoc = try encoder.encode(dataStruct) - expect(deferredDoc["data"]).to(bsonEqual(arrData)) + expect(deferredDoc["data"]?.arrayValue?.compactMap { $0.int32Value }).to(equal(arrData)) let roundTripDeferred = try decoder.decode(DataWrapper.self, from: deferredDoc) expect(roundTripDeferred.data).to(equal(data)) expect(try decoder.decode(DataWrapper.self, from: defaultDoc)).to(throwError(CodecTests.typeMismatchErr)) @@ -963,52 +971,50 @@ final class DocumentTests: MongoSwiftTestCase { encoder.dataEncodingStrategy = .base64 decoder.dataDecodingStrategy = .base64 let base64Doc = try encoder.encode(dataStruct) - expect(base64Doc["data"]).to(bsonEqual(data.base64EncodedString())) + expect(base64Doc["data"]?.stringValue).to(equal(data.base64EncodedString())) let roundTripBase64 = try decoder.decode(DataWrapper.self, from: base64Doc) expect(roundTripBase64.data).to(equal(data)) expect(try decoder.decode(DataWrapper.self, from: ["data": "this is not base64 encoded~"])) .to(throwError(CodecTests.dataCorruptedErr)) - let customEncodedDoc = ["d": data.base64EncodedString(), "hash": data.hashValue] as Document + let customEncodedDoc: Document = [ + "d": .string(data.base64EncodedString()), + "hash": .int64(Int64(data.hashValue)) + ] encoder.dataEncodingStrategy = .custom({ d, encoder in var container = encoder.singleValueContainer() try container.encode(customEncodedDoc) }) decoder.dataDecodingStrategy = .custom({ decoder in let doc = try Document(from: decoder) - guard let d = Data(base64Encoded: doc["d"] as! String) else { + guard let d = Data(base64Encoded: doc["d"]!.stringValue!) else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "bad base64")) } expect(d.hashValue).to(equal(data.hashValue)) return d }) let customDoc = try encoder.encode(dataStruct) - expect(customDoc["data"]).to(bsonEqual(customEncodedDoc)) + expect(customDoc["data"]).to(equal(.document(customEncodedDoc))) let roundTripCustom = try decoder.decode(DataWrapper.self, from: customDoc) expect(roundTripCustom.data).to(equal(data)) encoder.dataEncodingStrategy = .custom({ _, _ in }) - expect(try encoder.encode(dataStruct)).to(bsonEqual(["data": [:] as Document] as Document)) + expect(try encoder.encode(dataStruct)).to(equal(["data": [:]])) } - func testIntegerRetrieval() { - let doc: Document = ["int": 12, "int32": Int32(12), "int64": Int64(12)] - - // Int always goes in and comes out as int - expect(doc["int"]).to(bsonEqual(12)) - expect(doc["int"] as? Int).to(equal(12)) + func testIntegerLiteral() { + let doc: Document = ["int": 12] if MongoSwiftTestCase.is32Bit { - expect(doc["int32"] as? Int).to(equal(12)) - expect(doc["int32"] as? Int32).to(beNil()) - expect(doc["int64"] as? Int).to(beNil()) - expect(doc["int64"] as? Int64).to(equal(Int64(12))) + expect(doc["int"]).to(equal(.int32(12))) + expect(doc["int"]?.type).to(equal(.int32)) } else { - expect(doc["int32"] as? Int).to(beNil()) - expect(doc["int32"] as? Int32).to(equal(Int32(12))) - expect(doc["int64"] as? Int).to(equal(12)) - expect(doc["int64"] as? Int64).to(beNil()) + expect(doc["int"]?.type).to(equal(.int64)) + expect(doc["int"]).to(equal(.int64(12))) } + + let bson: BSON = 12 + expect(doc["int"]).to(equal(bson)) } func testInvalidBSON() throws { diff --git a/Tests/MongoSwiftTests/MongoClientTests.swift b/Tests/MongoSwiftTests/MongoClientTests.swift index 07c4751b4..ba3ecab1a 100644 --- a/Tests/MongoSwiftTests/MongoClientTests.swift +++ b/Tests/MongoSwiftTests/MongoClientTests.swift @@ -43,7 +43,7 @@ final class MongoClientTests: MongoSwiftTestCase { expect(try client.listDatabaseNames(["name": "db1"])).to(equal(["db1"])) let topSize = dbInfo.map { $0.sizeOnDisk }.max()! - expect(try client.listDatabases(["sizeOnDisk": ["$gt": topSize] as Document])).to(beEmpty()) + expect(try client.listDatabases(["sizeOnDisk": ["$gt": BSON(topSize)]])).to(beEmpty()) if MongoSwiftTestCase.topologyType == .sharded { expect(dbInfo.first?.shards).toNot(beNil()) @@ -71,7 +71,7 @@ final class MongoClientTests: MongoSwiftTestCase { let insertResult = try coll.insertOne([ "test": 42 ]) let findResult = try coll.find([ "_id": insertResult!.insertedId ]) let docs = Array(findResult) - expect(docs[0]["test"]).to(bsonEqual(42)) + expect(docs[0]["test"]).to(equal(42)) try db.drop() } @@ -161,14 +161,14 @@ final class MongoClientTests: MongoSwiftTestCase { // default behavior is .bsonDate, .binary, .binary let collDefault = defaultDb.collection(self.getCollectionName(), withType: Wrapper.self) - let defaultId = "default" - try collDefault.insertOne(wrapperWithId(defaultId)) + let defaultId: BSON = "default" + try collDefault.insertOne(wrapperWithId(defaultId.stringValue!)) var doc = try collDoc.find(["_id": defaultId]).nextOrError() expect(doc).toNot(beNil()) - expect(doc?["date"] as? Date).to(equal(date)) - expect(doc?["uuid"] as? Binary).to(equal(try Binary(from: uuid))) - expect(doc?["data"] as? Binary).to(equal(try Binary(data: data, subtype: .generic))) + expect(doc?["date"]?.dateValue).to(equal(date)) + expect(doc?["uuid"]?.binaryValue).to(equal(try Binary(from: uuid))) + expect(doc?["data"]?.binaryValue).to(equal(try Binary(data: data, subtype: .generic))) expect(try collDefault.find(["_id": defaultId]).nextOrError()).to(equal(wrapper)) @@ -181,14 +181,14 @@ final class MongoClientTests: MongoSwiftTestCase { let clientCustom = try SyncMongoClient.makeTestClient(options: custom) let collClient = clientCustom.db(defaultDb.name).collection(collDoc.name, withType: Wrapper.self) - let collClientId = "customClient" - try collClient.insertOne(wrapperWithId(collClientId)) + let collClientId: BSON = "customClient" + try collClient.insertOne(wrapperWithId(collClientId.stringValue!)) - doc = try collDoc.find(["_id": collClientId] as Document).nextOrError() + doc = try collDoc.find(["_id": collClientId]).nextOrError() expect(doc).toNot(beNil()) - expect(doc?["date"] as? Double).to(beCloseTo(date.timeIntervalSince1970, within: 0.001)) - expect(doc?["uuid"] as? String).to(equal(uuid.uuidString)) - expect(doc?["data"] as? String).to(equal(data.base64EncodedString())) + expect(doc?["date"]?.doubleValue).to(beCloseTo(date.timeIntervalSince1970, within: 0.001)) + expect(doc?["uuid"]?.stringValue).to(equal(uuid.uuidString)) + expect(doc?["data"]?.stringValue).to(equal(data.base64EncodedString())) expect(try collClient.find(["_id": collClientId]).nextOrError()).to(equal(wrapper)) @@ -201,16 +201,16 @@ final class MongoClientTests: MongoSwiftTestCase { let dbCustom = clientCustom.db(defaultDb.name, options: dbOpts) let collDb = dbCustom.collection(collClient.name, withType: Wrapper.self) - let customDbId = "customDb" - try collDb.insertOne(wrapperWithId(customDbId)) + let customDbId: BSON = "customDb" + try collDb.insertOne(wrapperWithId(customDbId.stringValue!)) - doc = try collDoc.find(["_id": customDbId] as Document).next() + doc = try collDoc.find(["_id": customDbId]).next() expect(doc).toNot(beNil()) - expect(doc?["date"] as? Double).to(beCloseTo(date.timeIntervalSinceReferenceDate, within: 0.001)) - expect(doc?["uuid"] as? Binary).to(equal(try Binary(from: uuid))) - expect(doc?["data"] as? Binary).to(equal(try Binary(data: data, subtype: .generic))) + expect(doc?["date"]?.doubleValue).to(beCloseTo(date.timeIntervalSinceReferenceDate, within: 0.001)) + expect(doc?["uuid"]?.binaryValue).to(equal(try Binary(from: uuid))) + expect(doc?["data"]?.binaryValue).to(equal(try Binary(data: data, subtype: .generic))) - expect(try collDb.find(["_id": customDbId] as Document).nextOrError()).to(equal(wrapper)) + expect(try collDb.find(["_id": customDbId]).nextOrError()).to(equal(wrapper)) // Construct collection with differing strategies from database let dbCollOpts = CollectionOptions( @@ -220,16 +220,16 @@ final class MongoClientTests: MongoSwiftTestCase { ) let collCustom = dbCustom.collection(collClient.name, withType: Wrapper.self, options: dbCollOpts) - let customDbCollId = "customDbColl" - try collCustom.insertOne(wrapperWithId(customDbCollId)) + let customDbCollId: BSON = "customDbColl" + try collCustom.insertOne(wrapperWithId(customDbCollId.stringValue!)) doc = try collDoc.find(["_id": customDbCollId]).nextOrError() expect(doc).toNot(beNil()) - expect(doc?["date"]).to(bsonEqual(date.msSinceEpoch)) - expect(doc?["uuid"] as? String).to(equal(uuid.uuidString)) - expect(doc?["data"] as? String).to(equal(data.base64EncodedString())) + expect(doc?["date"]?.int64Value).to(equal(date.msSinceEpoch)) + expect(doc?["uuid"]?.stringValue).to(equal(uuid.uuidString)) + expect(doc?["data"]?.stringValue).to(equal(data.base64EncodedString())) - expect(try collCustom.find(["_id": customDbCollId] as Document).nextOrError()) + expect(try collCustom.find(["_id": customDbCollId]).nextOrError()) .to(equal(wrapper)) try defaultDb.drop() diff --git a/Tests/MongoSwiftTests/MongoCollection+BulkWriteTests.swift b/Tests/MongoSwiftTests/MongoCollection+BulkWriteTests.swift index 420bb3b63..907ad9910 100644 --- a/Tests/MongoSwiftTests/MongoCollection+BulkWriteTests.swift +++ b/Tests/MongoSwiftTests/MongoCollection+BulkWriteTests.swift @@ -60,8 +60,8 @@ final class MongoCollection_BulkWriteTests: MongoSwiftTestCase { let result: BulkWriteResult! = try self.coll.bulkWrite(requests) expect(result.insertedCount).to(equal(2)) - expect(result.insertedIds[0]!).to(bsonEqual(1)) - expect(result.insertedIds[1]!).to(beAnInstanceOf(ObjectId.self)) + expect(result.insertedIds[0]!).to(equal(1)) + expect(result.insertedIds[1]!.type).to(equal(.objectId)) // verify inserted doc without _id was not modified. guard case let .insertOne(doc) = requests[1] else { @@ -76,9 +76,9 @@ final class MongoCollection_BulkWriteTests: MongoSwiftTestCase { } func testBulkWriteErrors() throws { - let id = ObjectId() - let id2 = ObjectId() - let id3 = ObjectId() + let id = BSON.objectId(ObjectId()) + let id2 = BSON.objectId(ObjectId()) + let id3 = BSON.objectId(ObjectId()) let doc = ["_id": id] as Document @@ -88,7 +88,7 @@ final class MongoCollection_BulkWriteTests: MongoSwiftTestCase { .insertOne(["_id": id2]), .insertOne(doc), .updateOne(filter: ["_id": id3], - update: ["$set": ["asdfasdf": 1] as Document], + update: ["$set": ["asdfasdf": 1]], options: UpdateModelOptions(upsert: true)) ] @@ -119,15 +119,15 @@ final class MongoCollection_BulkWriteTests: MongoSwiftTestCase { try createFixtures(4) let requests: [WriteModel] = [ - .updateOne(filter: ["_id": 2], update: ["$inc": ["x": 1] as Document], options: nil), - .updateMany(filter: ["_id": ["$gt": 2] as Document], update: ["$inc": ["x": -1] as Document], options: nil), + .updateOne(filter: ["_id": 2], update: ["$inc": ["x": 1]], options: nil), + .updateMany(filter: ["_id": ["$gt": 2]], update: ["$inc": ["x": -1]], options: nil), .updateOne(filter: ["_id": 5], - update: ["$set": ["x": 55] as Document], + update: ["$set": ["x": 55]], options: UpdateModelOptions(upsert: true)), .updateOne(filter: ["x": 66], - update: ["$set": ["x": 66] as Document], + update: ["$set": ["x": 66]], options: UpdateModelOptions(upsert: true)), - .updateMany(filter: ["x": ["$gt": 50] as Document], update: ["$inc": ["x": 1] as Document], options: nil) + .updateMany(filter: ["x": ["$gt": 50]], update: ["$inc": ["x": 1]], options: nil) ] let result: BulkWriteResult! = try self.coll.bulkWrite(requests) @@ -135,8 +135,8 @@ final class MongoCollection_BulkWriteTests: MongoSwiftTestCase { expect(result.matchedCount).to(equal(5)) expect(result.modifiedCount).to(equal(5)) expect(result.upsertedCount).to(equal(2)) - expect(result.upsertedIds[2]!).to(bsonEqual(5)) - expect(result.upsertedIds[3]!).to(beAnInstanceOf(ObjectId.self)) + expect(result.upsertedIds[2]!).to(equal(5)) + expect(result.upsertedIds[3]!.type).to(equal(.objectId)) let cursor = try coll.find() expect(cursor.next()).to(equal(["_id": 1, "x": 11])) @@ -153,7 +153,7 @@ final class MongoCollection_BulkWriteTests: MongoSwiftTestCase { let requests: [WriteModel] = [ .deleteOne(["_id": 1], options: nil), - .deleteMany(["_id": ["$gt": 2] as Document], options: nil) + .deleteMany(["_id": ["$gt": 2]], options: nil) ] let result: BulkWriteResult! = try self.coll.bulkWrite(requests) @@ -169,12 +169,12 @@ final class MongoCollection_BulkWriteTests: MongoSwiftTestCase { try createFixtures(3) let requests: [WriteModel] = [ - .updateOne(filter: ["_id": ["$gt": 1] as Document], - update: ["$inc": ["x": 1] as Document], + .updateOne(filter: ["_id": ["$gt": 1]], + update: ["$inc": ["x": 1]], options: nil), - .updateMany(filter: ["_id": ["$gt": 1] as Document], update: ["$inc": ["x": 1] as Document], options: nil), + .updateMany(filter: ["_id": ["$gt": 1]], update: ["$inc": ["x": 1]], options: nil), .insertOne(["_id": 4]), - .deleteMany(["x": ["$nin": [24, 34]] as Document], options: nil), + .deleteMany(["x": ["$nin": [24, 34]]], options: nil), .replaceOne(filter: ["_id": 4], replacement: ["_id": 4, "x": 44], options: ReplaceOneModelOptions(upsert: true)) @@ -183,11 +183,11 @@ final class MongoCollection_BulkWriteTests: MongoSwiftTestCase { let result: BulkWriteResult! = try self.coll.bulkWrite(requests) expect(result.insertedCount).to(equal(1)) - expect(result.insertedIds[2]!).to(bsonEqual(4)) + expect(result.insertedIds[2]!).to(equal(4)) expect(result.matchedCount).to(equal(3)) expect(result.modifiedCount).to(equal(3)) expect(result.upsertedCount).to(equal(1)) - expect(result.upsertedIds[4]!).to(bsonEqual(4)) + expect(result.upsertedIds[4]!).to(equal(4)) expect(result.deletedCount).to(equal(2)) let cursor = try coll.find() @@ -211,7 +211,7 @@ final class MongoCollection_BulkWriteTests: MongoSwiftTestCase { var documents: [Document] = [] for i in 1...n { - documents.append(["_id": i, "x": Int("\(i)\(i)")!]) + documents.append(["_id": BSON(i), "x": BSON(Int("\(i)\(i)")!)]) } try self.coll.insertMany(documents) diff --git a/Tests/MongoSwiftTests/MongoCollection+IndexTests.swift b/Tests/MongoSwiftTests/MongoCollection+IndexTests.swift index 0e68e855c..7d6744ae1 100644 --- a/Tests/MongoSwiftTests/MongoCollection+IndexTests.swift +++ b/Tests/MongoSwiftTests/MongoCollection+IndexTests.swift @@ -90,7 +90,7 @@ final class MongoCollection_IndexTests: MongoSwiftTestCase { name: "testOptions", sparse: false, sphereIndexVersion: 2, - storageEngine: ["wiredTiger": ["configString": "access_pattern_hint=random"] as Document], + storageEngine: ["wiredTiger": ["configString": "access_pattern_hint=random"]], textIndexVersion: 2, unique: true, version: 2, @@ -167,7 +167,7 @@ final class MongoCollection_IndexTests: MongoSwiftTestCase { expect(try self.coll.createIndex(model)).to(equal("cat_1")) let res = try self.coll.dropIndex(model) - expect((res["ok"] as? BSONNumber)?.doubleValue).to(bsonEqual(1.0)) + expect(res["ok"]?.asDouble()).to(equal(1.0)) // now there should only be _id_ left let indexes = try coll.listIndexes() @@ -181,7 +181,7 @@ final class MongoCollection_IndexTests: MongoSwiftTestCase { expect(try self.coll.createIndex(model)).to(equal("cat_1")) let res = try self.coll.dropIndex(["cat": 1]) - expect((res["ok"] as? BSONNumber)?.doubleValue).to(bsonEqual(1.0)) + expect(res["ok"]?.asDouble()).to(equal(1.0)) // now there should only be _id_ left let indexes = try coll.listIndexes() @@ -195,7 +195,7 @@ final class MongoCollection_IndexTests: MongoSwiftTestCase { expect(try self.coll.createIndex(model)).to(equal("cat_1")) let res = try self.coll.dropIndexes() - expect((res["ok"] as? BSONNumber)?.doubleValue).to(bsonEqual(1.0)) + expect(res["ok"]?.asDouble()).to(equal(1.0)) // now there should only be _id_ left let indexes = try coll.listIndexes() @@ -206,7 +206,7 @@ final class MongoCollection_IndexTests: MongoSwiftTestCase { func testListIndexNames() throws { let model1 = IndexModel(keys: ["cat": 1]) let model2 = IndexModel(keys: ["cat": -1], options: IndexOptions(name: "neg cat")) - expect( try self.coll.createIndexes([model1, model2]) ).to(equal(["cat_1", "neg cat"])) + expect(try self.coll.createIndexes([model1, model2])).to(equal(["cat_1", "neg cat"])) let indexNames = try coll.listIndexNames() expect(indexNames.count).to(equal(3)) @@ -242,7 +242,7 @@ final class MongoCollection_IndexTests: MongoSwiftTestCase { let dropIndexOpts = DropIndexOptions(maxTimeMS: maxTimeMS, writeConcern: wc) let res = try collection.dropIndex(model, options: dropIndexOpts) - expect((res["ok"] as? BSONNumber)?.doubleValue).to(bsonEqual(1.0)) + expect(res["ok"]?.asDouble()).to(equal(1.0)) // now there should only be _id_ left let indexes = try coll.listIndexes() @@ -254,10 +254,10 @@ final class MongoCollection_IndexTests: MongoSwiftTestCase { expect(receivedEvents.count).to(equal(2)) expect(receivedEvents[0].command["createIndexes"]).toNot(beNil()) expect(receivedEvents[0].command["maxTimeMS"]).toNot(beNil()) - expect(receivedEvents[0].command["maxTimeMS"]).to(bsonEqual(maxTimeMS)) + expect(receivedEvents[0].command["maxTimeMS"]).to(equal(.int64(maxTimeMS))) expect(receivedEvents[1].command["dropIndexes"]).toNot(beNil()) expect(receivedEvents[1].command["maxTimeMS"]).toNot(beNil()) - expect(receivedEvents[1].command["maxTimeMS"]).to(bsonEqual(maxTimeMS)) + expect(receivedEvents[1].command["maxTimeMS"]).to(equal(.int64(maxTimeMS))) } } @@ -280,7 +280,7 @@ extension IndexOptions: Equatable { lhs.min == rhs.min && lhs.bucketSize == rhs.bucketSize && lhs.partialFilterExpression == rhs.partialFilterExpression && - lhs.collation?["locale"] as? String == rhs.collation?["locale"] as? String + lhs.collation?["locale"] == rhs.collation?["locale"] // ^ server adds a bunch of extra fields and a version number // to collations. rather than deal with those, just verify the // locale matches. diff --git a/Tests/MongoSwiftTests/MongoCollectionTests.swift b/Tests/MongoSwiftTests/MongoCollectionTests.swift index 471e1011d..57a5d9774 100644 --- a/Tests/MongoSwiftTests/MongoCollectionTests.swift +++ b/Tests/MongoSwiftTests/MongoCollectionTests.swift @@ -70,8 +70,8 @@ final class MongoCollectionTests: MongoSwiftTestCase { func testInsertOne() throws { expect(try self.coll.deleteMany([:])).toNot(beNil()) - expect(try self.coll.insertOne(self.doc1)?.insertedId).to(bsonEqual(1)) - expect(try self.coll.insertOne(self.doc2)?.insertedId).to(bsonEqual(2)) + expect(try self.coll.insertOne(self.doc1)?.insertedId).to(equal(1)) + expect(try self.coll.insertOne(self.doc2)?.insertedId).to(equal(2)) expect(try self.coll.count()).to(equal(2)) // try inserting a document without an ID @@ -100,8 +100,8 @@ final class MongoCollectionTests: MongoSwiftTestCase { func testAggregate() throws { expect( - Array(try self.coll.aggregate([["$project": ["_id": 0, "cat": 1] as Document]]))) - .to(equal([["cat": "dog"], ["cat": "cat"]] as [Document])) + Array(try self.coll.aggregate([["$project": ["_id": 0, "cat": 1]]]))) + .to(equal([["cat": "dog"], ["cat": "cat"]] as [Document])) } func testDrop() throws { @@ -125,8 +125,8 @@ final class MongoCollectionTests: MongoSwiftTestCase { expect(event.command["drop"]).toNot(beNil()) expect(event.command["writeConcern"]).toNot(beNil()) - expect(event.command["writeConcern"] as? Document) - .to(sortedEqual(try? encoder.encode(expectedWriteConcern))) + expect(event.command["writeConcern"]?.documentValue) + .to(sortedEqual(try? encoder.encode(expectedWriteConcern))) } defer { center.removeObserver(observer) } @@ -149,10 +149,10 @@ final class MongoCollectionTests: MongoSwiftTestCase { // the inserted IDs should either be the ones we set, // or newly created ObjectIds for (_, v) in res!.insertedIds { - if let val = v as? BSONNumber { - expect([10, 11]).to(contain(val.intValue)) + if let val = v.asInt() { + expect([10, 11]).to(contain(val)) } else { - expect(v).to(beAnInstanceOf(ObjectId.self)) + expect(v.type).to(equal(.objectId)) } } @@ -160,10 +160,10 @@ final class MongoCollectionTests: MongoSwiftTestCase { expect(docNoId1).to(equal(["x": 1])) expect(docNoId2).to(equal(["x": 2])) - let newDoc1: Document = ["_id": ObjectId()] - let newDoc2: Document = ["_id": ObjectId()] - let newDoc3: Document = ["_id": ObjectId()] - let newDoc4: Document = ["_id": ObjectId()] + let newDoc1: Document = ["_id": .objectId(ObjectId())] + let newDoc2: Document = ["_id": .objectId(ObjectId())] + let newDoc3: Document = ["_id": .objectId(ObjectId())] + let newDoc4: Document = ["_id": .objectId(ObjectId())] let expectedResultOrdered = BulkWriteResult(insertedCount: 1, insertedIds: [0: newDoc1["_id"]!]) let expectedErrorsOrdered = [ @@ -246,7 +246,7 @@ final class MongoCollectionTests: MongoSwiftTestCase { func testUpdateOne() throws { let updateOneResult = try coll.updateOne( - filter: ["_id": 2], update: ["$set": ["apple": "banana"] as Document]) + filter: ["_id": 2], update: ["$set": ["apple": "banana"]]) expect(updateOneResult?.matchedCount).to(equal(1)) expect(updateOneResult?.modifiedCount).to(equal(1)) } @@ -254,13 +254,13 @@ final class MongoCollectionTests: MongoSwiftTestCase { func testUpdateOneWithUnacknowledgedWriteConcern() throws { let options = UpdateOptions(writeConcern: try WriteConcern(w: .number(0))) let updateOneResult = try coll.updateOne( - filter: ["_id": 2], update: ["$set": ["apple": "banana"] as Document], options: options) + filter: ["_id": 2], update: ["$set": ["apple": "banana"]], options: options) expect(updateOneResult).to(beNil()) } func testUpdateMany() throws { let updateManyResult = try coll.updateMany( - filter: [:], update: ["$set": ["apple": "pear"] as Document]) + filter: [:], update: ["$set": ["apple": "pear"]]) expect(updateManyResult?.matchedCount).to(equal(2)) expect(updateManyResult?.modifiedCount).to(equal(2)) } @@ -268,22 +268,22 @@ final class MongoCollectionTests: MongoSwiftTestCase { func testUpdateManyWithUnacknowledgedWriteConcern() throws { let options = UpdateOptions(writeConcern: try WriteConcern(w: .number(0))) let updateManyResult = try coll.updateMany( - filter: [:], update: ["$set": ["apple": "pear"] as Document], options: options) + filter: [:], update: ["$set": ["apple": "pear"]], options: options) expect(updateManyResult).to(beNil()) } func testDistinct() throws { let distinct1 = try coll.distinct(fieldName: "cat", filter: [:]) - expect((distinct1 as? [String])?.sorted()).to(equal(["cat", "dog"])) + expect(BSON.array(distinct1).arrayValue?.compactMap { $0.stringValue }.sorted()).to(equal(["cat", "dog"])) // nonexistent field expect(try self.coll.distinct(fieldName: "abc", filter: [:])).to(beEmpty()) // test a null distinct value - try coll.insertOne(["cat": BSONNull()]) + try coll.insertOne(["cat": .null]) let distinct2 = try coll.distinct(fieldName: "cat", filter: [:]) expect(distinct2).to(haveCount(3)) // swiftlint:disable trailing_closure - expect(distinct2).to(containElementSatisfying({ $0 is BSONNull })) + expect(distinct2).to(containElementSatisfying({ $0 == .null })) // swiftlint:enable trailing_closure } @@ -330,7 +330,7 @@ final class MongoCollectionTests: MongoSwiftTestCase { expect(try coll1.findOneAndReplace(filter: ["x": 1], replacement: b5)).to(equal(b1)) // test successfully decode to collection type - expect(try coll1.findOneAndUpdate(filter: ["x": 3], update: ["$set": ["x": 6] as Document])).to(equal(b3)) + expect(try coll1.findOneAndUpdate(filter: ["x": 3], update: ["$set": ["x": 6]])).to(equal(b3)) expect(try coll1.findOneAndDelete(["x": 4])).to(equal(b4)) } @@ -380,7 +380,7 @@ final class MongoCollectionTests: MongoSwiftTestCase { expect(try encoder.encode(stringHint)).to(equal(["hint": "hi"])) let docHint = AggregateOptions(hint: .indexSpec(["hi": 1])) - expect(try encoder.encode(docHint)).to(equal(["hint": ["hi": 1] as Document])) + expect(try encoder.encode(docHint)).to(equal(["hint": ["hi": 1]])) } func testFindOneAndDelete() throws { @@ -443,7 +443,7 @@ final class MongoCollectionTests: MongoSwiftTestCase { // test using maxTimeMS let opts1 = FindOneAndUpdateOptions(maxTimeMS: 100) let result1 = try self.coll.findOneAndUpdate(filter: ["cat": "cat"], - update: ["$set": ["cat": "blah"] as Document], + update: ["$set": ["cat": "blah"]], options: opts1) expect(result1).to(equal(self.doc2)) expect(try self.coll.count()).to(equal(2)) @@ -451,7 +451,7 @@ final class MongoCollectionTests: MongoSwiftTestCase { // test using bypassDocumentValidation let opts2 = FindOneAndUpdateOptions(bypassDocumentValidation: true) let result2 = try self.coll.findOneAndUpdate(filter: ["cat": "dog"], - update: ["$set": ["cat": "hi"] as Document], + update: ["$set": ["cat": "hi"]], options: opts2) expect(result2).to(equal(self.doc1)) expect(try self.coll.count()).to(equal(2)) @@ -459,7 +459,7 @@ final class MongoCollectionTests: MongoSwiftTestCase { // test using a write concern let opts3 = FindOneAndUpdateOptions(writeConcern: try WriteConcern(w: .majority)) let result3 = try self.coll.findOneAndUpdate(filter: ["cat": "blah"], - update: ["$set": ["cat": "cat"] as Document], + update: ["$set": ["cat": "cat"]], options: opts3) expect(result3).to(equal(["_id": 2, "cat": "blah"])) expect(try self.coll.count()).to(equal(2)) @@ -474,15 +474,15 @@ final class MongoCollectionTests: MongoSwiftTestCase { } func testNullIds() throws { - let result1 = try self.coll.insertOne(["_id": BSONNull(), "hi": "hello"]) + let result1 = try self.coll.insertOne(["_id": .null, "hi": "hello"]) expect(result1).toNot(beNil()) - expect(result1?.insertedId).to(bsonEqual(BSONNull())) + expect(result1?.insertedId).to(equal(.null)) - try self.coll.deleteOne(["_id": BSONNull()]) + try self.coll.deleteOne(["_id": .null]) - let result2 = try self.coll.insertMany([["_id": BSONNull()], ["_id": 20]]) + let result2 = try self.coll.insertMany([["_id": .null], ["_id": 20]]) expect(result2).toNot(beNil()) - expect(result2?.insertedIds[0]).to(bsonEqual(BSONNull())) - expect(result2?.insertedIds[1]).to(bsonEqual(20)) + expect(result2?.insertedIds[0]).to(equal(.null)) + expect(result2?.insertedIds[1]).to(equal(20)) } } diff --git a/Tests/MongoSwiftTests/MongoCursorTests.swift b/Tests/MongoSwiftTests/MongoCursorTests.swift index f75e532d0..38e115a0d 100644 --- a/Tests/MongoSwiftTests/MongoCursorTests.swift +++ b/Tests/MongoSwiftTests/MongoCursorTests.swift @@ -44,7 +44,7 @@ final class MongoCursorTests: MongoSwiftTestCase { expect(try cursor.nextOrError()).toNot(throwError()) // run killCursors so next iteration fails on the server - try db.runCommand(["killCursors": coll.name, "cursors": [cursor.id]]) + try db.runCommand(["killCursors": .string(coll.name), "cursors": [.int64(cursor.id!)]]) let expectedError2 = ServerError.commandError(code: 43, codeName: "CursorNotFound", message: "", @@ -94,7 +94,7 @@ final class MongoCursorTests: MongoSwiftTestCase { // insert 3 docs so the cursor loses track of its position for i in 4..<7 { - try coll.insertOne(["_id": i, "x": i]) + try coll.insertOne(["_id": BSON(i), "x": BSON(i)]) } let expectedError = ServerError.commandError(code: 136, diff --git a/Tests/MongoSwiftTests/MongoDatabaseTests.swift b/Tests/MongoSwiftTests/MongoDatabaseTests.swift index 083e04edd..7e70c1229 100644 --- a/Tests/MongoSwiftTests/MongoDatabaseTests.swift +++ b/Tests/MongoSwiftTests/MongoDatabaseTests.swift @@ -1,4 +1,3 @@ -import Foundation @testable import MongoSwift import Nimble import XCTest @@ -19,9 +18,9 @@ final class MongoDatabaseTests: MongoSwiftTestCase { let client = try SyncMongoClient.makeTestClient() let db = client.db(type(of: self).testDatabase) - let command: Document = ["create": self.getCollectionName(suffix: "1")] + let command: Document = ["create": .string(self.getCollectionName(suffix: "1"))] let res = try db.runCommand(command) - expect((res["ok"] as? BSONNumber)?.doubleValue).to(bsonEqual(1.0)) + expect(res["ok"]?.asDouble()).to(equal(1.0)) expect(try (Array(db.listCollections())).count).to(equal(1)) // create collection using createCollection @@ -36,7 +35,7 @@ final class MongoDatabaseTests: MongoSwiftTestCase { expect(db.name).to(equal(type(of: self).testDatabase)) // error code 59: CommandNotFound - expect(try db.runCommand(["asdfsadf": ObjectId()])) + expect(try db.runCommand(["asdfsadf": .objectId(ObjectId())])) .to(throwError(ServerError.commandError(code: 59, codeName: "CommandNotFound", message: "", @@ -65,7 +64,7 @@ final class MongoDatabaseTests: MongoSwiftTestCase { expect(event.command["dropDatabase"]).toNot(beNil()) let expectedWriteConcern = try? encoder.encode(expectedWriteConcerns[eventsSeen]) - expect(event.command["writeConcern"] as? Document).to(sortedEqual(expectedWriteConcern)) + expect(event.command["writeConcern"]?.documentValue).to(sortedEqual(expectedWriteConcern)) eventsSeen += 1 } @@ -88,7 +87,7 @@ final class MongoDatabaseTests: MongoSwiftTestCase { let db = client.db(type(of: self).testDatabase) let indexOpts: Document = - ["storageEngine": ["wiredTiger": ["configString": "access_pattern_hint=random"] as Document] as Document] + ["storageEngine": ["wiredTiger": ["configString": "access_pattern_hint=random"]]] // test non-view options let fooOptions = CreateCollectionOptions( @@ -98,17 +97,17 @@ final class MongoDatabaseTests: MongoSwiftTestCase { indexOptionDefaults: indexOpts, max: 1000, size: 10240, - storageEngine: ["wiredTiger": ["configString": "access_pattern_hint=random"] as Document], + storageEngine: ["wiredTiger": ["configString": "access_pattern_hint=random"]], validationAction: "warn", validationLevel: "moderate", - validator: ["phone": ["$type": "string"] as Document], + validator: ["phone": ["$type": "string"]], writeConcern: try WriteConcern(w: .majority) ) expect(try db.createCollection("foo", options: fooOptions)).toNot(throwError()) // test view options let viewOptions = CreateCollectionOptions( - pipeline: [["$project": ["a": 1] as Document]], + pipeline: [["$project": ["a": 1]]], viewOn: "foo" ) @@ -162,24 +161,24 @@ final class MongoDatabaseTests: MongoSwiftTestCase { expect(collectionNames[0]).to(equal("capped")) expect(collectionNames[1]).to(equal("uncapped")) - let filteredCollectionNames = try db.listCollectionNames(["name": "nonexistent"] as Document) + let filteredCollectionNames = try db.listCollectionNames(["name": "nonexistent"]) expect(filteredCollectionNames).to(haveCount(0)) - let cappedNames = try db.listCollectionNames(["options.capped": true] as Document) + let cappedNames = try db.listCollectionNames(["options.capped": true]) expect(cappedNames).to(haveCount(1)) expect(cappedNames[0]).to(equal("capped")) - let mongoCollections = try db.listMongoCollections(["options.capped": true] as Document) + let mongoCollections = try db.listMongoCollections(["options.capped": true]) expect(mongoCollections).to(haveCount(1)) expect(mongoCollections[0].name).to(equal("capped")) } expect(listNamesEvent).to(haveCount(4)) // Check nameOnly flag passed to server for respective listCollection calls. - expect((listNamesEvent[0] as? CommandStartedEvent)?.command["nameOnly"]).to(bsonEqual(true)) - expect((listNamesEvent[1] as? CommandStartedEvent)?.command["nameOnly"]).to(bsonEqual(true)) - expect((listNamesEvent[2] as? CommandStartedEvent)?.command["nameOnly"]).to(bsonEqual(false)) - expect((listNamesEvent[3] as? CommandStartedEvent)?.command["nameOnly"]).to(bsonEqual(false)) + expect((listNamesEvent[0] as? CommandStartedEvent)?.command["nameOnly"]).to(equal(true)) + expect((listNamesEvent[1] as? CommandStartedEvent)?.command["nameOnly"]).to(equal(true)) + expect((listNamesEvent[2] as? CommandStartedEvent)?.command["nameOnly"]).to(equal(false)) + expect((listNamesEvent[3] as? CommandStartedEvent)?.command["nameOnly"]).to(equal(false)) } } @@ -199,7 +198,7 @@ extension CreateCollectionOptions: Equatable { lhs.indexOptionDefaults == rhs.indexOptionDefaults && lhs.viewOn == rhs.viewOn && lhs.pipeline == rhs.pipeline && - lhs.collation?["locale"] as? String == rhs.collation?["locale"] as? String + lhs.collation?["locale"] == rhs.collation?["locale"] // ^ server adds a bunch of extra fields and a version number // to collations. rather than deal with those, just verify the // locale matches. diff --git a/Tests/MongoSwiftTests/MongoError+Equatable.swift b/Tests/MongoSwiftTests/MongoError+Equatable.swift index 885834db4..0c4bc442d 100644 --- a/Tests/MongoSwiftTests/MongoError+Equatable.swift +++ b/Tests/MongoSwiftTests/MongoError+Equatable.swift @@ -54,11 +54,8 @@ extension ServerError: Equatable { extension BulkWriteResult: Equatable { public static func == (lhs: BulkWriteResult, rhs: BulkWriteResult) -> Bool { - let iidsEqual = lhs.insertedIds.mapValues { AnyBSONValue($0) } == rhs.insertedIds.mapValues { AnyBSONValue($0) } - let uidsEqual = lhs.upsertedIds.mapValues { AnyBSONValue($0) } == rhs.upsertedIds.mapValues { AnyBSONValue($0) } - - return iidsEqual - && uidsEqual + return lhs.insertedIds == rhs.insertedIds + && lhs.upsertedIds == rhs.upsertedIds && lhs.upsertedCount == rhs.upsertedCount && lhs.modifiedCount == rhs.modifiedCount && lhs.matchedCount == rhs.matchedCount diff --git a/Tests/MongoSwiftTests/ReadPreferenceTests.swift b/Tests/MongoSwiftTests/ReadPreferenceTests.swift index 464adc79b..50f82b6d0 100644 --- a/Tests/MongoSwiftTests/ReadPreferenceTests.swift +++ b/Tests/MongoSwiftTests/ReadPreferenceTests.swift @@ -100,17 +100,17 @@ final class ReadPreferenceTests: MongoSwiftTestCase { defer { try? db.drop() } let coll = try db.createCollection(self.getCollectionName(suffix: "1")) - let command: Document = ["count": coll.name] + let command: Document = ["count": .string(coll.name)] // expect runCommand to return a success response when passing in a valid read preference let opts = RunCommandOptions(readPreference: ReadPreference(.secondaryPreferred)) let res = try db.runCommand(command, options: opts) - expect((res["ok"] as? BSONNumber)?.doubleValue).to(bsonEqual(1.0)) + expect(res["ok"]?.asDouble()).to(equal(1.0)) // expect running other commands to not throw errors when passing in a valid read preference expect(try coll.find(options: FindOptions(readPreference: ReadPreference(.primary)))).toNot(throwError()) - expect(try coll.aggregate([["$project": ["a": 1] as Document]], + expect(try coll.aggregate([["$project": ["a": 1]]], options: AggregateOptions(readPreference: ReadPreference(.secondaryPreferred)))) .toNot(throwError()) diff --git a/Tests/MongoSwiftTests/ReadWriteConcernTests.swift b/Tests/MongoSwiftTests/ReadWriteConcernTests.swift index 4aab77fd8..e53648866 100644 --- a/Tests/MongoSwiftTests/ReadWriteConcernTests.swift +++ b/Tests/MongoSwiftTests/ReadWriteConcernTests.swift @@ -8,16 +8,16 @@ extension WriteConcern { /// use `decode` because the format is different in spec tests /// ("journal" instead of "j", etc.) fileprivate init(_ doc: Document) throws { - let j = doc["journal"] as? Bool + let j = doc["journal"]?.boolValue var w: W? - if let wtag = doc["w"] as? String { + if let wtag = doc["w"]?.stringValue { w = wtag == "majority" ? .majority : .tag(wtag) - } else if let wInt = (doc["w"] as? BSONNumber)?.int32Value { + } else if let wInt = doc["w"]?.asInt32() { w = .number(wInt) } - let wt = (doc["wtimeoutMS"] as? BSONNumber)?.int64Value + let wt = doc["wtimeoutMS"]?.asInt64() try self.init(journal: j, w: w, wtimeoutMS: wt) } @@ -383,17 +383,17 @@ final class ReadWriteConcernTests: MongoSwiftTestCase { defer { try? db.drop() } let coll = try db.createCollection(self.getCollectionName()) - let command: Document = ["count": coll.name] + let command: Document = ["count": .string(coll.name)] // run command with a valid readConcern let options1 = RunCommandOptions(readConcern: ReadConcern(.local)) let res1 = try db.runCommand(command, options: options1) - expect((res1["ok"] as? BSONNumber)?.doubleValue).to(bsonEqual(1.0)) + expect(res1["ok"]?.asDouble()).to(equal(1.0)) // run command with an empty readConcern let options2 = RunCommandOptions(readConcern: ReadConcern()) let res2 = try db.runCommand(command, options: options2) - expect((res2["ok"] as? BSONNumber)?.doubleValue).to(bsonEqual(1.0)) + expect(res2["ok"]?.asDouble()).to(equal(1.0)) // running command with an invalid RC level should throw let options3 = RunCommandOptions(readConcern: ReadConcern("blah")) @@ -407,7 +407,7 @@ final class ReadWriteConcernTests: MongoSwiftTestCase { // try various command + read concern pairs to make sure they work expect(try coll.find(options: FindOptions(readConcern: ReadConcern(.local)))).toNot(throwError()) - expect(try coll.aggregate([["$project": ["a": 1] as Document]], + expect(try coll.aggregate([["$project": ["a": 1]]], options: AggregateOptions(readConcern: ReadConcern(.majority)))).toNot(throwError()) expect(try coll.count(options: CountOptions(readConcern: ReadConcern(.majority)))).toNot(throwError()) @@ -457,7 +457,7 @@ final class ReadWriteConcernTests: MongoSwiftTestCase { var counter = 0 func nextDoc() -> Document { defer { counter += 1 } - return ["x": counter] + return ["x": BSON(integerLiteral: counter)] } let coll = try db.createCollection(self.getCollectionName()) @@ -465,17 +465,17 @@ final class ReadWriteConcernTests: MongoSwiftTestCase { let wc2 = WriteConcern() let wc3 = try WriteConcern(journal: true) - let command: Document = ["insert": coll.name, "documents": [nextDoc()] as [Document]] + let command: Document = ["insert": .string(coll.name), "documents": [.document(nextDoc())]] // run command with a valid writeConcern let options1 = RunCommandOptions(writeConcern: wc1) let res1 = try db.runCommand(command, options: options1) - expect((res1["ok"] as? BSONNumber)?.doubleValue).to(bsonEqual(1.0)) + expect(res1["ok"]?.asDouble()).to(equal(1.0)) // run command with an empty writeConcern let options2 = RunCommandOptions(writeConcern: wc2) let res2 = try db.runCommand(command, options: options2) - expect((res2["ok"] as? BSONNumber)?.doubleValue).to(bsonEqual(1.0)) + expect(res2["ok"]?.asDouble()).to(equal(1.0)) expect(try coll.insertOne(nextDoc(), options: InsertOneOptions(writeConcern: wc1))).toNot(throwError()) expect(try coll.insertOne(nextDoc(), options: InsertOneOptions(writeConcern: wc3))).toNot(throwError()) @@ -486,22 +486,22 @@ final class ReadWriteConcernTests: MongoSwiftTestCase { options: InsertManyOptions(writeConcern: wc3))).toNot(throwError()) expect(try coll.updateOne(filter: ["x": 1], - update: ["$set": nextDoc()], + update: ["$set": .document(nextDoc())], options: UpdateOptions(writeConcern: wc2))).toNot(throwError()) expect(try coll.updateOne(filter: ["x": 2], - update: ["$set": nextDoc()], + update: ["$set": .document(nextDoc())], options: UpdateOptions(writeConcern: wc3))).toNot(throwError()) expect(try coll.updateMany(filter: ["x": 3], - update: ["$set": nextDoc()], + update: ["$set": .document(nextDoc())], options: UpdateOptions(writeConcern: wc2))).toNot(throwError()) expect(try coll.updateMany(filter: ["x": 4], - update: ["$set": nextDoc()], + update: ["$set": .document(nextDoc())], options: UpdateOptions(writeConcern: wc3))).toNot(throwError()) let coll2 = try db.createCollection(self.getCollectionName(suffix: "2")) defer { try? coll2.drop() } - let pipeline: [Document] = [["$out": "\(db.name).\(coll2.name)"]] + let pipeline: [Document] = [["$out": .string("\(db.name).\(coll2.name)")]] expect(try coll.aggregate(pipeline, options: AggregateOptions(writeConcern: wc1))).toNot(throwError()) expect(try coll.replaceOne(filter: ["x": 5], @@ -531,7 +531,7 @@ final class ReadWriteConcernTests: MongoSwiftTestCase { subdirectory: "connection-string", asType: Document.self) for (_, asDocument) in testFiles { - let tests: [Document] = try asDocument.get("tests") + let tests: [Document] = asDocument["tests"]!.arrayValue!.compactMap { $0.documentValue } for test in tests { let description: String = try test.get("description") // skipping because C driver does not comply with these; see CDRIVER-2621 @@ -540,14 +540,14 @@ final class ReadWriteConcernTests: MongoSwiftTestCase { let valid: Bool = try test.get("valid") if valid { let client = try SyncMongoClient(uri) - if let readConcern = test["readConcern"] as? Document { + if let readConcern = test["readConcern"]?.documentValue { let rc = ReadConcern(readConcern) if rc.isDefault { expect(client.readConcern).to(beNil()) } else { expect(client.readConcern).to(equal(rc)) } - } else if let writeConcern = test["writeConcern"] as? Document { + } else if let writeConcern = test["writeConcern"]?.documentValue { let wc = try WriteConcern(writeConcern) if wc.isDefault { expect(client.writeConcern).to(beNil()) @@ -569,10 +569,10 @@ final class ReadWriteConcernTests: MongoSwiftTestCase { asType: Document.self) for (_, asDocument) in testFiles { - let tests: [Document] = try asDocument.get("tests") + let tests = asDocument["tests"]!.arrayValue!.compactMap { $0.documentValue } for test in tests { let valid: Bool = try test.get("valid") - if let rcToUse = test["readConcern"] as? Document { + if let rcToUse = test["readConcern"]?.documentValue { let rc = ReadConcern(rcToUse) let isDefault: Bool = try test.get("isServerDefault") @@ -584,7 +584,7 @@ final class ReadWriteConcernTests: MongoSwiftTestCase { } else { expect(try encoder.encode(rc)).to(equal(expected)) } - } else if let wcToUse = test["writeConcern"] as? Document { + } else if let wcToUse = test["writeConcern"]?.documentValue { if valid { let wc = try WriteConcern(wcToUse) @@ -598,8 +598,8 @@ final class ReadWriteConcernTests: MongoSwiftTestCase { if expected == [:] { expect(try encoder.encode(wc)).to(beNil()) } else { - if let wtimeoutMS = expected["wtimeout"] as? BSONNumber { - expected["wtimeout"] = wtimeoutMS.int64Value! + if let wtimeoutMS = expected["wtimeout"] { + expected["wtimeout"] = .int64(wtimeoutMS.asInt64()!) } expect(try encoder.encode(wc)).to(sortedEqual(expected)) } diff --git a/Tests/MongoSwiftTests/SpecTestRunner/Match.swift b/Tests/MongoSwiftTests/SpecTestRunner/Match.swift index 03cdeafe8..866b376b5 100644 --- a/Tests/MongoSwiftTests/SpecTestRunner/Match.swift +++ b/Tests/MongoSwiftTests/SpecTestRunner/Match.swift @@ -10,54 +10,65 @@ internal protocol Matchable { /// This assumes `expected` is NOT a placeholder value (i.e. 42/"42"). Use `matches` if `expected` may be a /// placeholder. /// https://github.com/mongodb/specifications/tree/master/source/connection-monitoring-and-pooling/tests#spec-test-match-function - func contentMatches(expected: Any) -> Bool + func contentMatches(expected: Self) -> Bool + + /// Determines if this value is considered a wildcard for the purposes of the MATCHES function. + func isPlaceholder() -> Bool } // swiftlint:enable line_length extension Matchable { + internal func isPlaceholder() -> Bool { + return false + } + /// Returns whether this MATCHES the expected value according to the function defined in the spec. - internal func matches(expected: Any) -> Bool { - return isPlaceholder(expected) || self.contentMatches(expected: expected) + internal func matches(expected: T) -> Bool { + guard !expected.isPlaceholder() else { + return true + } + + guard let expected = expected as? Self else { + return false + } + return self.contentMatches(expected: expected) + } +} + +extension Matchable where Self: Equatable { + internal func contentMatches(expected: Self) -> Bool { + return self == expected + } +} + +extension Int: Matchable { + internal func isPlaceholder() -> Bool { + return self == 42 + } +} + +extension String: Matchable { + internal func isPlaceholder() -> Bool { + return self == "42" } } /// Extension that adds MATCHES functionality to `Array`. -extension Array: Matchable { - internal func contentMatches(expected: Any) -> Bool { - guard let expected = expected as? [Any], expected.count <= self.count else { +extension Array: Matchable where Element: Matchable { + internal func contentMatches(expected: [Element]) -> Bool { + guard expected.count <= self.count else { return false } - for (aV, eV) in zip(self, expected) { - if let matchable = aV as? Matchable { - guard matchable.matches(expected: eV) else { - return false - } - } else if let actualBSON = aV as? BSONValue, let expectedBSON = eV as? BSONValue { - guard actualBSON.bsonMatches(expected: expectedBSON) else { - return false - } - } else { - return false - } - } - return true + return zip(self, expected).allSatisfy { aV, eV in aV.matches(expected: eV) } } } /// Extension that adds MATCHES functionality to `Document`. extension Document: Matchable { - internal func contentMatches(expected: Any) -> Bool { - guard !isPlaceholder(expected) else { - return true - } - - guard let expected = expected as? Document else { - return false - } - + internal func contentMatches(expected: Document) -> Bool { for (eK, eV) in expected { - guard let aV = self[eK], aV.bsonMatches(expected: eV) else { + guard let aV = self[eK], aV.matches(expected: eV) else { return false } } @@ -65,20 +76,28 @@ extension Document: Matchable { } } -/// Extension that adds MATCHES functionality to `BSONValue`. -extension BSONValue { - internal func bsonMatches(expected: BSONValue) -> Bool { - if let matchable = self as? Matchable { - return matchable.matches(expected: expected) +/// Extension that adds MATCHES functionality to `BSON`. +extension BSON: Matchable { + internal func isPlaceholder() -> Bool { + return self.asInt()?.isPlaceholder() == true || self.stringValue?.isPlaceholder() == true + } + + internal func contentMatches(expected: BSON) -> Bool { + switch (self, expected) { + case let (.document(actual), .document(expected)): + return actual.matches(expected: expected) + case let (.array(actual), .array(expected)): + return actual.matches(expected: expected) + default: + return self == expected } - return isPlaceholder(expected) || self.bsonEquals(expected) } } // swiftlint:disable line_length /// A Nimble matcher for the MATCHES function defined in the spec. /// https://github.com/mongodb/specifications/tree/master/source/connection-monitoring-and-pooling/tests#spec-test-match-function -internal func match(_ expectedValue: Any?) -> Predicate { +internal func match(_ expectedValue: V?) -> Predicate { // swiftlint:enable line_length return Predicate.define("match <\(stringify(expectedValue))>") { actualExpression, msg in let actualValue = try actualExpression.evaluate() @@ -93,8 +112,3 @@ internal func match(_ expectedValue: Any?) -> Predicate { } } } - -/// Determines if an expected value is considered a wildcard for the purposes of the MATCHES function. -internal func isPlaceholder(_ expected: Any) -> Bool { - return (expected as? BSONNumber)?.intValue == 42 || expected as? String == "42" -} diff --git a/Tests/MongoSwiftTests/SpecTestRunner/SpecTest.swift b/Tests/MongoSwiftTests/SpecTestRunner/SpecTest.swift index 7182ed92f..ab9c28d0b 100644 --- a/Tests/MongoSwiftTests/SpecTestRunner/SpecTest.swift +++ b/Tests/MongoSwiftTests/SpecTestRunner/SpecTest.swift @@ -33,14 +33,10 @@ internal struct TestCommandStartedEvent: Decodable, Matchable { self.databaseName = try eventContainer.decode(String.self, forKey: .databaseName) } - internal func contentMatches(expected: Any) -> Bool { - guard let expected = expected as? TestCommandStartedEvent else { - return false - } - - return self.commandName == expected.commandName && - self.databaseName == expected.databaseName && - self.command.matches(expected: expected.command) + internal func contentMatches(expected: TestCommandStartedEvent) -> Bool { + return self.commandName.matches(expected: expected.commandName) + && self.databaseName.matches(expected: expected.databaseName) + && self.command.matches(expected: expected.command) } } @@ -75,7 +71,7 @@ internal struct FailPoint: Decodable { /// The fail point being configured. internal var name: String { - return self.failPoint["configureFailPoint"] as? String ?? "" + return self.failPoint["configureFailPoint"]?.stringValue ?? "" } private init(_ document: Document) { @@ -95,12 +91,12 @@ internal struct FailPoint: Decodable { // Need to convert error codes to int32's due to c driver bug (CDRIVER-3121) if k == "data", - var data = v as? Document, - var wcErr = data["writeConcernError"] as? Document, - let code = wcErr["code"] as? BSONNumber { - wcErr["code"] = code.int32Value - data["writeConcernError"] = wcErr - commandDoc["data"] = data + var data = v.documentValue, + var wcErr = data["writeConcernError"]?.documentValue, + let code = wcErr["code"] { + wcErr["code"] = .int32(code.asInt32()!) + data["writeConcernError"] = .document(wcErr) + commandDoc["data"] = .document(data) } else { commandDoc[k] = v } @@ -112,7 +108,7 @@ internal struct FailPoint: Decodable { internal func disable() { do { let client = try SyncMongoClient.makeTestClient() - try client.db("admin").runCommand(["configureFailPoint": self.name, "mode": "off"]) + try client.db("admin").runCommand(["configureFailPoint": .string(self.name), "mode": "off"]) } catch { print("Failed to disable fail point \(self.name): \(error)") } @@ -125,14 +121,14 @@ internal struct FailPoint: Decodable { case off case activationProbability(Double) - internal func toBSONValue() -> BSONValue { + internal func toBSON() -> BSON { switch self { case let .times(i): - return ["times": i] as Document + return ["times": BSON(i)] case let .activationProbability(d): - return ["activationProbability": d] as Document + return ["activationProbability": .double(d)] default: - return String(describing: self) + return .string(String(describing: self)) } } } @@ -147,22 +143,22 @@ internal struct FailPoint: Decodable { errorCode: Int? = nil, writeConcernError: Document? = nil) -> FailPoint { var data: Document = [ - "failCommands": failCommands + "failCommands": .array(failCommands.map { .string($0) }) ] if let close = closeConnection { - data["closeConnection"] = close + data["closeConnection"] = .bool(close) } if let code = errorCode { - data["errorCode"] = code + data["errorCode"] = BSON(code) } if let writeConcernError = writeConcernError { - data["writeConcernError"] = writeConcernError + data["writeConcernError"] = .document(writeConcernError) } let command: Document = [ "configureFailPoint": "failCommand", - "mode": mode.toBSONValue(), - "data": data + "mode": mode.toBSON(), + "data": .document(data) ] return FailPoint(command) } diff --git a/Tests/MongoSwiftTests/SpecTestRunner/SpecTestUtils.swift b/Tests/MongoSwiftTests/SpecTestRunner/SpecTestUtils.swift index 3a21b101d..245017ac4 100644 --- a/Tests/MongoSwiftTests/SpecTestRunner/SpecTestUtils.swift +++ b/Tests/MongoSwiftTests/SpecTestRunner/SpecTestUtils.swift @@ -49,19 +49,20 @@ internal func retrieveSpecTestFiles(specName: String, internal func rearrangeDoc(_ input: Document, toLookLike standard: Document) -> Document { var output = Document() for (k, v) in standard { - // if it's a document, recursively rearrange to look like corresponding sub-document - if let sDoc = v as? Document, let iDoc = input[k] as? Document { - output[k] = rearrangeDoc(iDoc, toLookLike: sDoc) - - // if it's an array, recursively rearrange to look like corresponding sub-array - } else if let sArr = v as? [Document], let iArr = input[k] as? [Document] { - var newArr = [Document]() + switch (v, input[k]) { + case let (.document(sDoc), .document(iDoc)?): + output[k] = .document(rearrangeDoc(iDoc, toLookLike: sDoc)) + case let (.array(sArr), .array(iArr)?): + var newArr: [BSON] = [] for (i, el) in iArr.enumerated() { - newArr.append(rearrangeDoc(el, toLookLike: sArr[i])) + if let docEl = el.documentValue, let sDoc = sArr[i].documentValue { + newArr.append(.document(rearrangeDoc(docEl, toLookLike: sDoc))) + } else { + newArr.append(el) + } } - output[k] = newArr - // just copy the value over as is - } else { + output[k] = .array(newArr) + default: output[k] = input[k] } } diff --git a/Tests/MongoSwiftTests/SpecTestRunner/TestOperation.swift b/Tests/MongoSwiftTests/SpecTestRunner/TestOperation.swift index a88cbfc3c..fa67953a1 100644 --- a/Tests/MongoSwiftTests/SpecTestRunner/TestOperation.swift +++ b/Tests/MongoSwiftTests/SpecTestRunner/TestOperation.swift @@ -439,7 +439,7 @@ struct RenameCollection: TestOperation { session: SyncClientSession? = nil) throws -> TestOperationResult? { let fromNamespace = database.name + "." + collection.name let toNamespace = database.name + "." + self.to - let cmd: Document = ["renameCollection": fromNamespace, "to": toNamespace] + let cmd: Document = ["renameCollection": .string(fromNamespace), "to": .string(toNamespace)] return TestOperationResult(from: try client.db("admin").runCommand(cmd)) } } diff --git a/Tests/MongoSwiftTests/SpecTestRunner/TestOperationResult.swift b/Tests/MongoSwiftTests/SpecTestRunner/TestOperationResult.swift index 966a90149..7ab777aea 100644 --- a/Tests/MongoSwiftTests/SpecTestRunner/TestOperationResult.swift +++ b/Tests/MongoSwiftTests/SpecTestRunner/TestOperationResult.swift @@ -6,7 +6,7 @@ enum TestOperationResult: Decodable, Equatable { case int(Int) /// Result of CRUD operations that return an array of `BSONValues` (e.g. `distinct`). - case array([BSONValue]) + case array([BSON]) /// Result of CRUD operations that return a single `Document` (e.g. `findOneAndDelete`). case document(Document) @@ -29,7 +29,7 @@ enum TestOperationResult: Decodable, Equatable { } public init(from cursor: SyncMongoCursor) { - self = .array(Array(cursor)) + self = .array(cursor.map { .document($0) }) } public init(from decoder: Decoder) throws { @@ -41,8 +41,8 @@ enum TestOperationResult: Decodable, Equatable { self = .bulkWrite(bulkWriteResult) } else if let int = try? Int(from: decoder) { self = .int(int) - } else if let array = try? [AnyBSONValue](from: decoder) { - self = .array(array.map { $0.value }) + } else if let array = try? [BSON](from: decoder) { + self = .array(array) } else if let doc = try? Document(from: decoder) { self = .document(doc) } else { @@ -60,7 +60,7 @@ enum TestOperationResult: Decodable, Equatable { case let (.int(lhsInt), .int(rhsInt)): return lhsInt == rhsInt case let (.array(lhsArray), .array(rhsArray)): - return lhsArray.bsonEquals(rhsArray) + return lhsArray == rhsArray case let(.document(lhsDoc), .document(rhsDoc)): return lhsDoc.sortedEquals(rhsDoc) default: @@ -93,7 +93,7 @@ extension InsertOneResult: BulkWriteResultConvertible { extension UpdateResult: BulkWriteResultConvertible { internal var bulkResultValue: BulkWriteResult { - var upsertedIds: [Int: BSONValue]? + var upsertedIds: [Int: BSON]? if let upsertedId = self.upsertedId { upsertedIds = [0: upsertedId] } diff --git a/Tests/MongoSwiftTests/TestUtils.swift b/Tests/MongoSwiftTests/TestUtils.swift index 26deb44ef..fa8fed45c 100644 --- a/Tests/MongoSwiftTests/TestUtils.swift +++ b/Tests/MongoSwiftTests/TestUtils.swift @@ -43,7 +43,7 @@ class MongoSwiftTestCase: XCTestCase { } // indicates whether we are running on a 32-bit platform - static let is32Bit = Int.bsonType == .int32 + static let is32Bit = MemoryLayout.size == 4 /// Generates a unique collection name of the format "__". If no suffix is provided, /// the last underscore is omitted. @@ -137,7 +137,7 @@ extension SyncMongoClient { let reply = try self.db("admin").runCommand([cmd: 1], options: RunCommandOptions( readPreference: ReadPreference(.primary))) - guard let versionString = reply["version"] as? String else { + guard let versionString = reply["version"]?.stringValue else { throw TestError(message: " reply missing version string: \(reply)") } return try ServerVersion(versionString) @@ -147,7 +147,7 @@ extension SyncMongoClient { internal func maxWireVersion() throws -> Int { let options = RunCommandOptions(readPreference: ReadPreference(.primary)) let isMaster = try self.db("admin").runCommand(["isMaster": 1], options: options) - guard let max = (isMaster["maxWireVersion"] as? BSONNumber)?.intValue else { + guard let max = isMaster["maxWireVersion"]?.asInt() else { throw TestError(message: "isMaster reply missing maxwireversion \(isMaster)") } return max @@ -256,22 +256,6 @@ internal func sortedEqual(_ expectedValue: Document?) -> Predicate { } } -/// A Nimble matcher for testing BSONValue equality. -internal func bsonEqual(_ expectedValue: BSONValue?) -> Predicate { - return Predicate.define("equal <\(stringify(expectedValue))>") { actualExpression, msg in - let actualValue = try actualExpression.evaluate() - switch (expectedValue, actualValue) { - case (nil, _?): - return PredicateResult(status: .fail, message: msg.appendedBeNilHint()) - case (nil, nil), (_, nil): - return PredicateResult(status: .fail, message: msg) - case let (expected?, actual?): - let matches = expected.bsonEquals(actual) - return PredicateResult(bool: matches, message: msg) - } - } -} - /// Captures any command monitoring events filtered by type and name that are emitted during the execution of the /// provided closure. Only events emitted by the provided client will be captured. internal func captureCommandEvents(from client: SyncMongoClient,