继续创作,加快成长!这是我参与「日新计划 10 月更文挑战」的第23天,点击查看活动概况
什么是 Sourcery?
Sourcery 是当下最流行的 Swift 代码生成东西之一。其背后运用了 SwiftSyntax,旨在经过自动生成样板代码来节约开发人员的时刻。Sourcery 经过扫描一组输入文件,然后借助模板的协助,自动生成模板中界说的 Swift 代码。
示例
考虑一个为摄像机会话服务供给公共 API 的协议:
protocol Camera {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}
当运用此新的 Camera service
进行单元测验时,咱们希望保证 AVCaptureSession
没有被真的创立。咱们只是希望确认 camera service
被测验体系(SUT)正确的调用了,而不是去测验 camera service
自身。
因而,创立一个协议的 mock 完结,运用空办法和一组变量来协助咱们进行单元测验,并断言(asset
)进行了正确的调用是有意义的。这是软件开发中非常常见的一个场景,假如你曾保护过一个包括大量单元测验的大型代码库,这么做也或许有点庸俗。
好吧~不用忧虑!Sourcery 会协助你!⭐️ 它有一个叫做 AutoMockable 的模板,此模板会为恣意输入文件中遵守 AutoMockable
协议的协议生成 mock 完结。
注意:在本文中,我扩展地运用了术语
Mock
,由于它与 Sourcery 模板运用的术语一致。Mock
是一个相当重载的术语,但通常,假如我要创立一个 两层测验,我会依据它的用途进一步指定类型的名称(或许是Spy
、Fake
、Stub
等)。假如您有兴趣了解更多关于两层测验的信息,马丁福勒(Martin Fowler)有一篇非常好的文章,能够解释这些差异。
现在,咱们让 Camera
遵守 AutoMockable
。该接口的唯一目的是充当 Sourcery 的方针,从中查找并生成代码。
import UIKit
// Protocol to be matched
protocol AutoMockable {}
public protocol Camera: AutoMockable {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}
此刻,能够在上面的输入文件上运转 Sourcery 指令,指定 AutoMockable
模板的途径:
sourcery --sources Camera.swift --templates AutoMockable.stencil --output .
本文经过供给一个 .sourcery.yml
文件来配置 Sourcery 插件。假如供给了配置文件或 Sourcery 能够找到配置文件,则将疏忽与其值冲突的一切指令行参数。假如您想了解有关配置文件的更多信息,Sourcery的 repo 中有一节介绍了该主题。
指令履行结束后,在输出目录下会生成一个 模板名
加 .generated.swift
为后缀的文件。在此例是 ./AutoMockable.generated.swift
:
// Generated using Sourcery 1.8.2 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// swiftlint:disable line_length
// swiftlint:disable variable_name
import Foundation
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
class CameraMock: Camera {
//MARK: - start
var startCallsCount = 0
var startCalled: Bool {
return startCallsCount > 0
}
var startClosure: (() -> Void)?
func start() {
startCallsCount += 1
startClosure?()
}
//MARK: - stop
var stopCallsCount = 0
var stopCalled: Bool {
return stopCallsCount > 0
}
var stopClosure: (() -> Void)?
func stop() {
stopCallsCount += 1
stopClosure?()
}
//MARK: - capture
var captureCallsCount = 0
var captureCalled: Bool {
return captureCallsCount > 0
}
var captureReceivedCompletion: ((UIImage?) -> Void)?
var captureReceivedInvocations: [((UIImage?) -> Void)] = []
var captureClosure: ((@escaping (UIImage?) -> Void) -> Void)?
func capture(_ completion: @escaping (UIImage?) -> Void) {
captureCallsCount += 1
captureReceivedCompletion = completion
captureReceivedInvocations.append(completion)
captureClosure?(completion)
}
//MARK: - rotate
var rotateCallsCount = 0
var rotateCalled: Bool {
return rotateCallsCount > 0
}
var rotateClosure: (() -> Void)?
func rotate() {
rotateCallsCount += 1
rotateClosure?()
}
}
上面的文件(AutoMockable.generated.swift
)包括了你对mock的希望:运用空办法完结与方针协议的一致性,以及查看是否调用了这些协议办法的一组变量。最棒的是… Sourcery 为您编写了这一切!
怎样运转 Sourcery?
怎样运用 Swift package 运转 Sourcery?
至此你或许在想怎么以及怎样在 Swift package 中运转 Sourcery。你能够手动履行,然后讲文件拖到包中,或者从包目录中的指令运转脚本。可是关于 Swift Package 有两种内置方式运转可履行文件:
- 经过指令行插件,可依据用户输入恣意运转
- 经过构建东西插件,该插件作为构建过程的一部分运转。
在本文中,我将介绍 Sourcery 指令行插件,但我现已在编写第二部分,其间我将创立构建东西插件,这带来了许多风趣的挑战。
创立插件包
让咱们首要创立一个空包,并去掉测验和其他咱们现在不需求的文件夹。然后咱们能够创立一个新的插件 Target
并添加 Sourcery 的二进制文件作为其依靠项。
为了让消费者运用这个插件,它还需求被界说为一个产品:
// swift-tools-version: 5.6
import PackageDescription
let package = Package(
name: "SourceryPlugins",
products: [
.plugin(name: "SourceryCommand", targets: ["SourceryCommand"])
],
targets: [
// 1
.plugin(
name: "SourceryCommand",
// 2
capability: .command(
intent: .custom(verb: "sourcery-code-generation", description: "Generates Swift files from a given set of inputs"),
// 3
permissions: [.writeToPackageDirectory(reason: "Need access to the package directory to generate files")]
),
dependencies: ["Sourcery"]
),
// 4
.binaryTarget(
name: "Sourcery",
path: "Sourcery.artifactbundle"
)
]
)
让咱们一步一步地细心查看上面的代码:
- 界说插件方针。
- 以
custom
为目的,界说了.command
功用,由于没有任何默认功用(documentationGeneration
和sourceCodeFormatting
)与该指令的用例匹配。给动词一个合理的名称很重要,由于这是从指令行调用插件的方式。 - 插件需求向用户请求写入包目录的权限,由于生成的文件将被转储到该目录。
- 为插件界说了一个二进制方针文件。这将答应插件经过其上下文访问可履行文件。
我知道我并没有详细介绍上面的一些概念,但假如您想了解更多关于指令插件的信息,这里有一篇由 Tibor Bdecs 写的超级棒的文章⭐。假如你还想了解更多关于 Swift Packages 中二级制的方针(文件),我相同有一篇现今 Swift 包中的二进制方针。
编写插件
现在现已创立了包,是时分编写一些代码了!咱们首要在 Plugins/SourceryCommand
下创立一个名为 SourceryCommand.swift
的文件,然后添加一个 CommandPlugin
协议的结构体,这将作为该插件的进口:
import PackagePlugin
import Foundation
@main
struct SourceryCommand: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {
}
}
然后咱们为指令编写完结:
func performCommand(context: PluginContext, arguments: [String]) async throws {
// 1
let configFilePath = context.package.directory.appending(subpath: ".sourcery.yml").string
guard FileManager.default.fileExists(atPath: configFilePath) else {
Diagnostics.error("Could not find config at: \(configFilePath)")
return
}
//2
let sourceryExecutable = try context.tool(named: "sourcery")
let sourceryURL = URL(fileURLWithPath: sourceryExecutable.path.string)
// 3
let process = Process()
process.executableURL = sourceryURL
// 4
process.arguments = [
"--disableCache"
]
// 5
try process.run()
process.waitUntilExit()
// 6
let gracefulExit = process.terminationReason == .exit && process.terminationStatus == 0
if !gracefulExit {
Diagnostics.error(" The plugin execution failed")
}
}
让咱们细心看看上面的代码:
- 首要
.sourcery.yml
文件有必要在包的根目录,否则将报错。这将使 Sourcery 神奇的作业,并使包可配置。 - 可履行文件途径的 URL 是从指令的上下文中检索的。
- 创立一个进程,并将 Sourcery 的可履行文件的 URL 设置为其可履行文件途径。
- 这一步有点费事。Sourcery 运用缓存来减少后续运转的代码生成时刻,但问题是这些缓存是在包文件夹之外读取和写入的文件。插件的沙箱规则不答应这样做,因而
--disableCache
标志用于禁用此行为并答应指令运转。 - 进程同步运转并等待。
- 最终,查看进程停止状况和代码,以保证进程已正常退出。在任何其他情况下,经过
Diagnostics
API 向用户告知过错。
就这样!现在让咱们运用它
运用(插件)包
考虑一个用户正在运用插件,该插件将依靠项引入了他们的 Package.swift
文件:
// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "SourceryPluginSample",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SourceryPluginSample",
targets: ["SourceryPluginSample"]),
],
dependencies: [
.package(url: "https://github.com/pol-piella/sourcery-plugins.git", branch: "main")
],
targets: [
.target(
name: "SourceryPluginSample",
dependencies: [],
exclude: ["SourceryTemplates"]
),
]
)
注意,与构建东西插件不同,指令插件不需求应用于任何方针,由于它们需求手动运转。
用户只运用了上面的 AutoMockable
模板(能够在 Sources/SourceryPluginSample/SourceryTemplates
下找到),与本文前面显示的示例相匹配:
protocol AutoMockable {}
protocol Camera: AutoMockable {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}
依据插件的要求,用户还供给了一个位于 SourceryPluginSample
目录下的 .sourcery.yml
配置文件:
sources:
- Sources/SourceryPluginSample
templates:
- Sources/SourceryPluginSample/SourceryTemplates
output: Sources/SourceryPluginSample
运转指令
用户现已设置好了,可是他们现在怎么运转包? 有两种办法:
指令行
运转插件的一种办法是用指令行。能够经过从包目录中运转 swift package plugin --list
来检索特定包的可用插件列表。然后能够从列表中选择一个包,并经过运转 swift package <command's verb>
来履行,在这个特别的例子中,运转: swift package sourcery-code-generation
。
注意,由于此包需求特别权限,因而 --allow-writing-to-package-directory
有必要与指令一同运用。
此刻,你或许会想,为什么我要操心编写一个插件,仍然有必要从指令行运转,而我能够用一个简略的脚本在几行 bash 中完结相同的作业?好吧,让咱们来看看 Xcode 14 中会呈现什么,你会明白为什么我会发起编写插件。
Xcode
这是运转指令插件最令人兴奋的方式,但不幸的是,它仅在 Xcode 14 中可用。因而,假如您需求运转指令,但尚未运用 Xcode 14,请参阅指令行部分。
假如你正好在运用 Xcode 14,你能够经过在文件资源管理器中右键单击包,从列表中找到要履行的插件,然后单击它来履行包的任何指令。
下一步
这是插件的初始完结。我将研究怎么改善它,使它更加健壮。和往常一样,我非常致力于揭露构建,并使我的文章中的一切内容都开源,这样任何人都能够提交问题或创立任何具有改善或修复的 PRs。这没有什么不同, 这是 公共库房的链接。
此外,假如您喜欢这篇文章,请关注行将到来的第二部分,其间我将制作一个 Sourcery 构建东西插件。我知道这听起来不多,但这不是一项简单的任务!
关于咱们
咱们是由 Swift 爱好者一起保护,咱们会共享以 Swift 实战、SwiftUI、Swift 基础为中心的技能内容,也整理搜集优异的学习资料。