diff --git a/Directory.Build.props b/Directory.Build.props index 118b552..3a3a01b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ latest True - 2.4.0 + 2.4.1 ../../output/$(MSBuildProjectName) true diff --git a/samples/Sample04.ResultTransitions/Program.cs b/samples/Sample04.ResultTransitions/Program.cs index 0cb79dd..992a8a3 100644 --- a/samples/Sample04.ResultTransitions/Program.cs +++ b/samples/Sample04.ResultTransitions/Program.cs @@ -1,12 +1,95 @@ // Copyright Xeno Innovations, Inc. 2026 // See the LICENSE file in the project root for more information. +using Lite.StateMachine; + namespace Sample04.ResultTransitions; -internal class Program +/// State definitions. +public enum StateId +{ + State1, + State2, + State3, +} + +public class Program { - private static void Main(string[] args) + private static async Task Main(string[] args) + { + await ResultsStateMachine.RunAsync(); + } +} + +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Ignore")] +public class ResultsStateMachine +{ + /// Example asynchronous run method. + /// Task. + public static async Task RunAsync() + { + var machine = new StateMachine(); + machine.RegisterState(StateId.State1, onSuccess: StateId.State1, onError: StateId.State2, onFailure: null, subscriptionTypes: null); + machine.RegisterState(StateId.State2, onSuccess: StateId.State1, onError: null, onFailure: StateId.State3, subscriptionTypes: null); + machine.RegisterState(StateId.State3, subscriptionTypes: null); + + // Async Example! + await machine.RunAsync(StateId.State1); + } + + public class State1 : IState + { + public Task OnEnter(Context context) + { + // Simulate an Error state transition so we can continue. + context.NextState(Result.Error); + + Console.WriteLine($"[State1][OnEnter].OnSuccess goto: '{context.NextStates.OnSuccess}'"); + Console.WriteLine($"[State1][OnEnter].OnError goto: '{context.NextStates.OnError}'"); + Console.WriteLine($"[State1][OnEnter].OnFailure goto: '{context.NextStates.OnFailure}'"); + return Task.CompletedTask; + } + + public Task OnEntering(Context context) => Task.CompletedTask; + + public Task OnExit(Context context) => Task.CompletedTask; + } + + public class State2 : IState { - Console.WriteLine("Coming Soon!"); + public Task OnEnter(Context context) + { + // Simulate a Failure state transition so we can continue. + context.NextState(Result.Failure); + + Console.WriteLine($"[State2][OnEnter].OnSuccess goto: '{context.NextStates.OnSuccess}'"); + Console.WriteLine($"[State2][OnEnter].OnError goto: '{context.NextStates.OnError}'"); + Console.WriteLine($"[State2][OnEnter].OnFailure goto: '{context.NextStates.OnFailure}'"); + + return Task.CompletedTask; + } + + public Task OnEntering(Context context) => Task.CompletedTask; + + public Task OnExit(Context context) => Task.CompletedTask; + } + + public class State3 : IState + { + public Task OnEnter(Context context) + { + // Set Failure so we can continue. + context.NextState(Result.Success); + + Console.WriteLine($"[State3][OnEnter].OnSuccess goto: '{context.NextStates.OnSuccess}'"); + Console.WriteLine($"[State3][OnEnter].OnError goto: '{context.NextStates.OnError}'"); + Console.WriteLine($"[State3][OnEnter].OnFailure goto: '{context.NextStates.OnFailure}'"); + + return Task.CompletedTask; + } + + public Task OnEntering(Context context) => Task.CompletedTask; + + public Task OnExit(Context context) => Task.CompletedTask; } } diff --git a/samples/Sample04.ResultTransitions/Sample04.ResultTransitions.csproj b/samples/Sample04.ResultTransitions/Sample04.ResultTransitions.csproj index ed9781c..c3ee00d 100644 --- a/samples/Sample04.ResultTransitions/Sample04.ResultTransitions.csproj +++ b/samples/Sample04.ResultTransitions/Sample04.ResultTransitions.csproj @@ -7,4 +7,8 @@ enable + + + + diff --git a/source/Lite.StateMachine.Tests/StateTests/NextStateResultTests.cs b/source/Lite.StateMachine.Tests/StateTests/NextStateResultTests.cs new file mode 100644 index 0000000..0846074 --- /dev/null +++ b/source/Lite.StateMachine.Tests/StateTests/NextStateResultTests.cs @@ -0,0 +1,87 @@ +// Copyright Xeno Innovations, Inc. 2026 +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Lite.StateMachine.Tests.StateTests; + +[TestClass] +public class NextStateResultTests : TestBase +{ + /// State definitions. + public enum StateId + { + State1, + State2, + State3, + } + + [TestMethod] + public async Task NextState_Error_Failure__SuccessTestAsync() + { + var machine = new StateMachine(); + machine.RegisterState(StateId.State1, onSuccess: StateId.State1, onError: StateId.State2, onFailure: null, subscriptionTypes: null); + machine.RegisterState(StateId.State2, onSuccess: StateId.State1, onError: null, onFailure: StateId.State3, subscriptionTypes: null); + machine.RegisterState(StateId.State3, subscriptionTypes: null); + + // Async Example! + await machine.RunAsync(StateId.State1); + + // Assert Results + AssertMachineNotNull(machine); + + // Ensure all states are registered + var enums = Enum.GetValues().Cast(); + Assert.IsNotNull(enums); + Assert.HasCount(enums.Count(), machine.States); + Assert.IsTrue(enums.All(k => machine.States.Contains(k))); + } + + private class State1 : IState + { + public Task OnEnter(Context context) + { + // Simulate an Error state transition so we can continue. + context.NextState(Result.Error); + + Console.WriteLine($"[State1][OnEnter].OnSuccess goto: '{context.NextStates.OnSuccess}'"); + Console.WriteLine($"[State1][OnEnter].OnError goto: '{context.NextStates.OnError}'"); + Console.WriteLine($"[State1][OnEnter].OnFailure goto: '{context.NextStates.OnFailure}'"); + return Task.CompletedTask; + } + + public Task OnEntering(Context context) => Task.CompletedTask; + + public Task OnExit(Context context) => Task.CompletedTask; + } + + private class State2 : IState + { + public Task OnEnter(Context context) + { + // Simulate a Failure state transition so we can continue. + context.NextState(Result.Failure); + return Task.CompletedTask; + } + + public Task OnEntering(Context context) => Task.CompletedTask; + + public Task OnExit(Context context) => Task.CompletedTask; + } + + private class State3 : IState + { + public Task OnEnter(Context context) + { + // Set Failure so we can continue. + context.NextState(Result.Success); + return Task.CompletedTask; + } + + public Task OnEntering(Context context) => Task.CompletedTask; + + public Task OnExit(Context context) => Task.CompletedTask; + } +} diff --git a/source/Lite.StateMachine/Lite.StateMachine.csproj b/source/Lite.StateMachine/Lite.StateMachine.csproj index 32faade..08e3beb 100644 --- a/source/Lite.StateMachine/Lite.StateMachine.csproj +++ b/source/Lite.StateMachine/Lite.StateMachine.csproj @@ -21,6 +21,9 @@ statemachine;lite;fsm;suesslabs;xeno-innovations LICENSE.md + New Features in v2.4: + * Added custom State Results (Success, Error, Failure, etc.) and ability to override the default Result.Success/.Error./Failure. + New Features in v2.2: * New: Renamed, Result.Ok -> Result.Success (Breaking Change). diff --git a/source/Lite.StateMachine/StateMachine.cs b/source/Lite.StateMachine/StateMachine.cs index e0bb1a2..237a6a8 100644 --- a/source/Lite.StateMachine/StateMachine.cs +++ b/source/Lite.StateMachine/StateMachine.cs @@ -133,7 +133,7 @@ public StateMachine RegisterState( IReadOnlyCollection? subscriptionTypes = null) where TStateClass : class, IState { - return RegisterState(stateId, onSuccess, onError: null, onFailure: null, parentStateId: null, isCompositeParent: false, initialChildStateId: null, subscriptionTypes: subscriptionTypes); + return RegisterState(stateId, onSuccess, onError: onError, onFailure: onFailure, parentStateId: null, isCompositeParent: false, initialChildStateId: null, subscriptionTypes: subscriptionTypes); } ///