run async instructions in Vapor?
The async / await characteristic is comparatively new in Swift and a few framework authors have not transformed every part to reap the benefits of these new key phrases. Presently, that is the state of affairs with the Command API in Vapor 4. You may already outline async instructions, however there is not any approach to register them utilizing the Vapor framework. Thankfully, there’s a comparatively simple workaround that you should use if you wish to execute instructions utilizing an asynchronous context. 🔀
First we’ll outline a helper protocol and create an asyncRun perform. We’re going to lengthen the unique Command protocol and supply a default implementation for the run technique.
import Vapor
public protocol AsyncCommand: Command {
func asyncRun(
utilizing context: CommandContext,
signature: Signature
) async throws
}
public extension AsyncCommand {
func run(
utilizing context: CommandContext,
signature: Signature
) throws {
let promise = context
.software
.eventLoopGroup
.subsequent()
.makePromise(of: Void.self)
promise.completeWithTask {
strive await asyncRun(
utilizing: context,
signature: signature
)
}
strive promise.futureResult.wait()
}
}
This fashion it’s best to be capable to create a brand new async command and it’s best to implement the asyncRun technique if you wish to name some asynchronous Swift code.
import Vapor
closing class MyAsyncCommand: AsyncCommand {
static let title = "async"
let assist = "This command run asynchronously."
struct Signature: CommandSignature {}
func asyncRun(
utilizing context: CommandContext,
signature: Signature
) async throws {
context.console.data("That is async.")
}
}
It’s potential to register the command utilizing the configure technique, you possibly can do that out by working the swift run Run async snippet if you’re utilizing the usual Vapor template. 💧
import Vapor
public func configure(
_ app: Software
) throws {
app.instructions.use(
MyAsyncCommand(),
as: MyAsyncCommand.title
)
strive routes(app)
}
As you possibly can see it is a fairly neat trick, it is also talked about on GitHub, however hopefully we do not want this workaround for too lengthy and correct async command help will arrive in Vapor 4.x.
Unit testing Vapor instructions
This subject has actually zero documentation, so I believed it might be good to inform you a bit about how one can unit take a look at scripts created through ConsoleKit. To begin with we’d like a TestConsole that we are able to use to gather the output of our instructions. This can be a shameless ripoff from ConsoleKit. 😅
import Vapor
closing class TestConsole: Console {
var testInputQueue: [String]
var testOutputQueue: [String]
var userInfo: [AnyHashable : Any]
init() {
self.testInputQueue = []
self.testOutputQueue = []
self.userInfo = [:]
}
func enter(isSecure: Bool) -> String {
testInputQueue.popLast() ?? ""
}
func output(_ textual content: ConsoleText, newLine: Bool) {
let line = textual content.description + (newLine ? "n" : "")
testOutputQueue.insert(line, at: 0)
}
func report(error: String, newLine: Bool) {
}
func clear(_ kind: ConsoleClear) {
}
var measurement: (width: Int, peak: Int) {
(0, 0)
}
}
Now contained in the take a look at suite, it’s best to create a brand new software occasion utilizing the take a look at setting and configure it for testing functions. Then it’s best to provoke the command that you simply’d like to check and run it utilizing the take a look at console. You simply need to create a brand new context and a correct enter with the required arguments and the console.run
perform will maintain every part else.
@testable import App
import XCTVapor
closing class AppTests: XCTestCase {
func testCommand() throws {
let app = Software(.testing)
defer { app.shutdown() }
strive configure(app)
let command = MyAsyncCommand()
let arguments = ["async"]
let console = TestConsole()
let enter = CommandInput(arguments: arguments)
var context = CommandContext(
console: console,
enter: enter
)
context.software = app
strive console.run(command, with: context)
let output = console
.testOutputQueue
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
let expectation = [
"This is async."
]
XCTAssertEqual(output, expectation)
}
}
The good factor about this resolution is that the ConsoleKit framework will mechanically parse the arguments, choices and the flags. You may present these as standalone array parts utilizing the enter arguments array (e.g. ["arg1", "--option1", "value1", "--flag1"]
).
It’s potential to check command teams, you simply have so as to add the particular command title as the primary argument that you simply’d wish to run from the group and you may merely test the output by way of the take a look at console if you’re on the lookout for the precise command outcomes. 💪