From 9e576ca439bd48e3b14171efdb30eb43d528f90b Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 23 May 2023 20:59:34 +0600 Subject: [PATCH] Dependency injection test implementation --- DependencyInjection/main.swift | 240 +++++++++ MacroExamples.xcodeproj/project.pbxproj | 125 +++++ .../contents.xcworkspacedata | 2 +- MacroExamples/main.swift | 61 ++- MacroExamplesLib/Macros.swift | 15 + .../DictionaryIndirectionMacro.swift | 3 +- MacroExamplesPlugin/InjectableMacro.swift | 470 ++++++++++++++++++ .../WrapStoredPropertiesMacro.swift | 6 + Package.resolved | 14 + 9 files changed, 929 insertions(+), 7 deletions(-) create mode 100644 DependencyInjection/main.swift create mode 100644 MacroExamplesPlugin/InjectableMacro.swift create mode 100644 Package.resolved diff --git a/DependencyInjection/main.swift b/DependencyInjection/main.swift new file mode 100644 index 0000000..596853c --- /dev/null +++ b/DependencyInjection/main.swift @@ -0,0 +1,240 @@ +// +// main.swift +// DependencyInjection +// +// Created by Alexey Martemianov on 21/5/23. +// + +import Foundation +import MacroExamplesLib + +// MARK: - Dependencies + +protocol Service { + func request() -> Int +} +struct ServiceImp: Service { + static var counter: Int = 1 + var a: Int = { defer { Self.counter += 1 }; return Self.counter }() + + func request() -> Int { + 3 + } +} + +protocol Service1 { + var a: Int { get } +} +struct Service1Impl: Service1 { + static var counter: Int = 1 + var a: Int = { defer { Self.counter += 1 }; return Self.counter }() +} +protocol Service2 { + var b: Int { get } +} +struct Service2Impl: Service2 { + static var counter: Int = 1 + var b: Int = { defer { Self.counter += 1 }; return Self.counter }() +} +protocol Service3 { + var c: Int { get } +} +struct Service3Impl: Service3 { + static var counter: Int = 1 + var c: Int = { defer { Self.counter += 1 }; return Self.counter }() +} +protocol Factory1 { + func m1() +} +struct Factory1Impl: Factory1 { + func m1() {} +} +protocol Factory2 { + func m2() +} +struct Factory2Impl: Factory2 { + func m2() {} +} +protocol Factory3 { + func m3() +} +struct Factory3Impl: Factory3 { + func m3() {} +} + + +// MARK: - Root Struct + +@Injectable +@InjectedDependencies(for: ChildStruct.self) // only providing dependencies used by ChildStruct +struct MyInjectableStruct { + var a: Int + var z: Int + + var pre: Int { 3 } + + @Injected + var service: Service + + private init(a: Int) { + self.a = a + self.z = dependencyProvider.service.request() + + print("init \(type(of: self))", dependencyProvider.service, dependencyProvider._storage) + } + + func test() { + _=service.request() + } + + func testMake() async { + let cs = ChildStruct.make(with: self.dependencyProvider, val1: "val1", val2: 5) { + $0.service1 = Service1Impl() + print($0.service1) + } + cs.testMake() + + let cs2 = ChildStruct2.make(with: dependencyProvider, c: "c", d: 6, e: 7) + cs2.test() + + // MARK: using MyInjectableStruct and appending them with missing Service3 + let deps = AnotherChildStruct.makeDependencies(service3: Service3Impl(), + nested: self.dependencyProvider) + let acs = AnotherChildStruct.make(with: deps, b: "b", c: 9) + acs.testMake() + + } + +} + +@Injectable +@InjectedDependencies(for: ChildStruct2.self) +struct ChildStruct { + + let val1: String + let val2: Int + + @Injected + var service1: Service1 + @Injected + var factory1: Factory1 + + private init(val1: String, val2: Int) { + self.val1 = val1 + self.val2 = val2 + + print("init \(type(of: self))", dependencyProvider.service1, dependencyProvider.factory1) + } + + func testMake() { + let cs2 = ChildStruct2.make(with: self.dependencyProvider, c: "hi!", d: -1, e: 3.14159) { + print($0.service2, $0.service) + } + cs2.test() + } + +} + +@Injectable +struct ChildStruct2 { + + @Injected + var service: Service + @Injected + var service2: Service2 + @Injected + var factory2: Factory2 + + // MARK: Uncomment this to get a dependency tree updated till the root +// @Injected +// var factory3: Factory3 + + let c: String + let d: Int + let e: Double + + private init(c: String, d: Int, e: Double) { + self.c = c + self.d = d + self.e = e + + print("init \(type(of: self))", dependencyProvider.service, dependencyProvider.service2, dependencyProvider.factory2) + } + + func test() { + + } + +} + +@Injectable +@InjectedDependencies(for: ChildStruct2.self) +struct AnotherChildStruct { + + let b: String + let c: Int + + @Injected + var service3: Service3 + + private init(b: String, c: Int) { + self.b = b + self.c = c + + print("init \(type(of: self))", dependencyProvider.service3) + } + + func testMake() { + let cs2 = ChildStruct2.make(with: self.dependencyProvider, c: "hi!", d: -1, e: 3.14159) { + print($0.service2, $0.service) + } + cs2.test() + } + +} + +/// helper struct used for resolving the dependencies +@dynamicMemberLookup +public struct MutableDynamicDependencies { + var storagePtr: UnsafeMutablePointer<[AnyKeyPath: Any]> + + public init(_ storagePtr: UnsafeMutablePointer<[AnyKeyPath: Any]>) { + self.storagePtr = storagePtr + } + public subscript(dynamicMember keyPath: KeyPath) -> T { + get { + self.storagePtr.pointee[keyPath] as! T + } + nonmutating set { + self.storagePtr.pointee[keyPath] = newValue + } + } +} + +// MARK: - Testing + +/// Test dependency provider +struct MainDepProvider: MyInjectableStruct.DependencyProvider { + + var service: any Service + + var service1: any Service1 + + var factory1: any Factory1 + + var service2: any Service2 + + var factory2: any Factory2 + +} +let mainDepProvider = MainDepProvider(service: ServiceImp(), service1: Service1Impl(), factory1: Factory1Impl(), service2: Service2Impl(), factory2: Factory2Impl()) + +// instantiate the root struct +let injstr = MyInjectableStruct.make(with: mainDepProvider, a: 1) { + // modify provided depdendencies in the time of consturction + $0.service = ServiceImp() +} + +await injstr.testMake() + + diff --git a/MacroExamples.xcodeproj/project.pbxproj b/MacroExamples.xcodeproj/project.pbxproj index e873eaf..2cacd88 100644 --- a/MacroExamples.xcodeproj/project.pbxproj +++ b/MacroExamples.xcodeproj/project.pbxproj @@ -17,6 +17,9 @@ 88E54A5229B5475400252D99 /* MetaEnumMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E54A5129B5475400252D99 /* MetaEnumMacro.swift */; }; 88E54A5429B5520A00252D99 /* MetaEnumMacroTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E54A5329B5520A00252D99 /* MetaEnumMacroTests.swift */; }; 911F5F8829BB5A150081AF9C /* URLMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911F5F8729BB5A150081AF9C /* URLMacro.swift */; }; + B66E4FF52A188BB00000CC5F /* InjectableMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = B66E4FF42A188BB00000CC5F /* InjectableMacro.swift */; }; + B69583402A1A0153000023BE /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = B695833F2A1A0153000023BE /* main.swift */; }; + B69583442A1A016E000023BE /* libMacroExamplesLib.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = BD8A3126294947A100E83EB9 /* libMacroExamplesLib.dylib */; }; BD2CDEE9298B24040015A701 /* Diagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2CDEE8298B24040015A701 /* Diagnostics.swift */; }; BD2CDEEB298B34730015A701 /* WrapStoredPropertiesMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2CDEEA298B34730015A701 /* WrapStoredPropertiesMacro.swift */; }; BD2CDEED298B4A650015A701 /* DictionaryIndirectionMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2CDEEC298B4A650015A701 /* DictionaryIndirectionMacro.swift */; }; @@ -35,6 +38,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + B69583462A1A016E000023BE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BDF5AFD82947E5B000FA119B /* Project object */; + proxyType = 1; + remoteGlobalIDString = BD8A3125294947A100E83EB9; + remoteInfo = MacroExamplesLib; + }; + B69583492A1A0178000023BE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BDF5AFD82947E5B000FA119B /* Project object */; + proxyType = 1; + remoteGlobalIDString = BDF5AFED2947E61100FA119B; + remoteInfo = MacroExamplesPlugin; + }; BD8A31322949480C00E83EB9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BDF5AFD82947E5B000FA119B /* Project object */; @@ -66,6 +83,15 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + B695833B2A1A0153000023BE /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; BDF5AFDE2947E5B000FA119B /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -88,6 +114,9 @@ 88E54A5129B5475400252D99 /* MetaEnumMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaEnumMacro.swift; sourceTree = ""; }; 88E54A5329B5520A00252D99 /* MetaEnumMacroTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaEnumMacroTests.swift; sourceTree = ""; }; 911F5F8729BB5A150081AF9C /* URLMacro.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLMacro.swift; sourceTree = ""; }; + B66E4FF42A188BB00000CC5F /* InjectableMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InjectableMacro.swift; sourceTree = ""; }; + B695833D2A1A0153000023BE /* DependencyInjection */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = DependencyInjection; sourceTree = BUILT_PRODUCTS_DIR; }; + B695833F2A1A0153000023BE /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; BD2CDEE8298B24040015A701 /* Diagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Diagnostics.swift; sourceTree = ""; }; BD2CDEEA298B34730015A701 /* WrapStoredPropertiesMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapStoredPropertiesMacro.swift; sourceTree = ""; }; BD2CDEEC298B4A650015A701 /* DictionaryIndirectionMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryIndirectionMacro.swift; sourceTree = ""; }; @@ -109,6 +138,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + B695833A2A1A0153000023BE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B69583442A1A016E000023BE /* libMacroExamplesLib.dylib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; BD8A3124294947A100E83EB9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -142,6 +179,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + B695833E2A1A0153000023BE /* DependencyInjection */ = { + isa = PBXGroup; + children = ( + B695833F2A1A0153000023BE /* main.swift */, + ); + path = DependencyInjection; + sourceTree = ""; + }; BD841F80294CE14500DA4D81 /* MacroExamplesPlugin */ = { isa = PBXGroup; children = ( @@ -162,6 +207,7 @@ 1D682A95299E3313006F9F78 /* CustomCodable.swift */, BD48319229AFF87200F3123A /* OptionSetMacro.swift */, 88E54A5129B5475400252D99 /* MetaEnumMacro.swift */, + B66E4FF42A188BB00000CC5F /* InjectableMacro.swift */, ); path = MacroExamplesPlugin; sourceTree = ""; @@ -183,6 +229,7 @@ BDF5AFE22947E5B000FA119B /* MacroExamples */, BDFB14B32948484000708DA6 /* MacroExamplesPluginTest */, BD8A3127294947A100E83EB9 /* MacroExamplesLib */, + B695833E2A1A0153000023BE /* DependencyInjection */, BDF5AFE12947E5B000FA119B /* Products */, BDF5B0162947FE1A00FA119B /* Frameworks */, ); @@ -196,6 +243,7 @@ BDF5AFEE2947E61100FA119B /* libMacroExamplesPlugin.dylib */, BDFB14B22948484000708DA6 /* MacroExamplesPluginTest.xctest */, BD8A3126294947A100E83EB9 /* libMacroExamplesLib.dylib */, + B695833D2A1A0153000023BE /* DependencyInjection */, ); name = Products; sourceTree = ""; @@ -245,6 +293,25 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + B695833C2A1A0153000023BE /* DependencyInjection */ = { + isa = PBXNativeTarget; + buildConfigurationList = B69583412A1A0153000023BE /* Build configuration list for PBXNativeTarget "DependencyInjection" */; + buildPhases = ( + B69583392A1A0153000023BE /* Sources */, + B695833A2A1A0153000023BE /* Frameworks */, + B695833B2A1A0153000023BE /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + B695834A2A1A0178000023BE /* PBXTargetDependency */, + B69583472A1A016E000023BE /* PBXTargetDependency */, + ); + name = DependencyInjection; + productName = DependencyInjection; + productReference = B695833D2A1A0153000023BE /* DependencyInjection */; + productType = "com.apple.product-type.tool"; + }; BD8A3125294947A100E83EB9 /* MacroExamplesLib */ = { isa = PBXNativeTarget; buildConfigurationList = BD8A312C294947A100E83EB9 /* Build configuration list for PBXNativeTarget "MacroExamplesLib" */; @@ -331,6 +398,9 @@ LastSwiftUpdateCheck = 1430; LastUpgradeCheck = 1430; TargetAttributes = { + B695833C2A1A0153000023BE = { + CreatedOnToolsVersion = 14.3; + }; BD8A3125294947A100E83EB9 = { CreatedOnToolsVersion = 14.3; LastSwiftMigration = 1430; @@ -366,6 +436,7 @@ BDF5AFED2947E61100FA119B /* MacroExamplesPlugin */, BDFB14B12948484000708DA6 /* MacroExamplesPluginTest */, BD8A3125294947A100E83EB9 /* MacroExamplesLib */, + B695833C2A1A0153000023BE /* DependencyInjection */, ); }; /* End PBXProject section */ @@ -381,6 +452,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + B69583392A1A0153000023BE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B69583402A1A0153000023BE /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; BD8A3123294947A100E83EB9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -414,6 +493,7 @@ BD2CDEE9298B24040015A701 /* Diagnostics.swift in Sources */, 1D682A96299E3313006F9F78 /* CustomCodable.swift in Sources */, BD2CDEED298B4A650015A701 /* DictionaryIndirectionMacro.swift in Sources */, + B66E4FF52A188BB00000CC5F /* InjectableMacro.swift in Sources */, BD752BE7294D461B00D00A2E /* FontLiteralMacro.swift in Sources */, BD7324B829989178009C6A08 /* AddCompletionHandlerMacro.swift in Sources */, BD48319329AFF87200F3123A /* OptionSetMacro.swift in Sources */, @@ -435,6 +515,16 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + B69583472A1A016E000023BE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BD8A3125294947A100E83EB9 /* MacroExamplesLib */; + targetProxy = B69583462A1A016E000023BE /* PBXContainerItemProxy */; + }; + B695834A2A1A0178000023BE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BDF5AFED2947E61100FA119B /* MacroExamplesPlugin */; + targetProxy = B69583492A1A0178000023BE /* PBXContainerItemProxy */; + }; BD8A31332949480C00E83EB9 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = BD8A3125294947A100E83EB9 /* MacroExamplesLib */; @@ -461,6 +551,32 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + B69583422A1A0153000023BE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = E3R68ED529; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 13.3; + OTHER_SWIFT_FLAGS = "-Xfrontend -enable-experimental-feature -Xfrontend Macros -Xfrontend -load-plugin-library -Xfrontend ${BUILD_DIR}/${CONFIGURATION}/libMacroExamplesPlugin.dylib -Xfrontend -dump-macro-expansions"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + B69583432A1A0153000023BE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = E3R68ED529; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 13.3; + OTHER_SWIFT_FLAGS = "-Xfrontend -enable-experimental-feature -Xfrontend Macros -Xfrontend -load-plugin-library -Xfrontend ${BUILD_DIR}/${CONFIGURATION}/libMacroExamplesPlugin.dylib -Xfrontend -dump-macro-expansions"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; BD8A312D294947A100E83EB9 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -718,6 +834,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + B69583412A1A0153000023BE /* Build configuration list for PBXNativeTarget "DependencyInjection" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B69583422A1A0153000023BE /* Debug */, + B69583432A1A0153000023BE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; BD8A312C294947A100E83EB9 /* Build configuration list for PBXNativeTarget "MacroExamplesLib" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/MacroExamples.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/MacroExamples.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 2fc20b4..52ebbcd 100644 --- a/MacroExamples.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/MacroExamples.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:/Users/mallexxx/Downloads/swift-macro-examples-main/MacroExamples.xcodeproj"> diff --git a/MacroExamples/main.swift b/MacroExamples/main.swift index aa1ed07..a47bf31 100644 --- a/MacroExamples/main.swift +++ b/MacroExamples/main.swift @@ -11,9 +11,9 @@ print(#stringify(x + y)) // "AddBlocker" complains about addition operations. We emit a warning // so it doesn't block compilation. -print(#addBlocker(x * y + z)) +//print(#addBlocker(x * y + z)) -#myWarning("remember to pass a string literal here") +//#myWarning("remember to pass a string literal here") // Uncomment to get an error out of the macro. // let text = "oops" @@ -41,15 +41,66 @@ struct OldStorage { var x: Int } +@propertyWrapper +struct Injected { + + init() { + + } +// init(initialValue: Value) { +// self.init(wrappedValue: initialValue) +// } +// +// init(wrappedValue: Value) { +// subject = CurrentValueSubject(wrappedValue) +// } + + private var _wrappedValue: Value? +// var wrappedValue: Value { +// get { +// _wrappedValue! +// } +// set { +// _wrappedValue = newValue +// } +// } + + var projectedValue: Injected { + return self + } + + @available(*, unavailable, message: "@PublishedAfter is only available on properties of classes") + var wrappedValue: Value { + get { fatalError() } + // swiftlint:disable unused_setter_value + set { fatalError() } + // swiftlint:enable unused_setter_value + } + + static subscript( + _enclosingInstance object: EnclosingSelf, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath> + ) -> Value { + get { + object[keyPath: storageKeyPath]._wrappedValue! + } + set { + object[keyPath: storageKeyPath]._wrappedValue = newValue + } + } + +} + // The deprecation warning below comes from the deprecation attribute // introduced by @wrapStoredProperties on OldStorage. -_ = OldStorage(x: 5).x +//_ = OldStorage(x: 5).x // Move the storage from each of the stored properties into a dictionary // called `_storage`, turning the stored properties into computed properties. @DictionaryStorage struct Point { - var x: Int = 1 + var x: Int = 2 var y: Int = 2 } @@ -155,7 +206,7 @@ Task { let myStruct = MyStruct() let a = try? await myStruct.c(a: 5, for: "Test", 20) - await myStruct.d(a: 10, for: "value", 40) +// await myStruct.d(a: 10, for: "value", 40) } MyStruct().f(a: 1, for: "hello", 3.14159) { result in diff --git a/MacroExamplesLib/Macros.swift b/MacroExamplesLib/Macros.swift index bdd0bd9..e24fd55 100644 --- a/MacroExamplesLib/Macros.swift +++ b/MacroExamplesLib/Macros.swift @@ -39,6 +39,20 @@ public protocol ExpressibleByFontLiteral { @attached(memberAttribute) public macro wrapStoredProperties(_ attributeName: String) = #externalMacro(module: "MacroExamplesPlugin", type: "WrapStoredPropertiesMacro") +@attached(member, names: named(Dependencies), named(DynamicDependencies), named(DependencyProvider), named(DynamicDependencyProvider), named(dependencyProvider), named(_currentDependencies), named(getAllDependencyProviderKeyPaths(from:)), named(makeDependencies), named(make)) +@attached(memberAttribute) +@attached(peer, names: suffixed(_DependencyProvider), suffixed(_DependencyProvider_allKeyPaths), suffixed(_DynamicDependencyProvider)) +public macro Injectable() = #externalMacro(module: "MacroExamplesPlugin", type: "InjectableMacro") + +@attached(accessor) +public macro Injected() = #externalMacro(module: "MacroExamplesPlugin", type: "InjectedMacro") + +//@freestanding(declaration, names: arbitrary) +//public macro InjectedDependencies(for type: Any.Type) = #externalMacro(module: "MacroExamplesPlugin", type: "InjectedDependenciesMacro") +@attached(conformance) +@attached(member, names: arbitrary) +public macro InjectedDependencies(for type: Any.Type...) = #externalMacro(module: "MacroExamplesPlugin", type: "InjectedDependenciesMacro") + /// Wrap up the stored properties of the given type in a dictionary, /// turning them into computed properties. /// @@ -54,6 +68,7 @@ public macro wrapStoredProperties(_ attributeName: String) = #externalMacro(modu @attached(memberAttribute) public macro DictionaryStorage() = #externalMacro(module: "MacroExamplesPlugin", type: "DictionaryStorageMacro") + public protocol Observable {} public protocol Observer { diff --git a/MacroExamplesPlugin/DictionaryIndirectionMacro.swift b/MacroExamplesPlugin/DictionaryIndirectionMacro.swift index 13743ee..de779d1 100644 --- a/MacroExamplesPlugin/DictionaryIndirectionMacro.swift +++ b/MacroExamplesPlugin/DictionaryIndirectionMacro.swift @@ -1,7 +1,8 @@ import SwiftSyntax import SwiftSyntaxMacros -public struct DictionaryStorageMacro { } +public struct DictionaryStorageMacro { +} extension DictionaryStorageMacro: AccessorMacro { public static func expansion< diff --git a/MacroExamplesPlugin/InjectableMacro.swift b/MacroExamplesPlugin/InjectableMacro.swift new file mode 100644 index 0000000..ca6c76f --- /dev/null +++ b/MacroExamplesPlugin/InjectableMacro.swift @@ -0,0 +1,470 @@ +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct InjectableMacro: MemberMacro { + + // Add members to Injectable + public static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + + guard let declaration = declaration.as(StructDeclSyntax.self) else { return [] } //.description.trimmingCharacters(in: .whitespacesAndNewlines) + + // guard let i = declaration.as(InitializerDeclSyntax.self) else { return [] } + +// let memberList = MemberDeclListSyntax( +// declaration.memberBlock.members +// // .filter { +// // $0.decl.isObservableStoredProperty +// // } +// ) +// let members = declaration.memberBlock.members.map { "\($0.decl.kind): " + $0.decl.description } + + let injectedDependenciesInjectables = try declaration.attributes?.lazy + .compactMap { + $0.as(AttributeSyntax.self) + } + .first(where: { + $0.attributeName.description == "InjectedDependencies" + }) + .map { attribute -> [String] in + guard case let .argumentList(arguments) = attribute.argument else { + throw CustomError.message("InjectedDependencies invalid declaration") + } + return arguments.compactMap { + $0.expression.as(MemberAccessExprSyntax.self)?.base?.as(IdentifierExprSyntax.self)?.identifier.text + } + } ?? [] + let compositions = injectedDependenciesInjectables.isEmpty ? "" : "& " + injectedDependenciesInjectables.map { + $0 + ".DependencyProvider" + }.joined(separator: " & ") + let dynamicCompositions = injectedDependenciesInjectables.isEmpty ? "" : "& " + injectedDependenciesInjectables.map { + $0 + ".DynamicDependencyProvider" + }.joined(separator: " & ") + let keyPathsGetters = injectedDependenciesInjectables.map { + "result.formUnion(\($0).getAllDependencyProviderKeyPaths(from: dependencyProvider))" + }.joined(separator: "\n") + + + var paramList: FunctionParameterListSyntax! + for member in declaration.memberBlock.members { + guard let initializer = member.decl.as(InitializerDeclSyntax.self) else { continue } + guard initializer.modifiers?.contains(where: { $0.name.text == "private" }) == true else { + // TODO: File/Line + throw CustomError.message("Initializer for @Injectable should be declared `private`") + } +// let modifier = initializer.modifiers!.first(where: { $0.name.text == "private" })! +// let modifier = initializer.modifiers!.first!.name //.as(DeclModifierSyntax.self)! + paramList = initializer.signature.input.parameterList +// initializer. + } + + + // throw CustomError.message("\(i.description)") + // guard let property = member.as(VariableDeclSyntax.self), + // property.isStoredProperty + // else { + // return [] + // } + + // guard case let .argumentList(arguments) = node.argument, + // let firstElement = arguments.first, + // let stringLiteral = firstElement.expression + // .as(StringLiteralExprSyntax.self), + // stringLiteral.segments.count == 1, + // case let .stringSegment(wrapperName)? = stringLiteral.segments.first else { + // throw CustomError.message("macro requires a string literal containing the name of an attribute") + // } + +// let storage: DeclSyntax = """ +// @AddCompletionHandler +// func test(a: Int, for b: String, _ value: Double) async -> String { +// return b +// } +// """ +// let storage: DeclSyntax = "var _storage: [String: Any] = [:]" + +// return [ +// storage.with(\.leadingTrivia, [.newlines(1), .spaces(2)]) +// ] + + +// return [ + // "init(a: Int, b: Int) { fatalError() }" + + // AttributeSyntax(i)! + // AttributeSyntax( + // attributeName: SimpleTypeIdentifierSyntax( + // name: .identifier(wrapperName.content.text) + // ) + // ) + // .with(\.leadingTrivia, [.newlines(1), .spaces(2)]) +// ] + + let members = declaration.memberBlock.members.compactMap { + $0.decl.as(VariableDeclSyntax.self) + }.filter { + $0.attributes?.first?.trimmed.description == "@Injected" + } + let vars = members.map { + let binding = $0.bindings.first! + return (name: binding.pattern.as(IdentifierPatternSyntax.self)!.identifier.text, + type: binding.typeAnnotation!.type.as(SimpleTypeIdentifierSyntax.self)!.name.text) + } + +// let vars = members.map { +// $0.description +// .trimmingCharacters(in: .whitespacesAndNewlines) +// .dropping(prefix: "@Injected") +// .trimmingCharacters(in: .whitespacesAndNewlines) +//// + " { fatalError() }" +// } + let initVars = paramList.map { + let varName = $0.firstName.text + return "\(varName): \(varName)" + }.joined(separator: ", ") +// throw CustomError.message("`\(initVars)`") + + let dependencyInitArguments = vars.map { + "\($0.name): \($0.type)" + }.joined(separator: ", ") + let dynamicDependencyProviderInitArguments = dependencyInitArguments + + (dynamicCompositions.isEmpty ? "" : ", nested nestedProvider: ") + dynamicCompositions.dropping(prefix: "& ") + var storageInitLiteral = "[\n" + vars.map { + "\\\(declaration.identifier.text)_DependencyProvider.\($0.name): \($0.name)" + }.joined(separator: ",\n") + "\n]" + if !dynamicCompositions.isEmpty { + storageInitLiteral = "nestedProvider._storage.merging(\(storageInitLiteral)) { $1 }" + } + + + +// \(raw: vars.joined(separator: "\n")) + let identifier = declaration.identifier.text +// return [""" +// typealias DependencyProvider = \(raw: declaration.identifier.text)_DependencyProvider +// +// @dynamicMemberLookup +// struct Dependencies: DependencyProvider { +// var _storage = [AnyKeyPath: Any]() +// +// init() { +// self.init(with: \(raw: declaration.identifier.text)._currentDependencies) +// } +// +// init(with dependencyProvider: DependencyProvider) { +// (raw: initVarsFromProvider) +// } +// +// subscript(dynamicMember keyPath: KeyPath) -> T { +// self.value[keyPath: keyPath] +// } +// +// } +// let dependencyProvider = Dependencies() +// +// @TaskLocal private static var _currentDependencies: DependencyProvider! +// """] + return [""" + typealias DependencyProvider = \(raw: identifier)_DependencyProvider \(raw: compositions) + typealias DynamicDependencyProvider = \(raw: identifier)_DynamicDependencyProvider \(raw: dynamicCompositions) + + static func getAllDependencyProviderKeyPaths(from dependencyProvider: DependencyProvider) -> Set { + var result = Set() + result.formUnion(\(raw: identifier)_DependencyProvider_allKeyPaths()) + \(raw: keyPathsGetters) + return result + } + + @dynamicMemberLookup + struct DynamicDependencies: DynamicDependencyProvider { + var _storage: [AnyKeyPath: Any] + + init() { + self._storage = \(raw: identifier)._currentDependencies._storage + } + init(_ storage: [AnyKeyPath: Any]) { + self._storage = storage + } + init(_ dependencyProvider: DependencyProvider) { + self._storage = \(raw: identifier).getAllDependencyProviderKeyPaths(from: dependencyProvider).reduce(into: [:]) { + $0[$1] = dependencyProvider[keyPath: $1] + } + } + init(_ dependencyProvider: DynamicDependencyProvider) { + self._storage = dependencyProvider._storage + } + + subscript(dynamicMember keyPath: KeyPath<\(raw: identifier)_DependencyProvider, T>) -> T { + self._storage[keyPath] as! T + } + } + + static func makeDependencies(\(raw: dynamicDependencyProviderInitArguments)) -> DynamicDependencies { + DynamicDependencies(\(raw: storageInitLiteral)) + } + + let dependencyProvider = DynamicDependencies() + @TaskLocal private static var _currentDependencies: DynamicDependencies! + + static func make(with dependencies: DependencyProvider, \(paramList)\(paramList.isEmpty ? "" : ",") updateValues: ((MutableDynamicDependencies<\(raw: identifier)_DependencyProvider>) throws -> Void)? = nil) rethrows -> Self { + var dependencies = DynamicDependencies(dependencies) + try updateValues?(MutableDynamicDependencies(&dependencies._storage)) + return self.$_currentDependencies.withValue(dependencies) { + return self.init(\(raw: initVars)) + } + } + + static func make(with dependencies: DynamicDependencyProvider, \(paramList)\(paramList.isEmpty ? "" : ",") updateValues: ((MutableDynamicDependencies<\(raw: identifier)_DependencyProvider>) throws -> Void)? = nil) rethrows -> Self { + var dependencies = DynamicDependencies(dependencies) + try updateValues?(MutableDynamicDependencies(&dependencies._storage)) + return self.$_currentDependencies.withValue(dependencies) { + return self.init(\(raw: initVars)) + } + } + """] + } + +} + + +extension InjectableMacro: MemberAttributeMacro { + + public static func expansion( + of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, + providingAttributesFor member: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [AttributeSyntax] { + + guard let initializer = member.as(InitializerDeclSyntax.self) else { return [] } + + let descr = member.description.trimmingCharacters(in: .whitespacesAndNewlines) +// guard let property = member.as(VariableDeclSyntax.self), +// property.isStoredProperty, +// !descr.hasPrefix("@"), !descr.contains("<"), !descr.contains("z"), +// descr.hasPrefix("var z: Int") +// else { + return [] +// } + + return [ + AttributeSyntax( + attributeName: SimpleTypeIdentifierSyntax( + name: .identifier("InjectableInit") + ) + ) + .with(\.leadingTrivia, [.newlines(1), .spaces(2)]) + ] + } + +} + +extension String { + func dropping(prefix: String) -> String { + return hasPrefix(prefix) ? String(dropFirst(prefix.count)) : self + } +} +//var injectableProtocolVars = [String: Set]() +extension InjectableMacro: PeerMacro { + + public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: Declaration, in context: Context) throws -> [DeclSyntax] where Context : MacroExpansionContext, Declaration : DeclSyntaxProtocol { + + guard let decl = declaration.as(StructDeclSyntax.self) else { return [] } //.description.trimmingCharacters(in: .whitespacesAndNewlines) + let a: AttributeListSyntax! +// a.description + + let vars = decl.memberBlock.members.compactMap { + $0.decl.as(VariableDeclSyntax.self) + }.filter { + $0.attributes?.first?.trimmed.description == "@Injected" + }.map { + $0.description + .trimmingCharacters(in: .whitespacesAndNewlines) + .dropping(prefix: "@Injected") + .trimmingCharacters(in: .whitespacesAndNewlines) + } +// injectableProtocolVars[decl.identifier.text, default: []].formUnion(vars) + + let identifier = decl.identifier.text + + let protocolVars = vars.map { $0 + " { get }" } + let keyPaths = vars.map { + "\\" + identifier + "_DependencyProvider." + $0.split(separator: " ", maxSplits: 1).last!.components(separatedBy: ":").first! // TODO: get identifier + } +// throw CustomError.message("`\(d)`") + +// let memberList = try MemberDeclListSyntax( +// decl.memberBlock.members.filter { +// +// if let property = $0.as(VariableDeclSyntax.self), +// property.attributes?.isEmpty == false { +// throw CustomError.message("`\(property.attributes!.first!.description)`") +// return true +// } //.contains(where: { $0. }) +// return false +//// $0.decl.isObservableStoredProperty +// } +// ) +// throw CustomError.message("`\(memberList.first!.description)`") + +// let descr = decl.identifier.text + +// throw CustomError.message("`\(descr)`") + + return [""" + protocol \(raw: identifier)_DependencyProvider { + \(raw: protocolVars.joined(separator: "\n")) + } + func \(raw: identifier)_DependencyProvider_allKeyPaths() -> Set { + [ + \(raw: keyPaths.joined(separator: ",\n")) + ] + } + + protocol \(raw: identifier)_DynamicDependencyProvider { + var _storage: [AnyKeyPath: Any] { get set } + } + + """] +// return [ +// """ +// protocol \(raw: decl.identifier.text)_DependencyProvider { +// \(raw: protocolVars.joined(separator: "\n")) +// } +// """ +// ] + } + +} + + +public struct InjectedMacro { +} + +// TODO: Cache injections for InjectedDependenciesMacro +extension InjectedMacro: AccessorMacro { + public static func expansion< + Context: MacroExpansionContext, + Declaration: DeclSyntaxProtocol + >( + of node: AttributeSyntax, + providingAccessorsOf declaration: Declaration, + in context: Context + ) throws -> [AccessorDeclSyntax] { + guard let varDecl = declaration.as(VariableDeclSyntax.self), + let binding = varDecl.bindings.first, + let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier + else { + return [] + } + +// if injectables.contains("ChildStruct2") { +// throw CustomError.message("ChildStruct2 `\(vars)`") +// } + + // TODO: allow default value +// guard let defaultValue = binding.initializer?.value else { +// throw CustomError.message("stored property must have an initializer") +// } + + return [ + """ + get { + dependencyProvider.\(raw: identifier.text) + } + """ + ] + } +} + +struct InjectedDependenciesMacro: MemberMacro { + + static func expansion(of node: AttributeSyntax, providingMembersOf declaration: Declaration, in context: Context) throws -> [DeclSyntax] where Declaration : DeclGroupSyntax, Context : MacroExpansionContext { + + guard case let .argumentList(arguments) = node.argument else { + throw CustomError.message("InjectedDependencies invalid declaration") + } + +// let injectables = arguments.compactMap { $0.expression.as(MemberAccessExprSyntax.self)?.base?.as(IdentifierExprSyntax.self)?.identifier.text } + +// let mapping = injectables.map { "id `\($0)`: \(injectableProtocolVars[$0]?.joined(separator: ",") ?? "nil")" } + +// let commonSet = injectables.reduce(into: Set()) { $0.formUnion(injectableProtocolVars[$1, default: []]) } + +// let vars = commonSet.map { $0 + " { fatalError() }" } + +// if injectables.contains("ChildStruct2") { +// +// +// throw CustomError.message("ChildStruct2 `\(vars)`") +// } + +// let labels = arguments.map { $0.expression.as(MemberAccessExprSyntax.self)?.base?.as(IdentifierExprSyntax.self).ra } + +// let optionEnumNameArg = arguments.first(labeled: optionsEnumNameArgumentLabel) { + + +// guard let argument = node.argument else { //. .argumentList.first?.expression else { +// throw CustomError.message("InjectedDependencies invalid declaration") +// } + +// throw CustomError.message("`\(labels)`") + + return [ +// "\(raw: vars.joined(separator: "\n"))" + ] + } + +} + +extension InjectedDependenciesMacro: ConformanceMacro { + + public static func expansion( + of attribute: AttributeSyntax, + providingConformancesOf decl: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { + +// guard case let .argumentList(arguments) = attribute.argument else { +// throw CustomError.message("InjectedDependencies invalid declaration") +// } + +// let injectables = arguments.compactMap { $0.expression.as(MemberAccessExprSyntax.self)?.base?.as(IdentifierExprSyntax.self) } + + // TODO: filter out reduntant conformance +// // If there is an explicit conformance to OptionSet already, don't add one. +// if let inheritedTypes = structDecl.inheritanceClause?.inheritedTypeCollection, +// inheritedTypes.contains(where: { inherited in inherited.typeName.trimmedDescription == "OptionSet" }) { +// return [] +// } + +// throw CustomError.message("`\(context)`") +// return injectables.map { ("\($0).DependencyProvider", nil) } + return [] + } + +} + +//extension InjectedDependenciesMacro: DeclarationMacro { +// +// static func expansion(of node: Node, in context: Context) throws -> [DeclSyntax] where Node : FreestandingMacroExpansionSyntax, Context : MacroExpansionContext { +// +// guard let argument = node.argumentList.first?.expression else { +// throw CustomError.message("InjectedDependencies invalid declaration") +// } +//// let segments = argument.as(StringLiteralExprSyntax.self)?.segments, +//// segments.count == 1, +//// case .stringSegment(let literalSegment)? = segments.first +//// else { +//// throw CustomError.message("#URL requires a static string literal") +//// } +// +// +// throw CustomError.message("`\(argument)`") +// +// } +// +//} diff --git a/MacroExamplesPlugin/WrapStoredPropertiesMacro.swift b/MacroExamplesPlugin/WrapStoredPropertiesMacro.swift index a923253..09c4d90 100644 --- a/MacroExamplesPlugin/WrapStoredPropertiesMacro.swift +++ b/MacroExamplesPlugin/WrapStoredPropertiesMacro.swift @@ -33,6 +33,12 @@ public struct WrapStoredPropertiesMacro: MemberAttributeMacro { throw CustomError.message("macro requires a string literal containing the name of an attribute") } + let ad = AttributeSyntax( + attributeName: SimpleTypeIdentifierSyntax( + name: .identifier(wrapperName.content.text) + ) + ).description + return [ AttributeSyntax( attributeName: SimpleTypeIdentifierSyntax( diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..df8e2b3 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "branch" : "main", + "revision" : "0711d213f136b17e6775c89f7b528ff20a03f94c" + } + } + ], + "version" : 2 +}