Skip to content

Commit efe1cf5

Browse files
authored
SWIFT-1396 Update Vapor example to use async/await (#726)
1 parent 75335a9 commit efe1cf5

File tree

3 files changed

+84
-80
lines changed

3 files changed

+84
-80
lines changed

Examples/VaporExample/Package.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
// swift-tools-version:5.2
1+
// swift-tools-version:5.5
22
import PackageDescription
33

44
let package = Package(
55
name: "VaporExample",
66
platforms: [
7-
.macOS(.v10_15)
7+
.macOS(.v12)
88
],
99
dependencies: [
10-
.package(url: "https://github.com/vapor/vapor", .upToNextMajor(from: "4.7.0")),
10+
.package(url: "https://github.com/vapor/vapor", .upToNextMajor(from: "4.50.0")),
1111
.package(url: "https://github.com/vapor/leaf", .upToNextMajor(from: "4.0.0")),
12-
.package(url: "https://github.com/mongodb/mongodb-vapor", .upToNextMajor(from: "1.0.0"))
12+
.package(url: "https://github.com/mongodb/mongodb-vapor", .exact("1.1.0-alpha.1"))
1313
],
1414
targets: [
1515
.target(
@@ -18,9 +18,16 @@ let package = Package(
1818
.product(name: "Vapor", package: "vapor"),
1919
.product(name: "Leaf", package: "leaf"),
2020
.product(name: "MongoDBVapor", package: "mongodb-vapor")
21+
],
22+
swiftSettings: [
23+
// Enable better optimizations when building in Release configuration. Despite the use of
24+
// the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
25+
// builds. For details, see
26+
// <https://github.com/swift-server/guides/blob/main/docs/building.md#building-for-production>.
27+
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
2128
]
2229
),
23-
.target(name: "Run", dependencies: [
30+
.executableTarget(name: "Run", dependencies: [
2431
.target(name: "App"),
2532
.product(name: "MongoDBVapor", package: "mongodb-vapor")
2633
])

Examples/VaporExample/README.md

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,22 @@ This application is intended to demonstrate best practices for integrating the d
88

99
The application contains both a REST API as well as a minimal frontend built with Vapor's templating language [Leaf](https://github.com/vapor/leaf).
1010

11-
This application require Swift 5.2 and MongoDB 3.6+. It will run on Linux as well as macOS 10.15+.
11+
This application requires Swift 5.5+ and MongoDB 3.6+. It will run on Linux as well as macOS 12+.
12+
13+
If you are looking for an example application that uses older Swift versions prior to the introduction of structured concurrency, please refer to a previous version of this application and README [here](https://github.com/mongodb/mongo-swift-driver/tree/5d9fad121d7cb3ded61087de300ff007766ccd55/Examples/VaporExample).
1214

1315
## Building and Running the Application
1416
1. Install MongoDB on your system if you haven't already. Downloads are available [here](https://www.mongodb.com/download-center/community).
1517
1. Start up MongoDB running locally: `mongod --dbpath some-directory-here`. You may need to specify a `dbpath` directory for the database to use.
1618
1. Run `./loadData.sh` to load example application data into the database.
17-
1. Install Swift 5.2+ on your system if you haven't already. You can download Swift and find instructions for installing it [here](https://swift.org/download/).
19+
1. Install Swift 5.5+ on your system if you haven't already. You can download Swift and find instructions for installing it [here](https://swift.org/download/).
1820
1. From the root directory of the project, run `swift build`. This will likely take a while the first time you do so.
1921
1. Once building has completed, run `swift run` from the root directory. You should get a message that the server has started running on `http://127.0.0.1:8080`.
2022
1. Open up your browser and visit `http://127.0.0.1:8080`. You should see the application and be able to test out adding, deleting, and editing data in the collection.
2123

2224
## Application Architecture
2325

24-
This is a fully asynchronous application. At its core is [SwiftNIO](https://github.com/apple/swift-nio), which is used to implement both Vapor and the MongoDB driver.
25-
26-
The application has both web and REST API interfaces, which support storing a list of kittens and details about them.
26+
This is a fully asynchronous application with both web and REST API interfaces, which support storing a list of kittens and details about them.
2727

2828
The server will handle the following types of web requests:
2929
1. A GET request at the root URL `/` loads the main index page containing a list of kittens.
@@ -43,19 +43,14 @@ If you'd like to point the application to a MongoDB server elsewhere (e.g. on [M
4343

4444
The call to `configure()` initializes a global `MongoClient` to back your application. `MongoClient` is implemented with that approach in mind: it is safe to use across threads, and is backed by a [connection pool](https://en.wikipedia.org/wiki/Connection_pool) which enables sharing resources throughout the application.
4545

46-
Throughout your application, you can access the global client via `app.mongoDB.client`. Note that the global client may return `EventLoopFuture`s on *any* `EventLoop` in the application's `EventLoopGroup`, so if you use this client you will need to ensure you "hop" the futures back to the event loop you are currently on. See the `EventLoopFuture` [documentation](https://apple.github.io/swift-nio/docs/current/NIO/Classes/EventLoopFuture.html) for more details.
47-
48-
To avoid the need to hop `EventLoop`s, whenever you are using MongoDB in a request handler, we strongly recommend you use an `EventLoopBoundMongoClient` instead, accessible via `req.mongoDB.client`. This type is a small wrapper around the global client, which returns `EventLoopFuture`s on a specific `EventLoop` which it is "bound" to. Using an `EventLoopBoundMongoClient` that is backed by the same `EventLoop` as a `Request` means you can use the client within a request handler without worrying about thread safety. You can access an `EventLoopBoundMongoClient` for a `Request` via `req.mongoDB.client`.
49-
50-
`MongoDatabase`s and `MongoCollection`s you retrieve from an `EventLoopBoundMongoClient` will automatically be bound to the same event loop as the parent client.
46+
Throughout your application, you can access the global client via `app.mongoDB.client`.
5147

5248
For convenience, we recommend adding your own computed properties to `Request` that return `MongoDatabase`s and `MongoCollection`s you frequently access, as is shown in `Sources/App/routes.swift`
5349
with the `kittenCollection` property:
5450
```swift
5551
extension Request {
56-
/// Convenience extension for obtaining a collection which uses the same event loop as a request.
5752
var kittenCollection: MongoCollection<Kitten> {
58-
self.mongoDB.client.db("home").collection("kittens", withType: Kitten.self)
53+
self.application.mongoDB.client.db("home").collection("kittens", withType: Kitten.self)
5954
}
6055
}
6156
```
@@ -70,7 +65,7 @@ When creating a `MongoCollection`, you can pass in the name of a `Codable` type:
7065
let collection = req.mongoDB.client.db("home").collection("kittens", withType: Kitten.self)
7166
```
7267

73-
This will instantiate a `MongoCollection<Kitten>`. You can then use `Kitten` directly with many API methods -- for example, `insertOne` will directly accept a `Kitten` instance, and `findOne` will return an `EventLoopFuture<Kitten>`.
68+
This will instantiate a `MongoCollection<Kitten>`. You can then use `Kitten` directly with many API methods -- for example, `insertOne` will directly accept a `Kitten` instance, and `findOne` will return a `Kitten?`.
7469

7570
Sometimes you may need to work with the `BSONDocument` type as well, for example when providing a query filter. If you want to construct these documents from `Codable` types you may do so using `BSONEncoder`, as we do with the `updateDocument` in the `updateKitten()` method via the `KittenUpdate` struct.
7671

Examples/VaporExample/Sources/App/routes.swift

Lines changed: 64 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,17 @@ import Vapor
44
/// Adds a collection of web routes to the application.
55
func webRoutes(_ app: Application) throws {
66
/// Handles a request to load the main index page containing a list of kittens.
7-
app.get { req -> EventLoopFuture<View> in
8-
req.findKittens().flatMap { kittens in
9-
// Return the corresponding Leaf view, providing the list of kittens as context.
10-
req.view.render("index.leaf", ["kittens": kittens])
11-
}
7+
app.get { req async throws -> View in
8+
let kittens = try await req.findKittens()
9+
// Return the corresponding Leaf view, providing the list of kittens as context.
10+
return try await req.view.render("index.leaf", ["kittens": kittens])
1211
}
1312

1413
/// Handles a request to load a page with info about a particular kitten.
15-
app.get("kittens", ":name") { req -> EventLoopFuture<View> in
16-
try req.findKitten().flatMap { kitten in
17-
// Return the corresponding Leaf view, providing the kitten as context.
18-
req.view.render("kitten.leaf", kitten)
19-
}
14+
app.get("kittens", ":name") { req async throws -> View in
15+
let kitten = try await req.findKitten()
16+
// Return the corresponding Leaf view, providing the kitten as context.
17+
return try await req.view.render("kitten.leaf", kitten)
2018
}
2119
}
2220

@@ -25,33 +23,33 @@ func restAPIRoutes(_ app: Application) throws {
2523
let rest = app.grouped("rest")
2624

2725
/// Handles a request to load the list of kittens.
28-
rest.get { req -> EventLoopFuture<[Kitten]> in
29-
req.findKittens()
26+
rest.get { req async throws -> [Kitten] in
27+
try await req.findKittens()
3028
}
3129

3230
/// Handles a request to add a new kitten.
33-
rest.post { req -> EventLoopFuture<Response> in
34-
try req.addKitten()
31+
rest.post { req async throws -> Response in
32+
try await req.addKitten()
3533
}
3634

3735
/// Handles a request to load info about a particular kitten.
38-
rest.get("kittens", ":name") { req -> EventLoopFuture<Kitten> in
39-
try req.findKitten()
36+
rest.get("kittens", ":name") { req async throws -> Kitten in
37+
try await req.findKitten()
4038
}
4139

42-
rest.delete("kittens", ":name") { req -> EventLoopFuture<Response> in
43-
try req.deleteKitten()
40+
rest.delete("kittens", ":name") { req async throws -> Response in
41+
try await req.deleteKitten()
4442
}
4543

46-
rest.patch("kittens", ":name") { req -> EventLoopFuture<Response> in
47-
try req.updateKitten()
44+
rest.patch("kittens", ":name") { req async throws -> Response in
45+
try await req.updateKitten()
4846
}
4947
}
5048

5149
extension Request {
52-
/// Convenience extension for obtaining a collection which uses the same event loop as a request.
50+
/// Convenience extension for obtaining a collection.
5351
var kittenCollection: MongoCollection<Kitten> {
54-
self.mongoDB.client.db("home").collection("kittens", withType: Kitten.self)
52+
self.application.mongoDB.client.db("home").collection("kittens", withType: Kitten.self)
5553
}
5654

5755
/// Constructs a document using the name from this request which can be used a filter for MongoDB
@@ -65,69 +63,73 @@ extension Request {
6563
return ["name": .string(name)]
6664
}
6765

68-
func findKittens() -> EventLoopFuture<[Kitten]> {
69-
self.kittenCollection.find().flatMap { cursor in
70-
cursor.toArray()
71-
}.flatMapErrorThrowing { error in
66+
func findKittens() async throws -> [Kitten] {
67+
do {
68+
return try await self.kittenCollection.find().toArray()
69+
} catch {
7270
throw Abort(.internalServerError, reason: "Failed to load kittens: \(error)")
7371
}
7472
}
7573

76-
func findKitten() throws -> EventLoopFuture<Kitten> {
74+
func findKitten() async throws -> Kitten {
7775
let nameFilter = try self.getNameFilter()
78-
return self.kittenCollection.findOne(nameFilter)
79-
.unwrap(or: Abort(.notFound, reason: "No kitten with matching name"))
76+
guard let kitten = try await self.kittenCollection.findOne(nameFilter) else {
77+
throw Abort(.notFound, reason: "No kitten with matching name")
78+
}
79+
return kitten
8080
}
8181

82-
func addKitten() throws -> EventLoopFuture<Response> {
82+
func addKitten() async throws -> Response {
8383
let newKitten = try self.content.decode(Kitten.self)
84-
return self.kittenCollection.insertOne(newKitten)
85-
.map { _ in Response(status: .created) }
86-
.flatMapErrorThrowing { error in
87-
// Give a more helpful error message in case of a duplicate key error.
88-
if let err = error as? MongoError.WriteError, err.writeFailure?.code == 11000 {
89-
throw Abort(.conflict, reason: "A kitten with the name \(newKitten.name) already exists!")
90-
}
91-
throw Abort(.internalServerError, reason: "Failed to save new kitten: \(error)")
84+
do {
85+
try await self.kittenCollection.insertOne(newKitten)
86+
return Response(status: .created)
87+
} catch {
88+
// Give a more helpful error message in case of a duplicate key error.
89+
if let err = error as? MongoError.WriteError, err.writeFailure?.code == 11000 {
90+
throw Abort(.conflict, reason: "A kitten with the name \(newKitten.name) already exists!")
9291
}
92+
throw Abort(.internalServerError, reason: "Failed to save new kitten: \(error)")
93+
}
9394
}
9495

95-
func deleteKitten() throws -> EventLoopFuture<Response> {
96+
func deleteKitten() async throws -> Response {
9697
let nameFilter = try self.getNameFilter()
97-
return self.kittenCollection.deleteOne(nameFilter)
98-
.flatMapErrorThrowing { error in
99-
throw Abort(.internalServerError, reason: "Failed to delete kitten: \(error)")
98+
do {
99+
// since we aren't using an unacknowledged write concern we can expect deleteOne to return a non-nil result.
100+
guard let result = try await self.kittenCollection.deleteOne(nameFilter) else {
101+
throw Abort(.internalServerError, reason: "Unexpectedly nil response from database")
100102
}
101-
// since we are not using an unacknowledged write concern we can expect deleteOne to return
102-
// a non-nil result.
103-
.unwrap(or: Abort(.internalServerError, reason: "Unexpectedly nil response from database"))
104-
.flatMapThrowing { result in
105-
guard result.deletedCount == 1 else {
106-
throw Abort(.notFound, reason: "No kitten with matching name")
107-
}
108-
return Response(status: .ok)
103+
guard result.deletedCount == 1 else {
104+
throw Abort(.notFound, reason: "No kitten with matching name")
109105
}
106+
return Response(status: .ok)
107+
} catch {
108+
throw Abort(.internalServerError, reason: "Failed to delete kitten: \(error)")
109+
}
110110
}
111111

112-
func updateKitten() throws -> EventLoopFuture<Response> {
112+
func updateKitten() async throws -> Response {
113113
let nameFilter = try self.getNameFilter()
114114
// Parse the update data from the request.
115115
let update = try self.content.decode(KittenUpdate.self)
116116
/// Create a document using MongoDB update syntax that specifies we want to set a field.
117117
let updateDocument: BSONDocument = ["$set": .document(try BSONEncoder().encode(update))]
118118

119-
return self.kittenCollection.updateOne(filter: nameFilter, update: updateDocument)
120-
.flatMapErrorThrowing { error in
121-
throw Abort(.internalServerError, reason: "Failed to update kitten: \(error)")
119+
do {
120+
// since we aren't using an unacknowledged write concern we can expect updateOne to return a non-nil result.
121+
guard let result = try await self.kittenCollection.updateOne(
122+
filter: nameFilter,
123+
update: updateDocument
124+
) else {
125+
throw Abort(.internalServerError, reason: "Unexpectedly nil response from database")
122126
}
123-
// since we are not using an unacknowledged write concern we can expect updateOne to return
124-
// a non-nil result.
125-
.unwrap(or: Abort(.internalServerError, reason: "Unexpectedly nil response from database"))
126-
.flatMapThrowing { result in
127-
guard result.matchedCount == 1 else {
128-
throw Abort(.notFound, reason: "No kitten with matching name")
129-
}
130-
return Response(status: .ok)
127+
guard result.matchedCount == 1 else {
128+
throw Abort(.notFound, reason: "No kitten with matching name")
131129
}
130+
return Response(status: .ok)
131+
} catch {
132+
throw Abort(.internalServerError, reason: "Failed to update kitten: \(error)")
133+
}
132134
}
133135
}

0 commit comments

Comments
 (0)