Sources/NeedleFoundation/Pluginized/PluginizedComponent.swift (115 lines of code) (raw):

// // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR COITIONS OF ANY KI, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation /// The base protocol for a scope that's pluginized. It defines the /// pairing methods to manage its and its corresponding non-core /// scope's lifecycle. /// /// - 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 /// to the given scope's lifecycle. /// /// - note: This method must be invoked when using a `PluginizedComponent`, /// to avoid memory leak of the component and the non-core component. /// - note: This method is required, because the non-core component /// reference cannot be made weak. If the non-core component is weak, /// it is deallocated before the plugin points are created lazily. /// - parameter observable: The `PluginizedScopeLifecycleObservable` to /// bind to. func bind(to observable: PluginizedScopeLifecycleObservable) } /// 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 component \(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. public var nonCoreComponent: AnyObject { guard let value = releasableNonCoreComponent else { // This case should not occur if the pluginized component is properly // paired with a consumer. This only occurs if the `nonCoreComponent` // is accessed after the `consumerWillDeinit` method is invoked. fatalError("Attempt to access non-core component of \(self) after it has been released.") } return value } /// Initializer. /// /// - 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 /// the associated non-core component is notified and released according /// to the given scope's lifecycle. /// /// - note: This method must be invoked when using a `PluginizedComponent`, /// to avoid memory leak of the component and the non-core component. /// - note: This method is required, because the non-core component /// reference cannot be made weak. If the non-core component is weak, /// it is deallocated before the plugin points are created lazily. /// - parameter observable: The `PluginizedScopeLifecycleObservable` to /// bind to. public func bind(to observable: PluginizedScopeLifecycleObservable) { guard lifecycleObserverDisposable == nil else { return } lifecycleObserverDisposable = observable.observe { (event: PluginizedScopeLifecycle) in switch event { case .active: self.releasableNonCoreComponent?.scopeDidBecomeActive() case .inactive: self.releasableNonCoreComponent?.scopeDidBecomeInactive() case .deinit: self.scopeWillDeinit() // Only release the non-core component after the consumer, which should // be the owner reference to the component is released. Cannot release // the non-core component when the bound lifecyle is deactivated. The // consumer may later require the same instance of this component again. // In that case, this component will try to access its released non-core // component to recreate plugins. self.releasableNonCoreComponent = nil } } } /// Indicates that the corresponding scope will deinit /// /// - note: This method is automatically invoked when the bound `PluginizedScopeLifecycleObservable` /// enters its `deinit` state open func scopeWillDeinit() {} // MARK: - Private private var lifecycleObserverDisposable: ObserverDisposable? // Must retain the non-core component so it doesn't get deallocated before it's used // to pull plugin points, since the plugin points are created lazily. private var releasableNonCoreComponent: NonCoreScope? // TODO: Replace this with an `open` method, once Swift supports extension overriding methods. private func createPluginExtensionProvider() -> PluginExtensionType { let provider = __PluginExtensionProviderRegistry.instance.pluginExtensionProvider(for: self) if let pluginExtension = provider as? PluginExtensionType { return pluginExtension } else { // This case should never occur with properly generated Needle code. // Needle's official generator should guarantee the correctness. fatalError("Plugin extension provider factory for \(self) returned incorrect type. Should be of type \(String(describing: PluginExtensionType.self)). Actual type is \(String(describing: provider))") } } #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 // should be bound to a lifecycle allowing the non-core component // to trigger its lifecycle. fatalError("\(self) should be bound to its corresponding lifecyle to avoid memory leaks.") } lifecycleObserverDisposable.dispose() } }