Smoke Framework 是一个轻量级的服务器端服务框架,用 Swift 编写,默认情况下使用 SwiftNIO 作为其网络层。该框架可用于类 REST 或类 RPC 的服务,并可与来自服务模型(如 Swagger/OpenAPI)的代码生成器结合使用。
该框架内置了对 JSON 编码的请求和响应有效负载的支持。
SmokeFramework 遵循与 SmokeAWS 相同的支持策略,请参阅此处。
Smoke Framework 提供了为服务应用程序需要执行的操作指定处理程序的能力。当收到请求时,框架会将请求解码为操作的输入。当处理程序返回时,其响应(如果有)将被编码并在响应中发送。
处理程序的每次调用也会传递一个特定于应用程序的上下文,允许将应用程序范围或调用范围的实体(如其他服务客户端)传递给操作处理程序。使用上下文允许操作处理程序保持纯函数(其返回值由函数的逻辑和输入值决定),因此易于测试。
请参阅 此存储库,了解 Smoke Framework 和相关的 Smoke* 存储库的实际应用示例。
Smoke Framework 提供了一个 代码生成器,它将从 Swagger 2.0 规范文件中生成一个完整的基于 SmokeFramework 的服务的 Swift Package Manager 存储库。
请参阅代码生成器存储库中的说明,了解如何开始使用。
以下步骤假定您刚刚使用 swift package init --type executable
创建了一个新的 swift 应用程序。
Smoke Framework 使用 Swift Package Manager。 要使用该框架,请将以下依赖项添加到您的 Package.swift 中-
dependencies: [
.package(url: "https://github.com/amzn/smoke-framework.git", from: "2.0.0")
]
.target(name: ..., dependencies: [
...,
.product(name: "SmokeOperationsHTTP1Server", package: "smoke-framework"),
]),
如果您尝试编译应用程序,您将收到错误
the product 'XXX' requires minimum platform version 10.12 for macos platform
这是因为 SmokeFramework 项目具有最低 MacOS 版本依赖项。 要更正此问题,需要在 Package.swift 文件中进行一些添加。
指定应用程序支持的语言版本-
targets: [
...
],
swiftLanguageVersions: [.v5]
指定应用程序支持的平台-
name: "XXX",
platforms: [
.macOS(.v10_15), .iOS(.v10)
],
products: [
上下文类型的实例将传递给需要处理的操作的每次调用。 可以将此实例设置为每次调用初始化一次,也可以为应用程序初始化一次。
您需要创建此上下文类型。 没有要求将任何类型作为上下文传递。 以下代码显示了创建上下文类型的示例-
public struct MyApplicationContext {
let logger: Logger
// TODO: Add properties to be accessed by the operation handlers
public init(logger: Logger) {
self.logger = logger
}
}
使用 Smoke Framework 的下一步是定义一个或多个函数来执行您的应用程序所需的操作。 以下代码显示了此类函数的示例-
extension MyApplicationContext {
func handleTheOperation(input: OperationInput) throws -> OperationOutput {
return OperationOutput()
}
}
此特定操作函数接受操作的输入,并且位于上下文的扩展中(使其可以访问此类型的任何属性或函数),同时返回操作的输出。
对于 HTTP1,操作输入可以符合 OperationHTTP1InputProtocol,它定义了如何从 HTTP1 请求构造输入类型。 类似地,操作输出可以符合 OperationHTTP1OutputProtocol,它定义了如何从输出类型构造 HTTP1 响应。 两者还必须符合 Validatable 协议,从而有机会验证任何字段约束。
作为替代方法,如果操作输入和输出仅从 HTTP1 请求和响应的一个部分构造,则它们都可以符合 Codable
协议。
Smoke Framework 还支持其他内置和自定义操作函数签名。 有关更多信息,请参阅操作函数和扩展点部分。
定义了所需的操作处理程序后,就可以指定如何为传入的请求选择它们。
Smoke Framework 提供了 SmokeHTTP1HandlerSelector 协议,用于将处理程序添加到选择器。
import SmokeOperationsHTTP1
public enum MyOperations: String, Hashable, CustomStringConvertible {
case theOperation = "TheOperation"
public var description: String {
return rawValue
}
public var operationPath: String {
switch self {
case .theOperation:
return "/theOperation"
}
}
}
public extension MyOperations {
static func addToSmokeServer<SelectorType: SmokeHTTP1HandlerSelector>(selector: inout SelectorType)
where SelectorType.ContextType == MyApplicationContext,
SelectorType.OperationIdentifer == MyOperations {
let allowedErrorsForTheOperation: [(MyApplicationErrors, Int)] = [(.unknownResource, 404)]
selector.addHandlerForOperationProvider(.theOperation, httpMethod: .POST,
operationProvider: MyApplicationContext.handleTheOperation,
allowedErrors: allowedErrorsForTheOperation)
}
}
添加的每个处理程序都需要指定以下参数
CustomStringConvertible
,该错误类型返回当前错误的标识。Codable
时才需要)Codable
时才需要)最后一步是将应用程序设置为操作服务器。
import Foundation
import SmokeOperationsHTTP1Server
import AsyncHTTPClient
import NIO
import SmokeHTTP1
struct MyPerInvocationContextInitializer: StandardJSONSmokeServerPerInvocationContextInitializer {
typealias ContextType = MyApplicationContext
typealias OperationIdentifer = MyOperations
let serverName = "MyService"
// specify the operations initializer
let operationsInitializer: OperationsInitializerType = MyOperations.addToSmokeServer
/**
On application startup.
*/
init(eventLoopGroup: EventLoopGroup) throws {
// set up any of the application-wide context
}
/**
On invocation.
*/
public func getInvocationContext(
invocationReporting: SmokeServerInvocationReporting<SmokeInvocationTraceContext>) -> MyApplicationContext {
// create an invocation-specific context to be passed to an operation handler
return MyApplicationContext(logger: invocationReporting.logger)
}
/**
On application shutdown.
*/
func onShutdown() throws {
// shutdown anything before the application closes
}
}
SmokeHTTP1Server.runAsOperationServer(MyPerInvocationContextInitializer.init)
您现在可以运行该应用程序,服务器将在端口 8080 上启动。 应用程序将在 SmokeHTTP1Server.runAsOperationServer
调用中阻止。 当服务器已完全关闭并已完成所有请求后,将调用 onShutdown
。 在此函数中,您可以关闭/关闭应用程序启动时创建的任何客户端或凭据。
可选配置步骤是为 Smoke Framework 发出的指标设置报告配置。 这涉及覆盖初始化程序上的默认 reportingConfiguration
属性。 为了发出指标,需要初始化一个 swift-metrics
后端 - 例如 CloudWatchMetricsFactory。
...
struct MyPerInvocationContextInitializer: StandardJSONSmokeServerPerInvocationContextInitializer {
typealias ContextType = MyApplicationContext
typealias OperationIdentifer = MyOperations
let reportingConfiguration: SmokeReportingConfiguration<OperationIdentifer>
let serverName = "MyService"
// specify the operations initializer
let operationsInitializer: OperationsInitializerType = MyOperations.addToSmokeServer
/**
On application startup.
*/
init(eventLoopGroup: EventLoopGroup) throws {
// set up any of the application-wide context
// for the server, only report the latency metrics
// only report 5XX error counts for TheOperation (even if additional operations are added in the future)
// only report 4XX error counts for operations other than TheOperation (as they are added in the future)
self.reportingConfiguration = SmokeReportingConfiguration(
successCounterMatchingRequests: .none,
failure5XXCounterMatchingRequests: .onlyForOperations([.theOperation]),
failure4XXCounterMatchingRequests: .exceptForOperations([.theOperation]),
requestReadLatencyTimerMatchingRequests: .none,
latencyTimerMatchingRequests: .all,
serviceLatencyTimerMatchingRequests: .all,
outwardServiceCallLatencyTimerMatchingRequests: .all,
outwardServiceCallRetryWaitTimerMatchingRequests: .all)
}
...
}
SmokeHTTP1Server.runAsOperationServer(MyPerInvocationContextInitializer.init)
要让您的应用程序参与分布式跟踪,请在应用程序初始化程序中添加属性 enableTracingWithSwiftConcurrency
,其值为 true。
struct MyPerInvocationContextInitializer: StandardJSONSmokeServerPerInvocationContextInitializer {
let enableTracingWithSwiftConcurrency = true
...
}
这将为使用 Swift 并发(async/await)的任何操作处理程序启用跟踪。 您还需要按照 此处 的说明设置 Instrumentation 后端。
Smoke Framework 提供了一个元数据提供程序,可用于修饰从操作处理程序根植的结构化并发树发出的任何日志。 这意味着诸如 internalRequestId
和 incomingOperation
之类的元数据将被添加到从操作处理程序调用的库发出的日志中,即使没有将显式记录器实例传递到库函数中。
import Logging
import SmokeOperations
...
let metadataProvider = Logging.MetadataProvider.smokeframework
let factory = <provided by your logging backend>
LoggingSystem.bootstrap(factory, metadataProvider: metadataProvider)
应用程序上下文类型的实例在应用程序启动时创建,并传递给操作处理程序的每次调用。 该框架对此类型没有任何限制,只是将其传递给操作处理程序。 建议此上下文是不可变的,因为它可能会同时传递给多个处理程序。 否则,上下文类型负责处理其自身的线程安全性。
建议应用程序使用强类型上下文,而不是一堆东西,例如字典。
操作委托处理诸如将请求编码和解码为处理程序的输入和输出之类的细节。
Smoke Framework 提供了 JSONPayloadHTTP1OperationDelegate 实现,该实现期望 JSON 编码的请求正文作为处理程序的输入,并将输出作为 JSON 编码的响应正文返回。
每个 addHandlerForOperation
调用都可以选择接受一个操作委托,以便在该处理程序被选中时使用。 这可以在操作具有特定的编码或解码要求时使用。 在服务器启动时会设置一个默认操作委托,用于没有特定处理程序的操作,或者当没有处理程序匹配请求时使用。
JSONPayloadHTTP1OperationDelegate
采用符合 HTTP1OperationTraceContext 协议的泛型参数。 此协议可用于提供请求级别的跟踪。 此协议的要求定义此处。
一个默认实现 - SmokeInvocationTraceContext - 使用请求和响应标头提供一些基本跟踪。
每个处理程序都提供一个函数,以便在选择处理程序时调用。 默认情况下,Smoke 框架在上下文类型上提供四个函数签名,此函数可以符合以下签名-
((InputType) throws -> ())
:没有输出的同步方法。((InputType) throws -> OutputType)
:带有输出的同步方法。((InputType, (Swift.Error?) -> ()) throws -> ())
:没有输出的异步方法。((InputType, (Result<OutputType, Error>) -> ()) throws -> ())
:带有输出的异步方法。由于 Swift 类型推断,处理程序可以在这些不同的签名之间切换,而无需更改处理程序选择器声明 - 只需更改函数签名就足够了。
同步变体将在函数返回时立即返回响应,无论是否带有空正文或编码的返回值。 异步变体将在调用提供的结果处理程序时返回响应。
public protocol Validatable {
func validate() throws
}
在所有情况下,InputType 和 OutputType 类型都必须符合 Validatable
协议。 此协议使类型有机会验证其字段 - 例如字符串长度、数值范围验证。 Smoke Framework 将在将操作输入传递给处理程序之前以及从处理程序接收操作输出之后调用 validate-
如果操作输入未能通过其验证调用(通过抛出错误),则框架将以 400 ValidationError 响应使操作失败,表明调用方存在错误(框架也会在信息级别记录此事件)。
如果操作输出未能通过其验证调用(通过抛出错误),则框架将以 500 内部服务器错误使操作失败,表明服务逻辑存在错误(框架也会在错误级别记录此事件)。
此外,Smoke Framework 还提供了一个选项,可以将操作处理程序声明为上下文类型之外的独立函数。 上下文类型直接传递给这些函数。
func handleTheOperation(input: OperationInput, context: MyApplicationContext) throws -> OperationOutput {
return OperationOutput()
}
在这种情况下,添加处理程序选择略有不同 -
import SmokeOperationsHTTP1
public func addOperations<SelectorType: SmokeHTTP1HandlerSelector>(selector: inout SelectorType)
where SelectorType.ContextType == MyApplicationContext,
SelectorType.OperationIdentifer == MyOperations {
let allowedErrorsForTheOperation: [(MyApplicationErrors, Int)] = [(.unknownResource, 404)]
selector.addHandlerForOperation(.theOperation, httpMethod: .POST,
operation: handleTheOperation,
allowedErrors: allowedErrorsForTheOperation)
}
当使用这种样式的操作处理程序时,也可以使用四种函数签名。
((InputType, ContextType) throws -> ())
: 无输出的同步方法。((InputType, ContextType) throws -> OutputType)
: 有输出的同步方法。((InputType, ContextType, (Swift.Error?) -> ()) throws -> ())
: 无输出的异步方法。((InputType, ContextType, (Result<OutputType, Error>) -> ()) throws -> ())
: 有输出的异步方法。默认情况下,从操作处理程序抛出的任何错误都会导致操作失败,并且框架将向调用者返回 500 内部服务器错误(框架还会在Error级别记录此事件)。此行为可防止内部错误信息的任何意外泄漏。
public typealias ErrorIdentifiableByDescription = Swift.Error & CustomStringConvertible
public typealias SmokeReturnableError = ErrorIdentifiableByDescription & Encodable
可以通过遵循 Swift.Error
、CustomStringConvertible
和 Encodable
协议,并且在设置操作处理程序的 addHandlerForUri
调用中的 allowedErrors 下指定错误,来显式编码错误并将其返回给调用者。 例如 -
public enum MyError: Swift.Error {
case theError(reason: String)
enum CodingKeys: String, CodingKey {
case reason = "Reason"
}
}
extension MyError: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .theError(reason: let reason):
try container.encode(reason, forKey: .reason)
}
}
}
extension MyError: CustomStringConvertible {
public var description: String {
return "TheError"
}
}
当从操作处理程序返回此类错误时 -
Smoke Framework 旨在使操作处理程序的测试变得简单。 建议操作处理程序是纯函数(其返回值由函数的逻辑和输入值决定)。 在这种情况下,可以在单元测试中调用该函数,并使用适当构造的输入和上下文实例。
建议使用特定于应用程序的上下文来区分发布和测试执行之间的行为 - 例如模拟服务客户端、随机数生成器等。 通常,这将通过将所有测试逻辑保留在测试函数中来创建更易于维护的测试。
如果要运行 Smoke Framework 中的所有测试用例,请打开命令行并转到 smoke-framework
(根)目录,运行 swift test
,然后您应该能够看到测试用例结果。
Smoke Framework 旨在扩展到其当前功能之外 -
JSONPayloadHTTP1OperationDelegate
提供基本的 JSON 有效负载编码和解码。 HTTP1OperationDelegate
协议可用于创建提供替代有效负载编码和解码的委托。 当需要 HttpRequestHead 和请求正文时,此协议的实例会在解码输入和编码输出时获得整个 HttpRequestHead 和请求正文。StandardSmokeHTTP1HandlerSelector
提供了一个处理程序选择器,该选择器比较 HTTP URI 和动词以选择处理程序。 SmokeHTTP1HandlerSelector
协议可用于创建一个选择器,该选择器可以使用 HTTPRequestHead 中的任何属性(例如标头)来选择处理程序。StandardSmokeHTTP1HandlerSelector
符合您的要求,也可以对其进行扩展以支持其他函数签名。 有关此方面的示例,请参见内置的函数签名(可以在 OperationHandler+nonblockingWithInputWithOutput.swift 中找到)。OperationHandler
的初始化程序提供了一个与协议无关的层 - 例如 [1] - 可以被特定于协议的层(例如 HTTP1 的 [2])使用,以抽象不同操作类型的特定于协议的处理。本库基于 Apache 2.0 许可证授权。