From d9b5c988e3ca60d461cf361f8e78d8bcc6d56337 Mon Sep 17 00:00:00 2001 From: Laimonas Turauskas Date: Wed, 29 Nov 2023 10:14:49 -0800 Subject: [PATCH] Add a new global plugin API. (#311) * Add a new global plugin API. * Add test. * Feedback. --- .../formula/coroutines/FlowRuntime.kt | 8 +- .../formula/rxjava3/RxJavaRuntime.kt | 7 +- .../com/instacart/formula/FormulaPlugins.kt | 21 ++++++ .../main/java/com/instacart/formula/Plugin.kt | 15 ++++ .../formula/internal/ListInspector.kt | 55 ++++++++++++++ .../instacart/formula/FormulaRuntimeTest.kt | 75 +++++++++++++------ .../formula/internal/ClearPluginsRule.kt | 21 ++++++ 7 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 formula/src/main/java/com/instacart/formula/FormulaPlugins.kt create mode 100644 formula/src/main/java/com/instacart/formula/Plugin.kt create mode 100644 formula/src/main/java/com/instacart/formula/internal/ListInspector.kt create mode 100644 formula/src/test/java/com/instacart/formula/internal/ClearPluginsRule.kt diff --git a/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowRuntime.kt b/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowRuntime.kt index 263a1077..df3fdefe 100644 --- a/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowRuntime.kt +++ b/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowRuntime.kt @@ -1,5 +1,6 @@ package com.instacart.formula.coroutines +import com.instacart.formula.FormulaPlugins import com.instacart.formula.FormulaRuntime import com.instacart.formula.IFormula import com.instacart.formula.Inspector @@ -26,13 +27,18 @@ object FlowRuntime { return callbackFlow { threadChecker.check("Need to subscribe on main thread.") + val mergedInspector = FormulaPlugins.inspector( + type = formula.type(), + local = inspector, + ) + val runtimeFactory = { FormulaRuntime( threadChecker = threadChecker, formula = formula, onOutput = this::trySendBlocking, onError = this::close, - inspector = inspector, + inspector = mergedInspector, isValidationEnabled = isValidationEnabled, ) } diff --git a/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntime.kt b/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntime.kt index a7daf42b..09101a17 100644 --- a/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntime.kt +++ b/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntime.kt @@ -1,5 +1,6 @@ package com.instacart.formula.rxjava3 +import com.instacart.formula.FormulaPlugins import com.instacart.formula.FormulaRuntime import com.instacart.formula.IFormula import com.instacart.formula.Inspector @@ -17,13 +18,17 @@ object RxJavaRuntime { ): Observable { val threadChecker = ThreadChecker(formula) return Observable.create { emitter -> + val mergedInspector = FormulaPlugins.inspector( + type = formula.type(), + local = inspector, + ) val runtimeFactory = { FormulaRuntime( threadChecker = threadChecker, formula = formula, onOutput = emitter::onNext, onError = emitter::onError, - inspector = inspector, + inspector = mergedInspector, isValidationEnabled = isValidationEnabled, ) } diff --git a/formula/src/main/java/com/instacart/formula/FormulaPlugins.kt b/formula/src/main/java/com/instacart/formula/FormulaPlugins.kt new file mode 100644 index 00000000..396acfc5 --- /dev/null +++ b/formula/src/main/java/com/instacart/formula/FormulaPlugins.kt @@ -0,0 +1,21 @@ +package com.instacart.formula + +import com.instacart.formula.internal.ListInspector +import kotlin.reflect.KClass + +object FormulaPlugins { + private var plugin: Plugin? = null + + fun setPlugin(plugin: Plugin?) { + this.plugin = plugin + } + + fun inspector(type: KClass<*>, local: Inspector?): Inspector? { + val global = plugin?.inspector(type) + return when { + global == null -> local + local == null -> global + else -> ListInspector(listOf(global, local)) + } + } +} \ No newline at end of file diff --git a/formula/src/main/java/com/instacart/formula/Plugin.kt b/formula/src/main/java/com/instacart/formula/Plugin.kt new file mode 100644 index 00000000..fda75d2f --- /dev/null +++ b/formula/src/main/java/com/instacart/formula/Plugin.kt @@ -0,0 +1,15 @@ +package com.instacart.formula + +import kotlin.reflect.KClass + +interface Plugin { + /** + * A global callback to create [Inspector] for any formula. This will be called once when + * formula is initially started. + * + * @param type Formula type. + */ + fun inspector(type: KClass<*>): Inspector? { + return null + } +} \ No newline at end of file diff --git a/formula/src/main/java/com/instacart/formula/internal/ListInspector.kt b/formula/src/main/java/com/instacart/formula/internal/ListInspector.kt new file mode 100644 index 00000000..cedc9ae6 --- /dev/null +++ b/formula/src/main/java/com/instacart/formula/internal/ListInspector.kt @@ -0,0 +1,55 @@ +package com.instacart.formula.internal + +import com.instacart.formula.DeferredAction +import com.instacart.formula.Inspector +import kotlin.reflect.KClass + +internal class ListInspector( + private val inspectors: List, +) : Inspector { + override fun onFormulaStarted(formulaType: KClass<*>) { + forEachInspector { onFormulaStarted(formulaType) } + } + + override fun onFormulaFinished(formulaType: KClass<*>) { + forEachInspector { onFormulaFinished(formulaType) } + } + + override fun onEvaluateStarted(formulaType: KClass<*>, state: Any?) { + forEachInspector { onEvaluateStarted(formulaType, state) } + } + + override fun onInputChanged(formulaType: KClass<*>, prevInput: Any?, newInput: Any?) { + forEachInspector { onInputChanged(formulaType, prevInput, newInput) } + } + + override fun onEvaluateFinished(formulaType: KClass<*>, output: Any?, evaluated: Boolean) { + forEachInspector { onEvaluateFinished(formulaType, output, evaluated) } + } + + override fun onActionStarted(formulaType: KClass<*>, action: DeferredAction<*>) { + forEachInspector { onActionStarted(formulaType, action) } + } + + override fun onActionFinished(formulaType: KClass<*>, action: DeferredAction<*>) { + forEachInspector { onActionFinished(formulaType, action) } + } + + override fun onStateChanged(formulaType: KClass<*>, old: Any?, new: Any?) { + forEachInspector { onStateChanged(formulaType, old, new) } + } + + override fun onRunStarted(evaluate: Boolean) { + forEachInspector { onRunStarted(evaluate) } + } + + override fun onRunFinished() { + forEachInspector { onRunFinished() } + } + + private inline fun forEachInspector(callback: Inspector.() -> Unit) { + for (inspector in inspectors) { + inspector.callback() + } + } +} \ No newline at end of file diff --git a/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt b/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt index 2252e52c..d8f52823 100644 --- a/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt +++ b/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt @@ -3,6 +3,7 @@ package com.instacart.formula import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat import com.instacart.formula.actions.EmptyAction +import com.instacart.formula.internal.ClearPluginsRule import com.instacart.formula.internal.TestInspector import com.instacart.formula.internal.Try import com.instacart.formula.rxjava3.RxAction @@ -79,6 +80,7 @@ import org.junit.rules.RuleChain import org.junit.rules.TestName import org.junit.runner.RunWith import org.junit.runners.Parameterized +import kotlin.reflect.KClass @RunWith(Parameterized::class) class FormulaRuntimeTest(val runtime: TestableRuntime, val name: String) { @@ -92,7 +94,10 @@ class FormulaRuntimeTest(val runtime: TestableRuntime, val name: String) { } @get:Rule - val rule = RuleChain.outerRule(TestName()).around(runtime.rule) + val rule = RuleChain + .outerRule(TestName()) + .around(ClearPluginsRule()) + .around(runtime.rule) @Test fun `state change triggers an evaluation`() { val formula = EventCallbackFormula() @@ -1249,33 +1254,57 @@ class FormulaRuntimeTest(val runtime: TestableRuntime, val name: String) { @Test fun `inspector events`() { + val globalInspector = TestInspector() + FormulaPlugins.setPlugin(object : Plugin { + override fun inspector(type: KClass<*>): Inspector { + return globalInspector + } + }) val formula = StartStopFormula(runtime) - val inspector = TestInspector() - val subject = runtime.test(formula, Unit, inspector) + val localInspector = TestInspector() + val subject = runtime.test(formula, Unit, localInspector) subject.output { startListening() } subject.output { stopListening() } subject.dispose() - assertThat(inspector.events).containsExactly( - "formula-run-started", - "formula-started: com.instacart.formula.subjects.StartStopFormula", - "evaluate-started: com.instacart.formula.subjects.StartStopFormula", - "evaluate-finished: com.instacart.formula.subjects.StartStopFormula", - "formula-run-finished", - "state-changed: com.instacart.formula.subjects.StartStopFormula", - "formula-run-started", - "evaluate-started: com.instacart.formula.subjects.StartStopFormula", - "evaluate-finished: com.instacart.formula.subjects.StartStopFormula", - "action-started: com.instacart.formula.subjects.StartStopFormula", - "formula-run-finished", - "state-changed: com.instacart.formula.subjects.StartStopFormula", - "formula-run-started", - "evaluate-started: com.instacart.formula.subjects.StartStopFormula", - "evaluate-finished: com.instacart.formula.subjects.StartStopFormula", - "action-finished: com.instacart.formula.subjects.StartStopFormula", - "formula-run-finished", - "formula-finished: com.instacart.formula.subjects.StartStopFormula" - ).inOrder() + for (inspector in listOf(globalInspector, localInspector)) { + assertThat(inspector.events).containsExactly( + "formula-run-started", + "formula-started: com.instacart.formula.subjects.StartStopFormula", + "evaluate-started: com.instacart.formula.subjects.StartStopFormula", + "evaluate-finished: com.instacart.formula.subjects.StartStopFormula", + "formula-run-finished", + "state-changed: com.instacart.formula.subjects.StartStopFormula", + "formula-run-started", + "evaluate-started: com.instacart.formula.subjects.StartStopFormula", + "evaluate-finished: com.instacart.formula.subjects.StartStopFormula", + "action-started: com.instacart.formula.subjects.StartStopFormula", + "formula-run-finished", + "state-changed: com.instacart.formula.subjects.StartStopFormula", + "formula-run-started", + "evaluate-started: com.instacart.formula.subjects.StartStopFormula", + "evaluate-finished: com.instacart.formula.subjects.StartStopFormula", + "action-finished: com.instacart.formula.subjects.StartStopFormula", + "formula-run-finished", + "formula-finished: com.instacart.formula.subjects.StartStopFormula" + ).inOrder() + } + } + + @Test + fun `only global inspector events`() { + val globalInspector = TestInspector() + FormulaPlugins.setPlugin(object : Plugin { + override fun inspector(type: KClass<*>): Inspector { + return globalInspector + } + }) + + val formula = StartStopFormula(runtime) + val subject = runtime.test(formula, Unit) + subject.dispose() + + assertThat(globalInspector.events).isNotEmpty() } } diff --git a/formula/src/test/java/com/instacart/formula/internal/ClearPluginsRule.kt b/formula/src/test/java/com/instacart/formula/internal/ClearPluginsRule.kt new file mode 100644 index 00000000..e4dbb7b6 --- /dev/null +++ b/formula/src/test/java/com/instacart/formula/internal/ClearPluginsRule.kt @@ -0,0 +1,21 @@ +package com.instacart.formula.internal + +import com.instacart.formula.FormulaPlugins +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +class ClearPluginsRule : TestRule { + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + FormulaPlugins.setPlugin(null) + try { + base.evaluate() + } finally { + FormulaPlugins.setPlugin(null) + } + } + } + } +} \ No newline at end of file