diff --git a/Package.resolved b/Package.resolved index 40da984..809462e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -8,6 +8,15 @@ "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", "version" : "1.5.2" } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax", + "state" : { + "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", + "version" : "510.0.1" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 0629516..8ebdd90 100644 --- a/Package.swift +++ b/Package.swift @@ -1,8 +1,9 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 5.9 // swiftlint:disable explicit_acl explicit_top_level_acl import PackageDescription +import CompilerPluginSupport // let swiftSettings: [SwiftSetting] = [ // .enableUpcomingFeature("BareSlashRegexLiterals"), @@ -16,7 +17,7 @@ import PackageDescription let package = Package( name: "StealthyStash", - platforms: [.macOS(.v12), .iOS(.v14), .watchOS(.v7), .tvOS(.v14)], + platforms: [.macOS(.v12), .iOS(.v14), .watchOS(.v7), .tvOS(.v14), .macCatalyst(.v14), .visionOS(.v1)], products: [ .library( name: "StealthyStash", @@ -24,9 +25,17 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0") + .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-syntax", from: "510.0.0") ], targets: [ + .macro( + name: "StealthyStashMacros", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + ] + ), .target( name: "StealthyStash", dependencies: [ diff --git a/Samples/Demo/App/Keychain/CompositeCredentials.swift b/Samples/Demo/App/Keychain/CompositeCredentials.swift index 526182c..1e83fff 100644 --- a/Samples/Demo/App/Keychain/CompositeCredentials.swift +++ b/Samples/Demo/App/Keychain/CompositeCredentials.swift @@ -1,7 +1,127 @@ import StealthyStash struct CompositeCredentials: StealthyModel { - typealias QueryBuilder = CompositeCredentialsQueryBuilder + //typealias QueryBuilder = CompositeCredentialsQueryBuilder + + struct QueryBuilder: ModelQueryBuilder { + static func updates( + from previousItem: CompositeCredentials, + to newItem: CompositeCredentials + ) -> [StealthyPropertyUpdate] { + let newPasswordData = newItem.password.flatMap { + $0.data(using: .utf8) + }.map { + InternetPasswordItem(account: newItem.userName, data: $0) + } + + let oldPasswordData = previousItem.password.flatMap { + $0.data(using: .utf8) + }.map { + InternetPasswordItem(account: previousItem.userName, data: $0) + } + + let previousTokenData = previousItem.token.flatMap { + $0.data(using: .utf8) + }.map { + GenericPasswordItem(account: previousItem.userName, data: $0) + } + + let newTokenData = newItem.token.flatMap { + $0.data(using: .utf8) + }.map { + GenericPasswordItem(account: newItem.userName, data: $0) + } + + let passwordUpdate = StealthyPropertyUpdate( + previousProperty: oldPasswordData, + newProperty: newPasswordData + ) + let tokenUpdate = StealthyPropertyUpdate( + previousProperty: previousTokenData, + newProperty: newTokenData + ) + return [passwordUpdate, tokenUpdate] + } + + static func properties( + from model: CompositeCredentials, + for _: ModelOperation + ) -> [AnyStealthyProperty] { + let passwordData = model.password.flatMap { + $0.data(using: .utf8) + } + + let passwordProperty: AnyStealthyProperty = .init( + property: InternetPasswordItem( + account: model.userName, + data: passwordData ?? .init() + ) + ) + + let tokenData = model.token.flatMap { + $0.data(using: .utf8) + } + + let tokenProperty: AnyStealthyProperty = .init( + property: GenericPasswordItem( + account: model.userName, + data: tokenData ?? .init() + ) + ) + + return [passwordProperty, tokenProperty] + } + + static func queries(from _: Void) -> [String: Query] { + [ + "password": TypeQuery(type: .internet), + "token": TypeQuery(type: .generic) + ] + } + + static func model( + from properties: [String: [AnyStealthyProperty]] + ) throws -> CompositeCredentials? { + for internet in properties["password"] ?? [] { + for generic in properties["token"] ?? [] { + if internet.account == generic.account { + return .init( + userName: internet.account, + password: internet.dataString, + token: generic.dataString + ) + } + } + } + let properties = properties.values.flatMap { $0 }.enumerated().sorted { lhs, rhs in + if lhs.element.propertyType == rhs.element.propertyType { + return lhs.offset < rhs.offset + } else { + return lhs.element.propertyType == .internet + } + }.map(\.element) + + guard let username = properties.map(\.account).first else { + return nil + } + let password = properties + .first { $0.propertyType == .internet }? + .data + let token = properties.first { + $0.propertyType == .generic && $0.account == username + }?.data + + return CompositeCredentials( + userName: username, + password: password?.string(), + token: token?.string() + ) + } + + typealias QueryType = Void + + typealias StealthyModelType = CompositeCredentials + } internal init(userName: String, password: String?, token: String?) { self.userName = userName diff --git a/Samples/Demo/App/Keychain/CompositeCredentialsQueryBuilder.swift b/Samples/Demo/App/Keychain/CompositeCredentialsQueryBuilder.swift index da4df8d..928672f 100644 --- a/Samples/Demo/App/Keychain/CompositeCredentialsQueryBuilder.swift +++ b/Samples/Demo/App/Keychain/CompositeCredentialsQueryBuilder.swift @@ -1,5 +1,6 @@ import StealthyStash +@available(*, deprecated, message: "Use internal builder.") struct CompositeCredentialsQueryBuilder: ModelQueryBuilder { static func updates( from previousItem: CompositeCredentials, diff --git a/Sources/StealthyStashMacros/CompositeModelMacro.swift b/Sources/StealthyStashMacros/CompositeModelMacro.swift new file mode 100644 index 0000000..83f3478 --- /dev/null +++ b/Sources/StealthyStashMacros/CompositeModelMacro.swift @@ -0,0 +1,12 @@ +// +// File.swift +// +// +// Created by Leo Dion on 3/25/24. +// + +import SwiftSyntax +import SwiftSyntaxMacros + +//@attached(member, names: named(_$backingData), named(persistentBackingData), named(schemaMetadata), named(init), named(_$observationRegistrar), named(_SwiftDataNoType)) @attached(memberAttribute) @attached(extension, conformances: Observable, PersistentModel) macro Model() +