From 742d25989ca9190b858551dad0db6d2ae5264adf Mon Sep 17 00:00:00 2001 From: Rudro Samanta Date: Wed, 20 Jul 2022 23:38:06 -0700 Subject: [PATCH] Prep NeedleFoundation for dynamic code path - Note all the changes are pretty much dead-code for now as they are guarded by #if statements Start of implementation of #432 --- Sources/NeedleFoundation/Bootstrap.swift | 6 + Sources/NeedleFoundation/Component.swift | 172 +++++++++++++++++- .../Pluginized/NonCoreComponent.swift | 21 +++ .../Pluginized/PluginizedComponent.swift | 83 +++++++++ 4 files changed, 281 insertions(+), 1 deletion(-) diff --git a/Sources/NeedleFoundation/Bootstrap.swift b/Sources/NeedleFoundation/Bootstrap.swift index 96220804..cdf50a19 100644 --- a/Sources/NeedleFoundation/Bootstrap.swift +++ b/Sources/NeedleFoundation/Bootstrap.swift @@ -54,6 +54,12 @@ open class BootstrapComponent: Component { fatalError("BootstrapComponent does not have a parent, do not use this property.") } + #if NEEDLE_DYNAMIC + func find(property: String, skipThisLevel: Bool) -> T { + fatalError("Unable to find \(property) anywhere along the path to the root") + } + #endif + fileprivate init() {} } } diff --git a/Sources/NeedleFoundation/Component.swift b/Sources/NeedleFoundation/Component.swift index 0ac6fc70..b1048220 100644 --- a/Sources/NeedleFoundation/Component.swift +++ b/Sources/NeedleFoundation/Component.swift @@ -19,17 +19,185 @@ import Foundation /// The base protocol of a dependency, enabling Needle's parsing process. public protocol Dependency: AnyObject {} +#if NEEDLE_DYNAMIC +public protocol Registration { + func registerItems() +} +#endif + /// The base protocol of a DI scope. Application code should inherit /// from the `Component` base class, instead of using this protocol /// directly. +/// @CreateMock public protocol Scope: AnyObject { /// The path to reach this component on the dependnecy graph. var path: [String] { get } /// The parent of this component. - var parent: Scope { get } + var parent: NeedleFoundation.Scope { get } + + #if NEEDLE_DYNAMIC + func find(property: String, skipThisLevel: Bool) -> T + #endif +} + +#if NEEDLE_DYNAMIC + +@dynamicMemberLookup +public class DependencyProvider { + + /// The parent component of this provider. + let component: Component + let nonCore: Bool + + init(component: Component, nonCore: Bool) { + self.component = component + self.nonCore = nonCore + } + + public func find(property: String) -> T { + return component.parent.find(property: property, skipThisLevel: nonCore) + } + + public subscript(dynamicMember keyPath: KeyPath) -> T { + return lookup(keyPath: keyPath) + } + + public func lookup(keyPath: KeyPath) -> T { + guard let propertyName = component.keyPathToName[keyPath] else { + fatalError("Cound not find \(keyPath) in lookup table") + } + print("FIND2", self, propertyName) + return find(property: propertyName) + } + } +/// The base implementation of a dependency injection component. A subclass +/// defines a unique scope within the dependency injection tree, that +/// contains a set of properties it provides to units of its scope as well +/// as child scopes. A component instantiates child components that define +/// child scopes. +@dynamicMemberLookup +open class Component: Scope { + + /// The parent of this component. + public let parent: Scope + + /// The path to reach this scope on the dependnecy graph. + // Use `lazy var` to avoid computing the path repeatedly. Internally, + // this is always accessed with the `__DependencyProviderRegistry`'s lock + // acquired. + public lazy var path: [String] = { + let name = self.name + return parent.path + ["\(name)"] + }() + + /// The dependency of this component. + /// + /// - note: Accessing this property is not thread-safe. It should only be + /// accessed on the same thread as the one that instantiated this component. + public private(set) var dependency: DependencyProvider! + + /// Initializer. + /// + /// - parameter parent: The parent component of this component. + public init(parent: Scope) { + self.parent = parent + if let canRegister = self as? Registration { + canRegister.registerItems() + } + dependency = DependencyProvider(component: self, nonCore: false) + } + + /// Initializer. + /// + /// - parameter parent: The parent component of this component. + public init(parent: Scope, nonCore: Bool) { + self.parent = parent + + if let canRegister = self as? Registration { + canRegister.registerItems() + } + dependency = DependencyProvider(component: self, nonCore: nonCore) + } + + /// Share the enclosed object as a singleton at this scope. This allows + /// this scope as well as all child scopes to share a single instance of + /// the object, for as long as this component lives. + /// + /// - note: Shared dependency's constructor should avoid switching threads + /// as it may cause a deadlock. + /// + /// - parameter factory: The closure to construct the dependency object. + /// - returns: The dependency object instance. + public final func shared(__function: String = #function, _ factory: () -> T) -> T { + // Use function name as the key, since this is unique per component + // class. At the same time, this is also 150 times faster than + // interpolating the type to convert to string, `"\(T.self)"`. + sharedInstanceLock.lock() + defer { + sharedInstanceLock.unlock() + } + + // Additional nil coalescing is needed to mitigate a Swift bug appearing + // in Xcode 10. see https://bugs.swift.org/browse/SR-8704. Without this + // measure, calling `shared` from a function that returns an optional type + // will always pass the check below and return nil if the instance is not + // initialized. + if let instance = (sharedInstances[__function] as? T?) ?? nil { + return instance + } + let instance = factory() + sharedInstances[__function] = instance + + return instance + } + + public func find(property: String, skipThisLevel: Bool) -> T { + print("CHECK C", self, property, skipThisLevel) + guard let itemCloure = localTable[property] else { + return parent.find(property: property, skipThisLevel: false) + } + guard let result = itemCloure() as? T else { + fatalError("Incorrect type for \(property) found lookup table") + } + return result + } + + public subscript(dynamicMember keyPath: KeyPath) -> T { + return dependency.lookup(keyPath: keyPath) + } + + public var localTable = [String:()->Any]() + public var keyPathToName = [PartialKeyPath:String]() + + // MARK: - Private + + private let sharedInstanceLock = NSRecursiveLock() + private var sharedInstances = [String: Any]() + private lazy var name: String = { + let fullyQualifiedSelfName = String(describing: self) + let parts = fullyQualifiedSelfName.components(separatedBy: ".") + return parts.last ?? fullyQualifiedSelfName + }() + + // TODO: Replace this with an `open` method, once Swift supports extension + // overriding methods. + private func createDependencyProvider() -> DependencyType { + let provider = __DependencyProviderRegistry.instance.dependencyProvider(for: self) + if let dependency = provider as? DependencyType { + return dependency + } else { + // This case should never occur with properly generated Needle code. + // Needle's official generator should guarantee the correctness. + fatalError("Dependency provider factory for \(self) returned incorrect type. Should be of type \(String(describing: DependencyType.self)). Actual type is \(String(describing: dependency))") + } + } +} + +#else + /// The base implementation of a dependency injection component. A subclass /// defines a unique scope within the dependency injection tree, that /// contains a set of properties it provides to units of its scope as well @@ -123,3 +291,5 @@ open class Component: Scope { } } } + +#endif diff --git a/Sources/NeedleFoundation/Pluginized/NonCoreComponent.swift b/Sources/NeedleFoundation/Pluginized/NonCoreComponent.swift index b32b9889..246861db 100644 --- a/Sources/NeedleFoundation/Pluginized/NonCoreComponent.swift +++ b/Sources/NeedleFoundation/Pluginized/NonCoreComponent.swift @@ -43,6 +43,10 @@ public protocol NonCoreScope: AnyObject { /// is paired with a `PluginizableComponent` that is bound to a lifecycle. /// Otherwise, this method must be explicitly invoked. func scopeDidBecomeInactive() + + #if NEEDLE_DYNAMIC + func check(property: String) -> T? + #endif } /// The base non-core component class. All non-core components should inherit @@ -53,7 +57,11 @@ open class NonCoreComponent: Component, NonCoreS /// /// - parameter parent: The parent component of this component. public required override init(parent: Scope) { + #if NEEDLE_DYNAMIC + super.init(parent: parent, nonCore: true) + #else super.init(parent: parent) + #endif } /// Indicate the corresponding core scope has become active, thereby @@ -71,4 +79,17 @@ open class NonCoreComponent: Component, NonCoreS /// is paired with a `PluginizableComponent` that is bound to a lifecycle. /// Otherwise, this method must be explicitly invoked. open func scopeDidBecomeInactive() {} + + #if NEEDLE_DYNAMIC + public func check(property: String) -> T? { + print("CHECK NC", self, property) + guard let itemCloure = localTable[property] else { + return nil + } + guard let result = itemCloure() as? T else { + fatalError("Incorrect type for \(property) found in the lookup table") + } + return result + } + #endif } diff --git a/Sources/NeedleFoundation/Pluginized/PluginizedComponent.swift b/Sources/NeedleFoundation/Pluginized/PluginizedComponent.swift index fec0bad4..3086a1f0 100644 --- a/Sources/NeedleFoundation/Pluginized/PluginizedComponent.swift +++ b/Sources/NeedleFoundation/Pluginized/PluginizedComponent.swift @@ -23,6 +23,7 @@ import Foundation /// - note: A separate protocol is used to allow the consumer to declare /// a pluginized component generic without having to specify the nested /// generics. +/// @CreateMock public protocol PluginizedScope: Scope { /// Bind the pluginized component to the given lifecycle. This ensures /// the associated non-core component is notified and released according @@ -41,13 +42,57 @@ public protocol PluginizedScope: Scope { /// The base protocol of a plugin extension, enabling Needle's parsing process. public protocol PluginExtension: AnyObject {} +#if NEEDLE_DYNAMIC + +public protocol ExtensionRegistration { + func registerExtensionItems() +} + +@dynamicMemberLookup +public class PluginExtensionProvider { + + /// The parent component of this provider. + public let component: PluginizedComponent + + init(component: PluginizedComponent) { + self.component = component + } + + public func find(property: String) -> T { + // Plugin extension protocols don't allow you to "walk" up the tree, just check at the same level + guard let nonCore = (component.nonCoreComponent as? NonCoreScope) else { + fatalError("Non-core component of incorrect type: \(type(of: component.nonCoreComponent))") + } + guard let result: T = nonCore.check(property: property) else { + fatalError("Property \(property) not found in non-core compoenent \(nonCore)") + } + return result + } + + public subscript(dynamicMember keyPath: KeyPath) -> T { + guard let propertyName = component.extensionToName[keyPath] else { + fatalError("Cound not find \(keyPath) in lookup table") + } + print("FIND3", self, propertyName) + return find(property: propertyName) + } + +} + +#endif + /// The base pluginized component class. All core components that involve /// plugins should inherit from this class. open class PluginizedComponent: Component, PluginizedScope { /// The plugin extension granting access to plugin points provided by /// the corresponding non-core component of this component. + + #if NEEDLE_DYNAMIC + public private(set) var pluginExtension: PluginExtensionProvider! + #else public private(set) var pluginExtension: PluginExtensionType! + #endif /// The type-erased non-core component instance. Subclasses should not /// directly access this property. @@ -65,9 +110,18 @@ open class PluginizedComponent:String]() + + override public func find(property: String, skipThisLevel: Bool) -> T { + print("CHECK P", self, property, skipThisLevel) + if let itemCloure = localTable[property] { + guard let result = itemCloure() as? T else { + fatalError("Incorrect type for \(property) found lookup table") + } + return result + } else { + if let releasableNonCoreComponent = releasableNonCoreComponent, !skipThisLevel, let result: T = releasableNonCoreComponent.check(property: property) { + return result + } else { + return parent.find(property: property, skipThisLevel: false) + } + } + } + + public subscript(dynamicMember keyPath: KeyPath) -> T { + guard let propertyName = extensionToName[keyPath] else { + fatalError("Cound not find \(keyPath) in lookup table") + } + return find(property: propertyName, skipThisLevel: false) + } + + #endif + deinit { guard let lifecycleObserverDisposable = lifecycleObserverDisposable else { // This occurs with improper usages of a pluginized component. It