上篇介绍了Swift宏的定义与生声明,本篇主要看看是Swift宏的具体实现。结合Swift中Codable协议,封装一个工具让类或者结构体自动实现Codable协议,并且添加一些协议中没有的功能。
关于Codable协议
Codable很好,但是有一些缺陷:比如严格要求数据源,定义为String给了Int就抛异常、支持自定义CodingKey但是写法十分麻烦、缺字段的情况下不使用Optional会抛异常而不是使用缺省值等等。
基于以上情况,之前也写了一些Codable协议的补充,比如之前使用属性包装器增加了协议的默认值的提供具体地址https://github.com/duzhaoquan/DQTool.git
Swift Macro 的参考链接
- 【WWDC23】一文看懂 Swift Macro
- swift-macro-examples
- Swift AST Explorer
- CodableWrapper
实现目标:
Swift5.9之后新出了宏,通过宏可以更加优雅的封装Codable协议,增加新功能
- 支持缺省值,JSON缺少字段容错
- 支持
String
Bool
Number
等基本类型互转 - 驼峰大小写自动互转
- 自定义解析key
- 自定义解析规则 (Transformer)
- 方便的
Codable Class
子类
具体的实现
定义几个宏
@Codable
@CodableSubclass
@CodableKey(..)
@CodableNestedKey(..)
@CodableTransformer(..)
先简单的声明与实现
声明Codable和CodableKey宏。
// CodableWrapperMacros/CodableWrapper.swift
@attached(member, names: named(init(from:)), named(encode(to:)))
@attached(conformance)
public macro Codable() = #externalMacro(module: "CodableWrapperMacros", type: "Codable")
@attached(member)
public macro CodableKey(_ key: String ...) = #externalMacro(module: "CodableWrapperMacros", type: "CodableKey")
实现Codable和CodableKey宏。
// CodableWrapperMacros/Codable.swift
import SwiftSyntax
import SwiftSyntaxMacros
public struct Codable: MemberMacro {
public static func expansion(of _: AttributeSyntax,
providingConformancesOf declaration: some DeclGroupSyntax,
in _: some MacroExpansionContext) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)]
{
return []
}
public static func expansion(of node: SwiftSyntax.AttributeSyntax,
providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax,
in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax]
{
return []
}
}
// CodableWrapperMacros/CodableKey.swift
import SwiftSyntax
import SwiftSyntaxMacros
public struct CodableKey: ConformanceMacro, MemberMacro {
public static func expansion(of node: SwiftSyntax.AttributeSyntax,
providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax,
in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax]
{
return []
}
}
添加宏定义
// CodableWrapperMacros/Plugin.swift
import SwiftCompilerPlugin
import SwiftSyntaxMacros
@main
struct CodableWrapperPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
Codable.self,
CodableKey.self,
]
}
在这里,@Codable
实现了两种宏,一种是一致性宏(Conformance Macro),另一种是成员宏(Member Macro)。
一些关于这些宏的说明:
@Codable
和Codable
协议的宏名不会冲突,这样的命名一致性可以降低认知负担。- Conformance Macro用于自动让数据模型遵循Codable协议(如果尚未遵循)。
- Member Macro用于添加
init(from decoder: Decoder)
和func encode(to encoder: Encoder)
这两个方法。在@attached(member, named(init(from:)), named(encode(to:)))
中,必须声明新增方法的名称才是合法的。
实现自动遵循Codable协议
// CodableWrapperMacros/Codable.swift
public struct Codable: ConformanceMacro, MemberMacro {
public static func expansion(of node: AttributeSyntax,
providingConformancesOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
return [("Codable", nil)]
}
public static func expansion(of node: SwiftSyntax.AttributeSyntax,
providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax,
in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax]
{
return []
}
}
编译一下。右键@Codable
-> Expand Macro
查看扩写的代码,看起来还可以。
但如果BasicModel
本身就遵循了Codable
,编译就报错了。所以希望先检查数据模型是否遵循Codable
协议,如果没有的话再遵循它,怎么办呢? 打开Swift AST Explorer 编写一个简单Struct
和Class
,可以看到整个AST,declaration: some DeclGroupSyntax
对象根据模型是struct
还是class
分别对应了StructDecl
和ClassDecl
。补充上检查代码之后如下,增加了检查时否时class或者struct,否则抛出错误。代码如下
public static func expansion(of node: AttributeSyntax,
providingConformancesOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
var inheritedTypes: InheritedTypeListSyntax?
if let declaration = declaration.as(StructDeclSyntax.self) {
inheritedTypes = declaration.inheritanceClause?.inheritedTypeCollection
} else if let declaration = declaration.as(ClassDeclSyntax.self) {
inheritedTypes = declaration.inheritanceClause?.inheritedTypeCollection
} else {
throw ASTError("use @Codable in `struct` or `class`")
}
if let inheritedTypes = inheritedTypes,
inheritedTypes.contains(where: { inherited in inherited.typeName.trimmedDescription == "Codable" })
{
return []
}
return [("Codable" as TypeSyntax, nil)]
}
实现 @Codable
功能
先定义个 ModelMemberPropertyContainer
,init(from decoder: Decoder)
和 func encode(to encoder: Encoder)
的扩展都在里面实现。
public static func expansion(of node: SwiftSyntax.AttributeSyntax,
providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax,
in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax]
{
let propertyContainer = try ModelMemberPropertyContainer(decl: declaration, context: context)
let decoder = try propertyContainer.genDecoderInitializer(config: .init(isOverride: false))
let encoder = try propertyContainer.genEncodeFunction(config: .init(isOverride: false))
return [decoder, encoder]
}
// CodableWrapperMacros/ModelMemberPropertyContainer.swift
import SwiftSyntax
import SwiftSyntaxMacros
struct GenConfig {
let isOverride: Bool
}
struct ModelMemberPropertyContainer {
let context: MacroExpansionContext
fileprivate let decl: DeclGroupSyntax
init(decl: DeclGroupSyntax, context: some MacroExpansionContext) throws {
self.decl = decl
self.context = context
}
func genDecoderInitializer(config: GenConfig) throws -> DeclSyntax {
return """
init(from decoder: Decoder) throws {
fatalError()
}
""" as DeclSyntax
}
func genEncodeFunction(config: GenConfig) throws -> DeclSyntax {
return """
func encode(to encoder: Encoder) throws {
fatalError()
}
""" as DeclSyntax
}
}
填充init(from decoder: Decoder)
需要得知属性名、@CodableKey
的参数、@CodableNestedKey
的参数、@CodableTransformer
的参数、初始化表达式。获取memberProperties
列表:
struct ModelMemberPropertyContainer {
let context: MacroExpansionContext
fileprivate let decl: DeclGroupSyntax
fileprivate var memberProperties: [ModelMemberProperty] = []
init(decl: DeclGroupSyntax, context: some MacroExpansionContext) throws {
self.decl = decl
self.context = context
memberProperties = try fetchModelMemberProperties()
}
func fetchModelMemberProperties() throws -> [ModelMemberProperty] {
let memberList = decl.memberBlock.members
let memberProperties = try memberList.compactMap { member -> ModelMemberProperty? in
guard let variable = member.decl.as(VariableDeclSyntax.self),
variable.isStoredProperty
else {
return nil
}
// name
guard let name = variable.bindings.map(\.pattern).first(where: { $0.is(IdentifierPatternSyntax.self) })?.as(IdentifierPatternSyntax.self)?.identifier.text else {
return nil
}
guard let type = variable.inferType else {
throw ASTError("please declare property type: \(name)")
}
var mp = ModelMemberProperty(name: name, type: type)
let attributes = variable.attributes
// isOptional
mp.isOptional = variable.isOptionalType
// CodableKey
if let customKeyMacro = attributes?.first(where: { element in
element.as(AttributeSyntax.self)?.attributeName.as(SimpleTypeIdentifierSyntax.self)?.description == "CodableKey"
}) {
mp.normalKeys = customKeyMacro.as(AttributeSyntax.self)?.argument?.as(TupleExprElementListSyntax.self)?.compactMap { $0.expression.description } ?? []
}
// CodableNestedKey
if let customKeyMacro = attributes?.first(where: { element in
element.as(AttributeSyntax.self)?.attributeName.as(SimpleTypeIdentifierSyntax.self)?.description == "CodableNestedKey"
}) {
mp.nestedKeys = customKeyMacro.as(AttributeSyntax.self)?.argument?.as(TupleExprElementListSyntax.self)?.compactMap { $0.expression.description } ?? []
}
// CodableTransform
if let customKeyMacro = attributes?.first(where: { element in
element.as(AttributeSyntax.self)?.attributeName.as(SimpleTypeIdentifierSyntax.self)?.description == "CodableTransformer"
}) {
mp.transformerExpr = customKeyMacro.as(AttributeSyntax.self)?.argument?.as(TupleExprElementListSyntax.self)?.first?.expression.description
}
// initializerExpr
if let initializer = variable.bindings.compactMap(\.initializer).first {
mp.initializerExpr = initializer.value.description
}
return mp
}
return memberProperties
}
}
完善genDecoderInitializer
:
func genDecoderInitializer(config: GenConfig) throws -> DeclSyntax {
// memberProperties: [ModelMemberProperty]
let body = memberProperties.enumerated().map { idx, member in
if let transformerExpr = member.transformerExpr {
let transformerVar = context.makeUniqueName(String(idx))
let tempJsonVar = member.name
var text = """
let \(transformerVar) = \(transformerExpr)
let \(tempJsonVar) = try? container.decode(type: type(of: \(transformerVar)).JSON.self, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))])
"""
if let initializerExpr = member.initializerExpr {
text.append("""
self.\(member.name) = \(transformerVar).transformFromJSON(\(tempJsonVar), fallback: \(initializerExpr))
""")
} else {
text.append("""
self.\(member.name) = \(transformerVar).transformFromJSON(\(tempJsonVar))
""")
}
return text
} else {
let body = "container.decode(type: type(of: self.\(member.name)), keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))])"
if let initializerExpr = member.initializerExpr {
return "self.\(member.name) = (try? \(body)) ?? (\(initializerExpr))"
} else {
return "self.\(member.name) = try \(body)"
}
}
}
.joined(separator: "\n")
let decoder: DeclSyntax = """
\(raw: attributesPrefix(option: [.public, .required]))init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
\(raw: body)
}
"""
return decoder
}
-
let transformerVar = context.makeUniqueName(String(idx))
需要生成一个局部transformer变量,为了防止变量名冲突使用了makeUniqueName
生成唯一变量名 -
attributesPrefix(option: [.public, .required])
根据 struct/class 是 open/public 生成正确的修饰。所有情况展开如下:open class Model: Codable { public required init(from decoder: Decoder) throws {} } public class Model: Codable { public required init(from decoder: Decoder) throws {} } class Model: Codable { required init(from decoder: Decoder) throws {} } public struct Model: Codable { public init(from decoder: Decoder) throws {} } struct Model: Codable { init(from decoder: Decoder) throws {} }
填充
func encode(to encoder: Encoder)
func genEncodeFunction(config: GenConfig) throws -> DeclSyntax { let body = memberProperties.enumerated().map { idx, member in if let transformerExpr = member.transformerExpr { let transformerVar = context.makeUniqueName(String(idx)) if member.isOptional { return """ let \(transformerVar) = \(transformerExpr) if let \(member.name) = self.\(member.name), let value = \(transformerVar).transformToJSON(\(member.name)) { try container.encode(value: value, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))]) } """ } else { return """ let \(transformerVar) = \(transformerExpr) if let value = \(transformerVar).transformToJSON(self.\(member.name)) { try container.encode(value: value, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))]) } """ } } else { return "try container.encode(value: self.\(member.name), keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))])" } } .joined(separator: "\n") let encoder: DeclSyntax = """ \(raw: attributesPrefix(option: [.open, .public]))func encode(to encoder: Encoder) throws { let container = encoder.container(keyedBy: AnyCodingKey.self) \(raw: body) } """ return encoder }
@CodableKey
@CodableNestedKey
@CodableTransformer
增加Diagnostics
这些宏是用作占位标记的,不需要实际扩展。但为了增加一些严谨性,比如在以下情况下希望增加错误提示:
@CodableKey("a")
struct StructWraning1 {}
实现也很简单抛出异常即可
public struct CodableKey: MemberMacro {
public static func expansion(of node: AttributeSyntax, providingMembersOf _: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
throw ASTError("`\(self.self)` only use for `Property`")
}
}
这里也就印证了 @CodableKey
为什么不用 @attached(memberAttribute)
(Member Attribute Macro) 而使用 @attached(member)
(Member Macro) 的原因。如果不声明使用@attached(member)
,就不会执行MemberMacro
协议的实现,在MemberMacro
位置写上@CodableKey("a")
也就不会报错。
实现@CodableSubclass
,方便的Codable Class子类
先举例展示Codable Class子类
的缺陷。编写一个简单的测试用例:是不是出乎意料,原因是编译器只给ClassModel
添加了init(from decoder: Decoder)
,ClassSubmodel
则没有。要解决问题还需要手动实现子类的Codable
协议,十分不便:
@CodableSubclass
就是解决这个问题,实现也很简单,在适时的位置super call,方法标记成override
就可以了。
func genDecoderInitializer(config: GenConfig) throws -> DeclSyntax {
...
let decoder: DeclSyntax = """
\(raw: attributesPrefix(option: [.public, .required]))init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
\(raw: body)\(raw: config.isOverride ? "\ntry super.init(from: decoder)" : "")
}
"""
}
func genEncodeFunction(config: GenConfig) throws -> DeclSyntax {
...
let encoder: DeclSyntax = """
\(raw: attributesPrefix(option: [.open, .public]))\(raw: config.isOverride ? "override " : "")func encode(to encoder: Encoder) throws {
\(raw: config.isOverride ? "try super.encode(to: encoder)\n" : "")let container = encoder.container(keyedBy: AnyCodingKey.self)
\(raw: body)
}
"""
}
具体代码实现地址:GitHub - duzhaoquan/CodableTool