Skip to content

Commit 456bb5b

Browse files
authored
SWIFT-221 Provide better consistency around Int usage with Documents (#235)
1 parent 6712b1e commit 456bb5b

20 files changed

+359
-176
lines changed

Sources/MongoSwift/BSON/BSONValue.swift

Lines changed: 160 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,75 @@ extension BSONValue where Self: Equatable {
103103
}
104104
}
105105

106+
/// A protocol that numeric `BSONValue`s should conform to. It provides functionality for converting to BSON's native
107+
/// number types.
108+
public protocol BSONNumber: BSONValue {
109+
/// Create an `Int` from this `BSONNumber`.
110+
/// This will return nil if the conversion cannot result in an exact representation.
111+
var intValue: Int? { get }
112+
113+
/// Create an `Int32` from this `BSONNumber`.
114+
/// This will return nil if the conversion cannot result in an exact representation.
115+
var int32Value: Int32? { get }
116+
117+
/// Create an `Int64` from this `BSONNumber`.
118+
/// This will return nil if the conversion cannot result in an exact representation.
119+
var int64Value: Int64? { get }
120+
121+
/// Create a `Double` from this `BSONNumber`.
122+
/// This will return nil if the conversion cannot result in an exact representation.
123+
var doubleValue: Double? { get }
124+
125+
/// Create a `Decimal128` from this `BSONNumber`.
126+
/// This will return nil if the conversion cannot result in an exact representation.
127+
var decimal128Value: Decimal128? { get }
128+
}
129+
130+
/// Default conformance to `BSONNumber` for `BinaryInteger`s.
131+
extension BSONNumber where Self: BinaryInteger {
132+
/// Create an `Int` from this `BinaryInteger`.
133+
/// This will return nil if the conversion cannot result in an exact representation.
134+
public var intValue: Int? { return Int(exactly: self) }
135+
136+
/// Create an `Int32` from this `BinaryInteger`.
137+
/// This will return nil if the conversion cannot result in an exact representation.
138+
public var int32Value: Int32? { return Int32(exactly: self) }
139+
140+
/// Create an `Int64` from this `BinaryInteger`.
141+
/// This will return nil if the conversion cannot result in an exact representation.
142+
public var int64Value: Int64? { return Int64(exactly: self) }
143+
144+
/// Create a `Double` from this `BinaryInteger`.
145+
/// This will return nil if the conversion cannot result in an exact representation.
146+
public var doubleValue: Double? { return Double(exactly: self) }
147+
}
148+
149+
/// Default conformance to `BSONNumber` for `BinaryFloatingPoint`s.
150+
extension BSONNumber where Self: BinaryFloatingPoint {
151+
/// Create an `Int` from this `BinaryFloatingPoint`.
152+
/// This will return nil if the conversion cannot result in an exact representation.
153+
public var intValue: Int? { return Int(exactly: self) }
154+
155+
/// Create an `Int32` from this `BinaryFloatingPoint`.
156+
/// This will return nil if the conversion cannot result in an exact representation.
157+
public var int32Value: Int32? { return Int32(exactly: self) }
158+
159+
/// Create an `Int64` from this `BinaryFloatingPoint`.
160+
/// This will return nil if the conversion cannot result in an exact representation.
161+
public var int64Value: Int64? { return Int64(exactly: self) }
162+
163+
/// Create a `Double` from this `BinaryFloatingPoint`.
164+
/// This will return nil if the conversion cannot result in an exact representation.
165+
public var doubleValue: Double? { return Double(self) }
166+
}
167+
168+
/// Default implementation of `Decimal128` conversions for all `Numeric`s.
169+
extension BSONNumber where Self: Numeric {
170+
/// Create a `Decimal128` from this `Numeric`.
171+
/// This will return nil if the conversion cannot result in an exact representation.
172+
public var decimal128Value: Decimal128? { return Decimal128(String(describing: self)) }
173+
}
174+
106175
/// An extension of `Array` to represent the BSON array type.
107176
extension Array: BSONValue {
108177
public var bsonType: BSONType { return .array }
@@ -427,7 +496,7 @@ public struct DBPointer: BSONValue, Codable, Equatable {
427496
}
428497

429498
/// A struct to represent the BSON Decimal128 type.
430-
public struct Decimal128: BSONValue, Equatable, Codable, CustomStringConvertible {
499+
public struct Decimal128: BSONNumber, Equatable, Codable, CustomStringConvertible {
431500
public var bsonType: BSONType { return .decimal128 }
432501

433502
public var description: String {
@@ -502,8 +571,32 @@ public struct Decimal128: BSONValue, Equatable, Codable, CustomStringConvertible
502571
}
503572
}
504573

574+
/// Extension of `Decimal128` to add `BSONNumber` conformance.
575+
/// TODO: implement the missing converters (SWIFT-367)
576+
extension Decimal128 {
577+
/// Create an `Int` from this `Decimal128`.
578+
/// Note: this function is not implemented yet and will always return nil.
579+
public var intValue: Int? { return nil }
580+
581+
/// Create an `Int32` from this `Decimal128`.
582+
/// Note: this function is not implemented yet and will always return nil.
583+
public var int32Value: Int32? { return nil }
584+
585+
/// Create an `Int64` from this `Decimal128`.
586+
/// Note: this function is not implemented yet and will always return nil.
587+
public var int64Value: Int64? { return nil }
588+
589+
/// Create a `Double` from this `Decimal128`.
590+
/// Note: this function is not implemented yet and will always return nil.
591+
public var doubleValue: Double? { return nil }
592+
593+
/// Returns this `Decimal128`.
594+
/// This is implemented as part of `BSONNumber` conformance.
595+
public var decimal128Value: Decimal128? { return self }
596+
}
597+
505598
/// An extension of `Double` to represent the BSON Double type.
506-
extension Double: BSONValue {
599+
extension Double: BSONNumber {
507600
public var bsonType: BSONType { return .double }
508601

509602
public func encode(to storage: DocumentStorage, forKey key: String) throws {
@@ -524,39 +617,66 @@ extension Double: BSONValue {
524617
}
525618

526619
/// An extension of `Int` to represent the BSON Int32 or Int64 type.
527-
/// The `Int` will be encoded as an Int32 if possible, or an Int64 if necessary.
528-
extension Int: BSONValue {
529-
public var bsonType: BSONType { return self.int32Value != nil ? .int32 : .int64 }
620+
/// On 64-bit systems, `Int` corresponds to a BSON Int64. On 32-bit systems, it corresponds to a BSON Int32.
621+
extension Int: BSONNumber {
622+
/// `Int` corresponds to a BSON int32 or int64 depending upon whether the compilation system is 32 or 64 bit.
623+
/// Use MemoryLayout instead of Int.bitWidth to avoid a compiler warning.
624+
/// See: https://forums.swift.org/t/how-can-i-condition-on-the-size-of-int/9080/4
625+
internal static var bsonType: BSONType {
626+
return MemoryLayout<Int>.size == 4 ? .int32 : .int64
627+
}
628+
629+
public var bsonType: BSONType { return Int.bsonType }
530630

531-
internal var int32Value: Int32? { return Int32(exactly: self) }
532-
internal var int64Value: Int64? { return Int64(exactly: self) }
631+
// Return this `Int` as an `Int32` on 32-bit systems or an `Int64` on 64-bit systems
632+
internal var typedValue: BSONNumber {
633+
if self.bsonType == .int64 {
634+
return Int64(self)
635+
}
636+
return Int32(self)
637+
}
533638

534639
public func encode(to storage: DocumentStorage, forKey key: String) throws {
535-
if let int32 = self.int32Value {
536-
return try int32.encode(to: storage, forKey: key)
640+
try self.typedValue.encode(to: storage, forKey: key)
641+
}
642+
643+
public func bsonEquals(_ other: BSONValue?) -> Bool {
644+
guard let other = other, other.bsonType == self.bsonType else {
645+
return false
537646
}
538-
if let int64 = self.int64Value {
539-
return try int64.encode(to: storage, forKey: key)
647+
648+
if let otherInt = other as? Int {
649+
return self == otherInt
540650
}
541651

542-
throw RuntimeError.internalError(message: "`Int` value \(self) could not be encoded as `Int32` or `Int64`")
652+
switch (self.typedValue, other) {
653+
case let (self32 as Int32, other32 as Int32):
654+
return self32 == other32
655+
case let (self64 as Int64, other64 as Int64):
656+
return self64 == other64
657+
default:
658+
return false
659+
}
543660
}
544661

545662
public static func from(iterator iter: DocumentIterator) throws -> Int {
546-
return try iter.withBSONIterPointer { iterPtr in
547-
// TODO: handle this more gracefully (SWIFT-221)
548-
switch iter.currentType {
549-
case .int32, .int64:
550-
return self.init(Int(bson_iter_int32(iterPtr)))
551-
default:
552-
throw wrongIterTypeError(iter, expected: Int.self)
553-
}
663+
var val: Int?
664+
if Int.bsonType == .int64 {
665+
val = Int(exactly: try Int64.from(iterator: iter))
666+
} else {
667+
val = Int(exactly: try Int32.from(iterator: iter))
668+
}
669+
670+
guard let out = val else {
671+
// This should not occur
672+
throw RuntimeError.internalError(message: "Couldn't read `Int` from Document")
554673
}
674+
return out
555675
}
556676
}
557677

558678
/// An extension of `Int32` to represent the BSON Int32 type.
559-
extension Int32: BSONValue {
679+
extension Int32: BSONNumber {
560680
public var bsonType: BSONType { return .int32 }
561681

562682
public func encode(to storage: DocumentStorage, forKey key: String) throws {
@@ -574,10 +694,19 @@ extension Int32: BSONValue {
574694
self.init(bson_iter_int32(iterPtr))
575695
}
576696
}
697+
698+
public func bsonEquals(_ other: BSONValue?) -> Bool {
699+
if let other32 = other as? Int32 {
700+
return self == other32
701+
} else if let otherInt = other as? Int {
702+
return self == otherInt.typedValue as? Int32
703+
}
704+
return false
705+
}
577706
}
578707

579708
/// An extension of `Int64` to represent the BSON Int64 type.
580-
extension Int64: BSONValue {
709+
extension Int64: BSONNumber {
581710
public var bsonType: BSONType { return .int64 }
582711

583712
public func encode(to storage: DocumentStorage, forKey key: String) throws {
@@ -595,6 +724,15 @@ extension Int64: BSONValue {
595724
self.init(bson_iter_int64(iterPtr))
596725
}
597726
}
727+
728+
public func bsonEquals(_ other: BSONValue?) -> Bool {
729+
if let other64 = other as? Int64 {
730+
return self == other64
731+
} else if let otherInt = other as? Int {
732+
return self == otherInt.typedValue as? Int64
733+
}
734+
return false
735+
}
598736
}
599737

600738
/// A struct to represent the BSON Code and CodeWithScope types.

Sources/MongoSwift/BSON/CodableNumber.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ extension UInt8: CodableNumber {
8282
extension UInt16: CodableNumber {
8383
internal var bsonValue: BSONValue? {
8484
// UInt16 always fits in an Int32
85-
return Int(exactly: self)
85+
return Int32(exactly: self)
8686
}
8787
}
8888

Sources/MongoSwift/BSON/DocumentIterator.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,9 @@ public class DocumentIterator: IteratorProtocol {
203203
.javascript: CodeWithScope.self,
204204
.symbol: Symbol.self,
205205
.javascriptWithScope: CodeWithScope.self,
206-
.int32: Int.self,
206+
.int32: Int.bsonType == .int32 ? Int.self : Int32.self,
207207
.timestamp: Timestamp.self,
208-
.int64: Int64.self,
208+
.int64: Int.bsonType == .int64 ? Int.self : Int64.self,
209209
.decimal128: Decimal128.self,
210210
.minKey: MinKey.self,
211211
.maxKey: MaxKey.self,

Sources/MongoSwift/BSON/Overwritable.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ extension Bool: Overwritable {
2121

2222
extension Int: Overwritable {
2323
internal func writeToCurrentPosition(of iter: DocumentIterator) throws {
24-
if let int32 = self.int32Value {
24+
switch self.typedValue {
25+
case let int32 as Int32:
2526
return int32.writeToCurrentPosition(of: iter)
26-
} else if let int64 = self.int64Value {
27+
case let int64 as Int64:
2728
return int64.writeToCurrentPosition(of: iter)
29+
default:
30+
throw RuntimeError.internalError(message: "`Int` value \(self) could not be encoded as `Int32` or `Int64`")
2831
}
29-
30-
throw RuntimeError.internalError(message: "`Int` value \(self) could not be encoded as `Int32` or `Int64`")
3132
}
3233
}
3334

Sources/MongoSwift/MongoCollection+BulkWrite.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -443,18 +443,22 @@ public struct BulkWriteResult {
443443
* - `RuntimeError.internalError` if an unexpected error occurs reading the reply from the server.
444444
*/
445445
fileprivate init(reply: Document, insertedIds: [Int: BSONValue]) throws {
446-
self.deletedCount = try reply.getValue(for: "nRemoved") as? Int ?? 0
447-
self.insertedCount = try reply.getValue(for: "nInserted") as? Int ?? 0
446+
// These values are converted to Int via BSONNumber because they're returned from libmongoc as BSON int32s,
447+
// which are retrieved from documents as Ints on 32-bit systems and Int32s on 64-bit ones. To retrieve them in a
448+
// cross-platform manner, we must convert them this way. Also, regardless of how they are stored in the
449+
// we want to use them as Ints.
450+
self.deletedCount = (try reply.getValue(for: "nRemoved") as? BSONNumber)?.intValue ?? 0
451+
self.insertedCount = (try reply.getValue(for: "nInserted") as? BSONNumber)?.intValue ?? 0
448452
self.insertedIds = insertedIds
449-
self.matchedCount = try reply.getValue(for: "nMatched") as? Int ?? 0
450-
self.modifiedCount = try reply.getValue(for: "nModified") as? Int ?? 0
451-
self.upsertedCount = try reply.getValue(for: "nUpserted") as? Int ?? 0
453+
self.matchedCount = (try reply.getValue(for: "nMatched") as? BSONNumber)?.intValue ?? 0
454+
self.modifiedCount = (try reply.getValue(for: "nModified") as? BSONNumber)?.intValue ?? 0
455+
self.upsertedCount = (try reply.getValue(for: "nUpserted") as? BSONNumber)?.intValue ?? 0
452456

453457
var upsertedIds = [Int: BSONValue]()
454458

455459
if let upserted = try reply.getValue(for: "upserted") as? [Document] {
456460
for upsert in upserted {
457-
guard let index = try upsert.getValue(for: "index") as? Int else {
461+
guard let index = (try upsert.getValue(for: "index") as? BSONNumber)?.intValue else {
458462
throw RuntimeError.internalError(message: "Could not cast upserted index to `Int`")
459463
}
460464
upsertedIds[index] = upsert["_id"]

Sources/MongoSwift/MongoCollection+Indexes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public struct IndexModel: Encodable {
1616

1717
/// Gets the default name for this index.
1818
internal var defaultName: String {
19-
return String(cString: mongoc_collection_keys_to_index_string(self.keys.data))
19+
return self.keys.map { k, v in "\(k)_\(v)" }.joined(separator: "_")
2020
}
2121

2222
// Encode own data as well as nested options data

Sources/MongoSwift/MongoCollection+Write.swift

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -402,24 +402,6 @@ public struct InsertManyResult {
402402
/// Map of the index of the document in `values` to the value of its ID
403403
public let insertedIds: [Int: BSONValue]
404404

405-
/**
406-
* Create an `InsertManyResult` from a reply and map of inserted IDs.
407-
*
408-
* Note: we forgo using a Decodable initializer because we still need to
409-
* explicitly add `insertedIds`.
410-
*
411-
* - Parameters:
412-
* - reply: A `Document` result from `mongoc_collection_insert_many()`
413-
* - insertedIds: Map of inserted IDs
414-
*
415-
* - Throws:
416-
* - `RuntimeError.internalError` if an unexpected error occurs the reading server reply.
417-
*/
418-
fileprivate init(reply: Document, insertedIds: [Int: BSONValue]) throws {
419-
self.insertedCount = try reply.getValue(for: "insertedCount") as? Int ?? 0
420-
self.insertedIds = insertedIds
421-
}
422-
423405
/// Internal initializer used for converting from a `BulkWriteResult` optional to an `InsertManyResult` optional.
424406
internal init?(from result: BulkWriteResult?) {
425407
guard let result = result else {

Sources/MongoSwift/SDAM.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,11 @@ public struct ServerDescription {
125125
self.opTime = lastWrite["opTime"] as? ObjectId
126126
}
127127

128-
if let minVersion = isMaster["minWireVersion"] as? Int32 {
128+
if let minVersion = (isMaster["minWireVersion"] as? BSONNumber)?.int32Value {
129129
self.minWireVersion = minVersion
130130
}
131131

132-
if let maxVersion = isMaster["maxWireVersion"] as? Int32 {
132+
if let maxVersion = (isMaster["maxWireVersion"] as? BSONNumber)?.int32Value {
133133
self.maxWireVersion = maxVersion
134134
}
135135

@@ -156,14 +156,14 @@ public struct ServerDescription {
156156
}
157157

158158
self.setName = isMaster["setName"] as? String
159-
self.setVersion = isMaster["setVersion"] as? Int64
159+
self.setVersion = (isMaster["setVersion"] as? BSONNumber)?.int64Value
160160
self.electionId = isMaster["electionId"] as? ObjectId
161161

162162
if let primary = isMaster["primary"] as? String {
163163
self.primary = ConnectionId(primary)
164164
}
165165

166-
self.logicalSessionTimeoutMinutes = isMaster["logicalSessionTimeoutMinutes"] as? Int64
166+
self.logicalSessionTimeoutMinutes = (isMaster["logicalSessionTimeoutMinutes"] as? BSONNumber)?.int64Value
167167
}
168168

169169
/// An internal initializer to create a `ServerDescription` from an OpaquePointer to a

0 commit comments

Comments
 (0)