一个用于 Vapor 4 的开发者体验(DX)改进层。
Swift 是一门非常安全的语言,这主要归功于其出色的语法特性。这使得 Swift 非常适合服务器端工作,在服务器端,您希望尽可能减少停机时间。然而,Vapor 是基于 NIO 构建的,而 NIO 使用futures(未来值)。使用 future 使得利用 Swift 的安全性变得更加困难。
Sublimate 使 Vapor 的使用过程化,就像编写普通代码一样。
您会遇到更少这种情况。
您可以使用 Sublimate 编写普通的 Swift 代码。
README
! Vapor 的文档… 缺乏 ¯\(ツ)/¯func route(on rq: Request) -> EventLoopFuture<[String]> {
do {
let userID = try rq.auth.required(User.self).requireID()
guard let groupID = rq.parameters.get("groupID") else { throw Abort(.badRequest) }
return Group.find(groupID, on: rq)
.unwrap(or: Abort(.notFound))
.flatMap { group -> EventLoopFuture<[Association]> in
guard group.enrolled else {
return rq.eventLoop.makeFailedFuture(Abort(.notAcceptable))
}
guard group.ownerID == userID else {
return rq.eventLoop.makeFailedFuture(Abort(.unauthorized))
}
return group.$associations.query(on: rq).all()
}.map {
$0.map(\.email)
}
} catch {
return rq.eventLoop.makeFailedFuture(error)
}
}
与
let route = sublimate { (rq, user: User) -> [String] in // §
let group = try Group.find(or: .abort, id: rq.parameters.get("groupID"), on: rq) // †
guard group.enrolled else { throw Abort(.notAcceptable) }
guard group.ownerID == try user.requireID() else { throw Abort(.unauthorized) }
return try group.$associations.all(on: rq).map(\.email) // ‡
}
§ 如果您使用双参数版本的
sublimate()
,我们会自动为您解码Vapor.Authenticatable
。
† 我们的find
函数接受可选的 ID,并且可以为您抛出一个格式良好的Abort
。
‡ 我们提供便捷函数来保持代码简洁;在这里,您无需先调用query()
。
我们还提供 SublimateMigration
,它将您的常规迁移包装在 Sublimate+事务层中,以获得更具声明性的语法。
import Fluent
import SQLKit
public struct RenameMultipleItems: Migration {
public func prepare(on db: Database) -> EventLoopFuture<Void> {
db.transaction { db in
let tx = (db as! SQLDatabase)
return tx.raw(#"UPDATE payment_info SET "paymentStatus" = 'noPaymentMethod' WHERE "paymentID" IS NULL"#).run().flatMap {
tx.raw(#"UPDATE payment_info SET "paymentStatus" = 'paymentMethodReceived' WHERE "paymentID" IS NOT NULL AND "paymentStatus" = 'unpaid'"#).run().flatMap {
tx.raw(#"UPDATE payment_info SET "paymentStatus" = 'errored' WHERE "paymentStatus" = 'error'"#).run()
}
}
}
}
public func revert(on db: Database) -> EventLoopFuture<Void> {
db.transaction { db in
let tx = (db as! SQLDatabase)
return tx.raw(#"UPDATE payment_info SET "paymentStatus" = 'unpaid' WHERE "paymentID" IS NULL"#).run().flatMap {
tx.raw(#"UPDATE payment_info SET "paymentStatus" = 'unpaid' WHERE "paymentID" IS NOT NULL AND "paymentStatus" = 'paymentMethodReceived'"#).run().flatMap {
tx.raw(#"UPDATE payment_info SET "paymentStatus" = 'error' WHERE "paymentStatus" = 'errored'"#).run()
}
}
}
}
}
与
import Sublimate
import Fluent
public struct OnlyHaveOnePaymentStatusType: SublimateMigration {
public func prepare(on db: CO₂DB) throws {
try db.run(sql: #"UPDATE payment_info SET "paymentStatus" = 'noPaymentMethod' WHERE "paymentID" IS NULL"#)
try db.run(sql: #"UPDATE payment_info SET "paymentStatus" = 'paymentMethodReceived' WHERE "paymentID" IS NOT NULL AND "paymentStatus" = 'unpaid'"#)
try db.run(sql: #"UPDATE payment_info SET "paymentStatus" = 'errored' WHERE "paymentStatus" = 'error'"#)
}
public func revert(on db: CO₂DB) throws {
try db.run(sql: #"UPDATE payment_info SET "paymentStatus" = 'unpaid' WHERE "paymentID" IS NULL"#)
try db.run(sql: #"UPDATE payment_info SET "paymentStatus" = 'unpaid' WHERE "paymentID" IS NOT NULL AND "paymentStatus" = 'paymentMethodReceived'"#)
try db.run(sql: #"UPDATE payment_info SET "paymentStatus" = 'error' WHERE "paymentStatus" = 'errored'"#)
}
}
import Sublimate
import Vapor
private Response: Encodable {
let foo: Foo
let bar: Bool
}
let route = sublimate { rq -> Response in
// ^^ `rq` is not a `Vapor.Request`, it is our own object that wraps the Vapor `Request`
guard let parameter = rq.parameters.get("id") else {
throw Abort(.badRequest)
}
let foo = try Foo.query(on: rq)
.filter(\.$foo == parameter)
.first(or: .abort)
// ^^ `foo` is the model object, not a future
// `first(or: .abort)` throws a well formed `.notFound` error if no results are found
print(foo.baz)
return Foo(foo: foo, bar: Bool.random())
}
app.routes.get("foo", ":id", use: route)
import Sublimate
import Vapor
private Response: Content { // must be Content due to Vapor 4 restriction on returning Arrays
let foo: Int
let bar: Bool
}
let route = sublimate { (rq, user: User) -> [Response] in
// ^^ `User` is your `Authenticatable` implementation
// Sublimate automatically fetches this when you use the 2 parameter variant for your convenience
let foos = try Foo.query(on: rq)
.filter(\.$something == user.something)
.all()
// more easily use great Swift features like guard
guard foos.count >= 2 else { throw … }
// more easily use `for` loops and everything else too
for foo in foos where foo.baz == .baz {
guard … else { throw … }
}
// `Sequence.map` not `EventLoopFuture.map`
return foos.map {
try Response(foo: $0.foo, bar $0.bar == .bar)
}
}
app.routes.get("foo", use: route)
import Sublimate
import Vapor
let route = sublimate(in: .transaction) { rq in
// ^^ if you return `Void` we send back an HTTP 200 (`Void` is chosen by Swift if you specify no return type)
// Use `.transaction` to have the whole route in a transaction
let rows = try rq.raw(sql: """
SELECT * from \(raw: table)
""").all(decoding: MyModel.self)
// ^^ easy raw SQL
// …
}
我们尝试为 Vapor 和 Fluent 提供的所有内容提供 sublimation,因此通常您会发现它开箱即用。
首先在一个路由上试用,看看你是否喜欢它,您不必全部使用 Sublimate。
使用 sublimate(in: .transaction, use: myRoute)
,整个路由将在事务中执行。
我们提供 SublimateModelMiddleware
。
package.dependencies.append(.package(
name: "Sublimate",
url: "https://github.com/myhealthily/sublimate.git",
from: "1.0.0"
))
然后,一个 target 需要依赖 Sublimate
.target(name: …, dependencies: ["Sublimate"]),
Sublimate 是 Request
和 Database
对之上的一小段包装器,它镜像了大多数函数并调用 wait()
。
这是可行的,因为我们还生成一个单独的线程来在其中 wait()
。
我们发现,无论如何,在进行 Vapor 开发时,您通常必须一次获取一个东西。
如果您需要同时触发多个请求,仍然可以这样做(在您的 Sublimate 对象的 rq
属性上查询,然后使用我们的 flatten()
函数)。
Sublimate 与 Vapor 一样具有线程安全性;请参阅他们的指南。
† 我们运行一些 (基本) 指标,性能差异可以忽略不计
Content
)var Request.db
)†SQLDatabase.raw
,SQLKit 实际上是 Fluent 的依赖项无论如何)† 由于需要
rq.db
,我们不能只依赖 FluentKit。