|
| 1 | +# MongoDB BSON Library |
| 2 | +MongoDB stores and transmits data in the form of [BSON](bsonspec.org) documents, and this library be used to work with such documents. The following is an example of some of the functionality provided as part of that: |
| 3 | +```swift |
| 4 | +// Document construction. |
| 5 | +let doc: BSONDocument = [ |
| 6 | + "name": "Bob", |
| 7 | + "occupation": "Software Engineer", |
| 8 | + "projects": [ |
| 9 | + ["id": 76, "title": "Documentation"] |
| 10 | + ] |
| 11 | +] |
| 12 | +// Reading from documents. |
| 13 | +print(doc["name"]) // .string(Bob) |
| 14 | +print(doc["projects"]) // .array([.document({ "id": 76, "title": "Documentation" })]) |
| 15 | + |
| 16 | +// Document serialization and deserialization. |
| 17 | +struct Person: Codable { |
| 18 | + let name: String |
| 19 | + let occupation: String |
| 20 | +} |
| 21 | +print(try BSONDecoder().decode(Person.self, from: doc)) // Person(name: "Bob", occupation: "Software Engineer") |
| 22 | +print(try BSONEncoder().encode(Person(name: "Ted", occupation: "Janitor"))) // { "name": "Ted", "occupation": "Janitor" } |
| 23 | +``` |
| 24 | + |
| 25 | +## BSON values |
| 26 | +BSON values have many possible types, ranging from simple 32-bit integers to documents which store more BSON values themselves. To accurately model this, the driver defines the `BSON` enum, which has a distinct case for each BSON type. For the more simple cases such as BSON null, the case has no associated value. For the more complex ones, such as documents, a separate type is defined that the case wraps. Where possible, the enum case will wrap the standard library/Foundation equivalent (e.g. `Double`, `String`, `Date`) |
| 27 | +```swift |
| 28 | +public enum BSON { |
| 29 | + case .null, |
| 30 | + case .document(BSONDocument) |
| 31 | + case .double(Double) |
| 32 | + case .datetime(Date) |
| 33 | + case .string(String) |
| 34 | + // ...rest of the cases... |
| 35 | +} |
| 36 | +``` |
| 37 | +### Initializing a `BSON` |
| 38 | +This enum can be instantiated directly like any other enum in the Swift language, but it also conforms to a number of `ExpressibleByXLiteral` protocols, meaning it can be instantiated directly from numeric, string, boolean, dictionary, and array literals. |
| 39 | +```swift |
| 40 | +let int: BSON = 5 // .int64(5) on 64-bit systems |
| 41 | +let double: BSON = 5.5 // .double(5.5) |
| 42 | +let string: BSON = "hello world" // .string("hello world") |
| 43 | +let bool: BSON = false // .bool(false) |
| 44 | +let document: BSON = ["x": 5, "y": true, "z": ["x": 1]] // .document({ "x": 5, "y": true, "z": { "x": 1 } }) |
| 45 | +let array: BSON = ["1", true, 5.5] // .array([.string("1"), .bool(true), .double(5.5)]) |
| 46 | +``` |
| 47 | +All other cases must be initialized directly: |
| 48 | +```swift |
| 49 | +let date = BSON.datetime(Date()) |
| 50 | +let objectId = BSON.objectID() |
| 51 | +// ...rest of cases... |
| 52 | +``` |
| 53 | +### Unwrapping a `BSON` |
| 54 | +To get a `BSON` value as a specific type, you can use `switch` or `if/guard case let` like any other enum in Swift: |
| 55 | +```swift |
| 56 | +func foo(x: BSON, y: BSON) { |
| 57 | + switch x { |
| 58 | + case let .int32(int32): |
| 59 | + print("got an Int32: \(int32)") |
| 60 | + case let .objectID(oid): |
| 61 | + print("got an objectId: \(oid.hex)") |
| 62 | + default: |
| 63 | + print("got something else") |
| 64 | + } |
| 65 | + guard case let .double(d) = y else { |
| 66 | + print("y must be a double") |
| 67 | + return |
| 68 | + } |
| 69 | + print(d * d) |
| 70 | +} |
| 71 | +``` |
| 72 | +While these methods are good for branching, sometimes it is useful to get just the value (e.g. for optional chaining, passing as a parameter, or returning from a function). For those cases, `BSON` has computed properties for each case that wraps a type. These properties will return `nil` unless the underlying BSON value is an exact match to the return type of the property. |
| 73 | +```swift |
| 74 | +func foo(x: BSON) -> [BSONDocument] { |
| 75 | + guard let documents = x.arrayValue?.compactMap({ $0.documentValue }) else { |
| 76 | + print("x is not an array") |
| 77 | + return [] |
| 78 | + } |
| 79 | + return documents |
| 80 | +} |
| 81 | +print(BSON.int64(5).int32Value) // nil |
| 82 | +print(BSON.int32(5).int32Value) // Int32(5) |
| 83 | +print(BSON.double(5).int64Value) // nil |
| 84 | +print(BSON.double(5).doubleValue) // Double(5.0) |
| 85 | +``` |
| 86 | +### Converting a `BSON` |
| 87 | +In some cases, especially when dealing with numbers, it may make sense to coerce a `BSON`'s wrapped value into a similar one. For those situations, there are several conversion methods defined on `BSON` that will unwrap the underlying value and attempt to convert it to the desired type. If that conversion would be lossless, a non-`nil` value is returned. |
| 88 | +```swift |
| 89 | +func foo(x: BSON, y: BSON) { |
| 90 | + guard let x = x.toInt(), let y = y.toInt() else { |
| 91 | + print("provide two integer types") |
| 92 | + return |
| 93 | + } |
| 94 | + print(x + y) |
| 95 | +} |
| 96 | +foo(x: 5, y: 5.0) // 10 |
| 97 | +foo(x: 5, y: 5) // 10 |
| 98 | +foo(x: 5.0, y: 5.0) // 10 |
| 99 | +foo(x: .int32(5), y: .int64(5)) // 10 |
| 100 | +foo(x: 5.01, y: 5) // error |
| 101 | +``` |
| 102 | +There are similar conversion methods for the other types, namely `toInt32()`, `toDouble()`, `toInt64()`, and `toDecimal128()`. |
| 103 | + |
| 104 | +### Using a `BSON` value |
| 105 | +`BSON` conforms to a number of useful Foundation protocols, namely `Codable`, `Equatable`, and `Hashable`. This allows them to be compared, encoded/decoded, and used as keys in maps: |
| 106 | +```swift |
| 107 | +// Codable conformance synthesized by compiler. |
| 108 | +struct X: Codable { |
| 109 | + let _id: BSON |
| 110 | +} |
| 111 | +// Equatable |
| 112 | +let x: BSON = "5" |
| 113 | +let y: BSON = 5 |
| 114 | +let z: BSON = .string("5") |
| 115 | +print(x == y) // false |
| 116 | +print(x == z) // true |
| 117 | +// Hashable |
| 118 | +let map: [BSON: String] = [ |
| 119 | + "x": "string", |
| 120 | + false: "bool", |
| 121 | + [1, 2, 3]: "array", |
| 122 | + .objectID(): "oid", |
| 123 | + .null: "null", |
| 124 | + .maxKey: "maxKey" |
| 125 | +] |
| 126 | +``` |
| 127 | +## Documents |
| 128 | +BSON documents are the top-level structures that contain the aforementioned BSON values, and they are also BSON values themselves. The driver defines the `BSONDocument` struct to model this specific BSON type. |
| 129 | +### Initializing documents |
| 130 | +Like `BSON`, `BSONDocument` can also be initialized by a dictionary literal. The elements within the literal must be `BSON`s, so further literals can be embedded within the top level literal definition: |
| 131 | +```swift |
| 132 | +let x: BSONDocument = [ |
| 133 | + "x": 5, |
| 134 | + "y": 5.5, |
| 135 | + "z": [ |
| 136 | + "a": [1, true, .datetime(Date())] |
| 137 | + ] |
| 138 | +] |
| 139 | +``` |
| 140 | +Documents can also be initialized directly by passing in a `Data` containing raw BSON bytes: |
| 141 | +```swift |
| 142 | +try BSONDocument(fromBSON: Data(...)) |
| 143 | +``` |
| 144 | +Documents may be initialized from an [extended JSON](https://docs.mongodb.com/manual/reference/mongodb-extended-json/) string as well: |
| 145 | +```swift |
| 146 | +try BSONDocument(fromJSON: "{ \"x\": true }") // { "x": true } |
| 147 | +try BSONDocument(fromJSON: "{ x: false }}}") // error |
| 148 | +``` |
| 149 | +### Using documents |
| 150 | +Documents define the interface in which an application communicates with a MongoDB deployment. For that reason, `BSONDocument` has been fitted with functionality to make it both powerful and ergonomic to use for developers. |
| 151 | +#### Reading / writing to `BSONDocument` |
| 152 | +`BSONDocument` conforms to [`Collection`](https://developer.apple.com/documentation/swift/collection), which allows for easy reading and writing of elements via the subscript operator. On `BSONDocument`, this operator returns and accepts a `BSON?`: |
| 153 | +```swift |
| 154 | +var doc: BSONDocument = ["x": 1] |
| 155 | +print(doc["x"]) // .int64(1) |
| 156 | +doc["x"] = ["y": .null] |
| 157 | +print(doc["x"]) // .document({ "y": null }) |
| 158 | +doc["x"] = nil |
| 159 | +print(doc["x"]) // nil |
| 160 | +print(doc) // { } |
| 161 | +``` |
| 162 | +`BSONDocument` also has the `@dynamicMemberLookup` attribute, meaning it's values can be accessed directly as if they were properties on `BSONDocument`: |
| 163 | +```swift |
| 164 | +var doc: BSONDocument = ["x": 1] |
| 165 | +print(doc.x) // .int64(1) |
| 166 | +doc.x = ["y": .null] |
| 167 | +print(doc.x) // .document({ "y": null }) |
| 168 | +doc.x = nil |
| 169 | +print(doc.x) // nil |
| 170 | +print(doc) // { } |
| 171 | +``` |
| 172 | +`BSONDocument` also conforms to [`Sequence`](https://developer.apple.com/documentation/swift/sequence), which allows it to be iterated over: |
| 173 | +```swift |
| 174 | +for (k, v) in doc { |
| 175 | + print("\(k) = \(v)") |
| 176 | +} |
| 177 | +``` |
| 178 | +Conforming to `Sequence` also gives a number of useful methods from the functional programming world, such as `map` or `allSatisfy`: |
| 179 | +```swift |
| 180 | +let allEvens = doc.allSatisfy { _, v in v.toInt() ?? 1 % 2 == 0 } |
| 181 | +let squares = doc.map { k, v in v.toInt()! * v.toInt()! } |
| 182 | +``` |
| 183 | +See the documentation for `Sequence` for a full list of methods that `BSONDocument` implements as part of this. |
| 184 | + |
| 185 | +In addition to those protocol conformances, there are a few one-off helpers implemented on `BSONDocument` such as `filter` (that returns a `BSONDocument`) and `mapValues` (also returns a `BSONDocument`): |
| 186 | +```swift |
| 187 | +let doc: BSONDocument = ["_id": .objectID(), "numCats": 2, "numDollars": 1.56, "numPhones": 1] |
| 188 | +doc.filter { k, v in k.contains("num") && v.toInt() != nil }.mapValues { v in .int64(v.toInt64()! + 5) } // { "numCats": 7, "numPhones": 6 } |
| 189 | +``` |
| 190 | +See the driver's documentation for a full listing of `BSONDocument`'s public API. |
| 191 | +## `Codable` and `BSONDocument` |
| 192 | +[`Codable`](https://developer.apple.com/documentation/swift/codable) is a protocol defined in Foundation that allows for ergonomic conversion between various serialization schemes and Swift data types. As part of the BSON library, MongoSwift defines both `BSONEncoder` and `BSONDecoder` to facilitate this serialization and deserialization to and from BSON via `Codable`. This allows applications to work with BSON documents in a type-safe way, and it removes much of the runtime key presence and type checking required when working with raw documents. It is reccommended that users leverage `Codable` wherever possible in their applications that use the driver instead of accessing documents directly. |
| 193 | + |
| 194 | +For example, here is an function written using raw documents: |
| 195 | +```swift |
| 196 | +let person: BSONDocument = [ |
| 197 | + "name": "Bob", |
| 198 | + "occupation": "Software Engineer", |
| 199 | + "projects": [ |
| 200 | + ["id": 1, "title": "Server Side Swift Application"], |
| 201 | + ["id": 76, "title": "Write documentation"], |
| 202 | + ] |
| 203 | +] |
| 204 | + |
| 205 | +func prettyPrint(doc: BSONDocument) { |
| 206 | + guard let name = doc["name"]?.stringValue else { |
| 207 | + print("missing name") |
| 208 | + return |
| 209 | + } |
| 210 | + print("Name: \(name)") |
| 211 | + guard let occupation = doc["occupation"]?.stringValue else { |
| 212 | + print("missing occupation") |
| 213 | + return |
| 214 | + } |
| 215 | + print("Occupation: \(occupation)") |
| 216 | + guard let projects = doc["projects"]?.arrayValue?.compactMap({ $0.documentValue }) else { |
| 217 | + print("missing projects") |
| 218 | + return |
| 219 | + } |
| 220 | + print("Projects:") |
| 221 | + for project in projects { |
| 222 | + guard let title = project["title"] else { |
| 223 | + print("missing title") |
| 224 | + return |
| 225 | + } |
| 226 | + print(title) |
| 227 | + } |
| 228 | +} |
| 229 | +``` |
| 230 | +Due to the flexible nature of `BSONDocument`, a number of checks have to be put into the body of the function. This clutters the actual function's logic and requires a lot of boilerplate code. Now, consider the following function which does the same thing but is written leveraging `Codable`: |
| 231 | +```swift |
| 232 | +struct Project: Codable { |
| 233 | + let id: BSON |
| 234 | + let title: String |
| 235 | +} |
| 236 | + |
| 237 | +struct Person: Codable { |
| 238 | + let name: String |
| 239 | + let occupation: String |
| 240 | + let projects: [Project] |
| 241 | +} |
| 242 | + |
| 243 | +func prettyPrint(doc: BSONDocument) throws { |
| 244 | + let person = try BSONDecoder().decode(Person.self, from: doc) |
| 245 | + print("Name: \(person.name)") |
| 246 | + print("Occupation: \(person.occupation)") |
| 247 | + print("Projects:") |
| 248 | + for project in person.projects { |
| 249 | + print(project.title) |
| 250 | + } |
| 251 | +} |
| 252 | +``` |
| 253 | +In this version, the definition of the data type and the logic of the function are defined completely separately, and it leads to far more readable and concise versions of both. |
0 commit comments