I’m trying to create a custom macro that receives a static variable as a parameter. However, I can’t directly access the value of baseURL
from the expansion
method of my macro.
Here is my code:
enum AppAPI {
static var baseURL: URL = URL(string: "https://myBaseURL.com")!
@RouteAPI("users", baseURL: baseURL)
enum User {
@HTTP(.post, path: "login") case login(LoginReq)
@HTTP(.post, path: "register") case register(RegisterReq)
@HTTP(.get, path: .join(["me", "profile"])) case myProfile
@HTTP(.get, path: .join(["profile", .parameter("id"), .parameter("second")])) case profile2(id: Int, second: String)
}
}
Here is the syntax tree:
Optional(LabeledExprListSyntax
├─[0]: LabeledExprSyntax
│ ├─expression: StringLiteralExprSyntax
│ │ ├─openingQuote: stringQuote
│ │ ├─segments: StringLiteralSegmentListSyntax
│ │ │ ╰─[0]: StringSegmentSyntax
│ │ │ ╰─content: stringSegment("users")
│ │ ╰─closingQuote: stringQuote
│ ╰─trailingComma: comma
╰─[1]: LabeledExprSyntax
├─label: identifier("baseURL")
├─colon: colon
╰─expression: DeclReferenceExprSyntax
╰─baseName: identifier("baseURL"))
I can only see the name of the variable baseURL but not its value.
@attached(extension, conformances: APIRoute, names: arbitrary)
public macro RouteAPI(_ controller: String, baseURL: URL) = #externalMacro(module: "MyMacroMacros", type: "RouteAPI")
How can I access the value of baseURL
instead of its name in my macro?
Macros only have access to the things in the syntax tree. You must change your design.
One possible design is a usage like this:
enum AppAPI {
#BaseURL("https://google.com") {
@RouteAPI("users")
enum User {
}
// other @RouteAPIs with the same base URL...
}
}
BaseURL
is a declaration macro. For each declaration in the closure, it generates the same declaration, except for declarations that has the @RouteAPI
attribute. For declarations that have the @RouteAPI
attribute with only one parameter, it "replaces" the attribute with a @RouteAPI
attribute that also has a baseURL:
parameter. Macros can't normally "replace" things, but since this is in a closure passed to a declaration macro, you can generate any declaration you want. It can also generate the baseURL
property in your original design.
The one-parameter @RouteAPI
does nothing. You can even throw an error in its expansion
method, so that users cannot use it outside of #BaseURL { ... }
. It can be any type of macro except an extension macro.
The above code expands to:
enum AppAPI {
static var baseURL: URL = URL(string: "https://google.com")!
@RouteAPI("users", baseURL: URL(string: "https://google.com")!)
enum User {
}
}
And only after that does the two-parameter @RouteAPI
expand.
Here are the declarations for all 3 macros. Notice that the one-parameter RouteAPI
is implemented by a different type from the two-parameter RouteAPI
.
@freestanding(declaration, names: named(baseURL), arbitrary)
public macro BaseURL(_ url: String, _ block: () -> Void) = #externalMacro(module: "MyMacroMacros", type: "BaseURL")
@attached(extension, names: arbitrary)
public macro RouteAPI(_ name: String, baseURL: URL) = #externalMacro(module: "MyMacroMacros", type: "ActualRouteAPI")
@attached(member) // this doesn't have to be member - could also be peer, memberAttribute etc
public macro RouteAPI(_ name: String) = #externalMacro(module: "MyMacroMacros", type: "RouteAPI")
Here is an example implementation of BaseURL
that does not output any diagnostic messages. Consider adding those in in your actual implementation
enum BaseURL: DeclarationMacro {
static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
guard let baseURLParameter = node.argumentList.first?.expression,
let baseURL = baseURLParameter.as(StringLiteralExprSyntax.self)?.representedLiteralValue,
let block = node.trailingClosure else {
return []
}
let decls = block.statements.compactMap { $0.item.asProtocol(DeclGroupSyntax.self) }
return decls.map { replacingRouteAPIAttribute(of: $0, baseURL: baseURL) } + [
"static let baseURL = URL(string: \(literal: baseURL))!"
]
}
static func replacingRouteAPIAttribute(of decl: DeclGroupSyntax, baseURL: String) -> DeclSyntax {
var newDecl = decl
for i in newDecl.attributes.indices {
guard case var .attribute(attr) = newDecl.attributes[i],
let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text,
attrName == "RouteAPI" ,
case let .argumentList(argsList) = attr.arguments,
argsList.count == 1
else {
continue
}
let expr: ExprSyntax = "URL(string: \(literal: baseURL))!"
let newArgsList = LabeledExprListSyntax {
argsList
LabeledExprSyntax(label: "baseURL", expression: expr)
}
attr.arguments = .argumentList(newArgsList)
newDecl.attributes[i] = .attribute(attr)
}
return newDecl.as(DeclSyntax.self)!
}
}
A caveat is that #BaseURL
cannot be used at the top level because it introduces arbitrary names, but this is not that much of a problem in this particular context.