| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:io'; |
| import 'dart:async'; |
| |
| /// An abstract class describing a workflow action. |
| abstract class WorkflowAction {} |
| |
| /// [WaitForInputWorkflowAction] signals to the workflow engine that the current |
| /// step is waiting for user input. |
| class WaitForInputWorkflowAction extends WorkflowAction {} |
| |
| /// [NavigateStepWorkflowAction] signals to the workflow engine that a |
| /// transition to a new step [nextStep] should happen. It is possible to send |
| /// state along by using the [payload]. |
| class NavigateStepWorkflowAction<T> extends WorkflowAction { |
| WorkflowStep nextStep; |
| T payload; |
| NavigateStepWorkflowAction(this.nextStep, this.payload); |
| } |
| |
| /// [BackWorkflowAction] signals to the workflow engine that it should pop the |
| /// current step and return focus to the previous step. |
| class BackWorkflowAction extends WorkflowAction {} |
| |
| /// Base class for a single step in a larger [Workflow]. |
| abstract class WorkflowStep<T> { |
| /// The [onShow] is called whenever the step receives focus from the workflow |
| /// engine. This happens both when transitioning and when going back. When |
| /// going back [payload] is null. |
| Future<WorkflowAction> onShow(T payload); |
| |
| /// The [onLeave] is called on the current step, before the workflow engine |
| /// transitions to a new step. The transition can be cancelled by returning |
| /// true. |
| Future<bool> onLeave(); |
| |
| /// Input received by the workflow engine. The step should return what |
| /// workflow action to take after processing the input. |
| Future<WorkflowAction> input(String input); |
| } |
| |
| /// [Workflow] is a class that makes it easy to create workflows. A workflow is |
| /// a collection of steps, with transitions between the steps to complete the |
| /// workflow. Viewed steps are kept track of by a stack, which makes it easy for |
| /// steps to transition back. |
| /// |
| /// All access to navigation is hidden, thereby making steps unaware of the |
| /// workflow it is in. Navigation can only be changed by workflow actions. |
| class Workflow { |
| final List<WorkflowStep> _lastSteps = []; |
| |
| WorkflowStep get currentStep { |
| return _lastSteps.length > 0 ? _lastSteps.last : null; |
| } |
| |
| /// Start the workflow by providing the first step to show. |
| Future start(WorkflowStep firstStep) { |
| return _navigate(firstStep, null); |
| } |
| |
| /// Handling the leaving of a step and asks the step if it is ok to leave. |
| Future _navigateLeave() async { |
| if (currentStep != null) { |
| var result = await currentStep.onLeave(); |
| if (result == true) { |
| // Cancel the navigation. We should wait for input. |
| return _handleWorkflowAction(new WaitForInputWorkflowAction()); |
| } |
| } |
| } |
| |
| /// Handling the navigation to a step and adding the new step as the current. |
| Future _navigate<T>(WorkflowStep navigateTo, T payload) async { |
| await _navigateLeave(); |
| _lastSteps.add(navigateTo); |
| await _handleWorkflowAction(await currentStep.onShow(payload)); |
| } |
| |
| /// Handling the workflow actions a feedback recursive loop. |
| Future _handleWorkflowAction(WorkflowAction action) async { |
| if (action is WaitForInputWorkflowAction) { |
| String input = stdin.readLineSync(); |
| return _handleWorkflowAction(await currentStep.input(input)); |
| } else if (action is NavigateStepWorkflowAction) { |
| return _navigate(action.nextStep, action.payload); |
| } else if (action is BackWorkflowAction) { |
| await _navigateLeave(); |
| _lastSteps.removeLast(); |
| var lastStep = _lastSteps.last; |
| while (_lastSteps.isNotEmpty && lastStep is ComputeStep) { |
| lastStep = _lastSteps.removeLast(); |
| } |
| if (lastStep != null) { |
| return _handleWorkflowAction(await lastStep.onShow(null)); |
| } |
| } |
| } |
| } |
| |
| /// [ComputeStepPayload] is the payload for doing [ComputeStep]. |
| class ComputeStepPayload { |
| final Future action; |
| final String message; |
| final WorkflowStep completedStep; |
| ComputeStepPayload(this.action, this.message, this.completedStep); |
| } |
| |
| /// [ComputeStep] is similar to a loading screen, showing a dot every second |
| /// until the computation in the future [action] in the payload is completed. |
| /// When completed, it will ask for a transition by returning |
| /// [NavigateStepWorkflowAction]. |
| class ComputeStep extends WorkflowStep<ComputeStepPayload> { |
| @override |
| Future<bool> onLeave() async { |
| // Do nothing. |
| return false; |
| } |
| |
| @override |
| Future<WorkflowAction> onShow(ComputeStepPayload payload) async { |
| stdout.write(payload.message); |
| var timer = new Timer(new Duration(seconds: 1), () { |
| stdout.write("."); |
| }); |
| var result = await payload.action; |
| timer.cancel(); |
| stdout.write("\n"); |
| return new NavigateStepWorkflowAction(payload.completedStep, result); |
| } |
| |
| @override |
| Future<WorkflowAction> input(String input) { |
| // Do nothing. |
| return null; |
| } |
| } |