在我的工作项目中处理一些分析代码时,我一直在考虑一些我可以做的优化。
假设我们有一些符合协议的分析事件
protocol AnalyticsEvent {
var name: String { get }
var parameters: [String: Any] { get }
}
我们假设事件是这样的结构:
struct SomeEvent: AnalyticsEvent {
let name: String = "SomeEvent"
var someParam: String
var parameters: [String: Any] {
"someParamKey" : someParam
}
}
struct SomeOtherEvent: AnalyticsEvent {
let name: String = "SomeOtherEvent"
var parameter: String
var intValue: Int
var parameters: [String: Any] {
"parameterKey": parameter,
"intValueKey" : intValue
}
}
它有点工作,但不能扩展,因为你需要为每个结构单独编写参数字典。
我想要实现的是模仿 Codable 中的 CodingKeys,所以我可以这样做:
protocol AnalyticsEvent {
associatedtype CodingKeys: Hashable, RawRepresentable
var name: String { get }
func encode() -> [String: Any]
}
extension AnalyticsEvent {
func encode() -> [String: Any] {
var encoded: [String: Any] = [:]
...
return encoded
}
}
然后添加一些定义键属性映射的
CodingKeys
枚举,例如
struct SomeEvent: AnalyticsEvent {
enum CodingKeys: String {
case someParam = "someParamKey"
}
let name: String
var someParam: String
}
这是否是一种可行的方法? Struct 的属性可以通过 Mirror 通过名称轻松访问,但这似乎不适用于枚举情况,因此我无法轻松使用镜像将 CodingKeys 情况连接到属性并使用其 rawValue 来填充结果字典。
有办法做到这一点吗?或者也许我应该尝试编写一些更接近 Codable 堆栈的东西,例如 JSONDecoder()?
我觉得自己的想法有点被困住了,可能不会在产品应用程序中尝试一些超级危险的东西,但探索这似乎很有趣,我很乐意得到一些关键字或建议,如何进一步推动这一点。
谢谢!
parameters
可以用宏生成。用法看起来像这样:
@GenerateEventParameters
struct SomeOtherEvent: AnalyticsEvent {
let name = "SomeOtherEvent"
@Parameter("parameterKey")
var parameter: String
@Parameter("intValueKey")
var intValue: Int
}
GenerateEventParameters
是生成 MemberMacro
属性的 parameters
。 Parameter
不会扩展为任何内容,仅充当要读取的 GenerateEventParameters
宏的标记。
(TipKit 中也使用了名称
Parameter
。如果您在其他地方使用 TipKit,请考虑将其重命名为其他名称)
示例实现:
// declarations
@attached(peer)
public macro Parameter(_ name: String) = #externalMacro(module: "...", type: "AnalyticsEventParameterMacro")
@attached(member, names: named(parameters))
public macro GenerateEventParameters() = #externalMacro(module: "...", type: "GenerateEventParametersMacro")
// implementation
enum AnalyticsEventParameterMacro: PeerMacro {
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
[]
}
}
enum GenerateEventParametersMacro: MemberMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
var mapping: [String: TokenSyntax] = [:]
for member in declaration.memberBlock.members {
guard let propDecl = member.decl.as(VariableDeclSyntax.self),
let paramAttr = propDecl.attributes.findAttribute("Parameter"),
case let .argumentList(args) = paramAttr.arguments,
let paramName = args.first?.expression.as(StringLiteralExprSyntax.self)?.representedLiteralValue,
let propName = propDecl.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier
else { continue }
mapping[paramName] = propName
}
let dictLiteral = DictionaryExprSyntax {
for (key, value) in mapping {
DictionaryElementSyntax(
key: StringLiteralExprSyntax(content: key),
value: DeclReferenceExprSyntax(baseName: value)
)
}
}
return [
"""
var parameters: [String: Any] {
\(dictLiteral)
}
"""
]
}
}
extension AttributeListSyntax {
func findAttribute(_ name: String) -> AttributeSyntax? {
for elem in self {
if case let .attribute(attr) = elem,
attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text == name {
return attr
}
}
return nil
}
}
@main
struct YourMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
AnalyticsEventParameterMacro.self,
GenerateEventParametersMacro.self
]
}
我没有在这个简单的实现中包含任何错误消息。如果某些内容不是它所期望的,它不会执行任何操作,并且可能会导致编译器错误。以下是您应该检测并发出我能想到的适当消息的一些情况:
@Parameter
@Parameter
有关如何发出错误消息,请参阅我的帖子。
还可以考虑使用宏生成整个协议一致性,这样您就不再需要类型
let name = ...
或 : AnalyticsEvent
了。 name
属性可以作为参数传递给成员宏。用法类似于观察中的@Observable
。
// declaration
@attached(extension, conformances: AnalyticsEvent, names: named(name), named(parameters))
public macro AnalyticsEvent(_ name: String) = #externalMacro(module: "...", type: "AnalyticsEventMacro")
// implementation
enum AnalyticsEventMacro: ExtensionMacro {
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
guard let name = declaration.asProtocol(NamedDeclSyntax.self)?.name else { return [] }
guard case let .argumentList(args) = node.arguments,
let eventName = args.first?.expression.as(StringLiteralExprSyntax.self)
else { return [] }
var mapping: [String: TokenSyntax] = [:]
for member in declaration.memberBlock.members {
guard let propDecl = member.decl.as(VariableDeclSyntax.self),
let paramAttr = propDecl.attributes.findAttribute("Parameter"),
case let .argumentList(args) = paramAttr.arguments,
let paramName = args.first?.expression.as(StringLiteralExprSyntax.self)?.representedLiteralValue,
let propName = propDecl.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier
else { continue }
mapping[paramName] = propName
}
let dictLiteral = DictionaryExprSyntax {
for (key, value) in mapping {
DictionaryElementSyntax(
key: StringLiteralExprSyntax(content: key),
value: DeclReferenceExprSyntax(baseName: value)
)
}
}
return [
try ExtensionDeclSyntax("extension \(name): AnalyticsEvent") {
"""
var parameters: [String: Any] {
\(dictLiteral)
}
"""
"""
var name: String { \(eventName) }
"""
}
]
}
}
// usage:
@AnalyticsEvent("SomeOtherEvent")
struct SomeOtherEvent {
@Parameter("parameterKey")
var parameter: String
@Parameter("intValueKey")
var intValue: Int
}