I have found the solution I was looking for by carefully reading through this evolution: swift-evolution/proposals/0407-member-macro-conformances.md at main · swiftlang/swift-evolution · GitHub
EDIT: Here is how I did it.
First, here is the task: I would like to add created_at
and updated_at
Fluent
timestamp fields to models in my Vapor
app. I will call this macro TrackedModel
and I would like to add Model
conformance if the class does not have it yet. I would like to do it with a custom macro.
Let's say I have two targets called App
and Macros
(two separate folders under the Sources
folder)
Here is how I defined them in Package.swift
:
// swift-tools-version:6.0
import PackageDescription
import CompilerPluginSupport
(later in the file)
.macro(
name: "Macros",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
.executableTarget(
name: "App",
dependencies: [
.product(name: "Fluent", package: "fluent"),
.product(name: "Vapor", package: "vapor"),
"Macros",
]
),
Then I created a Macros
target with two files:
TrackedModelMacro.swift
:
import SwiftSyntax
import SwiftSyntaxMacros
public struct TrackedModelMacro: MemberMacro, ExtensionMacro {
public static func expansion(
of _: AttributeSyntax,
providingMembersOf _: some DeclGroupSyntax,
conformingTo _: [TypeSyntax],
in _: some MacroExpansionContext
) throws -> [DeclSyntax] {
[
"@Timestamp(key: \"created_at\", on: .create)",
"var createdAt: Date?",
"@Timestamp(key: \"updated_at\", on: .update)",
"var updatedAt: Date?",
]
}
public static func expansion(
of _: AttributeSyntax,
attachedTo _: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in _: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let containsProtocol = protocols.contains { p in
if let protocolDecl = p.as(IdentifierTypeSyntax.self),
protocolDecl.name.text == "Model"
{
return true
}
return false
}
guard containsProtocol else { return [] }
let modelExtension = try ExtensionDeclSyntax("extension \(type.trimmed): Model {}")
return [modelExtension]
}
}
and macros.swift
:
import SwiftCompilerPlugin
import SwiftSyntaxMacros
@main
struct Macros: CompilerPlugin {
var providingMacros: [Macro.Type] = [
TrackedModelMacro.self,
]
}
And finally, I need to properly set up this macro in my App
target. I created a separate file for this, called TrackedModel.swift
:
import Fluent
@attached(member, names: named(createdAt), named(updatedAt))
@attached(extension, conformances: Model)
macro TrackedModel() = #externalMacro(module: "Macros", type: "TrackedModelMacro")
And now one can use it with or without declaring Model
protocol. Here is an example usage inside the App
target:
import Foundation
import Fluent
@TrackedModel
final class A: @unchecked Sendable {
static let schema = "a"
@ID
var id: UUID?
}
@TrackedModel
final class B: Model, @unchecked Sendable {
static let schema = "b"
@ID
var id: UUID?
}
In addition, I might later add a Diagnostic
message that notifies me that an explicit model conformance is not needed.