Swift宏的实现

    上篇介绍了Swift宏的定义与生声明,本篇主要看看是Swift宏的具体实现。结合Swift中Codable协议,封装一个工具让类或者结构体自动实现Codable协议,并且添加一些协议中没有的功能。

关于Codable协议

    Codable很好,但是有一些缺陷:比如严格要求数据源,定义为String给了Int就抛异常、支持自定义CodingKey但是写法十分麻烦、缺字段的情况下不使用Optional会抛异常而不是使用缺省值等等。

基于以上情况,之前也写了一些Codable协议的补充,比如之前使用属性包装器增加了协议的默认值的提供具体地址https://github.com/duzhaoquan/DQTool.git

Swift Macro 的参考链接

  1. 【WWDC23】一文看懂 Swift Macro
  2. swift-macro-examples
  3. Swift AST Explorer
  4. CodableWrapper

实现目标:

Swift5.9之后新出了宏,通过宏可以更加优雅的封装Codable协议,增加新功能

  1. 支持缺省值,JSON缺少字段容错
  2. 支持 String Bool Number 等基本类型互转
  3. 驼峰大小写自动互转
  4. 自定义解析key
  5. 自定义解析规则 (Transformer)
  6. 方便的 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)。

一些关于这些宏的说明:

  • @CodableCodable协议的宏名不会冲突,这样的命名一致性可以降低认知负担。
  • 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 编写一个简单StructClass,可以看到整个AST,declaration: some DeclGroupSyntax对象根据模型是struct还是class分别对应了StructDeclClassDecl。补充上检查代码之后如下,增加了检查时否时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 功能

先定义个 ModelMemberPropertyContainerinit(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

  • 11
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值