// Copyright © 2019 Ooma Inc. All rights reserved. import Foundation import RIBs import RxSwift // MARK: - Plugin public protocol Plugin: AnyObject { associatedtype Component = Dependency associatedtype Listener = Interactable associatedtype Context /// **Unique** plugin name var killswitchName: String { get } /// Check the ability to create a RIB by this plugin. /// /// - Returns: Observable with checking result /// - Note: Checking process may take some time. func isApplicable(with context: Context) -> Observable /// Build RIB with provided dependecies. /// /// - Returns: Routing for target RIB. func build(dependency: Component, listener: Listener) -> Routing } public extension Plugin { func ereaseToAnyPlugin() -> AnyPlugin { return .init(self) } } // MARK: - AnyPlugin public final class AnyPlugin: Equatable, Hashable { var killswitchName: String { return _killswitchName() } init

(_ concrete: P) where P: Plugin { func cast(_ obj: T, toType: V.Type) -> V { guard let value = obj as? V else { fatalError("Couldn't cast \(T.self) to type \(V.self), check plugin: \(P.self)") } return value } _killswitchName = { concrete.killswitchName } _isApplicable = { { let context = cast($0, toType: P.Context.self) return concrete.isApplicable(with: context) } } _build = { { let component = cast($0, toType: P.Component.self) let listner = cast($1, toType: P.Listener.self) return concrete.build(dependency: component, listener: listner) } } } func isApplicable(with context: Any) -> Observable { return _isApplicable()(context) } func build(dependency: Any, listener: Any) -> Routing { return _build()(dependency, listener) } private let _killswitchName: () -> String private let _isApplicable: () -> (Any) -> Observable private let _build: () -> (Any, Any) -> Routing } public extension AnyPlugin { static func == (lhs: AnyPlugin, rhs: AnyPlugin) -> Bool { return lhs.killswitchName == rhs.killswitchName } func hash(into hasher: inout Hasher) { hasher.combine(killswitchName.hashValue) } } // MARK: - Registration public protocol PluginRegistrable: AnyObject { func registerPlugin(with registrationHandler: (AnyPlugin) -> Void) } // MARK: - PluginPoint public final class PluginPoint { init( routing: Routing, component: Component, listener: Listener, context: @escaping () -> Context, registrant: Registrant ) { self.routing = routing self.component = component self.listener = listener self.context = context self.registrant = registrant } func engage() { registrant.registerPlugin(with: { plugins.insert($0) }) startPlugins() } func check() { registrant.registerPlugin(with: { plugins.insert($0) }) plugins.forEach { _ = $0.build(dependency: component, listener: listener) } plugins.removeAll(keepingCapacity: true) } private let routing: Routing private let component: Component private let listener: Listener private let context: () -> Context private let registrant: Registrant private let disposeBag = DisposeBag() private(set) var plugins: Set = .init() } private extension PluginPoint { func startPlugins() { routing.lifecycle.filter { $0 == .didLoad }.flatMap { [buildApplicableRIBs] _ in buildApplicableRIBs() }.subscribe( onNext: { [routing] in $0.forEach { routing.attachChild($0) } }, onError: { fatalError($0.localizedDescription) } ).disposed(by: disposeBag) } func buildApplicableRIBs() -> Observable<[Routing]> { let contextData = context() return Observable.from( plugins ).flatMap { [component, listener] (plugin: AnyPlugin) in plugin.isApplicable(with: contextData).filter { $0 == true }.map { _ in plugin.build(dependency: component, listener: listener) } }.toArray() } } // MARK: - Test public class Test { public init() { let object = Object() let builder = ParentBuilder(dependency: object) routing = builder.build(withListener: object) } public func start() { routing.interactable.activate() routing.load() } private let routing: ParentRouting private class Object: ParentDependency, ParentListener {} }