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 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)。

一些关于这些宏的说明:

  • @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.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 编写一个简单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.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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/38065.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

编写Linux下共享库SDK

在Linux中,共享库(也称为动态链接库)是一种可以被多个程序共享的可执行代码和数据的集合。在编写共享库的SDK时,我们通常需要提供以下内容: 1. 头文件:包含了共享库提供的函数和数据结构的声明。这些头文件…

1688_item_search_shop接口技术详解

1688_item_search_shop接口技术详解 在B2B电商领域,当商家或消费者需要查找特定店铺的商品时,一个高效的店铺搜索接口显得尤为重要。1688平台作为中国领先的B2B电商平台,提供了item_search_shop接口,使得商家和消费者能够根据店铺…

一个c++的综合实例:log同步写入文件

1. 引言 功能 通过一个API函数把log写入到文件中,分4个log level 目录 ├── log ├── log.cpp ├── log.h ├── main.cpp └── Makefile 2.代码 文件:main.cpp #include "log.h"int main(int argc, char *argv[]) {Log::GetIns…

yaklang window安装 vscode运行得到“hello world”

资源来源:旅程伊始:Yak 语言环境安装与搭建环境 | Yak Program Language 安装yak语言非常简单,管理员权限打开命令行运行以下命令: powershell (new-object System.Net.WebClient).DownloadFile(https://yaklang.oss-cn-beijing…

1085 PAT单位排行(测试点5)

solution 测试点5:总分是在每个学生加权后再取整,所以用来存学生分数的变量要用浮点型学校排序: 若成绩不同,则按成绩降序若成绩相同,人数不同,则按成绩升序若成绩和人数都相同,则按单位名升序…

TS-字面量类型

字面量在代码中表示固定值。在TypeScript中,字面量包括字符串、数值、布尔值、长整型值、对象、数组、函数、正则表达式、null等,例如,以下都是字面量。 99.9 //数值字面量 true //布尔值字面量 "message" //字符…

理解GPT2:无监督学习的多任务语言模型

目录 一、背景与动机 二、卖点与创新 三、几个问题 四、具体是如何做的 1、更多、优质的数据,更大的模型 2、大数据量,大模型使得zero-shot成为可能 3、使用prompt做下游任务 五、一些资料 一、背景与动机 基于 Transformer 解码器的 GPT-1 证明…

NAS教程丨铁威马如何登录 SSH终端?

适用型号: 所有TNAS 型号 如您有特殊操作需要通过 SSH 终端登录 TNAS,请参照以下指引: (注意: 关于以下操作步骤中的"cd /"的指令,其作用是使当前 SSH/Telnet 连接的位置切换到根目录,以免造成对卷的占用.请不要遗漏它.) Windows…

数据分析的线上云端数据库搭建及Excel和Tableau连接

数据分析的线上云端数据库搭建及Excel和Tableau连接 SQL基础知识 线上SQL训练: SQlZOO: https://www.sqlzoo.net/wiki/SQL_Tutorial 牛客网SQL真题:https://www.nowcoder.com/ta/sql select,from,where, order by, limit, group by, having, substr(),…

【TensorFlow深度学习】图像旋转预测:一个无监督表征学习的实践案例

图像旋转预测:一个无监督表征学习的实践案例 理论背景方法概述实战代码结构导入必要的库定义数据增强构建模型训练流程主函数 结论 在机器学习领域,无监督表征学习正逐渐成为解锁大数据潜力的关键。其中,一种创新的方法——图像旋转预测&…

【SpringBoot】SpringBoot使用mail实现登录邮箱验证

📝个人主页:哈__ 期待您的关注 目录 一、前期准备 1 开启邮箱服务 2 SpringBoot导入依赖 3 创建application.yml配置文件 4 创建数据库文件 5 配置redis服务 二、验证邮件发送功能 三、注册功能实现邮箱验证 1 创建User实体类 2 创建UserPa…

HTTP 常见状态码

2xx: 代表请求已成功被服务器接收、理解、并接受。 3xx: 重定向,需要客户端采取进一步的操作才能完成请求 4xx: 客户端的请求错误 400 客户端错误403:服务器拒绝客户端的请求401:需要身份认证404:服务器找不到资源 5xx: 服务器在…

【INTEL(ALTERA)】Nios II手册缺少 alt_dcache_flush_no_writeback()

目录 说明 解决方法 说明 HAL函 alt_dcache_flush_no_writeback() 数是 该手册没有记录在 Nios II软件开发人员手册中。 该文档应记录在HAL中的"HAL API 函 数" API 参考 章节。 解决方法 请参阅以下函数描述: alt_dcache_flush_no_writeback&#…

DEBOPIE框架:打造最好的ChatGPT交易机器人

本文介绍了如何利用 DEBOPIE 框架并基于 ChatGPT 创建高效交易机器人,并强调了在使用 AI 辅助交易时需要注意的限制以及操作步骤。原文: Build the Best ChatGPT Trading Bots with my “DEBOPIE” Framework 如今有大量文章介绍如何通过 ChatGPT 帮助决定如何以及在…

linux高级编程(进程)(3)(exec族)

exec族: 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支), 子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的 用户空间代码和数据完全被新程序替换&…

什么是jar包

jar包就是别人已经写好的一些类,然后将这些类进行打包,你可以将这些jar包引入你的项目中,然后就可以直接使用这些jar包中的类和属性以及方法。 JAR(Java ARchive)是将一系列文件合并到单个压缩文件里,就象…

仓库管理系统12--供应商设置

1、添加供应商窗体 2、布局控件UI <UserControl x:Class"West.StoreMgr.View.SupplierView"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc"http://…

为什么前端传了token,后端一直获取不到?一直报跨域错误?

这是我的前端代码 这是我的后端拦截器 那就需要了解一下 预检请求 对于非简单请求&#xff08;如PUT、DELETE或包含自定义HTTP头的请求&#xff09;&#xff0c;浏览器会先发送一个OPTIONS请求到目标服务器&#xff0c;询问是否允许该跨域请求。这个过程称为预检请求。 当opt…

【爬虫实战】今日头条-关键词搜索-快速整理出1w条数据

快速整理头条关键词数据工具&#xff0c;学习效率妥妥翻倍&#xff01;&#xff01;&#xff01;本案例源码仅供学习参考&#xff01; 项目功能简介&#xff1a; 1.可视化式配置&#xff1b; 2.任意关键词&#xff1b; 3.自动翻页&#xff1b; 4.支持指定最大翻页页码&…

Foxit Reader高亮与下划线全指南:标记文档的大师级技巧

&#x1f58d;️ Foxit Reader高亮与下划线全指南&#xff1a;标记文档的大师级技巧 Foxit Reader是一款功能强大的PDF阅读器&#xff0c;它提供了一系列的注释工具&#xff0c;包括高亮和下划线&#xff0c;以增强用户的阅读体验和文档交互性。本文将详细介绍如何在Foxit Rea…