From 555e323def4ded2bd06ebc6324276dc0884c6f4e Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Wed, 15 May 2019 11:45:15 -0400 Subject: [PATCH 1/2] conform various options types to Decodable, make error parsing more flexible --- Sources/MongoSwift/MongoClient.swift | 22 +++++--- .../MongoCollection+BulkWrite.swift | 47 ++++++++++++++--- .../MongoCollection+FindAndModify.swift | 12 ++--- Sources/MongoSwift/MongoCollection+Read.swift | 24 ++++++--- .../MongoSwift/MongoCollection+Write.swift | 20 ++++++-- Sources/MongoSwift/MongoError.swift | 51 +++++++++---------- .../Operations/CountOperation.swift | 5 +- .../Operations/DistinctOperation.swift | 5 +- 8 files changed, 124 insertions(+), 62 deletions(-) diff --git a/Sources/MongoSwift/MongoClient.swift b/Sources/MongoSwift/MongoClient.swift index 247355ca8..cbf50d8a9 100644 --- a/Sources/MongoSwift/MongoClient.swift +++ b/Sources/MongoSwift/MongoClient.swift @@ -2,7 +2,7 @@ import Foundation import mongoc /// Options to use when creating a `MongoClient`. -public struct ClientOptions: CodingStrategyProvider { +public struct ClientOptions: CodingStrategyProvider, Decodable { /// Determines whether the client should retry supported write operations. public let retryWrites: Bool? @@ -14,24 +14,32 @@ public struct ClientOptions: CodingStrategyProvider { /// be used. public let readConcern: ReadConcern? - /// Specifies a ReadPreference to use for the client. - public let readPreference: ReadPreference? - /// Specifies a WriteConcern to use for the client. If one is not specified, the server's default write concern /// will be used. public let writeConcern: WriteConcern? + // swiftlint:disable redundant_optional_initialization + + /// Specifies a ReadPreference to use for the client. + public var readPreference: ReadPreference? = nil + /// Specifies the `DateCodingStrategy` to use for BSON encoding/decoding operations performed by this client and any /// databases or collections that derive from it. - public let dateCodingStrategy: DateCodingStrategy? + public var dateCodingStrategy: DateCodingStrategy? = nil /// Specifies the `UUIDCodingStrategy` to use for BSON encoding/decoding operations performed by this client and any /// databases or collections that derive from it. - public let uuidCodingStrategy: UUIDCodingStrategy? + public var uuidCodingStrategy: UUIDCodingStrategy? = nil /// Specifies the `DataCodingStrategy` to use for BSON encoding/decoding operations performed by this client and any /// databases or collections that derive from it. - public let dataCodingStrategy: DataCodingStrategy? + public var dataCodingStrategy: DataCodingStrategy? = nil + + // swiftlint:enable redundant_optional_initialization + + private enum CodingKeys: CodingKey { + case retryWrites, eventMonitoring, readConcern, writeConcern + } /// Convenience initializer allowing any/all to be omitted or optional. public init(eventMonitoring: Bool = false, diff --git a/Sources/MongoSwift/MongoCollection+BulkWrite.swift b/Sources/MongoSwift/MongoCollection+BulkWrite.swift index 153715137..0075b2fd3 100644 --- a/Sources/MongoSwift/MongoCollection+BulkWrite.swift +++ b/Sources/MongoSwift/MongoCollection+BulkWrite.swift @@ -41,7 +41,7 @@ extension MongoCollection { } /// A model for a `deleteOne` operation within a bulk write. - public struct DeleteOneModel: WriteModel { + public struct DeleteOneModel: WriteModel, Decodable { /// A `Document` representing the match criteria. public let filter: Document @@ -78,7 +78,7 @@ extension MongoCollection { } /// A model for a `deleteMany` operation within a bulk write. - public struct DeleteManyModel: WriteModel { + public struct DeleteManyModel: WriteModel, Decodable { /// A `Document` representing the match criteria. public let filter: Document @@ -115,7 +115,7 @@ extension MongoCollection { } /// A model for an `insertOne` operation within a bulk write. - public struct InsertOneModel: WriteModel { + public struct InsertOneModel: WriteModel, Decodable { /// The `CollectionType` to insert. public let document: CollectionType @@ -158,7 +158,7 @@ extension MongoCollection { } /// A model for a `replaceOne` operation within a bulk write. - public struct ReplaceOneModel: WriteModel { + public struct ReplaceOneModel: WriteModel, Decodable { /// A `Document` representing the match criteria. public let filter: Document @@ -216,7 +216,7 @@ extension MongoCollection { } /// A model for an `updateOne` operation within a bulk write. - public struct UpdateOneModel: WriteModel { + public struct UpdateOneModel: WriteModel, Decodable { /// A `Document` representing the match criteria. public let filter: Document @@ -278,7 +278,7 @@ extension MongoCollection { } /// A model for an `updateMany` operation within a bulk write. - public struct UpdateManyModel: WriteModel { + public struct UpdateManyModel: WriteModel, Decodable { /// A `Document` representing the match criteria. public let filter: Document @@ -418,7 +418,7 @@ public class BulkWriteOperation: Operation { } /// Options to use when performing a bulk write operation on a `MongoCollection`. -public struct BulkWriteOptions: Encodable { +public struct BulkWriteOptions: Codable { /// If `true`, allows the write to opt-out of document level validation. public let bypassDocumentValidation: Bool? @@ -454,7 +454,7 @@ public struct BulkWriteOptions: Encodable { } /// The result of a bulk write operation on a `MongoCollection`. -public struct BulkWriteResult { +public struct BulkWriteResult: Decodable { /// Number of documents deleted. public let deletedCount: Int @@ -476,6 +476,37 @@ public struct BulkWriteResult { /// Map of the index of the operation to the id of the upserted document. public let upsertedIds: [Int: BSONValue] + private enum CodingKeys: CodingKey { + case deletedCount, insertedCount, insertedIds, matchedCount, modifiedCount, upsertedCount, upsertedIds + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + // None of the results must be present themselves, but at least one must. + guard !container.allKeys.isEmpty else { + throw DecodingError.valueNotFound(BulkWriteResult.self, + DecodingError.Context(codingPath: decoder.codingPath, + debugDescription: "No results found")) + } + + self.deletedCount = try container.decodeIfPresent(Int.self, forKey: .deletedCount) ?? 0 + 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 } + 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 } + self.upsertedIds = upsertedIds + self.upsertedCount = try container.decodeIfPresent(Int.self, forKey: .upsertedCount) ?? upsertedIds.count + } + /** * Create a `BulkWriteResult` from a reply and map of inserted IDs. * diff --git a/Sources/MongoSwift/MongoCollection+FindAndModify.swift b/Sources/MongoSwift/MongoCollection+FindAndModify.swift index ce2324374..5f6612433 100644 --- a/Sources/MongoSwift/MongoCollection+FindAndModify.swift +++ b/Sources/MongoSwift/MongoCollection+FindAndModify.swift @@ -122,11 +122,11 @@ extension MongoCollection { } /// Indicates which document to return in a find and modify operation. -public enum ReturnDocument { +public enum ReturnDocument: String, Decodable { /// Indicates to return the document before the update, replacement, or insert occurred. - case before + case before = "Before" /// Indicates to return the document after the update, replacement, or insert occurred. - case after + case after = "After" } /// Indicates that an options type can be represented as a `FindAndModifyOptions` @@ -138,7 +138,7 @@ private protocol FindAndModifyOptionsConvertible { } /// Options to use when executing a `findOneAndDelete` command on a `MongoCollection`. -public struct FindOneAndDeleteOptions: FindAndModifyOptionsConvertible { +public struct FindOneAndDeleteOptions: FindAndModifyOptionsConvertible, Decodable { /// Specifies a collation to use. public let collation: Document? @@ -178,7 +178,7 @@ public struct FindOneAndDeleteOptions: FindAndModifyOptionsConvertible { } /// Options to use when executing a `findOneAndReplace` command on a `MongoCollection`. -public struct FindOneAndReplaceOptions: FindAndModifyOptionsConvertible { +public struct FindOneAndReplaceOptions: FindAndModifyOptionsConvertible, Decodable { /// If `true`, allows the write to opt-out of document level validation. public let bypassDocumentValidation: Bool? @@ -235,7 +235,7 @@ public struct FindOneAndReplaceOptions: FindAndModifyOptionsConvertible { } /// Options to use when executing a `findOneAndUpdate` command on a `MongoCollection`. -public struct FindOneAndUpdateOptions: FindAndModifyOptionsConvertible { +public struct FindOneAndUpdateOptions: FindAndModifyOptionsConvertible, Decodable { /// A set of filters specifying to which array elements an update should apply. public let arrayFilters: [Document]? diff --git a/Sources/MongoSwift/MongoCollection+Read.swift b/Sources/MongoSwift/MongoCollection+Read.swift index 106735d82..3f23b2c57 100644 --- a/Sources/MongoSwift/MongoCollection+Read.swift +++ b/Sources/MongoSwift/MongoCollection+Read.swift @@ -138,7 +138,7 @@ extension MongoCollection { } /// An index to "hint" or force MongoDB to use when performing a query. -public enum Hint: Encodable { +public enum Hint: Codable { /// Specifies an index to use by its name. case indexName(String) /// Specifies an index to use by a specification `Document` containing the index key(s). @@ -153,10 +153,19 @@ public enum Hint: Encodable { try container.encode(doc) } } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let str = try? container.decode(String.self) { + self = .indexName(str) + } else { + self = .indexSpec(try container.decode(Document.self)) + } + } } /// Options to use when executing an `aggregate` command on a `MongoCollection`. -public struct AggregateOptions: Encodable { +public struct AggregateOptions: Codable { /// Enables writing to temporary files. When set to true, aggregation stages /// can write data to the _tmp subdirectory in the dbPath directory. public let allowDiskUse: Bool? @@ -185,7 +194,8 @@ public struct AggregateOptions: Encodable { public let readConcern: ReadConcern? /// A ReadPreference to use for this operation. - public let readPreference: ReadPreference? + // swiftlint:disable:next redundant_optional_initialization + public var readPreference: ReadPreference? = nil /// A `WriteConcern` to use in `$out` stages of this operation. public let writeConcern: WriteConcern? @@ -265,7 +275,7 @@ public enum CursorType { } /// Options to use when executing a `find` command on a `MongoCollection`. -public struct FindOptions: Encodable { +public struct FindOptions: Codable { /// Get partial results from a mongos if some shards are down (instead of throwing an error). public let allowPartialResults: Bool? @@ -279,7 +289,8 @@ public struct FindOptions: Encodable { public let comment: String? /// Indicates the type of cursor to use. This value includes both the tailable and awaitData options. - public let cursorType: CursorType? + // swiftlint:disable:next redundant_optional_initialization + public var cursorType: CursorType? = nil /// If a `CursorType` is provided, indicates whether it is `.tailable` or .`tailableAwait`. private let tailable: Bool? @@ -333,7 +344,8 @@ public struct FindOptions: Encodable { public let readConcern: ReadConcern? /// A ReadPreference to use for this operation. - public let readPreference: ReadPreference? + // swiftlint:disable:next redundant_optional_initialization + public var readPreference: ReadPreference? = nil /// Convenience initializer allowing any/all parameters to be omitted or optional. public init(allowPartialResults: Bool? = nil, diff --git a/Sources/MongoSwift/MongoCollection+Write.swift b/Sources/MongoSwift/MongoCollection+Write.swift index 408202f31..cc04cb5b7 100644 --- a/Sources/MongoSwift/MongoCollection+Write.swift +++ b/Sources/MongoSwift/MongoCollection+Write.swift @@ -233,7 +233,7 @@ private extension BulkWriteOptionsConvertible { // Write command options structs /// Options to use when executing an `insertOne` command on a `MongoCollection`. -public struct InsertOneOptions: Encodable, BulkWriteOptionsConvertible { +public struct InsertOneOptions: Codable, BulkWriteOptionsConvertible { /// If true, allows the write to opt-out of document level validation. public let bypassDocumentValidation: Bool? @@ -251,7 +251,7 @@ public struct InsertOneOptions: Encodable, BulkWriteOptionsConvertible { public typealias InsertManyOptions = BulkWriteOptions /// Options to use when executing an `update` command on a `MongoCollection`. -public struct UpdateOptions: Encodable, BulkWriteOptionsConvertible { +public struct UpdateOptions: Codable, BulkWriteOptionsConvertible { /// A set of filters specifying to which array elements an update should apply. public let arrayFilters: [Document]? @@ -282,7 +282,7 @@ public struct UpdateOptions: Encodable, BulkWriteOptionsConvertible { } /// Options to use when executing a `replace` command on a `MongoCollection`. -public struct ReplaceOptions: Encodable, BulkWriteOptionsConvertible { +public struct ReplaceOptions: Codable, BulkWriteOptionsConvertible { /// If true, allows the write to opt-out of document level validation. public let bypassDocumentValidation: Bool? @@ -308,7 +308,7 @@ public struct ReplaceOptions: Encodable, BulkWriteOptionsConvertible { } /// Options to use when executing a `delete` command on a `MongoCollection`. -public struct DeleteOptions: Encodable, BulkWriteOptionsConvertible { +public struct DeleteOptions: Codable, BulkWriteOptionsConvertible { /// Specifies a collation. public let collation: Document? @@ -328,7 +328,11 @@ public struct DeleteOptions: Encodable, BulkWriteOptionsConvertible { // Write command results structs /// The result of an `insertOne` command on a `MongoCollection`. -public struct InsertOneResult { +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 @@ -342,6 +346,12 @@ public struct InsertOneResult { } 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`. diff --git a/Sources/MongoSwift/MongoError.swift b/Sources/MongoSwift/MongoError.swift index ee69ddece..935d92647 100644 --- a/Sources/MongoSwift/MongoError.swift +++ b/Sources/MongoSwift/MongoError.swift @@ -112,7 +112,7 @@ public struct WriteConcernError: Codable { public let code: ServerErrorCode /// A document identifying the write concern setting related to the error. - public let details: Document + public let details: Document? /// A description of the error. public let message: String @@ -243,41 +243,40 @@ private func getBulkWriteErrorFromReply( withResult result: BulkWriteResult? = nil, withWriteConcernError wcErr: WriteConcernError? = nil) throws -> MongoError? { let decoder = BSONDecoder() - var insertedIds = result?.insertedIds - var bulkWriteErrors: [BulkWriteError]? - if let writeErrors = reply["writeErrors"] as? [Document], !writeErrors.isEmpty { - bulkWriteErrors = try writeErrors.map { - let err = try decoder.decode(BulkWriteError.self, from: $0) - insertedIds?[err.index] = nil - return err - } - - let ordered = try bulkWrite.opts?.getValue(for: "ordered") as? Bool ?? true - - // If ordered, remove all inserted ids after the one that errored. - if ordered { - // we know bulkWriteErrors is non-nil because we initialize it above - // swiftlint:disable:next force_unwrapping - insertedIds = insertedIds?.filter { $0.key < bulkWriteErrors![0].index } - } - } - - guard wcErr != nil || bulkWriteErrors != nil else { - return nil + var bulkWriteErrors: [BulkWriteError] = [] + if let writeErrors = reply["writeErrors"] as? [Document] { + bulkWriteErrors = try writeErrors.map { try decoder.decode(BulkWriteError.self, from: $0) } } // Need to create new result that omits the ids that failed in insertedIds. var errResult: BulkWriteResult? if let result = result { + let ordered = try bulkWrite.opts?.getValue(for: "ordered") as? Bool ?? 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 [:] + } + + if ordered { // remove all after the last index that succeeded + let maxIndex = map.keys.sorted()[nSucceeded - 1] + return map.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) } + } + } + errResult = BulkWriteResult( deletedCount: result.deletedCount, insertedCount: result.insertedCount, - insertedIds: insertedIds, + insertedIds: filterFailures(result.insertedIds, result.insertedCount), matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, upsertedCount: result.upsertedCount, - upsertedIds: result.upsertedIds + upsertedIds: filterFailures(result.upsertedIds, result.upsertedCount) ) } @@ -297,8 +296,8 @@ internal func convertingBulkWriteErrors(_ body: () throws -> T) throws -> T { return try body() } catch let ServerError.bulkWriteError(bulkWriteErrors, writeConcernError, _, errorLabels) { var writeError: WriteError? - if let bwe = bulkWriteErrors?[0] { - writeError = WriteError(code: bwe.code, message: bwe.message) + if let bwes = bulkWriteErrors, !bwes.isEmpty { + writeError = WriteError(code: bwes[0].code, message: bwes[0].message) } throw ServerError.writeError(writeError: writeError, writeConcernError: writeConcernError, diff --git a/Sources/MongoSwift/Operations/CountOperation.swift b/Sources/MongoSwift/Operations/CountOperation.swift index ae10b3c76..71c35d3b2 100644 --- a/Sources/MongoSwift/Operations/CountOperation.swift +++ b/Sources/MongoSwift/Operations/CountOperation.swift @@ -1,7 +1,7 @@ import mongoc /// Options to use when executing a `count` command on a `MongoCollection`. -public struct CountOptions: Encodable { +public struct CountOptions: Codable { /// Specifies a collation. public let collation: Document? @@ -21,7 +21,8 @@ public struct CountOptions: Encodable { public let readConcern: ReadConcern? /// A ReadPreference to use for this operation. - public let readPreference: ReadPreference? + // swiftlint:disable:next redundant_optional_initialization + public var readPreference: ReadPreference? = nil /// Convenience initializer allowing any/all parameters to be optional public init(collation: Document? = nil, diff --git a/Sources/MongoSwift/Operations/DistinctOperation.swift b/Sources/MongoSwift/Operations/DistinctOperation.swift index 27846c1da..6dfc07977 100644 --- a/Sources/MongoSwift/Operations/DistinctOperation.swift +++ b/Sources/MongoSwift/Operations/DistinctOperation.swift @@ -1,7 +1,7 @@ import mongoc /// Options to use when executing a `distinct` command on a `MongoCollection`. -public struct DistinctOptions: Encodable { +public struct DistinctOptions: Codable { /// Specifies a collation. public let collation: Document? @@ -12,7 +12,8 @@ public struct DistinctOptions: Encodable { public let readConcern: ReadConcern? /// A ReadPreference to use for this operation. - public let readPreference: ReadPreference? + // swiftlint:disable:next redundant_optional_initialization + public var readPreference: ReadPreference? = nil /// Convenience initializer allowing any/all parameters to be optional public init(collation: Document? = nil, From 53a87ae2f4a50aa494673666b719d3b3693e60b2 Mon Sep 17 00:00:00 2001 From: Patrick Freed Date: Thu, 16 May 2019 13:27:29 -0400 Subject: [PATCH 2/2] fix docs --- Sources/MongoSwift/MongoCollection+Read.swift | 15 +++++++++------ .../MongoSwift/Operations/CountOperation.swift | 3 ++- .../MongoSwift/Operations/DistinctOperation.swift | 3 ++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Sources/MongoSwift/MongoCollection+Read.swift b/Sources/MongoSwift/MongoCollection+Read.swift index 3f23b2c57..0c5e9e87f 100644 --- a/Sources/MongoSwift/MongoCollection+Read.swift +++ b/Sources/MongoSwift/MongoCollection+Read.swift @@ -193,9 +193,10 @@ public struct AggregateOptions: Codable { /// A `ReadConcern` to use in read stages of this operation. public let readConcern: ReadConcern? + // swiftlint:disable redundant_optional_initialization /// A ReadPreference to use for this operation. - // swiftlint:disable:next redundant_optional_initialization public var readPreference: ReadPreference? = nil + // swiftlint:enable redundant_optional_initialization /// A `WriteConcern` to use in `$out` stages of this operation. public let writeConcern: WriteConcern? @@ -288,10 +289,6 @@ public struct FindOptions: Codable { /// Attaches a comment to the query. public let comment: String? - /// Indicates the type of cursor to use. This value includes both the tailable and awaitData options. - // swiftlint:disable:next redundant_optional_initialization - public var cursorType: CursorType? = nil - /// If a `CursorType` is provided, indicates whether it is `.tailable` or .`tailableAwait`. private let tailable: Bool? @@ -343,10 +340,16 @@ public struct FindOptions: Codable { /// A ReadConcern to use for this operation. public let readConcern: ReadConcern? + // swiftlint:disable redundant_optional_initialization + /// A ReadPreference to use for this operation. - // swiftlint:disable:next redundant_optional_initialization public var readPreference: ReadPreference? = nil + /// Indicates the type of cursor to use. This value includes both the tailable and awaitData options. + public var cursorType: CursorType? = nil + + // swiftlint:enable redundant_optional_initialization + /// Convenience initializer allowing any/all parameters to be omitted or optional. public init(allowPartialResults: Bool? = nil, batchSize: Int32? = nil, diff --git a/Sources/MongoSwift/Operations/CountOperation.swift b/Sources/MongoSwift/Operations/CountOperation.swift index 71c35d3b2..cdfac5fc1 100644 --- a/Sources/MongoSwift/Operations/CountOperation.swift +++ b/Sources/MongoSwift/Operations/CountOperation.swift @@ -20,9 +20,10 @@ public struct CountOptions: Codable { /// A ReadConcern to use for this operation. public let readConcern: ReadConcern? + // swiftlint:disable redundant_optional_initialization /// A ReadPreference to use for this operation. - // swiftlint:disable:next redundant_optional_initialization public var readPreference: ReadPreference? = nil + // swiftlint:enable redundant_optional_initialization /// Convenience initializer allowing any/all parameters to be optional public init(collation: Document? = nil, diff --git a/Sources/MongoSwift/Operations/DistinctOperation.swift b/Sources/MongoSwift/Operations/DistinctOperation.swift index 6dfc07977..ddce59f8e 100644 --- a/Sources/MongoSwift/Operations/DistinctOperation.swift +++ b/Sources/MongoSwift/Operations/DistinctOperation.swift @@ -11,9 +11,10 @@ public struct DistinctOptions: Codable { /// A ReadConcern to use for this operation. public let readConcern: ReadConcern? + // swiftlint:disable redundant_optional_initialization /// A ReadPreference to use for this operation. - // swiftlint:disable:next redundant_optional_initialization public var readPreference: ReadPreference? = nil + // swiftlint:enable redundant_optional_initialization /// Convenience initializer allowing any/all parameters to be optional public init(collation: Document? = nil,