Week 4
New Features
Services Created With Reference Types Are Always Singletons
In previous betas of Vapor 3, the service framework supported an isSingleton
property for services which controlled their behavior when requested from different containers and/or from different requesting interfaces (the for:
parameter of Container.make()
). Singletons would in theory have only the single instance across all invocations. However, this only held true for services whose underlying implementations were class
es; struct
-based services were never singletons even when the flag was specified. In addition, the flag offered a manual control for a behavior already baked into Swift via the difference between value and reference types. Therefore, the singleton flag on service types was removed entirely. Now, to get the “singleton” behavior, use a class
type to implement your service. struct
-based services now receive the previously default “instanced” behavior (a new instance of the service per request).
TL;DR: The redundant singleton flag on services was removed, and the behavior when requesting a given service more than once now corresponds to Swift’s standard value vs. reference semantics.
SQLite Models Are More Convenient
Neither typealias Database = ...
nor static let idKey = ...
are required any longer. This is true for both struct
- and class
-based models.
struct Foo: SQLiteModel {
var id: Int?
var name: String
}
final class Bar: SQLiteModel {
var id: Int?
var name: String
}
Debuggable
Debuggable
now conforms to Foundation’s LocalizedError
protocol, significantly improving debuggability across the board, especially including much improved log messages when an error is thrown from a unit test.
Custom URLSession
s for FoundationClient
It is now possible to provide a pre-configured URLSession
when using FoundationClient
. This especially allows proxying via HTTPS, along with other parameters configurable via URLSessionConfiguration
. (Vapor 2)
Client Redirection
EngineClient
will now properly follow redirects, as indicated by various HTTP status codes in the 3xx series. Redirection can be configured by invoking EngineClient
’s respond(to:maxRedirects:)
method with the desired maxium redirection count (including zero).
Under The Hood
Lazy Middleware
- Creates one set of middleware per event loop, preventing race conditions in middleware
- Losslessly optimizes DateMiddleware to only calculate a new RFC1123 timestamp once per second
- Adds a new
router.grouped(MiddlewareType.self)
overload for creating a route group around a middleware type that will be lazily initialized (on the event loop) when requested
router.grouped(DateMiddleware.self).get("datetest") { req in
return HTTPStatus.ok
}
In the above snippet, DateMiddleware won’t be initialized until the first time that route is hit. It will subsequently be initialized for each event loop, until all event loops have one cached.
- Adds application-wide timeout for requests whose runtime exceeds 30 seconds
Help Wanted
Leaf Syntax Highlighting For InteliJ
Any new framework is nothing without good tools, and Vapor is no different. While Swift syntax highlighting is fairly easy to come by, Leaf is not.
Community Contributions
Fix an Xcode Crash In API Template
Anyone who’s used Vapor knows that Xcode tends to experience problems during Vapor development that you won’t see when using Xcode only for iOS development. For example, if an error is raised to the top level in iOS, it gets caught by AppDelegate
. However, Vapor apps don’t have an AppDelegate
equivalent, so instead, errors raised to to the top level crash Xcode (please file bug reports with Apple when you see this happen!). This tends to trip up newer users, so in their joint PR, @twof and @0xTim fixed Vapor 3’s API template to catch top level errors, preventing these crashes.
MySQL Fixes To More Closely Conform To Client-Server Protocol
@SandorDobi noticed a few places where Vapor’s MySQL package wasn’t quite conforming to MySQL’s protocols and fixed them.
Dependency Updates
Same as last week. Tracking which dependencies need updates is hard!
Updated RoutingGroups For Better Handling of Middleware
After some discussion, @MaximBazarov made updates to Vapor’s routing groups.
let users = router.group("user").using(SomeMiddleware())
users.get("auth", use: userAuthHandler)
Use Cached Connection For Parameter Lookup
Fluent makes it easy to fetch a Model from its ID in a routing parameter (e.g. GET /photos/87
). Just conform the Model to Parameter
, and it can be used in the route and retrieved from the request:
final class PhotoController: RouteCollection {
func boot(router: Router) throws {
router.get("photos", Photo.parameter, use: getPhoto)
}
func getPhoto(_ req: Request) throws -> Future<Photo> {
return try req.parameter(Photo.self)
}
}
This PR (vapor/fluent#385) makes that lookup slightly more efficient by using an existing cached connection, or creating a new pooled connection.
Built For Vapor
This week we saw quite a few libraries spring up to ease Vapor 3 development. It’s super exciting to see people building off the basic types that Vapor provides, and I can’t wait to see what else people come up with!
Terse
Anyone who’s done any asynchronous programming in the past 20 years knows all too much about callback hell. While Vapor 3 attempts to limit nested callbacks by encouraging the use of Promise
chains, sometimes you just can’t get around it. Terse, created by @John-Connolly attempts to further alleviate callback hell by introducing functional programming-inspired custom infix operators.
Syntax example:
func get(with client: RedisClient, for data: RedisData) -> Future<RedisData> {
return client.command("GET", [data])
}
private func terse(_ req: Request) throws -> Future<RedisResponse> {
let client = try req.make(RedisClient.self)
return get(with: client, for: "KEY")
>>- curry(get)(client)
>>- curry(get)(client)
>>- curry(get)(client)
<^> RedisResponse.init
}
https://github.com/John-Connolly/terse
Awesome Vapor
Speaking of things built for Vapor, this next entry should make you happy if you ever needed to complete a task and wondered for a while whether there wasn’t an existing library that would help you reach your goal more easily. Well, wonder no more! The Awesome Vapor document aims to compile everything awesome that works out of the box with Vapor. Besides libraries, the catalogue also contains useful resources for learning more about Vapor and lists well-done open-source projects that might inspire you.
As is the case with all existing “awesome-” listings for other software projects, Awesome Vapor focuses on quality of its entries, not quantity, so if you decide to follow the list’s recommendation, you know you made the right choice.
https://github.com/Cellane/awesome-vapor
Vapor-Community Extensions
The community has been coming up with all sorts of useful extensions to core Vapor types, but not all of them belong in Vapor itself for one reason or another. @gperdomor has opened up extensions repos for a few packages to fill this void. By adding these to your project, you’ll get access to a slew of helper methods! If you’re looking to contribute, these are a good opportunity.
https://github.com/vapor-community/vapor-extensions
https://github.com/vapor-community/engine-extensions
https://github.com/vapor-community/async-extensions
https://github.com/vapor-community/fluent-extensions
Microtutorial
Standard promise chains are perfectly fine if only one Future
needs to complete before you can move onto the next step, but what if you need the results from two Future
s? One of the problems run into here is that you often need to start embedding promise callbacks and all of a sudden you’re back to callback hell.
Let’s say you have a route that creates a User
. That user has some metadata that is connected to it, but you store it in a different database table to normalize your database. You need to create a Pivot
so you can get the user’s metadata. Because a pivot is based on the models ID’s, both the models have to be saved in the database first. To make sure the models are saved before you create the pivot, you can use Async
’s global flatMap
method, which takes in a return type, and two or three futures. There is then a completion handler that passes in the results of the futures where you can create your pivot:
let user = User(name: "Jamie", age: 30)
let metadata = Metadata(dateCreated: Date())
flatMap(to: User.self, user.save(on: request), metadata.save(on: request)) { user, metadata in
return UserMetadata(user, metadata).save(on: request).transform(to: user)
}