Skip to content

Commit 4efdbd5

Browse files
authored
SWIFT-914 Add docs generation script (#41)
1 parent abfda49 commit 4efdbd5

File tree

3 files changed

+303
-0
lines changed

3 files changed

+303
-0
lines changed

Guides/BSON.md

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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.

etc/docs-main.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# swift-bson Documentation
2+
3+
This is the documentation for the official MongoDB Swift BSON library, [swift-bson](https://github.com/mongodb/swift-bson).
4+
5+
You can view the README for this project, including installation instructions, [here](https://github.com/mongodb/swift-bson/blob/master/README.md).
6+
7+
The documentation for the official MongoDB Swift driver, which depends on this BSON library, can be found [here](https://mongodb.github.io/mongo-swift-driver/MongoSwift/index.html).

etc/generate-docs.sh

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/bash
2+
3+
# usage: ./etc/generate-docs.sh [new version string]
4+
5+
# exit if any command fails
6+
set -e
7+
8+
if ! command -v jazzy > /dev/null; then
9+
gem install jazzy || { echo "ERROR: Failed to locate or install jazzy; please install yourself with 'gem install jazzy' (you may need to use sudo)"; exit 1; }
10+
fi
11+
12+
version=${1}
13+
14+
# Ensure version is non-empty
15+
[ ! -z "${version}" ] || { echo "ERROR: Missing version string"; exit 1; }
16+
17+
jazzy_args=(--clean
18+
--author "Neal Beeken, Nellie Spektor, Patrick Freed, and Kaitlin Mahar"
19+
--readme "etc/docs-main.md"
20+
--author_url https://github.com/mongodb/swift-bson
21+
--github_url https://github.com/mongodb/swift-bson
22+
--theme fullwidth
23+
--documentation "Guides/*.md"
24+
--github-file-prefix https://github.com/mongodb/swift-bson/tree/v${version}
25+
--module-version "${version}"
26+
--swift-build-tool spm)
27+
28+
modules=( BSON )
29+
30+
for module in "${modules[@]}"; do
31+
args=("${jazzy_args[@]}" --output "docs/${module}" --module "${module}"
32+
--root-url "https://mongodb.github.io/swift-bson/docs/${module}/")
33+
jazzy "${args[@]}"
34+
done
35+
36+
# switch to docs branch to commit and push
37+
git checkout gh-pages
38+
git add docs/
39+
git commit -m "${version} docs"
40+
git push
41+
42+
# go back to wherever we started
43+
git checkout -

0 commit comments

Comments
 (0)