Skip to content

Instantly share code, notes, and snippets.

@simonbs
Created November 4, 2023 19:09
Show Gist options
  • Save simonbs/4e92aaed85a93a35261ccc5db0cadc12 to your computer and use it in GitHub Desktop.
Save simonbs/4e92aaed85a93a35261ccc5db0cadc12 to your computer and use it in GitHub Desktop.
Property wrapper for proxying values.
@propertyWrapper
struct Proxy<EnclosingType, Value> {
typealias ValueKeyPath = ReferenceWritableKeyPath<EnclosingType, Value>
typealias SelfKeyPath = ReferenceWritableKeyPath<EnclosingType, Self>
static subscript(
_enclosingInstance instance: EnclosingType,
wrapped wrappedKeyPath: ValueKeyPath,
storage storageKeyPath: SelfKeyPath
) -> Value {
get {
let keyPath = instance[keyPath: storageKeyPath].keyPath
return instance[keyPath: keyPath]
}
set {
let keyPath = instance[keyPath: storageKeyPath].keyPath
instance[keyPath: keyPath] = newValue
}
}
@available(*, unavailable, message: "@Proxy can only be applied to classes")
var wrappedValue: Value {
get { fatalError() }
set { fatalError() }
}
private let keyPath: ValueKeyPath
init(_ keyPath: ValueKeyPath) {
self.keyPath = keyPath
}
}
@simonbs
Copy link
Author

simonbs commented Nov 4, 2023

I'm replacing the property wrapper with the following macro.

@attached(accessor)
public macro Proxy<T, U>(_ keyPath: ReferenceWritableKeyPath<T, U>) = #externalMacro(
    module: "PlaygroundMacros",
    type: "ProxyMacro"
)
import SwiftSyntax
import SwiftCompilerPlugin
import SwiftCompilerPluginMessageHandling
import SwiftSyntaxMacros
import SwiftDiagnostics

@main
struct PlaygroudMacrosPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        ProxyMacro.self
    ]
}

enum ProxyMacroDiagnostic: String, DiagnosticMessage {
    case missingArgument
    case notAKeyPath

    var severity: DiagnosticSeverity {
        .error
    }
    var diagnosticID: MessageID {
        MessageID(domain: "PlaygroundMacros", id: rawValue)
    }
    var message: String {
        switch self {
        case .missingArgument:
            "Please supply a key path as argument."
        case .notAKeyPath:
            "Supplied argument must be a key path."
        }
    }
}

public struct ProxyMacro: AccessorMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingAccessorsOf declaration: some DeclSyntaxProtocol,
        in context: some MacroExpansionContext
    ) throws -> [AccessorDeclSyntax] {
        guard case let .argumentList(argumentList) = node.arguments, let argument = argumentList.first else {
            let diagnostic = Diagnostic(node: node, message: ProxyMacroDiagnostic.missingArgument)
            context.diagnose(diagnostic)
            return []
        }
        guard let keyPathExpr = argument.as(LabeledExprSyntax.self)?.expression.as(KeyPathExprSyntax.self) else {
            let diagnostic = Diagnostic(node: argument, message: ProxyMacroDiagnostic.notAKeyPath)
            context.diagnose(diagnostic)
            return []
        }
        let getAccessor: AccessorDeclSyntax =
           """
           get {
               return self[keyPath: \(raw: keyPathExpr.description)]
           }
           """
        let setAccessor: AccessorDeclSyntax =
           """
           set {
               self[keyPath: \(raw: keyPathExpr.description)] = newValue
           }
           """
        return [getAccessor, setAccessor]
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment