Sourcery GitHub:Sourcery GitHub
Sourcery 官方的文档:Sourcery
Sourcery 入门的博客:Sourcery - Swift元编程实践,告别样板代码
Sourcery 大神的博客:不同角度看问题 - 从 Codable 到 Swift 元编程
Sourcery 是一个 Swift 代码生成的开源命令行工具,它 (通过 SourceKitten) 使用 Apple 的 SourceKit 框架,来分析你的源码中的各种声明和标注,然后套用你预先定义的 Stencil 模板 (一种语法和 Mustache 很相似的 Swift 模板语言) 或者 swifttemplate模版 (一种语法和 EJS 相似的模版) 进行代码生成。
Stencil 语法
语法总览
语法和Django一样,可以参考:模版语法
- {{ ... }}:变量语法,将中间的部分作为变量(或变量的表达式)来解析,解析后的值会作为结果插入到模板中的相应位置上。
- {% ... %}:标签语法(Tag),标签用来表示一些具有特殊功能的语法,比如用来实现判断的if和循环的for。
- {# ... #}:注释语法,不会出现在解析后的结果中。
除此之外还有一个名为Filter的概念,它的语法是这样的:{{ "stencil"|uppercase }},符号|左边是输入的变量,右边就是一个Filter,这里输出了字符串的大写形式。Filter可以有参数,Filter的参数跟随冒号之后并且总是以双引号包含,如:{{ bio|truncatewords:"30" }},这个将显示变量 bio 的前30个词。
常见语法
if的使用
{% if condition %}
... display
{% endif %}
{% if condition1 %}
... display 1
{% elif condition2 %}
... display 2
{% else %}
... display 3
{% endif %}
{% if %} 标签接受 and , or 或者 not 关键字来对多个变量做判断 ,或者对变量取反( not ),例如:
{% if athlete_list and not coach_list %}
athletes 是可用的 和 coaches 变量是不可用的。
{% endif %}
for的使用
{% for athlete in athlete_list %}
{{ athlete.name }}
{% endfor %}
for 可以和 where 一起使用,来更加准确的遍历
{% for type in types.enums where type.implements.AutoHashable or type|annotated:"AutoHashable" %}
{% endfor %}
上面的循环意思为,遍历所有enum类型,并且enum实现了 AutoHashable 或者 增加了 // sourcery: AutoHashable 注释
Swifttemplate 语法
比Stencil模版更加强大灵活,可以写swift代码
举个小🌰
很多时候我们会有想要得到 enum 中所有 case 的集合,以及确定一共有多少个 case 成员的需求。如果纯手写的话,大概是这样的:
enum HogwartsHouse {
// ...
static let all: [HogwartsHouse] = [
.gryffindor,
.hufflepuff,
.ravenclaw,
.slytherin
]
static let count = 4
}
显然这么做对于维护很不友好,没有人能确保时刻记住在添加新的 case 后一定会去更新 all 和 count。对其他有同样需求的 enum,我们也需要重复劳动。Sourcery 就是为了解决这样的需求而生的,相对于手写 all 和 count,我们可以定义一个空协议 EnumSet,然后让 HogwartsHouse 遵守它:
protocol EnumSet {}
extension HogwartsHouse: EnumSet {}
这个定义为 Sourcery 提供了一些提示,Sourcery 需要一些方式来确定为哪部分代码进行代码生成,“实现了某个协议”这个条件就是一个很有用的提示。现在,我们可以创建模板文件了,在同一个文件夹中,新建 enumset.stencil,并书写下面的内容:
{% for enum in types.implementing.EnumSet|enum %}
extension {{ enum.name }} {
{% if not enum.hasAssociatedValues %}
static let all: [{{ enum.name }}] = [
{% for case in enum.cases %} .{{ case.name }}{% if not forloop.last %},{% endif %}
{% endfor %}]
{% endif %}
static let count: Int = {{ enum.cases.count }}
}
{% endfor %}
第一行:{% for enum in types.implementing.EnumSet|enum %}
即“选取那些实现了 EnumSet 的类型,滤出其中所有的 enum 类型,然后对每个 enum 进行枚举”。接下来,我们对这个选出的 enum 类型,为它创建了一个 extension,并对其所有 case 进行迭代,生成 all 数组。最后,将 count 设置为成员个数。
一开始你可能会有不少疑问,types.implementing 是什么,我怎么能知道 enum 在有 name, cases,hasAssociatedValues 之类的属性?Sourcery 有非常详尽的文档,对上述问题,你可以在 Types 和 Enum 的相关页面找到答案。在初上手写模板逻辑时,参照文档是不可避免的。
一切就绪,现在我们可以将源文件喂给模板,来生成最后的代码了:
sourcery --sources ./source.swift --templates ./enumset.stencil
--sources 和 --templates 都可以接受文件夹,Sourcery 会按照后缀自行寻找源文件和模板文件,所以也可以用 sourcery --sources ./ --templates ./ 来替代上面的命令。不过实际操作中还是建议将源文件和模板文件放在不同的文件夹下,方便管理。
在同一个文件夹下,可以看到生成的 enumset.generated.swift 文件:
extension HogwartsHouse {
static let all: [HogwartsHouse] = [
.gryffindor,
.hufflepuff,
.ravenclaw,
.slytherin
]
static let count: Int = 4
}
也可以用swifttemplate来实现:
<%_ for type in types.enums where type.implements["EnumSet"] != nil { -%>
extension <%= type.name %> {
<%_ if !type.hasAssociatedValues {-%>
static let all: [<%= type.name %>] = [
<%_ for (index, typecase) in type.cases.enumerated() { -%>
.<%= typecase.name -%>
<%_ if index < type.cases.count-1 { %> , <% } %>
<%_ } -%>
]
<%_ } -%>
static let count: Int = <%= type.cases.count %>
}
<%_ } -%>
在 Xcode 中使用 Sourcery
安装
通过Cooapods安装pod 'Sourcery',Sourcery 会安装在 $PODS_ROOT/Sourcery/bin/sourcery
也可以通过 Homebrew 安装 brew install sourcery
使用
Sourcery 是一个命令行工具,在 Xcode 项目制定 Target 的 build phase 增加脚本:
./sourcery --sources <sources path> --templates <templates path> --output <output path>
注意:如果使用 Cocoapods 安装,则将 ./sourcery 替换为 "$PODS_ROOT/Sourcery/bin/sourcery"
也可以使用配置文件来指定 sourcery 参数,在项目根目录创建文件 .sourcery.yml ,文件内容:
# sourcery 运行在制定项目
project:
file: xxxx.xcodeproj
target:
name: xxxx
# 模版的路径,可以是相对路径
templates:
- Templates
# 输出配置
output:
# 输出目录或文件,如果是文件,则所有生成的内容都会写进这个文件
path: xxx/xx
# 将输出目录或文件连接到项目,只会连接到项目的根目录
link:
project: xxxx.xcodeproj
target: xxxx
group: xx
详情配置请看:Sourcery Usage
在 Xcode 项目制定 Target 的 build phase 增加脚本:
./sourcery
也可以通过 ./sourcery --config xxx.yml参数指定配置文件。
实战🌰
模仿文章开头大佬的博客实现用Sourcery模型转字典的功能,具体需求如下:
- 模型可以一键转字典,模型属性名为字典里的 key,模型属性值为字典的 value
- 模型里的可选属性为 null 时,不插入到字典里
- 可以主动忽略模型里的某一个属性
假如有以下一个结构体:
struct UserInfo {
let id: int
let age: int?
let weight: String?
}
把它转成字典,并且忽略'id'属性和为 null 的属性,如下代码:
let userInfo = UserInfo(id: 1, age: 18, weight: null)
// 转成[String: Any]类型字典
let dic = userInfo.value
// dic = ["age": 18]
上面代码中使用 userInfo.value 获取模型对应的字典,这个'value'属性就是使用 Sourcery 生生成,下面我们分解一下让 Sourcery 帮我们生成 value 属性的步骤:
- 我们需要告诉 Sourcery 哪些类型(我们自己创建的类、结构体、枚举等)需要有 value 属性
- 为了解决这个问题,我们创建一个空的协议
protocol DictionaryConvertible {} - 让所有需要有 value 属性的类型实现该协议
DictionaryConvertible - 在 Stencil 中使用
types.implementing.DictionaryConvertible就可以找到所有想要生成 value 属性的类型
- 为了解决这个问题,我们创建一个空的协议
- 我们需要告诉 Sourcery 哪些属性需要忽略
- 上面我们提到过,Sourcery 支持注解,我们给需要忽略的属性增加注解
// sourcery: skipConvertDic - 在 Stencil 中使用
annotated:"skipConvertDic"可以判断属性是否有 "skipConvertDic" 注解
- 上面我们提到过,Sourcery 支持注解,我们给需要忽略的属性增加注解
- Sourcery 需要处理可选类型的属性
- 在 Stencil 中使用
isOptional可以判断属性是否时可选类型
- 在 Stencil 中使用
- 基本数据类型(String、Int、Bool、Double、Float等)也要有 value 属性
- 扩展这些基本类型,给它们增加 value 属性
我们先完成第 1、4 步骤:
protocol DictionaryConvertible {}
extension Int { var value: Int { return self } }
extension String { var value: String { return self } }
extension Bool { var value: Bool { return self } }
extension Double { var value: Double { return self } }
extension Float { var value: Float { return self } }
第2、3步骤就直接上 Stencil 代码了,代码里有详细注释:
{# 找到所有实现了 DictionaryConvertible 的类型 #}
{% for t in types.implementing.DictionaryConvertible %}
{# 为该类型 t 创建 extension #}
// MARK: - {{ t.name }} DictionaryConvertible
extension {{ t.name }} {
var value: [String: Any] {
var dicValue: [String: Any] = [String: Any]()
{# 对类型中的所有存储属性迭代,并跳过不需要转换的属性 #}
{% for val in t.storedVariables where val|!annotated:"skipConvertDic" %}
{# 非可选值 #}
{% if not val.typeName.isOptional %}
{% if val.isArray %} {# 如果变量是数组,map 其中的值进行嵌套 #}
dicValue["{{val.name}}"] = {{val.name}}.map { $0.value }
{% elif val.isDictionary %} {# 如果变量是字典,mapValues 字典值进行嵌套 #}
dicValue["{{val.name}}"] = {{val.name}}.mapValues { $0.value }
{% else %} {# 非容器类型,如果是其他的 DictionaryConvertible,则调用 value, 否则直接使用变量即可 #}
dicValue["{{val.name}}"] = {% if val.type.implements.DictionaryConvertible %}{{val.name}}.value{% else %}{{val.name}}{% endif %}
{% endif%}
{% else %}
if let {{val.name}} = {{val.name}} {
{% if val.isArray %} {# 如果变量是数组,map 其中的值进行嵌套 #}
dicValue["{{val.name}}"] = {{val.name}}.map { $0.value }
{% elif val.isDictionary %} {# 如果变量是字典,mapValues 字典值进行嵌套 #}
dicValue["{{val.name}}"] = {{val.name}}.mapValues { $0.value }
{% else %} {# 非容器类型,如果是其他的 DictionaryConvertible,则调用 value, 否则直接使用变量即可 #}
dicValue["{{val.name}}"] = {% if val.type.implements.DictionaryConvertible %}{{val.name}}.value{% else %}{{val.name}}{% endif %}
{% endif%}
}
{% endif %}
{% endfor %}
return dicValue
}
}
{# 对Array进行扩展 #}
extension Array where Element == {{ t.name }} {
var value: [Any] {
return map { $0.value }
}
}
{% endfor %}
这是上面 UserInfo 类型应该如下:
struct UserInfo {
// sourcery: skipConvertDic
let id: int
let age: int?
let weight: String?
}
extension UserInfo: DictionaryConvertible {}
let userInfo = UserInfo(id: 1, age: 18, weight: null)
// 转成[String: Any]类型字典
let dic = userInfo.value
// dic = ["age": 18]
扩展
github上有一个这样的库StencilSwiftKit,为Stencil扩展了许多更加便捷的语法。