我正在 Swift 中实现
MemberMacro
。我能够实现除一项要求之外的所有要求。
这是我到目前为止所能做的:
struct
中创建嵌入的 class
,我将宏附加到 基本上就是这样……
@RecordInterfacable
@Observable
class Model {
let id: UUID
var title: String
init(
id: UUID = UUID(),
title: String
) {
self.id = id
self.title = title
}
}
结果如下:
@Observable
class Model {
let id: UUID
var title: String
init(
id: UUID = UUID(),
title: String
) {
self.id = id
self.title = title
}
// I need to make this struct conform to protocols that I inject at call site of the macro – but how?
struct ModelRecord: Codable {
let id: UUID
var title: String
}
init(from record: ModelRecord) {
init(
id: record.id,
title: record.title
)
}
func convertToRecord() -> ModelRecord {
ModelRecord(
id: self.id,
title: self.title
)
}
以下是缺少的内容:
实现
MemberMacro
的函数是 static expansion(of:providingMembersOf:conformingTo:in:)
具有属性 conformingTo
。
此函数的文档对此属性有以下说明:
conformingTo
The set of protocols that were declared in the set of conformances for the macro and to
which the declaration does not explicitly conform. The member macro itself cannot declare
conformances to these protocols (only an extension macro can do that), but can provide
supporting declarations, such as a required initializer or stored property, that cannot be
written in an extension.
但事情是这样的:我还没有找到如何在调用站点声明这些一致性。
在调用站点,我目前只是用
@@RecordInterfacable
注释我的类 - 但我不知道如何在这里注入所需的一致性......有人可以给我一个关于如何做到这一点的提示吗?
这是宏的实现:
//RecordInterfacable
@attached(member, names: arbitrary)
public macro RecordInterfacable() = #externalMacro(module: "RecordInterfacableMacros", type: "RecordInterfacableMacro")
//RecordInterfacableMacro
public struct RecordInterfacableMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
let classDecl = try assertClassDecl(for: declaration)
let symbolName = try extractSymbolName(from: classDecl)
/// Extracts all the elements of the body of the given class.
/// This includes all properties and functions.
let membersDeclSyntax = declaration
.as(ClassDeclSyntax.self)?
.memberBlock
.members
.compactMap {
$0
.as(MemberBlockItemSyntax.self)?
.decl
.as(DeclSyntax.self)
}
/// Further extracts all variables
let membersVariableDeclSyntax = membersDeclSyntax?
.compactMap {
$0
.as(VariableDeclSyntax.self)
}
let memberBindingSpecifiers: [String]? = membersVariableDeclSyntax?
.compactMap { member in
guard let memberBindingSpecifier = member
.bindingSpecifier
.text
.split(separator: ".")
.last
else { fatalError() }
return "\(memberBindingSpecifier)"
}
guard let memberBindingSpecifiers else { fatalError() }
/// Create a string with the declaration of all members
let identifierTexts = membersVariableDeclSyntax?
.map { member in
let identifierText = member
.bindings
.compactMap {
$0
.as(PatternBindingSyntax.self)?
.pattern
.as(IdentifierPatternSyntax.self)?
.identifier
.text
}
.first
guard let identifierText else { fatalError() }
return identifierText
}
guard let identifierTexts
else { fatalError() }
let memberTypes = membersVariableDeclSyntax?
.map { member in
let memberType = member
.bindings
.compactMap {
$0
.as(PatternBindingSyntax.self)?
.typeAnnotation?
.type
.as(IdentifierTypeSyntax.self)?
.name
.text
}
.first
guard let memberType else { fatalError() }
return memberType
}
guard let memberTypes
else { fatalError() }
var memberStrings = [String]()
var initStrings = [String]()
var varString = [String]()
for i in 0..<identifierTexts.count {
memberStrings.append("\(memberBindingSpecifiers[i]) \(identifierTexts[i]): \(memberTypes[i])")
initStrings.append("\(identifierTexts[i]): record.\(identifierTexts[i])")
varString.append("\(identifierTexts[i]): self.\(identifierTexts[i])")
}
let memberString = memberStrings
.joined(separator: "\n")
let initString = initStrings
.joined(separator: ", ")
return [
DeclSyntax(
stringLiteral: """
struct \(symbolName)Record: Codable, FetchableRecord, PersistableRecord {
\(memberString)
}
"""
),
DeclSyntax(
extendedGraphemeClusterLiteral: """
convenience init(from record: \(symbolName)Record) {
self.init(\(initString))
}
"""
),
DeclSyntax(
stringLiteral: """
var record: \(symbolName)Record {
\(symbolName)Record(id: self.id, title: self.title)
}
"""
),
]
}
}
我认为一个可能的解决方案是创建单独的扩展宏来实现一致性(它取代了已弃用的“一致性”宏),然后将您想要符合的类型作为参数传递给您的
RecordableInterface
宏。
例如,如果您想让内部结构符合
Hashable
,您可以编写一个扩展宏来做到这一点。我不会在这里包含实现,因为如何创建扩展宏有点超出范围,但宏声明可能如下所示:
@attached(extension, conformances: Hashable)
public macro InnerHashable() = #externalMacro(module: "MacroImplementations", type: "InnerHashableMacro")
接下来,更新
RecordInterfaceable
宏的声明:
@attached(member, names: arbitrary)
public macro RecordInterfaceable(conformances: Any.Type...) = #externalMacro(module: "MacroExamplesImplementation", type: "RecordInterfaceableMacro")
现在在宏实现中,您可以使用
node
参数访问参数列表。然后,您可以检查传入的类型并选择适当的宏以添加到生成的嵌套类型中。这是一个截断的示例:
public struct RecordInterfacableMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let args = node.arguments?.as(LabeledExprListSyntax.self) else {
return []
}
let attributes = try args.compactMap { labeledExpr in
guard let declBase = labeledExpr
.expression.as(MemberAccessExprSyntax.self)?
.base?.as(DeclReferenceExprSyntax.self)
else {
return nil
}
//Handle as many possible macros as needed.
if declBase == "Hashable" {
return "@InnerHashable"
} else {
return nil
}
}
let innerAttribs = attributes.joined("\n")
...
return [
DeclSyntax(
stringLiteral: """
\(raw: innerAttribs)
struct \(symbolName)Record: Codable, FetchableRecord, PersistableRecord {
\(memberString)
}
"""
),
DeclSyntax(
extendedGraphemeClusterLiteral: """
convenience init(from record: \(symbolName)Record) {
self.init(\(initString))
}
"""
),
DeclSyntax(
stringLiteral: """
var record: \(symbolName)Record {
\(symbolName)Record(id: self.id, title: self.title)
}
"""
),
]
}
}
我还没有实际测试过这段代码,所以我确信还有一些细节需要解决,但我认为这个一般过程将实现你想要做的事情。