Skip to content
Merged
35 changes: 16 additions & 19 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 @@ -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
65 changes: 48 additions & 17 deletions Sources/MongoSwift/BSON/BSONEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,27 +149,38 @@ 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 {
let optionalBoxedValue = try encoder.box_(value)
guard let boxedValue = optionalBoxedValue 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 LocalizedError {
throw EncodingError.invalidValue(
value,
EncodingError.Context(
codingPath: [],
debugDescription: "Top-level \(T.self) was not encoded as a complete document."
debugDescription: error.errorDescription ?? "Error encoding BSON"
)
)
}

return dict.toDocument()
}

/**
Expand Down Expand Up @@ -733,7 +744,16 @@ 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 {
.array(self.array.map {
if let item = $0 as? MutableDictionary {
do { return try item.toDocument().bson } catch {
fatalError("Cannot convert to BSONDocument")
}
}
return $0.bson
})
}

fileprivate var array = [BSONValue]()

Expand Down Expand Up @@ -774,8 +794,13 @@ 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 {
do {
return .document(try self.toDocument())
} catch {
fatalError("Cannot convert MutableDictionary to BSONDocument")
}
}

// rather than using a dictionary, do this so we preserve key orders
fileprivate var keys = [String]()
Expand Down Expand Up @@ -803,10 +828,16 @@ 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
try convertingBSONErrors {
if let value = self.values[i] as? MutableDictionary {
try doc.setValue(for: self.keys[i], to: value.toDocument().bson)
} else {
try doc.setValue(for: self.keys[i], to: self.values[i].bson)
}
}
}
return doc
}
Expand Down
114 changes: 114 additions & 0 deletions Sources/MongoSwift/BSON/BSONError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import Foundation

/// An empty protocol for encapsulating all errors that BSON package can throw.
public protocol BSONErrorProtocol: LocalizedError {}

/// A protocol describing errors caused by improper usage of the BSON library by the user.
public protocol BSONUserError: BSONErrorProtocol {}

/// The possible errors that can occur unexpectedly BSON library-side.
public protocol BSONRuntimeError: BSONErrorProtocol {}

/// Namespace containing all the error types introduced by this BSON library and their dependent types.
public enum BSONError {
/// An error thrown when the user passes in invalid arguments to a BSON method.
public struct InvalidArgumentError: BSONUserError {
internal let message: String

public var errorDescription: String? { self.message }
}

/// An error thrown when the BSON library encounters a internal error not caused by the user.
/// This is usually indicative of a bug in the BSON library or system related failure.
public struct InternalError: BSONRuntimeError {
internal let message: String

public var errorDescription: String? { self.message }
}

/// An error thrown when the BSON library is incorrectly used.
public struct LogicError: BSONUserError {
internal let message: String

public var errorDescription: String? { self.message }
}
}

internal func bsonTooLargeError(value: BSONValue, forKey: String) -> BSONErrorProtocol {
BSONError.InternalError(
message:
"Failed to set value for key \(forKey) to \(value) with BSON type \(value.bsonType): document too large"
)
}

internal func wrongIterTypeError(_ iter: BSONDocumentIterator, expected type: BSONValue.Type) -> BSONErrorProtocol {
BSONError.LogicError(
message: "Tried to retreive a \(type) from an iterator whose next type " +
"is \(iter.currentType) for key \(iter.currentKey)"
)
}

/// Error thrown when a BSONValue type introduced by the driver (e.g. BSONObjectID) is encoded not using BSONEncoder
internal func bsonEncodingUnsupportedError<T: BSONValue>(value: T, at codingPath: [CodingKey]) -> EncodingError {
let description = "Encoding \(T.self) BSONValue type with a non-BSONEncoder is currently unsupported"

return EncodingError.invalidValue(
value,
EncodingError.Context(codingPath: codingPath, debugDescription: description)
)
}

/// Error thrown when a BSONValue type introduced by the driver (e.g. BSONObjectID) is decoded not using BSONDecoder
internal func bsonDecodingUnsupportedError<T: BSONValue>(type _: T.Type, at codingPath: [CodingKey]) -> DecodingError {
let description = "Initializing a \(T.self) BSONValue type with a non-BSONDecoder is currently unsupported"

return DecodingError.typeMismatch(
T.self,
DecodingError.Context(codingPath: codingPath, debugDescription: description)
)
}

/**
* Error thrown when a `BSONValue` type introduced by the driver (e.g. BSONObjectID) is decoded directly via the
* top-level `BSONDecoder`.
*/
internal func bsonDecodingDirectlyError<T: BSONValue>(type _: T.Type, at codingPath: [CodingKey]) -> DecodingError {
let description = "Cannot initialize BSONValue type \(T.self) directly from BSONDecoder. It must be decoded as " +
"a member of a struct or a class."

return DecodingError.typeMismatch(
T.self,
DecodingError.Context(codingPath: codingPath, debugDescription: description)
)
}

/**
* This function determines which error to throw when a driver-introduced BSON type is decoded via its init(decoder).
* The types that use this function are all BSON primitives, so they should be decoded directly in `_BSONDecoder`. If
* execution reaches their decoding initializer, it means something went wrong. This function determines an appropriate
* error to throw for each possible case.
*
* Some example cases:
* - Decoding directly from the BSONDecoder top-level (e.g. BSONDecoder().decode(BSONObjectID.self, from: ...))
* - Encountering the wrong type of BSONValue (e.g. expected "_id" to be an `BSONObjectID`, got a `BSONDocument`
* instead)
* - Attempting to decode a driver-introduced BSONValue with a non-BSONDecoder
*/
internal func getDecodingError<T: BSONValue>(type _: T.Type, decoder: Decoder) -> DecodingError {
if let bsonDecoder = decoder as? _BSONDecoder {
// Cannot decode driver-introduced BSONValues directly
if decoder.codingPath.isEmpty {
return bsonDecodingDirectlyError(type: T.self, at: decoder.codingPath)
}

// Got the wrong BSONValue type
return DecodingError._typeMismatch(
at: decoder.codingPath,
expectation: T.self,
reality: bsonDecoder.storage.topContainer.bsonValue
)
}

// Non-BSONDecoders are currently unsupported
return bsonDecodingUnsupportedError(type: T.self, at: decoder.codingPath)
}
17 changes: 17 additions & 0 deletions Sources/MongoSwift/BSON/BSONUtil.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* BSONUtil contains helpers to wrap the underlying BSON library to assist in providing a consistent API
*/

/// We don't want driver users to handle any BSONErrors
/// this will convert BSONError.* thrown from `fn` to MongoError.* and rethrow
internal func convertingBSONErrors<T>(_ fn: () throws -> T) rethrows -> T {
do {
return try fn()
} catch let error as BSONError.InvalidArgumentError {
throw MongoError.InvalidArgumentError(message: error.message)
} catch let error as BSONError.InternalError {
throw MongoError.InternalError(message: error.message)
} catch let error as BSONError.LogicError {
throw MongoError.LogicError(message: error.message)
}
}
Loading