I am using a compound predicate to filter Item
s for a View
based on a date.
let predicate = #Predicate<Item> { item in
createdOnDay.evaluate(item)
||
(isStartEnd.evaluate(item) && startEndShowsOnDay.evaluate(item))
}
Some of the predicates evaluated are themselves fairly complex. I have written a function to generate a predicate:
static func startEndAppliesDuring(_ range: ClosedRange<Date>) -> Predicate<Item>
{
let distantFuture = Date.distantFuture
return #Predicate<Item> { item in
((item.startDate ?? item.endDate ?? distantFuture) < range.upperBound)
&&
((item.endDate ?? item.startDate!) >= range.lowerBound)
}
}
...which I call from the View before evaluating with others:
let startEndShowsOnDay = Item.startEndAppliesDuring(dayRange)
Ideally, I would like to reuse the date logic inside the predicate macro, which takes a bit of thought to write and maintain. The predicate macro does not support calling methods on the items it is filtering, so I can't place the logic there.
Is there some way to put the closure and associated constants in a single location and reuse it for predicates and other logic. Or will I just need to cut and paste?
As you have already done in the #Predicate
macro, you can just write methods that calls the Predicate
-returning methods, and call evaluate(self)
on the returned Predicate
.
For example, for reusing startEndAppliesDuring
, you can write
func startEndAppliesDuring(_ range: ClosedRange<Date>) -> Bool {
let predicate = Item.startEndAppliesDuring(range)
return try! predicate.evaluate(self)
}
Here is a peer macro that can generate such a method from the static, Predicate
-returning method.
// declaration
@attached(peer, names: overloaded)
public macro InstancePredicate() = #externalMacro(module: "...", type: "InstancePredicate")
// implementation
enum InstancePredicate: PeerMacro {
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard var method = declaration.as(FunctionDeclSyntax.self),
let staticIndex = method.modifiers.firstIndex(where: { $0.name.text == "static" })
else {
throw "Must be applied on static method"
}
method.modifiers.remove(at: staticIndex)
method.removeMacro("InstancePredicate")
method.signature.returnClause?.type = "Bool"
let argumentList = LabeledExprListSyntax {
for parameter in method.signature.parameterClause.parameters {
if parameter.firstName.text != "_" {
LabeledExprSyntax(
label: parameter.firstName.text,
expression: DeclReferenceExprSyntax(baseName: parameter.secondName ?? parameter.firstName)
)
} else {
LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: parameter.secondName!))
}
}
}
let body = CodeBlockItemListSyntax {
"let predicate = Self.\(raw: method.name)(\(argumentList))"
"return try! predicate.evaluate(self)"
}
method.body?.statements = body
return [DeclSyntax(method)]
}
}
extension FunctionDeclSyntax {
mutating func removeMacro(_ name: String) {
attributes = attributes.filter { attribute in
if case let .attribute(attributeSyntax) = attribute,
let type = attributeSyntax.attributeName.as(IdentifierTypeSyntax.self),
type.name.text == name {
return false
} else {
return true
}
}
}
}
// Here I conformed String to Error to easily emit diagnostics
extension String: @retroactive Error {}
// usage
@InstancePredicate
static func startEndAppliesDuring(_ range: ClosedRange<Date>) -> Predicate<Item> { ... }
The macro assumes it is being attached to a static method returning Predicate<T>
that is declared in a @Model
class named T
, and that the static method's arguments can all be trivially delegated (see how argumentList
is created). That is, there is no tricky things like variadic parameters, autoclosures, inout
parameters, etc.