Skip to content

Commit 8456857

Browse files
authored
Enable context.key to handle all use cases. (#275)
* Enable nested scopes fully. * Rename to createScopedKey.
1 parent 73855ca commit 8456857

14 files changed

+117
-150
lines changed

.circleci/config.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ jobs:
155155
executor: android
156156
steps:
157157
- prepare
158-
- run: bundle exec danger --verbose
158+
# Disabled because the token for posting Danger comments has been revoked
159+
# Will fix this issue a bit later
160+
# - run: bundle exec danger --verbose
159161

160162
workflows:
161163
version: 2

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@
1414

1515
# Site
1616
/site
17+
18+
# Jenv
19+
.java-version

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
- Add `action.runAgain()` extension function
1515
- Add `action.cancelPrevious(previousAction)` extension function
1616
- Add `DelegateAction` abstract class
17+
- Add child formula support for `context.key` scopes.
18+
- Add support for nested `context.key` calls.
1719

1820
## [0.7.0] - June 30, 2021
1921
- **Breaking**: Remove `events(observable) { }` utility function.

formula/src/main/java/com/instacart/formula/FormulaContext.kt

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
package com.instacart.formula
22

3-
import com.instacart.formula.internal.JoinedKey
4-
import com.instacart.formula.internal.ScopedListeners
3+
import com.instacart.formula.internal.Listeners
54
import com.instacart.formula.internal.TransitionDispatcher
65
import com.instacart.formula.internal.UnitListener
6+
import kotlin.reflect.KClass
77

88
/**
99
* Provides functionality within [evaluate][Formula.evaluate] function to [compose][child]
1010
* child formulas, handle events [FormulaContext.onEvent], and [respond][FormulaContext.actions]
1111
* to arbitrary asynchronous events.
1212
*/
1313
abstract class FormulaContext<out Input, State> internal constructor(
14-
@PublishedApi internal val listeners: ScopedListeners,
14+
@PublishedApi internal val listeners: Listeners,
1515
internal val transitionDispatcher: TransitionDispatcher<Input, State>,
1616
) {
1717

@@ -24,7 +24,7 @@ abstract class FormulaContext<out Input, State> internal constructor(
2424
transition: Transition<Input, State, Event>,
2525
): Listener<Event> {
2626
return eventListener(
27-
key = transition.type(),
27+
key = createScopedKey(transition.type()),
2828
transition = transition
2929
)
3030
}
@@ -39,7 +39,7 @@ abstract class FormulaContext<out Input, State> internal constructor(
3939
transition: Transition<Input, State, Event>,
4040
): Listener<Event> {
4141
return eventListener(
42-
key = JoinedKey(key, transition.type()),
42+
key = createScopedKey(transition.type(), key),
4343
transition = transition
4444
)
4545
}
@@ -129,19 +129,30 @@ abstract class FormulaContext<out Input, State> internal constructor(
129129
* @param key Unique identifier that will be used for this block.
130130
*/
131131
inline fun <Value> key(key: Any, create: () -> Value): Value {
132-
listeners.enterScope(key)
132+
ensureNotRunning()
133+
134+
enterScope(key)
133135
val value = create()
134-
listeners.endScope()
136+
endScope()
135137
return value
136138
}
137139

138140
internal fun <Event> eventListener(
139141
key: Any,
140142
transition: Transition<Input, State, Event>
141143
): Listener<Event> {
144+
ensureNotRunning()
142145
val listener = listeners.initOrFindListener<Input, State, Event>(key)
143146
listener.transitionDispatcher = transitionDispatcher
144147
listener.transition = transition
145148
return listener
146149
}
150+
151+
// Internal key scope management
152+
@PublishedApi internal abstract fun enterScope(key: Any)
153+
@PublishedApi internal abstract fun endScope()
154+
@PublishedApi internal abstract fun createScopedKey(type: KClass<*>, key: Any? = null): Any
155+
156+
// Internal validation
157+
@PublishedApi internal abstract fun ensureNotRunning()
147158
}

formula/src/main/java/com/instacart/formula/Transition.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.instacart.formula
22

3+
import kotlin.reflect.KClass
4+
35
/**
46
* Transition is a function that is called when an [Event] happens and produces a [Result] which
57
* indicates what [Formula] should do in response to this event. It can contain a new [state][State]
@@ -67,7 +69,7 @@ fun interface Transition<in Input, State, in Event> {
6769
/**
6870
* Transition type is used as part of the key to distinguish different transitions.
6971
*/
70-
fun type(): Any {
72+
fun type(): KClass<*> {
7173
return this::class
7274
}
7375
}

formula/src/main/java/com/instacart/formula/internal/ActionBuilderImpl.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ internal class ActionBuilderImpl<out Input, State> internal constructor(
5151
stream: Action<Event>,
5252
transition: Transition<Input, State, Event>,
5353
): DeferredAction<Event> {
54-
val key = JoinedKey(stream.key(), transition.type())
54+
val key = snapshot.context.createScopedKey(transition.type(), stream.key())
5555
val listener = snapshot.context.eventListener(key, transition)
5656
return DeferredAction(
5757
key = key,

formula/src/main/java/com/instacart/formula/internal/FormulaKey.kt

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.instacart.formula.internal
33
import kotlin.reflect.KClass
44

55
internal data class FormulaKey(
6+
val scopeKey: Any?,
67
val type: KClass<*>,
78
val key: Any?
89
)

formula/src/main/java/com/instacart/formula/internal/FormulaManagerImpl.kt

+8-19
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ import com.instacart.formula.Transition
1717
internal class FormulaManagerImpl<Input, State, Output>(
1818
private val formula: Formula<Input, State, Output>,
1919
initialInput: Input,
20-
private val listeners: ScopedListeners,
20+
private val listeners: Listeners,
2121
private val transitionListener: TransitionListener
22-
) : SnapshotImpl.Delegate, FormulaManager<Input, Output> {
22+
) : FormulaManager<Input, Output> {
2323

2424
constructor(
2525
formula: Formula<Input, State, Output>,
2626
input: Input,
2727
transitionListener: TransitionListener
28-
): this(formula, input, ScopedListeners(formula), transitionListener)
28+
): this(formula, input, Listeners(), transitionListener)
2929

3030
private val actionManager = ActionManager()
3131

@@ -80,7 +80,6 @@ internal class FormulaManagerImpl<Input, State, Output>(
8080
state = formula.onInputChanged(prevInput, input, state)
8181
}
8282

83-
listeners.evaluationStarted()
8483
val transitionDispatcher = TransitionDispatcher(input, state, this::handleTransitionResult, transitionId)
8584
val snapshot = SnapshotImpl(transitionId, listeners, this, transitionDispatcher)
8685
val result = snapshot.run { formula.run { evaluate() } }
@@ -147,27 +146,27 @@ internal class FormulaManagerImpl<Input, State, Output>(
147146
return false
148147
}
149148

150-
override fun <ChildInput, ChildOutput> child(
149+
fun <ChildInput, ChildOutput> child(
150+
key: Any,
151151
formula: IFormula<ChildInput, ChildOutput>,
152152
input: ChildInput,
153153
transitionId: TransitionId
154154
): ChildOutput {
155155
@Suppress("UNCHECKED_CAST")
156156
val children = children ?: run {
157-
val initialized: SingleRequestMap<Any, FormulaManager<*, *>> = mutableMapOf()
157+
val initialized: SingleRequestMap<Any, FormulaManager<*, *>> = LinkedHashMap()
158158
this.children = initialized
159159
initialized
160160
}
161161

162-
val compositeKey = constructKey(formula, input)
163162
val manager = children
164-
.findOrInit(compositeKey) {
163+
.findOrInit(key) {
165164
val childTransitionListener = getOrInitChildTransitionListener()
166165
val implementation = formula.implementation()
167166
FormulaManagerImpl(implementation, input, childTransitionListener)
168167
}
169168
.requestAccess {
170-
throw IllegalStateException("There already is a child with same key: $compositeKey. Override [Formula.key] function.")
169+
throw IllegalStateException("There already is a child with same key: $key. Override [Formula.key] function.")
171170
} as FormulaManager<ChildInput, ChildOutput>
172171

173172
return manager.evaluate(input, transitionId).output
@@ -185,16 +184,6 @@ internal class FormulaManagerImpl<Input, State, Output>(
185184
listeners.disableAll()
186185
}
187186

188-
private fun <ChildInput, ChildOutput> constructKey(
189-
formula: IFormula<ChildInput, ChildOutput>,
190-
input: ChildInput
191-
): Any {
192-
return FormulaKey(
193-
type = formula.type(),
194-
key = formula.key(input)
195-
)
196-
}
197-
198187
private fun getOrInitChildTransitionListener(): TransitionListener {
199188
return childTransitionListener ?: run {
200189
TransitionListener { result, isChildValid ->
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.instacart.formula.internal
22

3-
data class JoinedKey(
3+
internal data class JoinedKey(
44
val left: Any?,
55
val right: Any?
66
)

formula/src/main/java/com/instacart/formula/internal/Listeners.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal class Listeners {
1212
return "Listener $key is already defined. Are you calling it in a loop or reusing a method? You can wrap the call with FormulaContext.key"
1313
}
1414

15-
fun <Input, State, Event> initOrFindCallback(key: Any): ListenerImpl<Input, State, Event> {
15+
fun <Input, State, Event> initOrFindListener(key: Any): ListenerImpl<Input, State, Event> {
1616
val listeners = listeners ?: run {
1717
val initialized: SingleRequestMap<Any, ListenerImpl<*, *, *>> = mutableMapOf()
1818
this.listeners = initialized

formula/src/main/java/com/instacart/formula/internal/ScopedListeners.kt

-102
This file was deleted.

0 commit comments

Comments
 (0)