From b1f52c70abefc2de2c14597e8200e469c0569310 Mon Sep 17 00:00:00 2001 From: Zachary Klippenstein Date: Tue, 26 Mar 2019 16:49:14 -0700 Subject: [PATCH] Factor out a common parent interface for StatelessWorkflow and Workflow (now StatefulWorkflow). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tl;dr: - `Workflow` -> `StatefulWorkflow` - `Workflow` is an interface, `StatefulWorkflow` and `StatelessWorkflow` are abstract classes that both implement `Workflow`. This change makes it much easier for consumers of this library to define public APIs with private implementations. The public API is an interface that extends `Workflow`. The private implementation is a class that subclasses either `StatefulWorkflow` or `StatelessWorkflow`, depending on whether it needs to keep track of any state. Either way, the workflow's state type is kept as a completely private concern. `interface Workflow` ----------------------------- This is the main currency of this library. `Workflow`s can compose other `Workflow`s. A `Workflow` is anything that can be expressed as a `StatefulWorkflow`. It has a single method, `asStatefulWorkflow()`, that is responsible for expressing the workflow in terms of a `StatefulWorkflow` – a workflow that has an internal state. This commit renames what used to be `Workflow` to `StatefulWorkflow`. `abstract class StatefulWorkflow` --------------------------------------------- This is what you subclass to write a `Workflow` that has internal state that persists as long as the workflow is active in its parent. It knows about things like initial state and snapshotting/restoring. `abstract class StatelessWorkflow` ------------------------------------------- This is what you subclass to get a `Workflow` that only has a single `compose` method. It can compose children workflows, transform inputs/outputs/renderings up/down the tree, and listen to external events and subscriptions, but it has no state. Closes #213. --- .../com/squareup/workflow/StatefulWorkflow.kt | 136 ++++++++++++++++++ .../squareup/workflow/StatelessWorkflow.kt | 93 +++++++++--- .../java/com/squareup/workflow/Workflow.kt | 118 +++++---------- .../com/squareup/workflow/WorkflowContext.kt | 22 +-- .../java/com/squareup/workflow/Workflows.kt | 20 +-- .../com/squareup/workflow/WorkflowHost.kt | 34 +++-- .../squareup/workflow/internal/Behavior.kt | 6 +- .../workflow/internal/RealWorkflowContext.kt | 10 +- .../workflow/internal/SubtreeManager.kt | 23 +-- .../squareup/workflow/internal/WorkflowId.kt | 18 +-- .../workflow/internal/WorkflowNode.kt | 69 +++++---- .../internal/RealWorkflowContextTest.kt | 21 +-- .../workflow/internal/SubtreeManagerTest.kt | 6 +- .../workflow/internal/WorkflowNodeTest.kt | 32 ++--- .../workflow/rx2/SubscriptionsTest.kt | 12 +- .../workflow/rx2/WorkflowContextsTest.kt | 4 +- .../workflow/testing/WorkflowTesting.kt | 17 +-- .../ChannelSubscriptionsIntegrationTest.kt | 2 +- .../com/squareup/workflow/TreeWorkflow.kt | 2 +- .../squareup/workflow/WorkflowTesterTest.kt | 6 +- 20 files changed, 403 insertions(+), 248 deletions(-) create mode 100644 kotlin/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt b/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt new file mode 100644 index 000000000..15bf536dc --- /dev/null +++ b/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2019 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.workflow + +/** + * A composable, stateful object that can [handle events][WorkflowContext.onEvent], + * [delegate to children][WorkflowContext.compose], [subscribe][onReceive] to arbitrary streams from + * the outside world, and be [saved][snapshotState] to a serialized form to be + * [restored][restoreState] later. + * + * The basic purpose of a `Workflow` is to take some [input][InputT] and return a + * [rendering][RenderingT]. To that end, a workflow may keep track of internal [state][StateT], + * recursively ask other workflows to render themselves, subscribe to data streams from the outside + * world, and handle events both from its [renderings][WorkflowContext.onEvent] and from workflows + * it's delegated to (its "children"). A `Workflow` may also emit [output events][OutputT] up to its + * parent `Workflow`. + * + * Workflows form a tree, where each workflow can have zero or more child workflows. Child workflows + * are started as necessary whenever another workflow asks for them, and are cleaned up automatically + * when they're no longer needed. [Input][InputT] propagates down the tree, [outputs][OutputT] and + * [renderings][RenderingT] propagate up the tree. + * + * @param InputT Typically a data class that is used to pass configuration information or bits of + * state that the workflow can always get from its parent and needn't duplicate in its own state. + * May be [Unit] if the workflow does not need any input data. + * + * @param StateT Typically a data class that contains all of the internal state for this workflow. + * The state is seeded via [input][InputT] in [initialState]. It can be [serialized][snapshotState] + * and later used to [restore][restoreState] the workflow. **Implementations of the `Workflow` + * interface should not generally contain their own state directly.** They may inject objects like + * instances of their child workflows, or network clients, but should not contain directly mutable + * state. This is the only type parameter that a parent workflow needn't care about for its children, + * and may just use star (`*`) instead of specifying it. May be [Unit] if the workflow does not have + * any internal state (see [StatelessWorkflow]). + * + * @param OutputT Typically a sealed class that represents "events" that this workflow can send + * to its parent. + * May be [Nothing] if the workflow doesn't need to emit anything. + * + * @param RenderingT The value returned to this workflow's parent during [composition][compose]. + * Typically represents a "view" of this workflow's input, current state, and children's renderings. + * A workflow that represents a UI component may use a view model as its rendering type. + * + * @see StatelessWorkflow + */ +abstract class StatefulWorkflow< + in InputT : Any, + StateT : Any, + out OutputT : Any, + out RenderingT : Any + > : Workflow { + + /** + * Called when the state machine is first started to get the initial state. + */ + abstract fun initialState(input: InputT): StateT + + /** + * Called whenever [WorkflowContext.compose] is about to get a new [input][InputT] value, to allow + * the workflow to modify its state in response. This method is called eagerly: `old` and `new` might + * be the same value, so it is up to implementing code to perform any diffing if desired. + * + * Default implementation does nothing. + */ + open fun onInputChanged( + old: InputT, + new: InputT, + state: StateT + ): StateT = state + + /** + * Called at least once† any time one of the following things happens: + * - This workflow's [input] changes (via the parent passing a different one in). + * - This workflow's [state] changes. + * - A child workflow's state changes. + * + * **Never call this method directly.** To get the rendering from a child workflow, pass the child + * and any required input to [WorkflowContext.compose]. + * + * This method *should not* have any side effects, and in particular should not do anything that + * blocks the current thread. It may be called multiple times for the same state. It must do all its + * work by calling methods on [context]. + * + * _† This method is guaranteed to be called *at least* once for every state, but may be called + * multiple times. Allowing this method to be invoked multiple times makes the internals simpler._ + */ + abstract fun compose( + input: InputT, + state: StateT, + context: WorkflowContext + ): RenderingT + + /** + * Called whenever the state changes to generate a new [Snapshot] of the state. + * + * **Snapshots must be lazy.** + * + * Serialization must not be done at the time this method is called, + * since the state will be snapshotted frequently but the serialized form may only be needed very + * rarely. + * + * If the workflow does not have any state, or should always be started from scratch, return + * [Snapshot.EMPTY] from this method. + * + * @see restoreState + */ + abstract fun snapshotState(state: StateT): Snapshot + + /** + * Deserialize a state value from a [Snapshot] previously created with [snapshotState]. + * + * If the workflow should always be started from scratch, this method can just ignore the snapshot + * and return the initial state. + * + * @see snapshotState + */ + abstract fun restoreState(snapshot: Snapshot): StateT + + /** + * Satisfies the [Workflow] interface by returning `this`. + */ + final override fun asStatefulWorkflow(): StatefulWorkflow = this +} diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt b/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt index 6a667bb2c..605c34656 100644 --- a/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt +++ b/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt @@ -16,15 +16,78 @@ package com.squareup.workflow /** - * A [Workflow] that has no state. + * A composable object that can [handle events][WorkflowContext.onEvent], + * [delegate to children][WorkflowContext.compose], and [subscribe][onReceive] to arbitrary streams + * from the outside world. + * + * The basic purpose of a `Workflow` is to take some [input][InputT] and return a + * [rendering][RenderingT]. To that end, a workflow may recursively ask other workflows to render + * themselves, subscribe to data streams from the outside world, and handle events both from its + * [renderings][WorkflowContext.onEvent] and from workflows it's delegated to (its "children"). A + * `Workflow` may also emit [output events][OutputT] up to its parent `Workflow`. + * + * Workflows form a tree, where each workflow can have zero or more child workflows. Child workflows + * are started as necessary whenever another workflow asks for them, and are cleaned up automatically + * when they're no longer needed. [Input][InputT] propagates down the tree, [outputs][OutputT] and + * [renderings][RenderingT] propagate up the tree. + * + * @param InputT Typically a data class that is used to pass configuration information or bits of + * state that the workflow can always get from its parent and needn't duplicate in its own state. + * May be [Unit] if the workflow does not need any input data. + * + * @param OutputT Typically a sealed class that represents "events" that this workflow can send + * to its parent. + * May be [Nothing] if the workflow doesn't need to emit anything. + * + * @param RenderingT The value returned to this workflow's parent during [composition][compose]. + * Typically represents a "view" of this workflow's input, current state, and children's renderings. + * A workflow that represents a UI component may use a view model as its rendering type. + * + * @see StatefulWorkflow */ -typealias StatelessWorkflow = - Workflow +abstract class StatelessWorkflow : + Workflow { + + /** + * Called at least once any time one of the following things happens: + * - This workflow's [input] changes (via the parent passing a different one in). + * - A child workflow updates in some way (e.g. a stateful child's state changes). + * + * **Never call this method directly.** To get the rendering from a child workflow, pass the child + * and any required input to [WorkflowContext.compose]. + * + * This method *should not* have any side effects, and in particular should not do anything that + * blocks the current thread. It may be called multiple times for the same state. It must do all its + * work by calling methods on [context]. + */ + abstract fun compose( + input: InputT, + context: WorkflowContext + ): RenderingT + + /** + * Satisfies the [Workflow] interface by wrapping `this` in a [StatefulWorkflow] with `Unit` + * state. + */ + final override fun asStatefulWorkflow(): StatefulWorkflow = + object : StatefulWorkflow() { + override fun initialState(input: InputT) = Unit + + @Suppress("UNCHECKED_CAST") + override fun compose( + input: InputT, + state: Unit, + context: WorkflowContext + ): RenderingT = compose(input, context as WorkflowContext) + + override fun snapshotState(state: Unit) = Snapshot.EMPTY + override fun restoreState(snapshot: Snapshot) = Unit + } +} /** - * A convenience function to implement a [Workflow] that doesn't have any internal state. Such a - * workflow doesn't need to worry about initial state or snapshotting, so the entire workflow can - * be defined as a single [compose][Workflow.compose] function. + * A convenience function to implement [StatelessWorkflow] by just passing the + * [compose][StatelessWorkflow.compose] function as a lambda. * * Note that while a stateless workflow doesn't have any _internal_ state of its own, it may use * [input][InputT] received from its parent, and it may compose child workflows that do have their own @@ -34,20 +97,14 @@ typealias StatelessWorkflow = fun StatelessWorkflow( compose: ( input: InputT, - context: WorkflowContext + context: WorkflowContext ) -> RenderingT ): StatelessWorkflow = - object : Workflow { - override fun initialState(input: InputT) = Unit - + object : StatelessWorkflow() { override fun compose( input: InputT, - state: Unit, - context: WorkflowContext - ): RenderingT = compose(input, context) - - override fun snapshotState(state: Unit) = Snapshot.EMPTY - override fun restoreState(snapshot: Snapshot) = Unit + context: WorkflowContext + ): RenderingT = compose.invoke(input, context) } /** @@ -55,8 +112,8 @@ fun StatelessWorkflow( */ @Suppress("FunctionName") fun StatelessWorkflow( - compose: (context: WorkflowContext) -> RenderingT + compose: (context: WorkflowContext) -> RenderingT ): StatelessWorkflow = - StatelessWorkflow { _: Unit, context: WorkflowContext -> + StatelessWorkflow { _: Unit, context: WorkflowContext -> compose(context) } diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/Workflow.kt b/kotlin/workflow-core/src/main/java/com/squareup/workflow/Workflow.kt index df30dc0fe..870e33c3a 100644 --- a/kotlin/workflow-core/src/main/java/com/squareup/workflow/Workflow.kt +++ b/kotlin/workflow-core/src/main/java/com/squareup/workflow/Workflow.kt @@ -18,34 +18,43 @@ package com.squareup.workflow /** * A composable, optionally-stateful object that can [handle events][WorkflowContext.onEvent], * [delegate to children][WorkflowContext.compose], [subscribe][onReceive] to arbitrary streams from - * the outside world, and be [saved][snapshotState] to a serialized form to be - * [restored][restoreState] later. + * the outside world. * * The basic purpose of a `Workflow` is to take some [input][InputT] and return a - * [rendering][RenderingT]. To that end, a workflow may keep track of internal [state][StateT], - * recursively ask other workflows to render themselves, subscribe to data streams from the outside - * world, and handle events both from its [renderings][WorkflowContext.onEvent] and from workflows - * it's delegated to (its "children"). A `Workflow` may also emit [output events][OutputT] up to its - * parent `Workflow`. + * [rendering][RenderingT]. To that end, a workflow may keep track of internal + * [state][StatefulWorkflow], recursively ask other workflows to render themselves, subscribe to + * data streams from the outside world, and handle events both from its + * [renderings][WorkflowContext.onEvent] and from workflows it's delegated to (its "children"). A + * `Workflow` may also emit [output events][OutputT] up to its parent `Workflow`. * * Workflows form a tree, where each workflow can have zero or more child workflows. Child workflows - * are started as necessary whenever another workflow asks for them, and are cleaned up automatically - * when they're no longer needed. [Input][InputT] propagates down the tree, [outputs][OutputT] and - * [renderings][RenderingT] propagate up the tree. + * are started as necessary whenever another workflow asks for them, and are cleaned up + * automatically when they're no longer needed. [Input][InputT] propagates down the tree, + * [outputs][OutputT] and [renderings][RenderingT] propagate up the tree. + * + * ## Implementing `Workflow` + * + * The [Workflow] interface is useful as a facade for your API. You can publish an interface that + * extends `Workflow`, and keep the implementation (e.g. is your workflow state*ful* or + * state*less* a private implementation detail. + * + * ### [Stateful Workflows][StatefulWorkflow] + * + * If your workflow needs to keep track of internal state, implement the [StatefulWorkflow] + * interface. That interface has an additional type parameter, `StateT`, and allows you to specify + * [how to create the initial state][StatefulWorkflow.initialState] and how to + * [snapshot][StatefulWorkflow.snapshotState]/[restore][StatefulWorkflow.restoreState] your state. + * + * ### [Stateless Workflows][StatelessWorkflow] + * + * If your workflow simply needs to delegate to other workflows, maybe transforming inputs, outputs, + * or renderings, implement the [StatelessWorkflow] interface, or simply pass a lambda to the + * `StatelessWorkflow` function. * * @param InputT Typically a data class that is used to pass configuration information or bits of * state that the workflow can always get from its parent and needn't duplicate in its own state. * May be [Unit] if the workflow does not need any input data. * - * @param StateT Typically a data class that contains all of the internal state for this workflow. - * The state is seeded via [input][InputT] in [initialState]. It can be [serialized][snapshotState] - * and later used to [restore][restoreState] the workflow. **Implementations of the `Workflow` - * interface should not generally contain their own state directly.** They may inject objects like - * instances of their child workflows, or network clients, but should not contain directly mutable - * state. This is the only type parameter that a parent workflow needn't care about for its children, - * and may just use star (`*`) instead of specifying it. May be [Unit] if the workflow does not have - * any internal state (see [StatelessWorkflow]). - * * @param OutputT Typically a sealed class that represents "events" that this workflow can send * to its parent. * May be [Nothing] if the workflow doesn't need to emit anything. @@ -53,72 +62,11 @@ package com.squareup.workflow * @param RenderingT The value returned to this workflow's parent during [composition][compose]. * Typically represents a "view" of this workflow's input, current state, and children's renderings. * A workflow that represents a UI component may use a view model as its rendering type. + * + * @see StatefulWorkflow + * @see StatelessWorkflow */ -interface Workflow { - - /** - * Called when the state machine is first started to get the initial state. - */ - fun initialState(input: InputT): StateT - - /** - * Called whenever [WorkflowContext.compose] is about to get a new [input][InputT] value, to allow - * the workflow to modify its state in response. This method is called eagerly: `old` and `new` might - * be the same value, so it is up to implementing code to perform any diffing if desired. - * - * Default implementation does nothing. - */ - fun onInputChanged( - old: InputT, - new: InputT, - state: StateT - ): StateT = state - - /** - * Called at least once† any time one of the following things happens: - * - This workflow's [input] changes (via the parent passing a different one in). - * - This workflow's [state] changes. - * - A child workflow's state changes. - * - * **Never call this method directly.** To get the rendering from a child workflow, pass the child - * and any required input to [WorkflowContext.compose]. - * - * This method *should not* have any side effects, and in particular should not do anything that - * blocks the current thread. It may be called multiple times for the same state. It must do all its - * work by calling methods on [context]. - * - * _† This method is guaranteed to be called *at least* once for every state, but may be called - * multiple times. Allowing this method to be invoked multiple times makes the internals simpler._ - */ - fun compose( - input: InputT, - state: StateT, - context: WorkflowContext - ): RenderingT - - /** - * Called whenever the state changes to generate a new [Snapshot] of the state. - * - * **Snapshots must be lazy.** - * - * Serialization must not be done at the time this method is called, - * since the state will be snapshotted frequently but the serialized form may only be needed very - * rarely. - * - * If the workflow does not have any state, or should always be started from scratch, return - * [Snapshot.EMPTY] from this method. - * - * @see restoreState - */ - fun snapshotState(state: StateT): Snapshot +interface Workflow { - /** - * Deserialize a state value from a [Snapshot] previously created with [snapshotState]. - * - * If the workflow should always be started from scratch, this method can just ignore the snapshot - * and return the initial state. - * - * @see snapshotState - */ - fun restoreState(snapshot: Snapshot): StateT + fun asStatefulWorkflow(): StatefulWorkflow } diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowContext.kt b/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowContext.kt index 5e85a3a5c..535b56bba 100644 --- a/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowContext.kt +++ b/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowContext.kt @@ -27,7 +27,7 @@ import kotlin.reflect.KType /** * Facilities for a [Workflow] to interact with other [Workflow]s and the outside world from inside - * a [compose][Workflow.compose] function. + * a `compose` function. * * ## Handling Events * @@ -117,8 +117,8 @@ interface WorkflowContext { * @param key An optional string key that is used to distinguish between workflows of the same * type. */ - fun compose( - child: Workflow, + fun compose( + child: Workflow, input: ChildInputT, key: String = "", handler: (ChildOutputT) -> WorkflowAction @@ -156,12 +156,12 @@ inline fun WorkflowContext +fun WorkflowContext.compose( // Intellij refuses to format this parameter list correctly because of the weird line break, // and detekt will complain about it. // @formatter:off - child: Workflow, + child: Workflow, key: String = "", handler: (ChildOutputT) -> WorkflowAction ): ChildRenderingT = compose(child, Unit, key, handler) @@ -170,12 +170,12 @@ fun +fun WorkflowContext.compose( // Intellij refuses to format this parameter list correctly because of the weird line break, // and detekt will complain about it. // @formatter:off - child: Workflow, + child: Workflow, input: InputT, key: String = "" ): ChildRenderingT = compose(child, input, key) { WorkflowAction.noop() } @@ -184,12 +184,12 @@ fun +fun WorkflowContext.compose( // Intellij refuses to format this parameter list correctly because of the weird line break, // and detekt will complain about it. // @formatter:off - child: Workflow, + child: Workflow, key: String = "" ): ChildRenderingT = compose(child, Unit, key) { WorkflowAction.noop() } // @formatter:on @@ -197,7 +197,7 @@ fun /** * Will wait for [deferred] to complete, then pass its value to [handler]. Once the handler has been * invoked for a given deferred+key, it will not be invoked again until an invocation of - * [Workflow.compose] that does _not_ call this method with that deferred+[key]. + * `compose` that does _not_ call this method with that deferred+[key]. * * @param key An optional string key that is used to distinguish between subscriptions of the same * type. @@ -216,7 +216,7 @@ inline fun WorkflowContext - Workflow.mapRendering( + Workflow.mapRendering( transform: (FromRenderingT) -> ToRenderingT - ): StatelessWorkflow = StatelessWorkflow { input, context -> + ): Workflow = StatelessWorkflow { input, context -> // @formatter:on context.compose(this@mapRendering, input) { emitOutput(it) } .let(transform) } - -/** - * Returns a rendering that directly delegates to this [Workflow] but masks its state type. - * - * Useful for exposing a stateful workflow as part of a public API when you don't want your state - * type to be public. - */ -// Intellij refuses to format this parameter list correctly because of the weird line break, -// and detekt will complain about it. -// @formatter:off -fun - Workflow.hideState(): - StatelessWorkflow = StatelessWorkflow { input, context -> - context.compose(this@hideState, input) { emitOutput(it) } - } -// @formatter:on diff --git a/kotlin/workflow-host/src/main/java/com/squareup/workflow/WorkflowHost.kt b/kotlin/workflow-host/src/main/java/com/squareup/workflow/WorkflowHost.kt index b1f5b874d..2a5e60f57 100644 --- a/kotlin/workflow-host/src/main/java/com/squareup/workflow/WorkflowHost.kt +++ b/kotlin/workflow-host/src/main/java/com/squareup/workflow/WorkflowHost.kt @@ -70,15 +70,15 @@ interface WorkflowHost { * @param context The [CoroutineContext] used to run the workflow tree. Added to the [Factory]'s * context. */ - fun run( - workflow: Workflow, + fun run( + workflow: Workflow, input: InputT, snapshot: Snapshot? = null, context: CoroutineContext = EmptyCoroutineContext ): WorkflowHost = run(workflow.id(), workflow, input, snapshot, context) - fun run( - workflow: Workflow, + fun run( + workflow: Workflow, snapshot: Snapshot? = null, context: CoroutineContext = EmptyCoroutineContext ): WorkflowHost = run(workflow.id(), workflow, Unit, snapshot, context) @@ -93,26 +93,34 @@ interface WorkflowHost { */ @TestOnly fun runTestFromState( - workflow: Workflow, + workflow: StatefulWorkflow, input: InputT, initialState: StateT ): WorkflowHost { val workflowId = workflow.id() return object : WorkflowHost { val node = WorkflowNode(workflowId, workflow, input, null, baseContext, initialState) - override val updates: ReceiveChannel> = node.start(workflow, input) + override val updates: ReceiveChannel> = + node.start(workflow, input) } } - internal fun run( - id: WorkflowId, - workflow: Workflow, + internal fun run( + id: WorkflowId, + workflow: Workflow, input: InputT, snapshot: Snapshot?, context: CoroutineContext ): WorkflowHost = object : WorkflowHost { - val node = WorkflowNode(id, workflow, input, snapshot, baseContext + context) - override val updates: ReceiveChannel> = node.start(workflow, input) + val node = WorkflowNode( + id = id, + workflow = workflow.asStatefulWorkflow(), + initialInput = input, + snapshot = snapshot, + baseContext = baseContext + context + ) + override val updates: ReceiveChannel> = + node.start(workflow.asStatefulWorkflow(), input) } } } @@ -120,8 +128,8 @@ interface WorkflowHost { /** * Starts the coroutine that runs the coroutine loop. */ -internal fun WorkflowNode.start( - workflow: Workflow, +internal fun WorkflowNode.start( + workflow: StatefulWorkflow, input: I ): ReceiveChannel> = produce(capacity = 0) { try { diff --git a/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/Behavior.kt b/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/Behavior.kt index 762042e8a..edda2db2e 100644 --- a/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/Behavior.kt +++ b/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/Behavior.kt @@ -25,7 +25,7 @@ import kotlin.reflect.KType /** * An immutable description of the things a [Workflow] would like to do as the result of calling its - * [Workflow.compose] method. A `Behavior` is built up by calling methods on a + * `compose` method. A `Behavior` is built up by calling methods on a * [WorkflowContext][com.squareup.workflow.WorkflowContext] ([RealWorkflowContext] in particular). * * @see RealWorkflowContext @@ -43,8 +43,8 @@ internal data class Behavior( ParentStateT : Any, out ParentOutputT : Any >( - val workflow: Workflow<*, *, ChildOutputT, *>, - val id: WorkflowId, + val workflow: Workflow<*, ChildOutputT, *>, + val id: WorkflowId, val input: ChildInputT, val handler: (ChildOutputT) -> WorkflowAction ) { diff --git a/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/RealWorkflowContext.kt b/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/RealWorkflowContext.kt index 8bbbddce5..ebd583845 100644 --- a/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/RealWorkflowContext.kt +++ b/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/RealWorkflowContext.kt @@ -35,10 +35,10 @@ internal class RealWorkflowContext( ) : WorkflowContext { interface Composer { - fun compose( + fun compose( case: WorkflowOutputCase, - child: Workflow, - id: WorkflowId, + child: Workflow, + id: WorkflowId, input: ChildInputT ): ChildRenderingT } @@ -73,9 +73,9 @@ internal class RealWorkflowContext( } // @formatter:off - override fun + override fun compose( - child: Workflow, + child: Workflow, input: ChildInputT, key: String, handler: (ChildOutputT) -> WorkflowAction diff --git a/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt b/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt index 0179f42ab..97bc45195 100644 --- a/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt +++ b/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt @@ -16,11 +16,12 @@ package com.squareup.workflow.internal import com.squareup.workflow.Snapshot -import com.squareup.workflow.parse -import com.squareup.workflow.readByteStringWithLength +import com.squareup.workflow.StatefulWorkflow import com.squareup.workflow.Workflow import com.squareup.workflow.WorkflowAction import com.squareup.workflow.internal.Behavior.WorkflowOutputCase +import com.squareup.workflow.parse +import com.squareup.workflow.readByteStringWithLength import com.squareup.workflow.writeByteStringWithLength import kotlinx.coroutines.experimental.selects.SelectBuilder import kotlin.coroutines.experimental.CoroutineContext @@ -51,19 +52,19 @@ internal class SubtreeManager( ) // @formatter:off - override fun + override fun compose( case: WorkflowOutputCase, - child: Workflow, - id: WorkflowId, + child: Workflow, + id: WorkflowId, input: ChildInputT ): ChildRenderingT { // @formatter:on // Start tracking this case so we can be ready to compose it. @Suppress("UNCHECKED_CAST") - val host = hostLifetimeTracker.ensure(case) as - WorkflowNode - return host.compose(child, input) + val childNode = hostLifetimeTracker.ensure(case) as + WorkflowNode + return childNode.compose(child.asStatefulWorkflow(), input) } /** @@ -98,7 +99,7 @@ internal class SubtreeManager( return Snapshot.write { sink -> val childSnapshots = hostLifetimeTracker.lifetimes .entries - .map { (case, host) -> host.id to host.snapshot(case.workflow) } + .map { (case, host) -> host.id to host.snapshot(case.workflow.asStatefulWorkflow()) } sink.writeInt(childSnapshots.size) for ((id, snapshot) in childSnapshots) { sink.writeByteStringWithLength(id.toByteString()) @@ -129,8 +130,8 @@ internal class SubtreeManager( private fun WorkflowOutputCase.createNode(): WorkflowNode = WorkflowNode( - id as WorkflowId, - workflow as Workflow, + id, + workflow.asStatefulWorkflow() as StatefulWorkflow, input, snapshotCache[id], contextForChildren diff --git a/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/WorkflowId.kt b/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/WorkflowId.kt index 7ce996e62..04d28d7d7 100644 --- a/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/WorkflowId.kt +++ b/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/WorkflowId.kt @@ -15,44 +15,44 @@ */ package com.squareup.workflow.internal +import com.squareup.workflow.Workflow import com.squareup.workflow.parse import com.squareup.workflow.readUtf8WithLength -import com.squareup.workflow.Workflow import com.squareup.workflow.writeUtf8WithLength import okio.Buffer import okio.ByteString import kotlin.reflect.KClass import kotlin.reflect.jvm.jvmName -internal typealias AnyId = WorkflowId<*, *, *, *> +internal typealias AnyId = WorkflowId<*, *, *> /** * Value type that can be used to distinguish between different workflows of different types or * the same type (in that case using a [name]). */ -internal data class WorkflowId +internal data class WorkflowId @PublishedApi internal constructor( - internal val type: KClass>, + internal val type: KClass>, internal val name: String = "" ) @Suppress("unused") -internal fun , P : Any, S : Any, O : Any, R : Any> - W.id(key: String = ""): WorkflowId = +internal fun , P : Any, O : Any, R : Any> + W.id(key: String = ""): WorkflowId = WorkflowId(this::class, key) -internal fun WorkflowId<*, *, *, *>.toByteString(): ByteString = Buffer() +internal fun WorkflowId<*, *, *>.toByteString(): ByteString = Buffer() .also { sink -> sink.writeUtf8WithLength(type.jvmName) sink.writeUtf8WithLength(name) } .readByteString() -internal fun restoreId(bytes: ByteString): WorkflowId<*, *, *, *> = bytes.parse { source -> +internal fun restoreId(bytes: ByteString): WorkflowId<*, *, *> = bytes.parse { source -> val typeName = source.readUtf8WithLength() @Suppress("UNCHECKED_CAST") - val type = Class.forName(typeName) as Class> + val type = Class.forName(typeName) as Class> val name = source.readUtf8WithLength() return WorkflowId(type.kotlin, name) } diff --git a/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt b/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt index 988d4b500..a077cae2b 100644 --- a/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt +++ b/kotlin/workflow-host/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt @@ -15,13 +15,14 @@ */ package com.squareup.workflow.internal -import com.squareup.workflow.util.ChannelUpdate.Closed import com.squareup.workflow.Snapshot +import com.squareup.workflow.StatefulWorkflow import com.squareup.workflow.Workflow import com.squareup.workflow.WorkflowAction import com.squareup.workflow.internal.Behavior.SubscriptionCase import com.squareup.workflow.parse import com.squareup.workflow.readByteStringWithLength +import com.squareup.workflow.util.ChannelUpdate.Closed import com.squareup.workflow.writeByteStringWithLength import kotlinx.coroutines.experimental.CoroutineName import kotlinx.coroutines.experimental.CoroutineScope @@ -35,11 +36,11 @@ import kotlin.coroutines.experimental.CoroutineContext * A node in a state machine tree. Manages the actual state for a given [Workflow]. * * @param initialState Allows unit tests to start the node from a given state, instead of calling - * [Workflow.initialState]. + * [StatefulWorkflow.initialState]. */ internal class WorkflowNode( - val id: WorkflowId, - workflow: Workflow, + val id: WorkflowId, + workflow: StatefulWorkflow, initialInput: InputT, snapshot: Snapshot?, baseContext: CoroutineContext, @@ -91,31 +92,23 @@ internal class WorkflowNode, + workflow: StatefulWorkflow, input: InputT - ): RenderingT { - updateInputAndState(workflow, input) - - val context = RealWorkflowContext(subtreeManager) - val rendering = workflow.compose(input, state, context) - - behavior = context.buildBehavior() - // Start new children/subscriptions, and drop old ones. - subtreeManager.track(behavior.childCases) - subscriptionTracker.track(behavior.subscriptionCases) - - return rendering - } + ): RenderingT = + composeWithStateType(workflow as StatefulWorkflow, input) /** * Walk the tree of state machines again, this time gathering snapshots and aggregating them * automatically. */ - fun snapshot(workflow: Workflow<*, *, *, *>): Snapshot { + fun snapshot(workflow: StatefulWorkflow<*, *, *, *>): Snapshot { val childrenSnapshot = subtreeManager.createChildrenSnapshot() @Suppress("UNCHECKED_CAST") - return childrenSnapshot.withState(workflow as Workflow) + return childrenSnapshot.withState( + workflow as StatefulWorkflow + ) } /** @@ -172,8 +165,29 @@ internal class WorkflowNode, + input: InputT + ): RenderingT { + updateInputAndState(workflow, input) + + val context = RealWorkflowContext(subtreeManager) + val rendering = workflow.compose(input, state, context) + + behavior = context.buildBehavior() + // Start new children/subscriptions, and drop old ones. + subtreeManager.track(behavior.childCases) + subscriptionTracker.track(behavior.subscriptionCases) + + return rendering + } + private fun updateInputAndState( - workflow: Workflow, + workflow: StatefulWorkflow, newInput: InputT ) { state = workflow.onInputChanged(lastInput, newInput, state) @@ -181,7 +195,9 @@ internal class WorkflowNode): Snapshot { + private fun Snapshot.withState( + workflow: StatefulWorkflow + ): Snapshot { val stateSnapshot = workflow.snapshotState(state) return Snapshot.write { sink -> sink.writeByteStringWithLength(stateSnapshot.bytes) @@ -189,15 +205,18 @@ internal class WorkflowNode): StateT { + private fun Snapshot.restoreState( + workflow: StatefulWorkflow + ): StateT { val (state, childrenSnapshot) = parsePartial(workflow) subtreeManager.restoreChildrenFromSnapshot(childrenSnapshot) return state } /** @see Snapshot.withState */ - private fun Snapshot.parsePartial(workflow: Workflow): - Pair = + private fun Snapshot.parsePartial( + workflow: StatefulWorkflow + ): Pair = bytes.parse { source -> val stateSnapshot = source.readByteStringWithLength() val childrenSnapshot = source.readByteString() diff --git a/kotlin/workflow-host/src/test/java/com/squareup/workflow/internal/RealWorkflowContextTest.kt b/kotlin/workflow-host/src/test/java/com/squareup/workflow/internal/RealWorkflowContextTest.kt index 90c1f3466..21e58a7dd 100644 --- a/kotlin/workflow-host/src/test/java/com/squareup/workflow/internal/RealWorkflowContextTest.kt +++ b/kotlin/workflow-host/src/test/java/com/squareup/workflow/internal/RealWorkflowContextTest.kt @@ -16,6 +16,7 @@ package com.squareup.workflow.internal import com.squareup.workflow.Snapshot +import com.squareup.workflow.StatefulWorkflow import com.squareup.workflow.Workflow import com.squareup.workflow.WorkflowAction.Companion.emitOutput import com.squareup.workflow.WorkflowAction.Companion.noop @@ -36,23 +37,23 @@ class RealWorkflowContextTest { data class Rendering( val case: WorkflowOutputCase<*, *, *, *>, - val child: Workflow<*, *, *, *>, - val id: WorkflowId<*, *, *, *>, + val child: Workflow<*, *, *>, + val id: WorkflowId<*, *, *>, val input: Any ) @Suppress("UNCHECKED_CAST") - override fun compose( + override fun compose( case: WorkflowOutputCase, - child: Workflow, - id: WorkflowId, + child: Workflow, + id: WorkflowId, input: IC ): RC { return Rendering(case, child, id, input) as RC } } - private class TestWorkflow : Workflow { + private class TestWorkflow : StatefulWorkflow() { override fun initialState(input: String): String = fail() override fun compose( @@ -68,10 +69,10 @@ class RealWorkflowContextTest { } private class PoisonComposer : Composer { - override fun compose( + override fun compose( case: WorkflowOutputCase, - child: Workflow, - id: WorkflowId, + child: Workflow, + id: WorkflowId, input: IC ): RC = fail() } @@ -114,7 +115,7 @@ class RealWorkflowContextTest { assertSame(workflow, child) assertEquals(workflow.id("key"), id) assertEquals("input", input) - assertEquals>(workflow, case.workflow) + assertEquals>(workflow, case.workflow) assertEquals(workflow.id("key"), case.id) assertEquals("input", case.input) diff --git a/kotlin/workflow-host/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt b/kotlin/workflow-host/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt index f3a1793c2..d0c0c82dc 100644 --- a/kotlin/workflow-host/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt +++ b/kotlin/workflow-host/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt @@ -16,10 +16,10 @@ package com.squareup.workflow.internal import com.squareup.workflow.Snapshot -import com.squareup.workflow.Workflow -import com.squareup.workflow.WorkflowContext +import com.squareup.workflow.StatefulWorkflow import com.squareup.workflow.WorkflowAction import com.squareup.workflow.WorkflowAction.Companion.emitOutput +import com.squareup.workflow.WorkflowContext import com.squareup.workflow.internal.Behavior.WorkflowOutputCase import com.squareup.workflow.internal.SubtreeManagerTest.TestWorkflow.Rendering import kotlinx.coroutines.experimental.Dispatchers @@ -33,7 +33,7 @@ import kotlin.test.fail class SubtreeManagerTest { - private class TestWorkflow : Workflow { + private class TestWorkflow : StatefulWorkflow() { var started = 0 diff --git a/kotlin/workflow-host/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt b/kotlin/workflow-host/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt index 35a5604d5..9a5ca58c3 100644 --- a/kotlin/workflow-host/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt +++ b/kotlin/workflow-host/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt @@ -15,12 +15,9 @@ */ package com.squareup.workflow.internal -import com.squareup.workflow.util.ChannelUpdate -import com.squareup.workflow.util.ChannelUpdate.Closed -import com.squareup.workflow.util.ChannelUpdate.Value import com.squareup.workflow.EventHandler import com.squareup.workflow.Snapshot -import com.squareup.workflow.Workflow +import com.squareup.workflow.StatefulWorkflow import com.squareup.workflow.WorkflowAction.Companion.emitOutput import com.squareup.workflow.WorkflowAction.Companion.enterState import com.squareup.workflow.WorkflowContext @@ -29,6 +26,9 @@ import com.squareup.workflow.invoke import com.squareup.workflow.onReceive import com.squareup.workflow.parse import com.squareup.workflow.readUtf8WithLength +import com.squareup.workflow.util.ChannelUpdate +import com.squareup.workflow.util.ChannelUpdate.Closed +import com.squareup.workflow.util.ChannelUpdate.Value import com.squareup.workflow.writeUtf8WithLength import kotlinx.coroutines.experimental.Dispatchers import kotlinx.coroutines.experimental.Dispatchers.Unconfined @@ -48,14 +48,14 @@ import kotlin.test.fail class WorkflowNodeTest { - private interface StringWorkflow : Workflow { + private abstract class StringWorkflow : StatefulWorkflow() { override fun snapshotState(state: String): Snapshot = fail("not expected") override fun restoreState(snapshot: Snapshot): String = fail("not expected") } private class InputRenderingWorkflow( private val onInputChanged: (String, String, String) -> String - ) : StringWorkflow { + ) : StringWorkflow() { override fun initialState(input: String): String { return "starting:$input" @@ -121,7 +121,7 @@ class WorkflowNodeTest { @Test fun `accepts event`() { lateinit var eventHandler: (String) -> Unit - val workflow = object : StringWorkflow { + val workflow = object : StringWorkflow() { override fun initialState(input: String): String = input override fun compose( @@ -149,7 +149,7 @@ class WorkflowNodeTest { @Test fun `throws on subsequent events on same rendering`() { lateinit var eventHandler: (String) -> Unit - val workflow = object : StringWorkflow { + val workflow = object : StringWorkflow() { override fun initialState(input: String): String = input override fun compose( @@ -182,7 +182,7 @@ class WorkflowNodeTest { @Test fun `subscriptions detects value`() { val channel = Channel(capacity = 1) var update: ChannelUpdate? = null - val workflow = object : StringWorkflow { + val workflow = object : StringWorkflow() { override fun initialState(input: String): String = input override fun compose( @@ -233,7 +233,7 @@ class WorkflowNodeTest { @Test fun `subscriptions detects close`() { val channel = Channel(capacity = 0) var update: ChannelUpdate? = null - val workflow = object : StringWorkflow { + val workflow = object : StringWorkflow() { override fun initialState(input: String): String = input override fun compose( @@ -272,7 +272,7 @@ class WorkflowNodeTest { @Test fun `subscriptions unsubscribes`() { val channel = Channel(capacity = 0) lateinit var doClose: EventHandler - val workflow = object : StringWorkflow { + val workflow = object : StringWorkflow() { override fun initialState(input: String): String = input override fun compose( @@ -317,7 +317,7 @@ class WorkflowNodeTest { } @Test fun snapshots_nonEmpty_withoutChildren() { - val workflow = object : Workflow { + val workflow = object : StatefulWorkflow() { override fun initialState(input: String): String = input override fun compose( @@ -361,7 +361,7 @@ class WorkflowNodeTest { } @Test fun snapshots_empty_withoutChildren() { - val workflow = object : Workflow { + val workflow = object : StatefulWorkflow() { override fun initialState(input: String): String = input override fun compose( @@ -402,7 +402,7 @@ class WorkflowNodeTest { @Test fun snapshots_nonEmpty_withChildren() { var restoredChildState: String? = null var restoredParentState: String? = null - val childWorkflow = object : Workflow { + val childWorkflow = object : StatefulWorkflow() { override fun initialState(input: String): String = input override fun compose( @@ -423,7 +423,7 @@ class WorkflowNodeTest { .also { state -> restoredChildState = state } } } - val parentWorkflow = object : Workflow { + val parentWorkflow = object : StatefulWorkflow() { override fun initialState(input: String): String = input override fun compose( @@ -476,7 +476,7 @@ class WorkflowNodeTest { var restoreCalls = 0 // Track the number of times the snapshot is actually serialized, not snapshotState is called. var snapshotWrites = 0 - val workflow = object : Workflow { + val workflow = object : StatefulWorkflow() { override fun initialState(input: Unit) = Unit override fun compose( diff --git a/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/SubscriptionsTest.kt b/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/SubscriptionsTest.kt index 2877374a4..5de73b085 100644 --- a/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/SubscriptionsTest.kt +++ b/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/SubscriptionsTest.kt @@ -16,14 +16,14 @@ package com.squareup.workflow.rx2 import com.squareup.workflow.Snapshot -import com.squareup.workflow.util.ChannelUpdate -import com.squareup.workflow.util.ChannelUpdate.Closed -import com.squareup.workflow.util.ChannelUpdate.Value -import com.squareup.workflow.Workflow -import com.squareup.workflow.WorkflowContext +import com.squareup.workflow.StatefulWorkflow import com.squareup.workflow.WorkflowAction.Companion.emitOutput import com.squareup.workflow.WorkflowAction.Companion.enterState +import com.squareup.workflow.WorkflowContext import com.squareup.workflow.testing.testFromStart +import com.squareup.workflow.util.ChannelUpdate +import com.squareup.workflow.util.ChannelUpdate.Closed +import com.squareup.workflow.util.ChannelUpdate.Value import io.reactivex.Observable import io.reactivex.subjects.PublishSubject import kotlinx.coroutines.experimental.TimeoutCancellationException @@ -46,7 +46,7 @@ class SubscriptionsTest { */ private class SubscriberWorkflow( subject: Observable - ) : Workflow, (setSubscribed: Boolean) -> Unit> { + ) : StatefulWorkflow, (Boolean) -> Unit>() { var subscriptions = 0 private set diff --git a/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/WorkflowContextsTest.kt b/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/WorkflowContextsTest.kt index 5448e5ace..19ce17a74 100644 --- a/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/WorkflowContextsTest.kt +++ b/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/WorkflowContextsTest.kt @@ -17,8 +17,8 @@ package com.squareup.workflow.rx2 import com.squareup.workflow.EventHandler import com.squareup.workflow.Snapshot +import com.squareup.workflow.StatefulWorkflow import com.squareup.workflow.StatelessWorkflow -import com.squareup.workflow.Workflow import com.squareup.workflow.WorkflowAction.Companion.emitOutput import com.squareup.workflow.WorkflowAction.Companion.enterState import com.squareup.workflow.WorkflowAction.Companion.noop @@ -70,7 +70,7 @@ class WorkflowContextsTest { val single = singleSubject .doOnSubscribe { subscriptions++ } .doOnDispose { disposals++ } - val workflow = object : Workflow { + val workflow = object : StatefulWorkflow() { override fun initialState(input: Unit): Boolean = true override fun compose( diff --git a/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkflowTesting.kt b/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkflowTesting.kt index d13cbe274..f954258eb 100644 --- a/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkflowTesting.kt +++ b/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkflowTesting.kt @@ -16,6 +16,7 @@ package com.squareup.workflow.testing import com.squareup.workflow.Snapshot +import com.squareup.workflow.StatefulWorkflow import com.squareup.workflow.Workflow import com.squareup.workflow.WorkflowHost import com.squareup.workflow.WorkflowHost.Factory @@ -34,7 +35,7 @@ import kotlin.coroutines.experimental.EmptyCoroutineContext // @formatter:off @TestOnly fun - Workflow.testFromStart( + Workflow.testFromStart( input: InputT, snapshot: Snapshot? = null, context: CoroutineContext = EmptyCoroutineContext, @@ -48,7 +49,7 @@ fun * All workflow-related coroutines are cancelled when the block exits. */ @TestOnly -fun Workflow.testFromStart( +fun Workflow.testFromStart( snapshot: Snapshot? = null, context: CoroutineContext = EmptyCoroutineContext, block: (WorkflowTester) -> T @@ -56,15 +57,15 @@ fun Workflow. /** * Creates a [WorkflowTester] to run this workflow for unit testing. - * [Workflow.initialState] is not called. Instead, the workflow is started from the given - * [initialState]. + * If the workflow is [stateful][StatefulWorkflow], [initialState][StatefulWorkflow.initialState] + * is not called. Instead, the workflow is started from the given [initialState]. * * All workflow-related coroutines are cancelled when the block exits. */ // @formatter:off @TestOnly fun - Workflow.testFromState( + StatefulWorkflow.testFromState( input: InputT, initialState: StateT, context: CoroutineContext = EmptyCoroutineContext, @@ -74,15 +75,15 @@ fun /** * Creates a [WorkflowTester] to run this workflow for unit testing. - * [Workflow.initialState] is not called. Instead, the workflow is started from the given - * [initialState]. + * If the workflow is [stateful][StatefulWorkflow], [initialState][StatefulWorkflow.initialState] + * is not called. Instead, the workflow is started from the given [initialState]. * * All workflow-related coroutines are cancelled when the block exits. */ // @formatter:off @TestOnly fun - Workflow.testFromState( + StatefulWorkflow.testFromState( initialState: StateT, context: CoroutineContext = EmptyCoroutineContext, block: (WorkflowTester) -> Unit diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/ChannelSubscriptionsIntegrationTest.kt b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/ChannelSubscriptionsIntegrationTest.kt index ef27a0362..ce6f2a067 100644 --- a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/ChannelSubscriptionsIntegrationTest.kt +++ b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/ChannelSubscriptionsIntegrationTest.kt @@ -44,7 +44,7 @@ class ChannelSubscriptionsIntegrationTest { */ private class SubscriberWorkflow( private val channel: Channel - ) : Workflow, (setSubscribed: Boolean) -> Unit> { + ) : StatefulWorkflow, (Boolean) -> Unit>() { var subscriptions = 0 private set diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/TreeWorkflow.kt b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/TreeWorkflow.kt index 4668bf2c1..d20e5ad0c 100644 --- a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/TreeWorkflow.kt +++ b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/TreeWorkflow.kt @@ -23,7 +23,7 @@ import com.squareup.workflow.TreeWorkflow.Rendering internal class TreeWorkflow( private val name: String, private vararg val children: TreeWorkflow -) : Workflow { +) : StatefulWorkflow() { class Rendering( val data: String, diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkflowTesterTest.kt b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkflowTesterTest.kt index 5b7b5ae40..34f16759e 100644 --- a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkflowTesterTest.kt +++ b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkflowTesterTest.kt @@ -72,7 +72,7 @@ class WorkflowTesterTest { } @Test fun `propagates exception when workflow throws from initial state`() { - val workflow = object : Workflow { + val workflow = object : StatefulWorkflow() { override fun initialState(input: Unit) { throw ExpectedException() } @@ -102,7 +102,7 @@ class WorkflowTesterTest { } @Test fun `propagates exception when workflow throws from snapshot state`() { - val workflow = object : Workflow { + val workflow = object : StatefulWorkflow() { override fun initialState(input: Unit) { // Noop } @@ -132,7 +132,7 @@ class WorkflowTesterTest { } @Test fun `propagates exception when workflow throws from restore state`() { - val workflow = object : Workflow { + val workflow = object : StatefulWorkflow() { override fun initialState(input: Unit) { }