From a2aff74aafe2fc1b9bff187e7fd0a619d621911a Mon Sep 17 00:00:00 2001 From: Laimonas Turauskas Date: Fri, 27 Nov 2020 13:43:05 -0800 Subject: [PATCH 1/3] [core] adding functional formula creation. --- .../java/com/instacart/formula/Formula.kt | 4 ++ .../instacart/formula/FormulaExtensions.kt | 44 +++++++++++++++++++ .../formula/DynamicFormulaInputTest.kt | 8 ++-- .../instacart/formula/tests/EmitErrorTest.kt | 24 +++++----- 4 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 formula/src/main/java/com/instacart/formula/FormulaExtensions.kt diff --git a/formula/src/main/java/com/instacart/formula/Formula.kt b/formula/src/main/java/com/instacart/formula/Formula.kt index 4f9db778c..f4a020e65 100644 --- a/formula/src/main/java/com/instacart/formula/Formula.kt +++ b/formula/src/main/java/com/instacart/formula/Formula.kt @@ -62,4 +62,8 @@ interface Formula : IFormula { override fun implementation(): Formula { return this } + + companion object { + // Used to attach extension functions. + } } diff --git a/formula/src/main/java/com/instacart/formula/FormulaExtensions.kt b/formula/src/main/java/com/instacart/formula/FormulaExtensions.kt new file mode 100644 index 000000000..9ff6759ff --- /dev/null +++ b/formula/src/main/java/com/instacart/formula/FormulaExtensions.kt @@ -0,0 +1,44 @@ +package com.instacart.formula + + +inline fun Formula.Companion.stateless( + crossinline output: (Input, FormulaContext) -> Evaluation +): IFormula = DelegateFormula( + initialState = { Unit }, + evaluate = { input, state, context -> + output(input, context) + } +) + +inline fun Formula.Companion.stateless( + crossinline output: (FormulaContext) -> Evaluation +): IFormula = DelegateFormula( + initialState = { Unit }, + evaluate = { input, state, context -> + output(context) + } +) + + + +@PublishedApi +internal class DelegateFormula( + private val initialState: (Input) -> State, + private val onInputChanged: (Input, Input, State) -> State = { _, _, state -> state }, + private val evaluate: (Input, State, FormulaContext) -> Evaluation, + private val key: ((Input) -> Any?)? = null +): Formula { + override fun initialState(input: Input): State = initialState.invoke(input) + + override fun onInputChanged(oldInput: Input, input: Input, state: State): State { + return onInputChanged.invoke(oldInput, input, state) + } + + override fun evaluate(input: Input, state: State, context: FormulaContext): Evaluation { + return evaluate.invoke(input, state, context) + } + + override fun key(input: Input): Any? { + return key?.invoke(input) ?: super.key(input) + } +} \ No newline at end of file diff --git a/formula/src/test/java/com/instacart/formula/DynamicFormulaInputTest.kt b/formula/src/test/java/com/instacart/formula/DynamicFormulaInputTest.kt index 96dfd12dc..bb7b2c830 100644 --- a/formula/src/test/java/com/instacart/formula/DynamicFormulaInputTest.kt +++ b/formula/src/test/java/com/instacart/formula/DynamicFormulaInputTest.kt @@ -9,16 +9,14 @@ class DynamicFormulaInputTest { @Test fun `using dynamic input`() { - TestFormula() + formula() .test(Observable.just(1, 2, 3)) .apply { assertThat(values()).containsExactly(1, 2, 3).inOrder() } } - class TestFormula: StatelessFormula() { - override fun evaluate(input: Int, context: FormulaContext): Evaluation { - return Evaluation(output = input) - } + private fun formula() = Formula.stateless { input: Int, context -> + Evaluation(output = input) } } diff --git a/formula/src/test/java/com/instacart/formula/tests/EmitErrorTest.kt b/formula/src/test/java/com/instacart/formula/tests/EmitErrorTest.kt index 31c5ad6ce..def095641 100644 --- a/formula/src/test/java/com/instacart/formula/tests/EmitErrorTest.kt +++ b/formula/src/test/java/com/instacart/formula/tests/EmitErrorTest.kt @@ -1,25 +1,23 @@ package com.instacart.formula.tests import com.instacart.formula.Evaluation -import com.instacart.formula.FormulaContext -import com.instacart.formula.StatelessFormula +import com.instacart.formula.Formula import com.instacart.formula.Stream +import com.instacart.formula.stateless import com.instacart.formula.test.test import java.lang.IllegalStateException object EmitErrorTest { - fun test() = MyFormula().test() + fun test() = formula().test() - class MyFormula : StatelessFormula() { - override fun evaluate(input: Unit, context: FormulaContext): Evaluation { - return Evaluation( - output = Unit, - updates = context.updates { - events(Stream.onInit()) { - throw IllegalStateException("crashed") - } + private fun formula() = Formula.stateless { context -> + Evaluation( + output = Unit, + updates = context.updates { + events(Stream.onInit()) { + throw IllegalStateException("crashed") } - ) - } + } + ) } } \ No newline at end of file From 1ccd9dc4e9bfb8c6a7bdf5b152fad93d4658cda8 Mon Sep 17 00:00:00 2001 From: Laimonas Turauskas Date: Fri, 27 Nov 2020 13:49:16 -0800 Subject: [PATCH 2/3] Adding a couple more methods. --- .../instacart/formula/FormulaExtensions.kt | 16 ++++++++ .../instacart/formula/FetchDataExampleTest.kt | 38 ++++++++----------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/formula/src/main/java/com/instacart/formula/FormulaExtensions.kt b/formula/src/main/java/com/instacart/formula/FormulaExtensions.kt index 9ff6759ff..024a0f21b 100644 --- a/formula/src/main/java/com/instacart/formula/FormulaExtensions.kt +++ b/formula/src/main/java/com/instacart/formula/FormulaExtensions.kt @@ -19,7 +19,23 @@ inline fun Formula.Companion.stateless( } ) +fun Formula.Companion.create( + initialState: State, + evaluate: (Input, State, FormulaContext) -> Evaluation +): IFormula = DelegateFormula( + initialState = { initialState }, + evaluate = evaluate +) +inline fun Formula.Companion.create( + initialState: State, + crossinline evaluate: (State, FormulaContext) -> Evaluation +): IFormula = DelegateFormula( + initialState = { initialState }, + evaluate = { _, state, context -> + evaluate(state, context) + } +) @PublishedApi internal class DelegateFormula( diff --git a/formula/src/test/java/com/instacart/formula/FetchDataExampleTest.kt b/formula/src/test/java/com/instacart/formula/FetchDataExampleTest.kt index 37f0b2182..555c60d66 100644 --- a/formula/src/test/java/com/instacart/formula/FetchDataExampleTest.kt +++ b/formula/src/test/java/com/instacart/formula/FetchDataExampleTest.kt @@ -1,6 +1,7 @@ package com.instacart.formula import com.google.common.truth.Truth.assertThat +import com.instacart.formula.Transition.Factory.noEffects import com.instacart.formula.rxjava3.RxStream import com.instacart.formula.test.test import io.reactivex.rxjava3.core.Observable @@ -9,8 +10,7 @@ import org.junit.Test class FetchDataExampleTest { @Test fun `fake network example`() { - - MyFormula() + formula() .test() .apply { values().last().onChangeId("1") @@ -30,27 +30,20 @@ class FetchDataExampleTest { } } - class MyFormula : Formula { - private val dataRepo = DataRepo() - - data class State( - val selectedId: String? = null, - val response: DataRepo.Response? = null - ) + data class State( + val selectedId: String? = null, + val response: DataRepo.Response? = null + ) - class Output( - val title: String, - val onChangeId: (String) -> Unit - ) + class Output( + val title: String, + val onChangeId: (String) -> Unit + ) - override fun initialState(input: Unit): State = State() - - override fun evaluate( - input: Unit, - state: State, - context: FormulaContext - ): Evaluation { - return Evaluation( + private fun formula(): IFormula { + val dataRepo = DataRepo() + return Formula.create(State()) { state, context -> + Evaluation( output = Output( title = state.response?.name ?: "", onChangeId = context.eventCallback { id -> @@ -59,7 +52,7 @@ class FetchDataExampleTest { ), updates = context.updates { if (state.selectedId != null) { - events(dataRepo.fetch(state.selectedId)) { response -> + dataRepo.fetch(state.selectedId).onEvent { response -> state.copy(response = response).noEffects() } } @@ -67,5 +60,4 @@ class FetchDataExampleTest { ) } } - } From a25d08351bed182220575693b5efedef22612d5f Mon Sep 17 00:00:00 2001 From: Laimonas Turauskas Date: Fri, 27 Nov 2020 14:02:15 -0800 Subject: [PATCH 3/3] Working through the API. --- .../instacart/formula/FormulaExtensions.kt | 28 +++++----- .../instacart/formula/FetchDataExampleTest.kt | 5 +- .../com/instacart/formula/InputChangedTest.kt | 53 ++++++++----------- .../com/instacart/formula/StreamFormula.kt | 2 +- 4 files changed, 42 insertions(+), 46 deletions(-) diff --git a/formula/src/main/java/com/instacart/formula/FormulaExtensions.kt b/formula/src/main/java/com/instacart/formula/FormulaExtensions.kt index 024a0f21b..46cc44bdf 100644 --- a/formula/src/main/java/com/instacart/formula/FormulaExtensions.kt +++ b/formula/src/main/java/com/instacart/formula/FormulaExtensions.kt @@ -1,11 +1,10 @@ package com.instacart.formula - inline fun Formula.Companion.stateless( crossinline output: (Input, FormulaContext) -> Evaluation ): IFormula = DelegateFormula( initialState = { Unit }, - evaluate = { input, state, context -> + evaluate = { input, _, context -> output(input, context) } ) @@ -20,34 +19,39 @@ inline fun Formula.Companion.stateless( ) fun Formula.Companion.create( - initialState: State, + initialState: (Input) -> State, + onInputChanged: ((Input, Input, State) -> State)? = null, evaluate: (Input, State, FormulaContext) -> Evaluation ): IFormula = DelegateFormula( - initialState = { initialState }, + initialState = initialState, + onInputChanged = onInputChanged, evaluate = evaluate ) inline fun Formula.Companion.create( initialState: State, crossinline evaluate: (State, FormulaContext) -> Evaluation -): IFormula = DelegateFormula( - initialState = { initialState }, - evaluate = { _, state, context -> - evaluate(state, context) - } -) +): IFormula { + return create( + initialState = { _: Unit -> initialState }, + evaluate = { _, state, context -> + evaluate(state, context) + } + ) +} @PublishedApi internal class DelegateFormula( private val initialState: (Input) -> State, - private val onInputChanged: (Input, Input, State) -> State = { _, _, state -> state }, + private val onInputChanged: ((Input, Input, State) -> State)? = null, private val evaluate: (Input, State, FormulaContext) -> Evaluation, private val key: ((Input) -> Any?)? = null ): Formula { override fun initialState(input: Input): State = initialState.invoke(input) override fun onInputChanged(oldInput: Input, input: Input, state: State): State { - return onInputChanged.invoke(oldInput, input, state) + val override = onInputChanged?.invoke(oldInput, input, state) + return override ?: super.onInputChanged(oldInput, input, state) } override fun evaluate(input: Input, state: State, context: FormulaContext): Evaluation { diff --git a/formula/src/test/java/com/instacart/formula/FetchDataExampleTest.kt b/formula/src/test/java/com/instacart/formula/FetchDataExampleTest.kt index 555c60d66..ae5fe4753 100644 --- a/formula/src/test/java/com/instacart/formula/FetchDataExampleTest.kt +++ b/formula/src/test/java/com/instacart/formula/FetchDataExampleTest.kt @@ -1,7 +1,6 @@ package com.instacart.formula import com.google.common.truth.Truth.assertThat -import com.instacart.formula.Transition.Factory.noEffects import com.instacart.formula.rxjava3.RxStream import com.instacart.formula.test.test import io.reactivex.rxjava3.core.Observable @@ -47,13 +46,13 @@ class FetchDataExampleTest { output = Output( title = state.response?.name ?: "", onChangeId = context.eventCallback { id -> - state.copy(selectedId = id).noEffects() + transition(state.copy(selectedId = id)) } ), updates = context.updates { if (state.selectedId != null) { dataRepo.fetch(state.selectedId).onEvent { response -> - state.copy(response = response).noEffects() + transition(state.copy(response = response)) } } } diff --git a/formula/src/test/java/com/instacart/formula/InputChangedTest.kt b/formula/src/test/java/com/instacart/formula/InputChangedTest.kt index 5b8c93a49..48a225f44 100644 --- a/formula/src/test/java/com/instacart/formula/InputChangedTest.kt +++ b/formula/src/test/java/com/instacart/formula/InputChangedTest.kt @@ -6,8 +6,9 @@ import org.junit.Test class InputChangedTest { - @Test fun `input changes`() { - ParentFormula().test() + @Test + fun `input changes`() { + parentFormula().test() .output { onChildNameChanged("first") } .output { onChildNameChanged("second") } .apply { @@ -16,20 +17,16 @@ class InputChangedTest { } } - class ParentFormula : Formula { - private val childFormula = ChildFormula() + data class ParentOutput( + val childName: String, + val onChildNameChanged: (String) -> Unit + ) - data class Output(val childName: String, val onChildNameChanged: (String) -> Unit) - - override fun initialState(input: Unit): String = "default" - - override fun evaluate( - input: Unit, - state: String, - context: FormulaContext - ): Evaluation { - return Evaluation( - output = Output( + private fun parentFormula(): IFormula { + val childFormula = childFormula() + return Formula.create("default") { state, context -> + Evaluation( + output = ParentOutput( childName = context.child(childFormula, state), onChildNameChanged = context.eventCallback { name -> name.noEffects() @@ -39,20 +36,16 @@ class InputChangedTest { } } - class ChildFormula : Formula { - override fun initialState(input: String): String = input - - override fun onInputChanged(oldInput: String, input: String, state: String): String { - // We override our state with what parent provides. - return input - } - - override fun evaluate( - input: String, - state: String, - context: FormulaContext - ): Evaluation { - return Evaluation(output = state) - } + private fun childFormula(): IFormula { + return Formula.create( + initialState = { input: String -> input }, + onInputChanged = { _, new, _ -> + // We override our state with what parent provides. + new + }, + evaluate = { _, state, _ -> + Evaluation(output = state) + } + ) } } diff --git a/formula/src/test/java/com/instacart/formula/StreamFormula.kt b/formula/src/test/java/com/instacart/formula/StreamFormula.kt index 4f3a10597..765147cc9 100644 --- a/formula/src/test/java/com/instacart/formula/StreamFormula.kt +++ b/formula/src/test/java/com/instacart/formula/StreamFormula.kt @@ -9,7 +9,7 @@ class StreamFormula : Formula { val count: Int = 0 ) - class Output( + data class Output( val state: Int, val startListening: () -> Unit, val stopListening: () -> Unit