Skip to content

Commit 7e77a3d

Browse files
authored
SWIFT-878: Define BSON Specific Error Types (#492)
1 parent ca167f2 commit 7e77a3d

16 files changed

+337
-208
lines changed

Guides/Error-Handling.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
* [See Also](#see-also)
1616

1717
## Error Types
18-
The driver uses errors to communicate that an operation failed, an assumption wasn't met, or that the user did something incorrectly. Applications that use the driver can in turn catch these errors and respond appropriately without crashing or resulting in an otherwise inconsistent state. To correctly model the different sources of errors, the driver defines three separate caregories of errors (`MongoServerError`, `MongoUserError`, `MongoRuntimeError`), each of which are protocols that inherit from the `MongoErrorProtocol` protocol. These protocols are defined in `MongoError.swift`, and the structs that conform to them are outlined here. The documentation for every public function that throws lists some of the errors that could possibly be thrown and the reasons they might be. The errors listed there are not comprehensive but will generally cover the most common cases.
19-
18+
The driver uses errors to communicate that an operation failed, an assumption wasn't met, or that the user did something incorrectly. Applications that use the driver can in turn catch these errors and respond appropriately without crashing or resulting in an otherwise inconsistent state. To correctly model the different sources of errors, the driver defines three separate categories of errors (`MongoServerError`, `MongoUserError`, `MongoRuntimeError`), each of which are protocols that inherit from the `MongoErrorProtocol` protocol. These protocols are defined in `MongoError.swift`, and the structs that conform to them are outlined here. The documentation for every public function that throws lists some of the errors that could possibly be thrown and the reasons they might be. The errors listed there are not comprehensive but will generally cover the most common cases.
2019

2120
### Server Errors
2221
Server errors correspond to failures that occur in the database itself and are returned to the driver via some response to a command. Each error that conforms to `ServerError` contains at least one error code representing what went wrong on the server.
@@ -77,6 +76,15 @@ As part of the driver, `BSONEncoder` and `BSONDecoder` are implemented according
7776

7877
See the official documentation for both [`EncodingErrors`](https://developer.apple.com/documentation/swift/encodingerror) and [`DecodingErrors`](https://developer.apple.com/documentation/swift/decodingerror) for more information.
7978

79+
### BSON Errors
80+
81+
The BSON library has its own subset of errors that communicate issues when constructing or using BSON.
82+
BSON Errors can be found in [Sources/MongoSwift/BSON/BSONError.swift](Sources/MongoSwift/BSON/BSONError.swift) and are as follows:
83+
84+
- `BSONError.InvalidArgumentError` - This error is thrown when a BSON type is being incorrectly constructed.
85+
- `BSONError.InternalError` - This error is thrown when there is an issue that is a result of system failure (e.g, allocation issue).
86+
- `BSONError.LogicError` - This error is thrown when there is an unexpected usage of the the API.
87+
- `BSONError.DocumentTooLargeError` - This error is thrown when the document exceeds the maximum encoding size of 16MB.
8088

8189
## Examples
8290
### Handling any error thrown by the driver
@@ -154,6 +162,24 @@ Result:
154162
nInserted: 1
155163
InsertedIds: [0: 2]
156164
```
165+
166+
### Handling a BSONError
167+
168+
```swift
169+
var string = "+1..23e8"
170+
do {
171+
return try BSONDecimal128(string)
172+
} catch let bsonError as BSONError.InvalidArgumentError {
173+
print(bsonError.message)
174+
}
175+
```
176+
177+
Output:
178+
179+
```plain
180+
Invalid Decimal128 string +1..23e8
181+
```
182+
157183
## See Also
158184
- [Error handling in Swift](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html)
159185
- [List of server error codes](https://github.com/mongodb/mongo/blob/master/src/mongo/base/error_codes.err)

Sources/MongoSwift/BSON/BSONDecoder.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,17 @@ public class BSONDecoder {
142142
return doc
143143
}
144144
let _decoder = _BSONDecoder(referencing: .document(document), options: self.options)
145-
return try type.init(from: _decoder)
145+
do {
146+
return try type.init(from: _decoder)
147+
} catch let error as BSONErrorProtocol {
148+
let unknownErrorMessage = "Unknown Error occurred while decoding BSON"
149+
throw DecodingError.dataCorrupted(
150+
DecodingError.Context(
151+
codingPath: [],
152+
debugDescription: "Unable to decode BSON: \(error.errorDescription ?? unknownErrorMessage)"
153+
)
154+
)
155+
}
146156
}
147157

148158
/**

Sources/MongoSwift/BSON/BSONDocument.swift

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ internal typealias MutableBSONPointer = UnsafeMutablePointer<bson_t>
99
public struct BSONDocument {
1010
/// Error thrown when BSON buffer is too small.
1111
internal static let BSONBufferTooSmallError =
12-
MongoError.InternalError(message: "BSON buffer is unexpectedly too small (< 5 bytes)")
12+
BSONError.InternalError(message: "BSON buffer is unexpectedly too small (< 5 bytes)")
1313

1414
/// The storage backing a `BSONDocument`.
1515
private class Storage {
@@ -145,10 +145,7 @@ extension BSONDocument {
145145
* presence first.
146146
*
147147
* - Throws:
148-
* - `MongoError.InternalError` if the new value is an `Int` and cannot be written to BSON.
149-
* - `MongoError.LogicError` if the new value is a `BSONDecimal128` or `BSONObjectID` and is improperly formatted.
150-
* - `MongoError.LogicError` if the new value is an `Array` and it contains a non-`BSONValue` element.
151-
* - `MongoError.InternalError` if the underlying `bson_t` would exceed the maximum size by encoding this
148+
* - `BSONError.DocumentTooLargeError` if the underlying `bson_t` would exceed the maximum size by encoding this
152149
* key-value pair.
153150
*/
154151
internal mutating func setValue(for key: String, to newValue: BSON, checkForKey: Bool = true) throws {
@@ -169,7 +166,7 @@ extension BSONDocument {
169166
self.copyStorageIfRequired()
170167
// key is guaranteed present so initialization will succeed.
171168
// swiftlint:disable:next force_unwrapping
172-
try BSONDocumentIterator(over: self, advancedTo: key)!.overwriteCurrentValue(with: ov)
169+
BSONDocumentIterator(over: self, advancedTo: key)!.overwriteCurrentValue(with: ov)
173170

174171
// otherwise, we just create a new document and replace this key
175172
} else {
@@ -210,7 +207,7 @@ extension BSONDocument {
210207
/// Retrieves the value associated with `for` as a `BSON?`, which can be nil if the key does not exist in the
211208
/// `BSONDocument`.
212209
///
213-
/// - Throws: `MongoError.InternalError` if the BSON buffer is too small (< 5 bytes).
210+
/// - Throws: `BSONError.InternalError` if the BSON buffer is too small (< 5 bytes).
214211
internal func getValue(for key: String) throws -> BSON? {
215212
guard let iter = BSONDocumentIterator(over: self) else {
216213
throw BSONDocument.BSONBufferTooSmallError
@@ -232,7 +229,7 @@ extension BSONDocument {
232229
}
233230
}
234231
guard success else {
235-
throw MongoError.InternalError(
232+
throw BSONError.InternalError(
236233
message: "Failed to merge \(doc) with \(self). This is likely due to " +
237234
"the merged document being too large."
238235
)
@@ -255,15 +252,15 @@ extension BSONDocument {
255252
/// excluding a non-zero number of keys
256253
internal func copyElements(to otherDoc: inout BSONDocument, excluding keys: [String]) throws {
257254
guard !keys.isEmpty else {
258-
throw MongoError.InternalError(message: "No keys to exclude, use 'bson_copy' instead")
255+
throw BSONError.InternalError(message: "No keys to exclude, use 'bson_copy' instead")
259256
}
260257

261258
let cStrings: [ContiguousArray<CChar>] = keys.map { $0.utf8CString }
262259

263260
var cPtrs: [UnsafePointer<CChar>] = try cStrings.map { cString in
264261
let bufferPtr: UnsafeBufferPointer<CChar> = cString.withUnsafeBufferPointer { $0 }
265262
guard let cPtr = bufferPtr.baseAddress else {
266-
throw MongoError.InternalError(message: "Failed to copy strings")
263+
throw BSONError.InternalError(message: "Failed to copy strings")
267264
}
268265
return cPtr
269266
}
@@ -373,16 +370,16 @@ extension BSONDocument {
373370
* - Returns: the parsed `BSONDocument`
374371
*
375372
* - Throws:
376-
* - A `MongoError.InvalidArgumentError` if the data passed in is invalid JSON.
373+
* - A `BSONError.InvalidArgumentError` if the data passed in is invalid JSON.
377374
*/
378375
public init(fromJSON: Data) throws {
379376
self._storage = Storage(stealing: try fromJSON.withUnsafeBytePointer { bytes in
380377
var error = bson_error_t()
381378
guard let bson = bson_new_from_json(bytes, fromJSON.count, &error) else {
382379
if error.domain == BSON_ERROR_JSON {
383-
throw MongoError.InvalidArgumentError(message: "Invalid JSON: \(toErrorString(error))")
380+
throw BSONError.InvalidArgumentError(message: "Invalid JSON: \(toErrorString(error))")
384381
}
385-
throw MongoError.InternalError(message: toErrorString(error))
382+
throw BSONError.InternalError(message: toErrorString(error))
386383
}
387384

388385
return bson
@@ -391,7 +388,7 @@ extension BSONDocument {
391388

392389
/// Convenience initializer for constructing a `BSONDocument` from a `String`.
393390
/// - Throws:
394-
/// - A `MongoError.InvalidArgumentError` if the string passed in is invalid JSON.
391+
/// - A `BSONError.InvalidArgumentError` if the string passed in is invalid JSON.
395392
public init(fromJSON json: String) throws {
396393
// `String`s are Unicode under the hood so force unwrap always succeeds.
397394
// see https://www.objc.io/blog/2018/02/13/string-to-data-and-back/
@@ -401,15 +398,15 @@ extension BSONDocument {
401398
/**
402399
* Constructs a `BSONDocument` from raw BSON `Data`.
403400
* - Throws:
404-
* - A `MongoError.InvalidArgumentError` if `bson` is too short or too long to be valid BSON.
405-
* - A `MongoError.InvalidArgumentError` if the first four bytes of `bson` do not contain `bson.count`.
406-
* - A `MongoError.InvalidArgumentError` if the final byte of `bson` is not a null byte.
401+
* - A `BSONError.InvalidArgumentError` if `bson` is too short or too long to be valid BSON.
402+
* - A `BSONError.InvalidArgumentError` if the first four bytes of `bson` do not contain `bson.count`.
403+
* - A `BSONError.InvalidArgumentError` if the final byte of `bson` is not a null byte.
407404
* - SeeAlso: http://bsonspec.org/
408405
*/
409406
public init(fromBSON bson: Data) throws {
410407
self._storage = Storage(stealing: try bson.withUnsafeBytePointer { bytes in
411408
guard let data = bson_new_from_data(bytes, bson.count) else {
412-
throw MongoError.InvalidArgumentError(message: "Invalid BSON data")
409+
throw BSONError.InvalidArgumentError(message: "Invalid BSON data")
413410
}
414411
return data
415412
})
@@ -498,7 +495,7 @@ extension BSONDocument: BSONValue {
498495
try document.withMutableBSONPointer { docPtr in
499496
try self.withBSONPointer { nestedDocPtr in
500497
guard bson_append_document(docPtr, key, Int32(key.utf8.count), nestedDocPtr) else {
501-
throw bsonTooLargeError(value: self, forKey: key)
498+
throw BSONError.DocumentTooLargeError(value: self, forKey: key)
502499
}
503500
}
504501
}
@@ -520,7 +517,7 @@ extension BSONDocument: BSONValue {
520517
bson_iter_document(iterPtr, &length, document)
521518

522519
guard let docData = bson_new_from_data(document.pointee, Int(length)) else {
523-
throw MongoError.InternalError(message: "Failed to create a Document from iterator")
520+
throw BSONError.InternalError(message: "Failed to create a Document from iterator")
524521
}
525522

526523
return self.init(stealing: docData)

Sources/MongoSwift/BSON/BSONDocumentIterator.swift

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ public class BSONDocumentIterator: IteratorProtocol {
102102
/// Returns the current value (equivalent to the `currentValue` property) or throws on error.
103103
///
104104
/// - Throws:
105-
/// - `MongoError.InternalError` if the current value of this `BSONDocumentIterator` cannot be decoded to BSON.
105+
/// - `BSONError.InternalError` if the current value of this `BSONDocumentIterator` cannot be decoded to BSON.
106106
internal func safeCurrentValue() throws -> BSON {
107107
guard let bsonType = BSONDocumentIterator.bsonTypeMap[currentType] else {
108-
throw MongoError.InternalError(
108+
throw BSONError.InternalError(
109109
message: "Unknown BSONType for iterator's current value with type: \(self.currentType)"
110110
)
111111
}
@@ -173,19 +173,13 @@ public class BSONDocumentIterator: IteratorProtocol {
173173
self.advance() ? (self.currentKey, self.currentValue) : nil
174174
}
175175

176-
/**
177-
* Overwrites the current value of this `BSONDocumentIterator` with the supplied value.
178-
*
179-
* - Throws:
180-
* - `MongoError.InternalError` if the new value is an `Int` and cannot be written to BSON.
181-
* - `MongoError.LogicError` if the new value is a `BSONDecimal128` or `BSONObjectID` and is improperly formatted.
182-
*/
183-
internal func overwriteCurrentValue(with newValue: Overwritable) throws {
176+
/// Overwrites the current value of this `BSONDocumentIterator` with the supplied value.
177+
internal func overwriteCurrentValue(with newValue: Overwritable) {
184178
let newValueType = type(of: newValue).bsonType
185179
guard newValueType == self.currentType else {
186180
fatalError("Expected \(newValue) to have BSON type \(self.currentType), but has type \(newValueType)")
187181
}
188-
try newValue.writeToCurrentPosition(of: self)
182+
newValue.writeToCurrentPosition(of: self)
189183
}
190184

191185
/// Internal helper function for explicitly accessing the `bson_iter_t` as an unsafe pointer

Sources/MongoSwift/BSON/BSONEncoder.swift

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -149,27 +149,37 @@ public class BSONEncoder {
149149

150150
let encoder = _BSONEncoder(options: self.options)
151151

152-
guard let boxedValue = try encoder.box_(value) else {
153-
throw EncodingError.invalidValue(
154-
value,
155-
EncodingError.Context(
156-
codingPath: [],
157-
debugDescription: "Top-level \(T.self) did not encode any values."
152+
do {
153+
guard let boxedValue = try encoder.box_(value) else {
154+
throw EncodingError.invalidValue(
155+
value,
156+
EncodingError.Context(
157+
codingPath: [],
158+
debugDescription: "Top-level \(T.self) did not encode any values."
159+
)
158160
)
159-
)
160-
}
161+
}
162+
163+
guard let dict = boxedValue as? MutableDictionary else {
164+
throw EncodingError.invalidValue(
165+
value,
166+
EncodingError.Context(
167+
codingPath: [],
168+
debugDescription: "Top-level \(T.self) was not encoded as a complete document."
169+
)
170+
)
171+
}
161172

162-
guard let dict = boxedValue as? MutableDictionary else {
173+
return try dict.toDocument()
174+
} catch let error as BSONErrorProtocol {
163175
throw EncodingError.invalidValue(
164176
value,
165177
EncodingError.Context(
166178
codingPath: [],
167-
debugDescription: "Top-level \(T.self) was not encoded as a complete document."
179+
debugDescription: error.errorDescription ?? "Unknown Error occurred while encoding BSON"
168180
)
169181
)
170182
}
171-
172-
return dict.toDocument()
173183
}
174184

175185
/**
@@ -492,7 +502,7 @@ extension _BSONEncoder {
492502
if let bsonValue = value as? BSONValue {
493503
return bsonValue
494504
} else if let bsonArray = value as? [BSONValue] {
495-
return bsonArray.map { $0.bson }
505+
return try bsonArray.map { $0.bson }
496506
}
497507

498508
// The value should request a container from the _BSONEncoder.
@@ -733,7 +743,7 @@ extension _BSONEncoder: SingleValueEncodingContainer {
733743
private class MutableArray: BSONValue {
734744
fileprivate static var bsonType: BSONType { .array }
735745

736-
fileprivate var bson: BSON { .array(self.array.map { $0.bson }) }
746+
fileprivate var bson: BSON { fatalError("MutableArray: BSONValue.bson should be unused") }
737747

738748
fileprivate var array = [BSONValue]()
739749

@@ -766,6 +776,18 @@ private class MutableArray: BSONValue {
766776
required convenience init(from _: Decoder) throws {
767777
fatalError("`MutableArray` is not meant to be initialized from a `Decoder`")
768778
}
779+
780+
internal func toBSONArray() throws -> [BSON] {
781+
try self.array.map {
782+
if let item = $0 as? MutableDictionary {
783+
return try item.toDocument().bson
784+
}
785+
if let item = $0 as? MutableArray {
786+
return try item.toBSONArray().bson
787+
}
788+
return $0.bson
789+
}
790+
}
769791
}
770792

771793
/// A private class wrapping a Swift dictionary so we can pass it by reference
@@ -774,8 +796,7 @@ private class MutableArray: BSONValue {
774796
private class MutableDictionary: BSONValue {
775797
fileprivate static var bsonType: BSONType { .document }
776798

777-
// This is unused
778-
fileprivate var bson: BSON { .document(self.toDocument()) }
799+
fileprivate var bson: BSON { fatalError("MutableDictionary: BSONValue.bson should be unused") }
779800

780801
// rather than using a dictionary, do this so we preserve key orders
781802
fileprivate var keys = [String]()
@@ -803,10 +824,19 @@ private class MutableDictionary: BSONValue {
803824
}
804825

805826
/// Converts self to a `BSONDocument` with equivalent key-value pairs.
806-
fileprivate func toDocument() -> BSONDocument {
827+
fileprivate func toDocument() throws -> BSONDocument {
807828
var doc = BSONDocument()
808829
for i in 0..<self.keys.count {
809-
doc[self.keys[i]] = self.values[i].bson
830+
let value = self.values[i]
831+
switch value {
832+
case let val as MutableDictionary:
833+
try doc.setValue(for: self.keys[i], to: val.toDocument().bson)
834+
case let val as MutableArray:
835+
let array = try val.toBSONArray()
836+
try doc.setValue(for: self.keys[i], to: array.bson)
837+
default:
838+
try doc.setValue(for: self.keys[i], to: value.bson)
839+
}
810840
}
811841
return doc
812842
}

0 commit comments

Comments
 (0)