Skip to content

Commit

Permalink
Prep NeedleFoundation for dynamic code path
Browse files Browse the repository at this point in the history
- Note all the changes are pretty much dead-code for now as they are guarded by #if statements

Start of implementation of #432
  • Loading branch information
rudro committed Jul 21, 2022
1 parent 10f4dfc commit 742d259
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 1 deletion.
6 changes: 6 additions & 0 deletions Sources/NeedleFoundation/Bootstrap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ open class BootstrapComponent: Component<EmptyDependency> {
fatalError("BootstrapComponent does not have a parent, do not use this property.")
}

#if NEEDLE_DYNAMIC
func find<T>(property: String, skipThisLevel: Bool) -> T {
fatalError("Unable to find \(property) anywhere along the path to the root")
}
#endif

fileprivate init() {}
}
}
172 changes: 171 additions & 1 deletion Sources/NeedleFoundation/Component.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(property: String, skipThisLevel: Bool) -> T
#endif
}

#if NEEDLE_DYNAMIC

@dynamicMemberLookup
public class DependencyProvider<DependencyType> {

/// The parent component of this provider.
let component: Component<DependencyType>
let nonCore: Bool

init(component: Component<DependencyType>, nonCore: Bool) {
self.component = component
self.nonCore = nonCore
}

public func find<T>(property: String) -> T {
return component.parent.find(property: property, skipThisLevel: nonCore)
}

public subscript<T>(dynamicMember keyPath: KeyPath<DependencyType, T>) -> T {
return lookup(keyPath: keyPath)
}

public func lookup<T>(keyPath: KeyPath<DependencyType, T>) -> 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<DependencyType>: 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<DependencyType>!

/// 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<T>(__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<T>(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<T>(dynamicMember keyPath: KeyPath<DependencyType, T>) -> T {
return dependency.lookup(keyPath: keyPath)
}

public var localTable = [String:()->Any]()
public var keyPathToName = [PartialKeyPath<DependencyType>: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
Expand Down Expand Up @@ -123,3 +291,5 @@ open class Component<DependencyType>: Scope {
}
}
}

#endif
21 changes: 21 additions & 0 deletions Sources/NeedleFoundation/Pluginized/NonCoreComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(property: String) -> T?
#endif
}

/// The base non-core component class. All non-core components should inherit
Expand All @@ -53,7 +57,11 @@ open class NonCoreComponent<DependencyType>: Component<DependencyType>, 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
Expand All @@ -71,4 +79,17 @@ open class NonCoreComponent<DependencyType>: Component<DependencyType>, 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<T>(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
}
83 changes: 83 additions & 0 deletions Sources/NeedleFoundation/Pluginized/PluginizedComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<DependencyType, PluginExtensionType, NonCoreComponent: NonCoreScope> {

/// The parent component of this provider.
public let component: PluginizedComponent<DependencyType, PluginExtensionType, NonCoreComponent>

init(component: PluginizedComponent<DependencyType, PluginExtensionType, NonCoreComponent>) {
self.component = component
}

public func find<T>(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<T>(dynamicMember keyPath: KeyPath<PluginExtensionType, T>) -> 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<DependencyType, PluginExtensionType, NonCoreComponent: NonCoreScope>: Component<DependencyType>, 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<DependencyType, PluginExtensionType, NonCoreComponent>!
#else
public private(set) var pluginExtension: PluginExtensionType!
#endif

/// The type-erased non-core component instance. Subclasses should not
/// directly access this property.
Expand All @@ -65,9 +110,18 @@ open class PluginizedComponent<DependencyType, PluginExtensionType, NonCoreCompo
///
/// - parameter parent: The parent component of this component.
public override init(parent: Scope) {
#if NEEDLE_DYNAMIC
super.init(parent: parent, nonCore: true)
releasableNonCoreComponent = NonCoreComponent(parent: self)
if let registerable = self as? ExtensionRegistration {
registerable.registerExtensionItems()
}
pluginExtension = PluginExtensionProvider(component: self)
#else
super.init(parent: parent)
releasableNonCoreComponent = NonCoreComponent(parent: self)
pluginExtension = createPluginExtensionProvider()
#endif
}

/// Bind the pluginized component to the given lifecycle. This ensures
Expand Down Expand Up @@ -124,6 +178,35 @@ open class PluginizedComponent<DependencyType, PluginExtensionType, NonCoreCompo
}
}

#if NEEDLE_DYNAMIC

public var extensionToName = [PartialKeyPath<PluginExtensionType>:String]()

override public func find<T>(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<T>(dynamicMember keyPath: KeyPath<PluginExtensionType, T>) -> 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
Expand Down

0 comments on commit 742d259

Please sign in to comment.