Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow permitDynamic destination state to be calculated with an async function (Task) #595

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/Stateless/DynamicTriggerBehaviour.Async.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Threading.Tasks;

namespace Stateless
{
public partial class StateMachine<TState, TTrigger>
{
internal class DynamicTriggerBehaviourAsync : TriggerBehaviour
{
readonly Func<object[], Task<TState>> _destination;
internal Reflection.DynamicTransitionInfo TransitionInfo { get; private set; }

public DynamicTriggerBehaviourAsync(TTrigger trigger, Func<object[], Task<TState>> destination,
TransitionGuard transitionGuard, Reflection.DynamicTransitionInfo info)
: base(trigger, transitionGuard)
{
_destination = destination ?? throw new ArgumentNullException(nameof(destination));
TransitionInfo = info ?? throw new ArgumentNullException(nameof(info));
}

public async Task<TState> GetDestinationState(TState source, object[] args)
{
return await _destination(args);
}
}
}
}
4 changes: 4 additions & 0 deletions src/Stateless/Reflection/StateInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ internal static void AddRelationships<TState, TTrigger>(StateInfo info, StateMac
{
dynamicTransitions.Add(((StateMachine<TState, TTrigger>.DynamicTriggerBehaviour)item).TransitionInfo);
}
foreach (var item in triggerBehaviours.Value.Where(behaviour => behaviour is StateMachine<TState, TTrigger>.DynamicTriggerBehaviourAsync))
{
dynamicTransitions.Add(((StateMachine<TState, TTrigger>.DynamicTriggerBehaviourAsync)item).TransitionInfo);
}
}

info.AddRelationships(superstate, substates, fixedTransitions, dynamicTransitions);
Expand Down
556 changes: 556 additions & 0 deletions src/Stateless/StateConfiguration.Async.cs

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/Stateless/StateMachine.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,15 @@ async Task InternalFireOneAsync(TTrigger trigger, params object[] args)
// Handle transition, and set new state
var transition = new Transition(source, handler.Destination, trigger, args);
await HandleReentryTriggerAsync(args, representativeState, transition);
break;
}
case DynamicTriggerBehaviourAsync asyncHandler:
{
var destination = await asyncHandler.GetDestinationState(source, args);
// Handle transition, and set new state; reentry is permitted from dynamic trigger behaviours.
var transition = new Transition(source, destination, trigger, args);
await HandleTransitioningTriggerAsync(args, representativeState, transition);

break;
}
case DynamicTriggerBehaviour handler:
Expand Down
12 changes: 12 additions & 0 deletions src/Stateless/StateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,18 @@ private void InternalFireOne(TTrigger trigger, params object[] args)
HandleReentryTrigger(args, representativeState, transition);
break;
}
case DynamicTriggerBehaviourAsync asyncHandler:
{
asyncHandler.GetDestinationState(source, args)
.ContinueWith(t =>
{
var destination = t.Result;
// Handle transition, and set new state; reentry is permitted from dynamic trigger behaviours.
var transition = new Transition(source, destination, trigger, args);
return HandleTransitioningTriggerAsync(args, representativeState, transition);
});
break;
}
case DynamicTriggerBehaviour handler:
{
handler.GetDestinationState(source, args, out var destination);
Expand Down
84 changes: 78 additions & 6 deletions test/Stateless.Tests/DotGraphFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Xunit;
using Stateless.Reflection;
using Stateless.Graph;
using System.Threading.Tasks;

namespace Stateless.Tests
{
Expand Down Expand Up @@ -196,7 +197,7 @@ public void WhenDiscriminatedByAnonymousGuardWithDescription()
var expected = Prefix(Style.UML)
+ Box(Style.UML, "A") + Box(Style.UML, "B")
+ Line("A", "B", "X [description]")
+ suffix;
+ suffix;

var sm = new StateMachine<State, Trigger>(State.A);

Expand Down Expand Up @@ -258,6 +259,27 @@ public void DestinationStateIsDynamic()

string dotGraph = UmlDotGraph.Format(sm.GetInfo());

#if WRITE_DOTS_TO_FOLDER
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsDynamic.dot", dotGraph);
#endif

Assert.Equal(expected, dotGraph);
}

[Fact]
public void DestinationStateIsDynamicAsync()
{
var expected = Prefix(Style.UML)
+ Box(Style.UML, "A")
+ Decision(Style.UML, "Decision1", "Function")
+ Line("A", "Decision1", "X") + suffix;

var sm = new StateMachine<State, Trigger>(State.A);
sm.Configure(State.A)
.PermitDynamicAsync(Trigger.X, () => Task.FromResult(State.B));

string dotGraph = UmlDotGraph.Format(sm.GetInfo());

#if WRITE_DOTS_TO_FOLDER
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsDynamic.dot", dotGraph);
#endif
Expand All @@ -280,6 +302,27 @@ public void DestinationStateIsCalculatedBasedOnTriggerParameters()

string dotGraph = UmlDotGraph.Format(sm.GetInfo());

#if WRITE_DOTS_TO_FOLDER
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsCalculatedBasedOnTriggerParameters.dot", dotGraph);
#endif
Assert.Equal(expected, dotGraph);
}

[Fact]
public void DestinationStateIsCalculatedBasedOnTriggerParametersAsync()
{
var expected = Prefix(Style.UML)
+ Box(Style.UML, "A")
+ Decision(Style.UML, "Decision1", "Function")
+ Line("A", "Decision1", "X") + suffix;

var sm = new StateMachine<State, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int>(Trigger.X);
sm.Configure(State.A)
.PermitDynamicAsync(trigger, i =>Task.FromResult(i == 1 ? State.B : State.C));

string dotGraph = UmlDotGraph.Format(sm.GetInfo());

#if WRITE_DOTS_TO_FOLDER
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsCalculatedBasedOnTriggerParameters.dot", dotGraph);
#endif
Expand Down Expand Up @@ -398,7 +441,7 @@ public void OnEntryWithTriggerParameter()

Assert.Equal(expected, dotGraph);
}

[Fact]
public void SpacedUmlWithSubstate()
{
Expand All @@ -408,14 +451,14 @@ public void SpacedUmlWithSubstate()
string StateD = "State D";
string TriggerX = "Trigger X";
string TriggerY = "Trigger Y";

var expected = Prefix(Style.UML)
+ Subgraph(Style.UML, StateD, $"{StateD}\\n----------\\nentry / Enter D",
Box(Style.UML, StateB)
+ Box(Style.UML, StateC))
+ Box(Style.UML, StateA, new List<string> { "Enter A" }, new List<string> { "Exit A" })
+ Line(StateA, StateB, TriggerX) + Line(StateA, StateC, TriggerY)
+ Environment.NewLine
+ Environment.NewLine
+ $" init [label=\"\", shape=point];" + Environment.NewLine
+ $" init -> \"{StateA}\"[style = \"solid\"]" + Environment.NewLine
+ "}";
Expand Down Expand Up @@ -493,7 +536,36 @@ public void UmlWithDynamic()
var sm = new StateMachine<State, Trigger>(State.A);

sm.Configure(State.A)
.PermitDynamic(Trigger.X, DestinationSelector, null, new DynamicStateInfos { { State.B, "ChoseB"}, { State.C, "ChoseC" } });
.PermitDynamic(Trigger.X, DestinationSelector, null, new DynamicStateInfos { { State.B, "ChoseB" }, { State.C, "ChoseC" } });

sm.Configure(State.B);
sm.Configure(State.C);

string dotGraph = UmlDotGraph.Format(sm.GetInfo());
#if WRITE_DOTS_TO_FOLDER
System.IO.File.WriteAllText(DestinationFolder + "UmlWithDynamic.dot", dotGraph);
#endif

Assert.Equal(expected, dotGraph);
}

[Fact]
public void UmlWithDynamicAsync()
{
var expected = Prefix(Style.UML)
+ Box(Style.UML, "A")
+ Box(Style.UML, "B")
+ Box(Style.UML, "C")
+ Decision(Style.UML, "Decision1", "Function")
+ Line("A", "Decision1", "X")
+ Line("Decision1", "B", "X [ChoseB]")
+ Line("Decision1", "C", "X [ChoseC]")
+ suffix;

var sm = new StateMachine<State, Trigger>(State.A);

sm.Configure(State.A)
.PermitDynamicAsync(Trigger.X, () => Task.FromResult(DestinationSelector()), null, new DynamicStateInfos { { State.B, "ChoseB" }, { State.C, "ChoseC" } });

sm.Configure(State.B);
sm.Configure(State.C);
Expand All @@ -513,7 +585,7 @@ public void TransitionWithIgnoreAndEntry()
+ Box(Style.UML, "A", new List<string> { "DoEntry" })
+ Box(Style.UML, "B", new List<string> { "DoThisEntry" })
+ Line("A", "B", "X")
+ Line("A", "A", "Y")
+ Line("A", "A", "Y")
+ Line("B", "B", "Z / DoThisEntry")
+ suffix;

Expand Down
168 changes: 168 additions & 0 deletions test/Stateless.Tests/DynamicAsyncTriggerBehaviourAsyncFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using System;
using System.Threading.Tasks;
using Xunit;

namespace Stateless.Tests
{
public class DynamicAsyncTriggerBehaviourAsyncFixture
{
[Fact]
public async Task PermitDynamic_Selects_Expected_State_Async()
{
var sm = new StateMachine<State, Trigger>(State.A);
sm.Configure(State.A)
.PermitDynamicAsync(Trigger.X, async () => { await Task.Delay(100); return State.B; });

await sm.FireAsync(Trigger.X);

Assert.Equal(State.B, sm.State);
}

[Fact]
public async Task PermitDynamic_With_TriggerParameter_Selects_Expected_State_Async()
{
var sm = new StateMachine<State, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int>(Trigger.X);
sm.Configure(State.A)
.PermitDynamicAsync(trigger, async (i) => { await Task.Delay(100); return i == 1 ? State.B : State.C; });

await sm.FireAsync(trigger, 1);

Assert.Equal(State.B, sm.State);
}

[Fact]
public async Task PermitDynamic_Permits_Reentry_Async()
{
var sm = new StateMachine<State, Trigger>(State.A);
var onExitInvoked = false;
var onEntryInvoked = false;
var onEntryFromInvoked = false;
sm.Configure(State.A)
.PermitDynamicAsync(Trigger.X, async () => { await Task.Delay(100); return State.A; })
.OnEntry(() => onEntryInvoked = true)
.OnEntryFrom(Trigger.X, () => onEntryFromInvoked = true)
.OnExit(() => onExitInvoked = true);

await sm.FireAsync(Trigger.X);

Assert.True(onExitInvoked, "Expected OnExit to be invoked");
Assert.True(onEntryInvoked, "Expected OnEntry to be invoked");
Assert.True(onEntryFromInvoked, "Expected OnEntryFrom to be invoked");
Assert.Equal(State.A, sm.State);
}

[Fact]
public async Task PermitDynamic_Selects_Expected_State_Based_On_DestinationStateSelector_Function_Async()
{
var sm = new StateMachine<State, Trigger>(State.A);
var value = 'C';
sm.Configure(State.A)
.PermitDynamicAsync(Trigger.X, async () => { await Task.Delay(100); return value == 'B' ? State.B : State.C; });

await sm.FireAsync(Trigger.X);

Assert.Equal(State.C, sm.State);
}

[Fact]
public async Task PermitDynamicIf_With_TriggerParameter_Permits_Transition_When_GuardCondition_Met_Async()
{
var sm = new StateMachine<State, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int>(Trigger.X);
sm.Configure(State.A)
.PermitDynamicIfAsync(trigger, async (i) =>{ await Task.Delay(100); return i == 1 ? State.C : State.B; }, (i) => i == 1);

await sm.FireAsync(trigger, 1);

Assert.Equal(State.C, sm.State);
}

[Fact]
public async Task PermitDynamicIf_With_2_TriggerParameters_Permits_Transition_When_GuardCondition_Met_Async()
{
var sm = new StateMachine<State, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int, int>(Trigger.X);
sm.Configure(State.A).PermitDynamicIfAsync(
trigger,
async (i, j) => { await Task.Yield(); return i == 1 && j == 2 ? State.C : State.B; },
(i, j) => i == 1 && j == 2);

await sm.FireAsync(trigger, 1, 2);

Assert.Equal(State.C, sm.State);
}

[Fact]
public async Task PermitDynamicIf_With_3_TriggerParameters_Permits_Transition_When_GuardCondition_Met_Async()
{
var sm = new StateMachine<State, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int, int, int>(Trigger.X);
sm.Configure(State.A).PermitDynamicIfAsync(
trigger,
async (i, j, k) => { await Task.Delay(100); return i == 1 && j == 2 && k == 3 ? State.C : State.B; },
(i, j, k) => i == 1 && j == 2 && k == 3);

await sm.FireAsync(trigger, 1, 2, 3);

Assert.Equal(State.C, sm.State);
}

[Fact]
public async Task PermitDynamicIf_With_TriggerParameter_Throws_When_GuardCondition_Not_Met_Async()
{
var sm = new StateMachine<State, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int>(Trigger.X);
sm.Configure(State.A)
.PermitDynamicIfAsync(trigger, async (i) => { await Task.Delay(100); return i > 0 ? State.C : State.B; }, guard: (i) => i == 2);

await Assert.ThrowsAsync<InvalidOperationException>(async () => await sm.FireAsync(trigger, 1));
}

[Fact]
public async Task PermitDynamicIf_With_2_TriggerParameters_Throws_When_GuardCondition_Not_Met_Async()
{
var sm = new StateMachine<State, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int, int>(Trigger.X);
sm.Configure(State.A).PermitDynamicIfAsync(
trigger,
async (i, j) => { await Task.Delay(100); return i > 0 ? State.C : State.B; },
(i, j) => i == 2 && j == 3);

await Assert.ThrowsAsync<InvalidOperationException>(async () => await sm.FireAsync(trigger, 1, 2));
}

[Fact]
public async Task PermitDynamicIf_With_3_TriggerParameters_Throws_When_GuardCondition_Not_Met_Async()
{
var sm = new StateMachine<State, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int, int, int>(Trigger.X);
sm.Configure(State.A).PermitDynamicIfAsync(trigger,
async (i, j, k) => { await Task.Delay(100); return i > 0 ? State.C : State.B; },
(i, j, k) => i == 2 && j == 3 && k == 4);

await Assert.ThrowsAsync<InvalidOperationException>(async () => await sm.FireAsync(trigger, 1, 2, 3));
}

[Fact]
public async Task PermitDynamicIf_Permits_Reentry_When_GuardCondition_Met_Async()
{
var sm = new StateMachine<State, Trigger>(State.A);
var onExitInvoked = false;
var onEntryInvoked = false;
var onEntryFromInvoked = false;
sm.Configure(State.A)
.PermitDynamicIfAsync(Trigger.X, async () =>{ await Task.Delay(100); return State.A; }, () => true)
.OnEntry(() => onEntryInvoked = true)
.OnEntryFrom(Trigger.X, () => onEntryFromInvoked = true)
.OnExit(() => onExitInvoked = true);

await sm.FireAsync(Trigger.X);

Assert.True(onExitInvoked, "Expected OnExit to be invoked");
Assert.True(onEntryInvoked, "Expected OnEntry to be invoked");
Assert.True(onEntryFromInvoked, "Expected OnEntryFrom to be invoked");
Assert.Equal(State.A, sm.State);
}
}
}
Loading