Skip to content
Merged
29 changes: 27 additions & 2 deletions Guides/Error-Handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
* [See Also](#see-also)

## Error Types
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.

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.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

hard to pick out but this was just: caregories -> categories


### Server Errors
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.
Expand Down Expand Up @@ -77,6 +76,14 @@ As part of the driver, `BSONEncoder` and `BSONDecoder` are implemented according

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.

### BSON Errors

The BSON library has its own subset of errors that communicate issues when constructing or using BSON.
BSON Errors can be found in [Sources/MongoSwift/BSON/BSONError.swift](Sources/MongoSwift/BSON/BSONError.swift) and are as follows:
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I didn't know you could link like this on GitHub, very cool.


- `BSONError.InvalidArgumentError` - This error is thrown when a BSON type is being incorrectly constructed.
- `BSONError.InternalError` - This error is thrown when there is an issue that is a result of system failure (e.g, allocation issue).
- `BSONError.LogicError` - This error is thrown when there is an unexpected usage of the the API.

## Examples
### Handling any error thrown by the driver
Expand Down Expand Up @@ -154,6 +161,24 @@ Result:
nInserted: 1
InsertedIds: [0: 2]
```

### Handling a BSONError

```swift
var string = "+1..23e8"
do {
return try BSONDecimal128(string)
} catch let bsonError as BSONError.InvalidArgumentError {
print(bsonError.message)
}
```

Output:

```plain
Invalid Decimal128 string +1..23e8
```

## See Also
- [Error handling in Swift](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html)
- [List of server error codes](https://github.com/mongodb/mongo/blob/master/src/mongo/base/error_codes.err)
Expand Down
12 changes: 11 additions & 1 deletion Sources/MongoSwift/BSON/BSONDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,17 @@ public class BSONDecoder {
return doc
}
let _decoder = _BSONDecoder(referencing: .document(document), options: self.options)
return try type.init(from: _decoder)
do {
return try type.init(from: _decoder)
} catch let error as BSONErrorProtocol {
let unknownErrorMessage = "Unknown Error occurred while decoding BSON"
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: [],
debugDescription: "Unable to decode BSON: \(error.errorDescription ?? unknownErrorMessage)"
)
)
}
}

/**
Expand Down
37 changes: 17 additions & 20 deletions Sources/MongoSwift/BSON/BSONDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ internal typealias MutableBSONPointer = UnsafeMutablePointer<bson_t>
public struct BSONDocument {
/// Error thrown when BSON buffer is too small.
internal static let BSONBufferTooSmallError =
MongoError.InternalError(message: "BSON buffer is unexpectedly too small (< 5 bytes)")
BSONError.InternalError(message: "BSON buffer is unexpectedly too small (< 5 bytes)")

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

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

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

var cPtrs: [UnsafePointer<CChar>] = try cStrings.map { cString in
let bufferPtr: UnsafeBufferPointer<CChar> = cString.withUnsafeBufferPointer { $0 }
guard let cPtr = bufferPtr.baseAddress else {
throw MongoError.InternalError(message: "Failed to copy strings")
throw BSONError.InternalError(message: "Failed to copy strings")
}
return cPtr
}
Expand Down Expand Up @@ -373,16 +370,16 @@ extension BSONDocument {
* - Returns: the parsed `BSONDocument`
*
* - Throws:
* - A `MongoError.InvalidArgumentError` if the data passed in is invalid JSON.
* - A `BSONError.InvalidArgumentError` if the data passed in is invalid JSON.
*/
public init(fromJSON: Data) throws {
self._storage = Storage(stealing: try fromJSON.withUnsafeBytePointer { bytes in
var error = bson_error_t()
guard let bson = bson_new_from_json(bytes, fromJSON.count, &error) else {
if error.domain == BSON_ERROR_JSON {
throw MongoError.InvalidArgumentError(message: "Invalid JSON: \(toErrorString(error))")
throw BSONError.InvalidArgumentError(message: "Invalid JSON: \(toErrorString(error))")
}
throw MongoError.InternalError(message: toErrorString(error))
throw BSONError.InternalError(message: toErrorString(error))
}

return bson
Expand All @@ -391,7 +388,7 @@ extension BSONDocument {

/// Convenience initializer for constructing a `BSONDocument` from a `String`.
/// - Throws:
/// - A `MongoError.InvalidArgumentError` if the string passed in is invalid JSON.
/// - A `BSONError.InvalidArgumentError` if the string passed in is invalid JSON.
public init(fromJSON json: String) throws {
// `String`s are Unicode under the hood so force unwrap always succeeds.
// see https://www.objc.io/blog/2018/02/13/string-to-data-and-back/
Expand All @@ -401,15 +398,15 @@ extension BSONDocument {
/**
* Constructs a `BSONDocument` from raw BSON `Data`.
* - Throws:
* - A `MongoError.InvalidArgumentError` if `bson` is too short or too long to be valid BSON.
* - A `MongoError.InvalidArgumentError` if the first four bytes of `bson` do not contain `bson.count`.
* - A `MongoError.InvalidArgumentError` if the final byte of `bson` is not a null byte.
* - A `BSONError.InvalidArgumentError` if `bson` is too short or too long to be valid BSON.
* - A `BSONError.InvalidArgumentError` if the first four bytes of `bson` do not contain `bson.count`.
* - A `BSONError.InvalidArgumentError` if the final byte of `bson` is not a null byte.
* - SeeAlso: http://bsonspec.org/
*/
public init(fromBSON bson: Data) throws {
self._storage = Storage(stealing: try bson.withUnsafeBytePointer { bytes in
guard let data = bson_new_from_data(bytes, bson.count) else {
throw MongoError.InvalidArgumentError(message: "Invalid BSON data")
throw BSONError.InvalidArgumentError(message: "Invalid BSON data")
}
return data
})
Expand Down Expand Up @@ -498,7 +495,7 @@ extension BSONDocument: BSONValue {
try document.withMutableBSONPointer { docPtr in
try self.withBSONPointer { nestedDocPtr in
guard bson_append_document(docPtr, key, Int32(key.utf8.count), nestedDocPtr) else {
throw bsonTooLargeError(value: self, forKey: key)
throw BSONError.DocumentTooLargeError(value: self, forKey: key)
}
}
}
Expand All @@ -520,7 +517,7 @@ extension BSONDocument: BSONValue {
bson_iter_document(iterPtr, &length, document)

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

return self.init(stealing: docData)
Expand Down
16 changes: 5 additions & 11 deletions Sources/MongoSwift/BSON/BSONDocumentIterator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ public class BSONDocumentIterator: IteratorProtocol {
/// Returns the current value (equivalent to the `currentValue` property) or throws on error.
///
/// - Throws:
/// - `MongoError.InternalError` if the current value of this `BSONDocumentIterator` cannot be decoded to BSON.
/// - `BSONError.InternalError` if the current value of this `BSONDocumentIterator` cannot be decoded to BSON.
internal func safeCurrentValue() throws -> BSON {
guard let bsonType = BSONDocumentIterator.bsonTypeMap[currentType] else {
throw MongoError.InternalError(
throw BSONError.InternalError(
message: "Unknown BSONType for iterator's current value with type: \(self.currentType)"
)
}
Expand Down Expand Up @@ -173,19 +173,13 @@ public class BSONDocumentIterator: IteratorProtocol {
self.advance() ? (self.currentKey, self.currentValue) : nil
}

/**
* Overwrites the current value of this `BSONDocumentIterator` with the supplied value.
*
* - Throws:
* - `MongoError.InternalError` if the new value is an `Int` and cannot be written to BSON.
* - `MongoError.LogicError` if the new value is a `BSONDecimal128` or `BSONObjectID` and is improperly formatted.
*/
internal func overwriteCurrentValue(with newValue: Overwritable) throws {
/// Overwrites the current value of this `BSONDocumentIterator` with the supplied value.
internal func overwriteCurrentValue(with newValue: Overwritable) {
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)
newValue.writeToCurrentPosition(of: self)
}

/// Internal helper function for explicitly accessing the `bson_iter_t` as an unsafe pointer
Expand Down
66 changes: 48 additions & 18 deletions Sources/MongoSwift/BSON/BSONEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,27 +149,37 @@ public class BSONEncoder {

let encoder = _BSONEncoder(options: self.options)

guard let boxedValue = try encoder.box_(value) else {
throw EncodingError.invalidValue(
value,
EncodingError.Context(
codingPath: [],
debugDescription: "Top-level \(T.self) did not encode any values."
do {
guard let boxedValue = try encoder.box_(value) else {
throw EncodingError.invalidValue(
value,
EncodingError.Context(
codingPath: [],
debugDescription: "Top-level \(T.self) did not encode any values."
)
)
)
}
}

guard let dict = boxedValue as? MutableDictionary else {
throw EncodingError.invalidValue(
value,
EncodingError.Context(
codingPath: [],
debugDescription: "Top-level \(T.self) was not encoded as a complete document."
)
)
}

guard let dict = boxedValue as? MutableDictionary else {
return try dict.toDocument()
} catch let error as BSONErrorProtocol {
throw EncodingError.invalidValue(
value,
EncodingError.Context(
codingPath: [],
debugDescription: "Top-level \(T.self) was not encoded as a complete document."
debugDescription: error.errorDescription ?? "Unknown Error occurred while encoding BSON"
)
)
}

return dict.toDocument()
}

/**
Expand Down Expand Up @@ -492,7 +502,7 @@ extension _BSONEncoder {
if let bsonValue = value as? BSONValue {
return bsonValue
} else if let bsonArray = value as? [BSONValue] {
return bsonArray.map { $0.bson }
return try bsonArray.map { $0.bson }
}

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

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

fileprivate var array = [BSONValue]()

Expand Down Expand Up @@ -766,6 +776,18 @@ private class MutableArray: BSONValue {
required convenience init(from _: Decoder) throws {
fatalError("`MutableArray` is not meant to be initialized from a `Decoder`")
}

internal func toBSONArray() throws -> [BSON] {
try self.array.map {
if let item = $0 as? MutableDictionary {
return try item.toDocument().bson
}
if let item = $0 as? MutableArray {
return try item.toBSONArray().bson
}
return $0.bson
}
}
}

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

// This is unused
fileprivate var bson: BSON { .document(self.toDocument()) }
fileprivate var bson: BSON { fatalError("MutableDictionary: BSONValue.bson should be unused") }

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

/// Converts self to a `BSONDocument` with equivalent key-value pairs.
fileprivate func toDocument() -> BSONDocument {
fileprivate func toDocument() throws -> BSONDocument {
var doc = BSONDocument()
for i in 0..<self.keys.count {
doc[self.keys[i]] = self.values[i].bson
let value = self.values[i]
switch value {
case let val as MutableDictionary:
try doc.setValue(for: self.keys[i], to: val.toDocument().bson)
case let val as MutableArray:
let array = try val.toBSONArray()
try doc.setValue(for: self.keys[i], to: array.bson)
default:
try doc.setValue(for: self.keys[i], to: value.bson)
}
}
return doc
}
Expand Down
Loading