Skip to content

Instantly share code, notes, and snippets.

@dehrom
Last active August 30, 2022 01:12
Show Gist options
  • Save dehrom/ac1a50cfbee3b573fd590150e652f914 to your computer and use it in GitHub Desktop.
Save dehrom/ac1a50cfbee3b573fd590150e652f914 to your computer and use it in GitHub Desktop.
// 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 {}
}
@Solace-Studios
Copy link

@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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment