Sublimate badge-platforms badge-languages badge-ci badge-codecov

一个用于 Vapor 4 的开发者体验(DX)改进层。

原理

Swift 是一门非常安全的语言,这主要归功于其出色的语法特性。这使得 Swift 非常适合服务器端工作,在服务器端,您希望尽可能减少停机时间。然而,Vapor 是基于 NIO 构建的,而 NIO 使用futures(未来值)。使用 future 使得利用 Swift 的安全性变得更加困难。

Sublimate 使 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。

事务

使用 sublimate(in: .transaction, use: myRoute),整个路由将在事务中执行。

ModelMiddlware

我们提供 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 是 RequestDatabase 对之上的一小段包装器,它镜像了大多数函数并调用 wait()

这是可行的,因为我们还生成一个单独的线程来在其中 wait()

为什么这没问题

我们发现,无论如何,在进行 Vapor 开发时,您通常必须一次获取一个东西。

如果您需要同时触发多个请求,仍然可以这样做(在您的 Sublimate 对象的 rq 属性上查询,然后使用我们的 flatten() 函数)。

线程安全

Sublimate 与 Vapor 一样具有线程安全性;请参阅他们的指南。

注意事项

† 我们运行一些 (基本) 指标,性能差异可以忽略不计

API 文档

自动发布到我们发布版本的 wiki 上.

依赖项

† 由于需要 rq.db,我们不能只依赖 FluentKit。

建议用法