如何在自定义编码中模仿可编码的CodingKey

问题描述 投票:0回答:1

在我的工作项目中处理一些分析代码时,我一直在考虑一些我可以做的优化。

假设我们有一些符合协议的分析事件

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()?

我觉得自己的想法有点被困住了,可能不会在产品应用程序中尝试一些超级危险的东西,但探索这似乎很有趣,我很乐意得到一些关键字或建议,如何进一步推动这一点。

谢谢!

swift codable
1个回答
0
投票

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
}
最新问题
© www.soinside.com 2019 - 2025. All rights reserved.