I am implementing a MemberMacro
in Swift. I was able to implement everything but one requirement.
Here’s what I was able to do so far:
- create an embedded
struct
within theclass
I attach the macro to - this struct has the same properties as the class
So basically this …
@RecordInterfacable
@Observable
class Model {
let id: UUID
var title: String
init(
id: UUID = UUID(),
title: String
) {
self.id = id
self.title = title
}
}
results in this:
@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
)
}
Here’s what’s missing:
- I need the struct to conform to specific conformances on demand–meaning that I want to be able to define at call site, what the added, embedded struct conforms to
The function implementing the MemberMacro
is static expansion(of:providingMembersOf:conformingTo:in:)
has a the property conformingTo
.
The documentation for this func has the following to say concerning this property:
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.
Here’s the thing though: I have not found out how to declare those conformances at callsite.
At callsite, I currently just annotate my class with @@RecordInterfacable
-but I have no clue as to how to inject the desired conformances here … Can somebody give me a hint as to how to do this?
Here’s the macros implementation:
//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)
}
"""
),
]
}
}