Last active
August 30, 2022 01:12
-
-
Save dehrom/ac1a50cfbee3b573fd590150e652f914 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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<Bool> | |
/// 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<P>(_ concrete: P) where P: Plugin { | |
func cast<T, V>(_ 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<Bool> { | |
return _isApplicable()(context) | |
} | |
func build(dependency: Any, listener: Any) -> Routing { | |
return _build()(dependency, listener) | |
} | |
private let _killswitchName: () -> String | |
private let _isApplicable: () -> (Any) -> Observable<Bool> | |
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<Component: Dependency, Listener: Interactable, Context, Registrant: PluginRegistrable> { | |
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<AnyPlugin> = .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 {} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@dehrom same as @Alvazz , uber has said they aren't open sourcing their plugin framework because it is too tightly coupled with their feature flag system. This is the best example I've seen. Could you elaborate on this?