使用 Swift Package 插件生成代码
前言
不久前,我正在工作中开发一项新服务,该服务由 Swift Package 组成,该 Package 公开了一个类似于Decodable
协议,供我们应用程序的其余部分使用。事实上,该协议是从Decodable
本身继承下来的,看起来像这样:
Fetchable.swit
protocol Fetchable: Decodable, Equatable {}
新的 package 将采用符合Fetchable
的类型来尝试从远程或缓存的JSON数据块中解码它们。
由于这项服务对应用程序的正确运行至关重要,作为这项工作的一部分,我们希望确保始终存在故障安全( fail-safe)。因此,我们让该应用程序附带了一个备用的JSON文件,如果远程和缓存的数据解码失败,将使用该文件,来保证程序的正常运行。
无论如何,我们需要符合Fetchable
的新类型从备用数据中正确解码。然而,有一个问题,有时很难发现备用JSON文件或模型本身是否有任何错误,因为解码错误会在运行时发生,并且只有在访问某些屏幕/功能时才会发生。
为了让我们对我们要发送的代码更有信心,我们添加了一些单元测试,试图根据我们附带的备用JSON解码符合Fetchable
协议的每个模型。这些将使我们在CI上有一个早期指示,表明备用数据或模型中存在错误,如果所有测试都通过,我们将确定,一旦我们发布新服务,它始终具有故障安全功能。
我们手动编写了这些测试,但我们很快就意识到这个解决方案是不可扩展的,因为随着越来越多的符合Fetchable
协议的类型被添加,我们引入了大量的代码复制,并可能有人最终忘记为特定功能编写这些测试。
我们考虑过自动化该过程,但由于我们的代码库的性质,我们遇到了一些问题,代码库高度模块化,混合了Xcode项目和Swift Package。一些架构决策还意味着我们必须收集大量符号信息,才能获得生成测试的正确类型。
是什么让我再次关注到它?
在我忘记了这件事一段时间后,Xcode 14的公告允许在Xcode项目中使用 Swift Package 插件,以及一些架构更改使提取类型信息变得容易得多,这让我有动力再次开始研究这个问题。
请注意,Xcode项目的构建工具插件尚未按照发布说明在Xcode 14 Beta 2中提供,但将在Xcode 14的未来版本中提供。
在过去的几周里,我一直在研究如何使用软件包插件生成单元测试,在这篇文章中,我将解释我在向哪个方向尝试以及它涉及了什么。
实施细节
我开始了一项任务,即创建一个构建工具插件,与 Xcode 14 引入的命令插件不同,该插件可以任意运行并依赖用户输入,作为Swift软件包构建过程的一部分运行。
我知道我需要创建一个可执行文件,因为 Build Tool 插件依赖这些来执行操作。这个脚本将完全用 Swift 编写,因为这是我最熟悉的语言,并承担以下职责:
- 扫描目标目录并提取所有
.swift
文件。目标将被递归扫描,以确保不会错过子目录。 - 使用sourcekit,或者更具体地说,SourceKitten,扫描这些
.swift
文件并收集类型信息。这将允许提取符合Fetchable
协议的所有类型,以便可以针对它们编写测试。 - 获得这些类型后,生成一个带有
XCTestCase
的.swift
文件,其中包含每种类型的单元测试。
让我们写一些代码吧
与所有 Swift Package 一样,最简单的入门方法是在命令行上运行swift package init
。
这创建了两个目标,一个是包含Fetchable
协议定义和符合该定义的类型的实现代码,另一个是应用插件为此类类型生成单元测试的测试目标。
Package.swit
// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.import PackageDescriptionlet package = Package(name: "CodeGenSample",platforms: [.macOS(.v10_11)],products: [.library(name: "CodeGenSample",targets: ["CodeGenSample"]),],dependencies: [],targets: [.target(name: "CodeGenSample",dependencies: []),.testTarget(name: "CodeGenSampleTests",dependencies: ["CodeGenSample"])]
)
编写可执行文件
如前所述,所有构建工具插件都需要可执行文件来执行所有必要的操作。
为了帮助开发此命令行,将使用几个依赖项。第一个是SourceKitten——特别是其SourceKitten框架库,这是一个Swift包装器,用于帮助使用Swift代码编写sourcekit请求,第二个是快速参数解析器,这是苹果提供的软件包,可以轻松创建命令行工具,并以更快、更安全的方式解析在执行过程中传递的命令行参数。
在创建executableTarget
并赋予它两个依赖项后,Package.swift
就是这个样子:
Package.swift
// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.import PackageDescriptionlet package = Package(name: "CodeGenSample",platforms: [.macOS(.v10_11)],products: [.library(name: "CodeGenSample",targets: ["CodeGenSample"]),],dependencies: [.package(url: "https://github.com/jpsim/SourceKitten.git", exact: "0.32.0"),.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0")],targets: [.target(name: "CodeGenSample",dependencies: []),.testTarget(name: "CodeGenSampleTests",dependencies: ["CodeGenSample"]),.executableTarget(name: "PluginExecutable",dependencies: [.product(name: "SourceKittenFramework", package: "SourceKitten"),.product(name: "ArgumentParser", package: "swift-argument-parser")])]
)
可执行目标需要一个入口点,因此,在PluginExecutable
目标的源目录下,必须创建一个名为PluginExecutable.swift
的文件,其中所有可执行逻辑都需要创建。
请注意,这个文件可以随心所欲地命名,我倾向于以与我在
Package.swift
中创建的目标相同的方式命名它。
如下所示的脚本导入必要的依赖项,并创建可执行文件的入口点(必须用@main
装饰),并声明在执行时传递的4个输入。
所有逻辑和方法调用都存在于run
函数中,该函数是调用可执行文件时运行的方法。这是ArgumentParser
语法的一部分,如果您想了解更多信息,Andy Ibañez有一篇关于该主题的精彩文章,可能非常有帮助。
PluginExecutable.swift
import SourceKittenFramework
import ArgumentParser
import Foundation@main
struct PluginExecutable: ParsableCommand {@Argument(help: "The protocol name to match")var protocolName: String@Argument(help: "The module's name")var moduleName: String@Option(help: "Directory containing the swift files")var input: String@Option(help: "The path where the generated files will be created")var output: Stringfunc run() throws {// 1let files = try deepSearch(URL(fileURLWithPath: input, isDirectory: true))// 2setenv("IN_PROCESS_SOURCEKIT", "YES", 1)let structures = try files.map { try Structure(file: File(path: $0.path)!) }// 3var matchedTypes = [String]()structures.forEach { walkTree(dictionary: $0.dictionary, acc: &matchedTypes) }// 4try createOutputFile(withContent: matchedTypes)}// ...
}
现在让我们专注于上面的run
方法,以了解当插件运行可执行文件时会发生什么:
- 首先,扫描目标目录以找到其中的所有
.swift
文件。这是递归完成的,这样子目录就不会错过。此目录的路径作为参数传递给可执行文件。 - 对于上次调用中找到的每个文件,通过SourceKitten发出
Structure
请求,以查找文件中Swift代码的类型信息。请注意,环境变量(IN_PROCESS_SOURCEKIT
)也被设置为true。这需要确保选择源套件的进程中版本,以便它能够遵守插件的沙盒规则。
Xcode附带两个版本的sourcekit可执行文件,一个版本解析进程中的文件,另一个使用XPC向解析进程外文件的守护进程发送请求。后者是mac上的默认版本,为了能够将sourcekit用作插件进程的一部分,必须选择进程中版本。这最近在SourceKitten上作为环境变量实现,是运行引擎盖下使用sourcekit的其他可执行文件的关键,例如
SwiftLint
。
浏览上次调用的所有响应,并扫描类型信息以提取符合
Fetchable
协议的任何类型。在传递给可执行文件的
output
参数指定的位置创建一个输出文件,其中包含每种类型的单元测试。
请注意,上面没有重点介绍每个调用的具体细节,但如果你对实现感兴趣,包含所有代码的repo现在已经在Github上公开了!
创建该插件
与可执行文件一样,必须向Package.swift
添加.plugin
目标,并且必须创建包含插件实现的.swift
文件(Plugins/SourceKitPlugin/SourceKitPlugin.swift
)。
Package.swift
// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.import PackageDescriptionlet package = Package(name: "CodeGenSample",platforms: [.macOS(.v10_11)],products: [.library(name: "CodeGenSample",targets: ["CodeGenSample"]),],dependencies: [.package(url: "https://github.com/jpsim/SourceKitten.git", exact: "0.32.0"),.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0")],targets: [.target(name: "CodeGenSample",dependencies: []),.testTarget(name: "CodeGenSampleTests",dependencies: [“CodeGenSample"],
plugins: [“SourceKitPlugin”],),.executableTarget(name: "PluginExecutable",dependencies: [.product(name: "SourceKittenFramework", package: "SourceKitten"),.product(name: "ArgumentParser", package: "swift-argument-parser")]),.plugin(name: "SourceKitPlugin",capability: .buildTool(),dependencies: [.target(name: "PluginExecutable")])]
)
以下代码显示了插件的初始实现,其struct
符合BuildToolPlugin
的协议。这需要实现一个返回具有单个构建命令的数组的createBuildCommands
方法。
此插件使用
buildCommand
而不是preBuildCommand
,因为它需要作为构建过程的一部分运行,而不是在它之前运行,因此它有机会构建和使用它所依赖的可执行文件。在这种情况下,支持使用buildCommand
的另一点是,它只会在输入文件更改时运行,而不是每次构建目标时运行。
此命令必须为要运行的可执行文件提供名称和路径,这可以在插件的上下文中找到:
SourceKitPlugin.swift
import PackagePlugin@main
struct SourceKitPlugin: BuildToolPlugin {func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {return [.buildCommand(displayName: "Protocol Extraction!",executable: try context.tool(named: "PluginExecutable").path,arguments: ["FindThis",
使用 Swift Package 插件生成代码相关推荐
- mybaties插件生成代码
指定插件运行什么xml,关于如何用idea创建一个maven项目,可以看我以前写的博客 <?xml version="1.0" encoding="UTF-8&qu ...
- 使用 MyBatis 的 Maven 插件生成代码
我们无需手动编写 实体类.DAO.XML 配置文件,只需要使用 MyBatis 提供的一个 Maven 插件就可以自动生成所需的各种文件便能够满足基本的业务需求,如果业务比较复杂只需要修改相关文件即可 ...
- spring boot中利用mybatis-generator插件生成代码
使用Idea在spring boot中集成mybatis-generator,自动生成mapper.xml model dao 文件 一.配置 pom.xml 在pom.xml的<plugi ...
- 【MyBatis框架】mybatis逆向工程自动生成代码
逆向工程 1.什么是逆向工程 mybaits需要程序员自己编写sql语句,mybatis官方提供逆向工程 可以针对单表自动生成mybatis执行所需要的代码(mapper.java,mapper.xm ...
- idea package自动生成_懒人必备,IntelliJ IDEA中代码一键生成
之前有不少小伙伴问松哥微人事项目(https://github.com/lenve/vhr)使用的 MyBatis 逆向工程在哪里?其实旧版微人事当时没有使用逆向工程,是我自己手动敲出来的,当然手动敲 ...
- SpringBoot入门篇--整合mybatis+generator自动生成代码+druid连接池+PageHelper分页插件
我们这一一篇博客讲的是如何整合Springboot和Mybatis框架,然后使用generator自动生成mapper,pojo等文件.然后再使用阿里巴巴提供的开源连接池druid,这个连接池的好处我 ...
- mybatis-generator-maven-plugin插件自动生成代码的配置方法
1. 第一步,在pom文件中引入如下插件 <plugin><groupId>org.mybatis.generator</groupId><artifactI ...
- idea package自动生成_Idea 自动生成Junit单元测试插件JunitGenerator
JunitGenerator Idea中提供了可以自动生成Junit单元测试的插件,JunitGenerator.本篇文章将介绍如何在idea中安装.配置及使用JunitGenerator,以方便大家 ...
- 用Maven插件生成Mybatis代码/数据库
现在代码管理基本上是采用Maven管理,Maven的好处此处不多说,大家用百度搜索会有很多介绍,本文介绍一下用Maven工具如何生成Mybatis的代码及映射的文件. 一.配置Maven pom.xm ...
最新文章
- String,StringBuffer
- Forerunner:首个面向“多未来”的推测执行技术
- Nginx 介绍配置
- 计算机组成asr实验,计算机组成与结构实验讲义.doc
- mysql中如何去除重复数据_MySQL中如何删除重复数据只保留一条
- 制造业物料清单BOM、智能文档阅读、科学文献影响因子、Celebrated Italian mathematician ZepartzatT Gozinto 与 高津托图...
- codeforces George and Job
- Redis基础笔记 (二)
- Go 判断元素是否在切片中
- 深度学习:基本概要:监督,无监督,半监督,弱监督,多示例,迁移学习
- 【天光学术】社会语言学论文:委婉语合作原则违反的具体体现与影响(节选)
- 【Freeswitch从入门到精通】二、初识Freeswitch
- 让女人无法抗拒的30句表白【实用】
- 101. Domino 10 就要来了
- 共享新风机未来家居生活必备品新鲜空气齐分享
- Intel(R)Dual Band Wireless-AC 3165网卡驱动程序出现问题,WiFi,热点和以太网无法连接
- 系统迁移必知会(多年总结)
- 通过jsp向mysql批量导入数据_对大数据的批量导入MySQL数据库
- Unity 相机参数详解:多相机混合、小地图实现
- JQuery、Ajax基础语法
热门文章