上篇介绍了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 SwiftSyntaxMacrospublic 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 SwiftSyntaxMacrospublic 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.swiftpublic 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.swiftimport SwiftSyntax
import SwiftSyntaxMacrosstruct GenConfig {let isOverride: Bool
}struct ModelMemberPropertyContainer {let context: MacroExpansionContextfileprivate let decl: DeclGroupSyntaxinit(decl: DeclGroupSyntax, context: some MacroExpansionContext) throws {self.decl = declself.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: MacroExpansionContextfileprivate let decl: DeclGroupSyntaxfileprivate var memberProperties: [ModelMemberProperty] = []init(decl: DeclGroupSyntax, context: some MacroExpansionContext) throws {self.decl = declself.context = contextmemberProperties = try fetchModelMemberProperties()}func fetchModelMemberProperties() throws -> [ModelMemberProperty] {let memberList = decl.memberBlock.memberslet memberProperties = try memberList.compactMap { member -> ModelMemberProperty? inguard let variable = member.decl.as(VariableDeclSyntax.self),variable.isStoredPropertyelse {return nil}// nameguard 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// isOptionalmp.isOptional = variable.isOptionalType// CodableKeyif let customKeyMacro = attributes?.first(where: { element inelement.as(AttributeSyntax.self)?.attributeName.as(SimpleTypeIdentifierSyntax.self)?.description == "CodableKey"}) {mp.normalKeys = customKeyMacro.as(AttributeSyntax.self)?.argument?.as(TupleExprElementListSyntax.self)?.compactMap { $0.expression.description } ?? []}// CodableNestedKeyif let customKeyMacro = attributes?.first(where: { element inelement.as(AttributeSyntax.self)?.attributeName.as(SimpleTypeIdentifierSyntax.self)?.description == "CodableNestedKey"}) {mp.nestedKeys = customKeyMacro.as(AttributeSyntax.self)?.argument?.as(TupleExprElementListSyntax.self)?.compactMap { $0.expression.description } ?? []}// CodableTransformif let customKeyMacro = attributes?.first(where: { element inelement.as(AttributeSyntax.self)?.attributeName.as(SimpleTypeIdentifierSyntax.self)?.description == "CodableTransformer"}) {mp.transformerExpr = customKeyMacro.as(AttributeSyntax.self)?.argument?.as(TupleExprElementListSyntax.self)?.first?.expression.description}// initializerExprif 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 inif let transformerExpr = member.transformerExpr {let transformerVar = context.makeUniqueName(String(idx))let tempJsonVar = member.namevar 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 inif 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