mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
c53e0c1466
The build extensions package can be used for both macros and plugins.
179 lines
6.5 KiB
Swift
179 lines
6.5 KiB
Swift
//
|
|
// Copyright 2026 Element Creations Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import SwiftSyntax
|
|
import SwiftSyntaxMacros
|
|
|
|
/// Implements `@UserPreference`. The generated property reads and writes through the enclosing
|
|
/// type's `store` (a `UserDefaultsProtocol`), wrapped in `UserDefaultsStorage` for the
|
|
/// encode/decode handling, and publishes changes via a generated per-preference subject.
|
|
///
|
|
/// A `volatile` preference instead reads and writes through a generated in-memory store, so its
|
|
/// value is never persisted and resets on each launch.
|
|
public enum UserPreferenceMacro {
|
|
enum DiagnosticError: Error, CustomStringConvertible {
|
|
case notAStoredProperty
|
|
case missingTypeAnnotation
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .notAStoredProperty:
|
|
"@UserPreference can only be applied to a stored property."
|
|
case .missingTypeAnnotation:
|
|
"@UserPreference requires an explicit type annotation."
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct Preference {
|
|
let name: String
|
|
let type: TypeSyntax
|
|
let key: ExprSyntax
|
|
let defaultValue: ExprSyntax
|
|
let isVolatile: Bool
|
|
|
|
var subjectName: TokenSyntax {
|
|
"_\(raw: name)Subject"
|
|
}
|
|
|
|
/// The in-memory backing for a volatile preference.
|
|
var volatileValueName: TokenSyntax {
|
|
"_\(raw: name)VolatileValue"
|
|
}
|
|
|
|
var resetMethodName: TokenSyntax {
|
|
"reset\(raw: name.prefix(1).uppercased() + name.dropFirst())"
|
|
}
|
|
}
|
|
|
|
private static func preference(for declaration: some DeclSyntaxProtocol, node: AttributeSyntax) throws -> Preference {
|
|
guard let variable = declaration.as(VariableDeclSyntax.self),
|
|
let binding = variable.bindings.first,
|
|
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
|
|
throw DiagnosticError.notAStoredProperty
|
|
}
|
|
guard let type = binding.typeAnnotation?.type.trimmed else {
|
|
throw DiagnosticError.missingTypeAnnotation
|
|
}
|
|
|
|
var key: ExprSyntax?
|
|
var defaultValue: ExprSyntax?
|
|
var isVolatile = false
|
|
|
|
if let list = node.arguments?.as(LabeledExprListSyntax.self) {
|
|
for argument in list {
|
|
switch argument.label?.text {
|
|
case "key": key = argument.expression
|
|
case "defaultValue": defaultValue = argument.expression
|
|
case "volatile": isVolatile = argument.expression.as(BooleanLiteralExprSyntax.self)?.literal.text == "true"
|
|
default: break
|
|
}
|
|
}
|
|
}
|
|
|
|
let name = identifier.text
|
|
return Preference(name: name,
|
|
type: type,
|
|
key: key ?? "\(literal: name)",
|
|
defaultValue: defaultValue ?? "nil",
|
|
isVolatile: isVolatile)
|
|
}
|
|
}
|
|
|
|
extension UserPreferenceMacro: AccessorMacro {
|
|
public static func expansion(of node: AttributeSyntax,
|
|
providingAccessorsOf declaration: some DeclSyntaxProtocol,
|
|
in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
|
|
let preference = try preference(for: declaration, node: node)
|
|
|
|
if preference.isVolatile {
|
|
return [
|
|
"""
|
|
get {
|
|
\(preference.volatileValueName)
|
|
}
|
|
""",
|
|
"""
|
|
set {
|
|
\(preference.volatileValueName) = newValue
|
|
\(preference.subjectName).send(newValue)
|
|
}
|
|
"""
|
|
]
|
|
}
|
|
|
|
let type = preference.type
|
|
let key = preference.key
|
|
|
|
// Parenthesise a default that's a binary expression: `??` binds tighter than e.g. `==`,
|
|
// so `storage[key] ?? a == b` would otherwise parse as `(storage[key] ?? a) == b`.
|
|
let isBinaryExpression = preference.defaultValue.is(SequenceExprSyntax.self) || preference.defaultValue.is(InfixOperatorExprSyntax.self)
|
|
let defaultValue: ExprSyntax = isBinaryExpression ? "(\(preference.defaultValue))" : preference.defaultValue
|
|
|
|
return [
|
|
"""
|
|
get {
|
|
let storage = UserDefaultsStorage<\(type)>(userDefaults: store)
|
|
return storage[\(key)] ?? \(defaultValue)
|
|
}
|
|
""",
|
|
"""
|
|
set {
|
|
let storage = UserDefaultsStorage<\(type)>(userDefaults: store)
|
|
storage[\(key)] = newValue
|
|
\(preference.subjectName).send(newValue)
|
|
}
|
|
"""
|
|
]
|
|
}
|
|
}
|
|
|
|
extension UserPreferenceMacro: PeerMacro {
|
|
public static func expansion(of node: AttributeSyntax,
|
|
providingPeersOf declaration: some DeclSyntaxProtocol,
|
|
in context: some MacroExpansionContext) throws -> [DeclSyntax] {
|
|
let preference = try preference(for: declaration, node: node)
|
|
let type = preference.type
|
|
let name = preference.name
|
|
|
|
var peers: [DeclSyntax] = []
|
|
|
|
if preference.isVolatile {
|
|
peers.append("private var \(preference.volatileValueName): \(type) = \(preference.defaultValue)")
|
|
}
|
|
|
|
peers.append("private let \(preference.subjectName) = PassthroughSubject<\(type), Never>()")
|
|
peers.append("""
|
|
var \(raw: name)Publisher: AnyPublisher<\(type), Never> {
|
|
\(preference.subjectName).prepend(\(raw: name)).eraseToAnyPublisher()
|
|
}
|
|
""")
|
|
|
|
peers.append(resetMethod(for: preference))
|
|
|
|
return peers
|
|
}
|
|
|
|
/// A method that clears the stored value so it reverts to the default.
|
|
private static func resetMethod(for preference: Preference) -> DeclSyntax {
|
|
if preference.isVolatile {
|
|
"""
|
|
func \(preference.resetMethodName)() {
|
|
\(preference.volatileValueName) = \(preference.defaultValue)
|
|
}
|
|
"""
|
|
} else {
|
|
"""
|
|
func \(preference.resetMethodName)() {
|
|
let storage = UserDefaultsStorage<\(preference.type)>(userDefaults: store)
|
|
storage[\(preference.key)] = nil
|
|
}
|
|
"""
|
|
}
|
|
}
|
|
}
|