Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prep NeedleFoundation for dynamic code path #433

Merged
merged 3 commits into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,25 @@ Note: For now, the xcconfig is being used to pass in the DEBUG define.
### Debugging

Needle is intended to be heavily multi-threaded. This makes stepping through the code rather complicated. To simplify the debugging process, set the `SINGLE_THREADED` enviroment variable for your `Run` configuration in the Scheme Editor to `1` or `YES`.

### Structure of generated code

The shortest way to describe the code generated by needle is that it's primary purpose is to create classes that conform to all the `*Dependency` protocols in your application code.

In order to do this needle creates Provider classes which perform the job just described. So the providers need to have a number of vars, one for each of the vars in the protocol. The body of these vars is quite simple. Typically, something like:
```
var imageCache: ImageCaching {
return rootComponent.imageCache
}
```
The `rootComponent` property is of a concrete type so the body of the var above is quite safe. If the component does not have the var we're asking for, the compiler will error out. If the type of the var in the rootComponent has a different type, again, the compiler will complain.

In order to instantiate one of these providers, needle needs to pass it references to each of the components from which the provider will be getting vars from. This is where the slightly unsafe portion of the generated code comes in.

needle creates a tree of how all the components are linked together in a tree. It then generates snippets like ` rootComponent: parent4(component) as! RootComponent` (parent4 is a one-liner that is equivalent to `component.parent.parent.parent.parent`)

### Structure of generated code with dynamic runtime lookup

A new option for the generated code is to create extensions of all the various `Component` classes. Therse extensions will ge used to fill in dictionaries. There will be 2 dictionaries. The first one is easy to describe. It allows us to convert keypaths to strings. Ideally this would be something Swift supports, but unfortunately, there is no such functionality available today. The second dictionary is one that contains closures to fetch the vars using a string key.

Using dynimic member lookup, we'll intercept the references to the dependecny property and get a keypath. The first step is to convert the keypath into a string using the dictionary mentioned above. Then, the Component class will walk up the parents (using the parent property) and keep checking if the second dictionary contains the property we're looking for. The string key used will have a combination of the var name and type.
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() {}
}
}
170 changes: 169 additions & 1 deletion Sources/NeedleFoundation/Component.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,183 @@ 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")
}
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 {
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 +289,5 @@ open class Component<DependencyType>: Scope {
}
}
}

#endif
20 changes: 20 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,16 @@ 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? {
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
}
81 changes: 81 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,56 @@ 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")
}
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 +109,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 +177,34 @@ 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 {
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