I am trying to get my feet wet with Swift macros. The task is to get from
@RecordInterfacable
@Observable
class Model {
let id: UUID
var title: String
init(
id: UUID = UUID(),
title: String
) {
self.id = id
self.title = title
}
}
to
@Observable
class Model {
let id: UUID
var title: String
init(
id: UUID = UUID(),
title: String
) {
self.id = id
self.title = title
}
convenience init(from record: ModelRecord) {
self.init(id: record.id, title: record.title)
}
var record: ModelRecord {
ModelRecord(id: self.id, title: self.title)
}
struct ModelRecord {
let id: UUID
var title: String
}
}
Stubbing out the desired result compiles just fine, but when running my macro test, I am met with
Parsing a `DeclSyntax` node from string interpolation produced the following parsing errors.
Set a breakpoint in `SyntaxParseable.logStringInterpolationParsingError()` to debug the failure.
2 │ self.init(id: record.id, title: record.title)
3 │ }
4 │ struct ModelRecord: Codable {
│ ╰─ error: unexpected code in initializer
5 │ let id: UUID
6 │ var title: String
I am aware that the macro is not fully implemented yet, but the convenience init()
is giving me trouble already … I am surprised that this is the case, cause what I am returning in static expansion(of:providingMembersOf:conformingTo:in:)
is trying to implement the exact same code that I have stubbed out above:
static expansion(of:providingMembersOf:conformingTo:in:) {
// …
return [
DeclSyntax(
extendedGraphemeClusterLiteral: """
convenience init(from record: \(symbolName)Record) {
//this needs to be implemented:
self.init(id: record.id, title: record.title)
}
// var record is still missing
struct \(symbolName)Record: Codable {
\(memberStrings)
}
"""
)
]
}
… which according to the test console output produces:
Actual expanded source:
@Observable
class Model {
let id: UUID
var title: String
init(
id: UUID = UUID(),
title: String
) {
self.id = id
self.title = title
}
convenience init(from record: ModelRecord) {
self.init(id: record.id, title: record.title)
}
struct ModelRecord: Codable {
let id: UUID
var title: String
}
}
The complete macro implementation looks as follows:
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 symbolName = declaration
.as(ClassDeclSyntax.self)?
.name
.text
else {
fatalError("Could not extract symbol name.")
}
/// 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)
}
/// Create a string with the declaration of all members
let memberStrings = membersVariableDeclSyntax?
.map { member in
guard let memberBindingSpecifier = member
.bindingSpecifier
.text
.split(separator: ".")
.last
else { fatalError() }
let identifierText = member
.bindings
.compactMap {
$0
.as(PatternBindingSyntax.self)?
.pattern
.as(IdentifierPatternSyntax.self)?
.identifier
.text
}
.first
guard let identifierText else { fatalError() }
let memberType = member
.bindings
.compactMap {
$0
.as(PatternBindingSyntax.self)?
.typeAnnotation?
.type
.as(IdentifierTypeSyntax.self)?
.name
.text
}
.first
guard let memberType else { fatalError() }
return "\(memberBindingSpecifier) \(identifierText): \(memberType)"
}
.joined(separator: "\n")
guard let memberStrings else{ fatalError() }
return [
DeclSyntax(
extendedGraphemeClusterLiteral: """
convenience init(from record: \(symbolName)Record) {
//this needs to be implemented:
self.init(id: record.id, title: record.title)
}
// var record is still missing
struct \(symbolName)Record: Codable {
\(memberStrings)
}
"""
)
]
}
}
Turns out that I was holding it wrong–each DeclSyntax
needs to be declared separately:
return [
DeclSyntax(
extendedGraphemeClusterLiteral: """
convenience init(from record: \(symbolName)) {
self.init(id: record.id, title: record.title)
}
"""
),
DeclSyntax(
stringLiteral: """
struct \(symbolName)Record: Codable {
\(memberStrings)
}
"""
)
]
Now I can move on to complete my macro 😀