Skip to content

Commit 0f7b75a

Browse files
committed
I didn't like the OnUnhandled action because that means either you have to call out to a dependency or mutate the state. Yuck. Changed to returning a command.
1 parent 149a41f commit 0f7b75a

5 files changed

Lines changed: 79 additions & 20 deletions

File tree

FunctionalStateMachine.Core.Tests/StateMachineUnhandledTests.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ public void TryFire_ReturnsFalseWhenUnhandled()
2121
[Fact]
2222
public void OnUnhandled_InvokesHandler()
2323
{
24-
var log = new List<string>();
2524
var machine = StateMachine<State, Trigger, Data, CommandBase>.Create()
26-
.OnUnhandled((trigger, state, data) => log.Add($"{state}:{trigger}"))
25+
.OnUnhandled()
26+
.Execute((trigger, state) =>
27+
[
28+
new LogCommand($"{state}:{trigger}")
29+
])
2730
.For(State.Ready)
2831
.Build();
2932

@@ -32,8 +35,7 @@ public void OnUnhandled_InvokesHandler()
3235
var handled = machine.TryFire(Trigger.Start, currentState, currentData, out _, out _, out var commands);
3336

3437
Assert.True(handled);
35-
Assert.Empty(commands);
36-
Assert.Single(log);
38+
Assert.Single(commands);
3739
}
3840

3941
[Fact]
@@ -84,4 +86,6 @@ private sealed record Data(string Id)
8486
}
8587

8688
private abstract record CommandBase;
89+
90+
private sealed record LogCommand(string Message) : CommandBase;
8791
}

FunctionalStateMachine.Core/StateMachine.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public static StateMachineBuilder<TState, TTrigger, TData, TCommand> Create()
1212
}
1313

1414
private readonly Dictionary<TState, StateDefinition> _states = new();
15-
private Action<TTrigger, TState, TData>? _onUnhandled;
15+
private Func<TTrigger, TState, IEnumerable<TCommand>>? _onUnhandled;
1616
private bool _hasInitialState;
1717
private TState? _initialState;
1818

@@ -33,7 +33,7 @@ internal StateConfiguration For(TState state)
3333
}
3434

3535
internal StateMachine<TState, TTrigger, TData, TCommand> OnUnhandled(
36-
Action<TTrigger, TState, TData> handler)
36+
Func<TTrigger, TState, IEnumerable<TCommand>> handler)
3737
{
3838
_onUnhandled = handler;
3939
return this;
@@ -191,10 +191,10 @@ private bool HandleUnhandled(
191191
{
192192
if (_onUnhandled != null)
193193
{
194-
_onUnhandled(trigger, currentState, currentData);
194+
var commandList = _onUnhandled(trigger, currentState)?.ToList() ?? new List<TCommand>();
195195
newState = currentState;
196196
newData = currentData;
197-
commands = [];
197+
commands = commandList.Count == 0 ? Array.Empty<TCommand>() : new ReadOnlyCollection<TCommand>(commandList);
198198
return true;
199199
}
200200

@@ -1914,9 +1914,10 @@ internal void Validate()
19141914

19151915
public TState CreateState(TState state) => _inner.CreateState(state, new NoData()).State;
19161916

1917-
internal StateMachine<TState, TTrigger, TCommand> OnUnhandled(Action<TTrigger, TState> handler)
1917+
internal StateMachine<TState, TTrigger, TCommand> OnUnhandled(
1918+
Func<TTrigger, TState, IEnumerable<TCommand>> handler)
19181919
{
1919-
_inner.OnUnhandled((trigger, state, data) => handler(trigger, state));
1920+
_inner.OnUnhandled((trigger, state) => handler(trigger, state));
19201921
return this;
19211922
}
19221923

FunctionalStateMachine.Core/StateMachineBuilder.cs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,32 @@ public StateMachineBuilder<TState, TTrigger, TData, TCommand> StartWith(TState s
1313
return this;
1414
}
1515

16-
public StateMachineBuilder<TState, TTrigger, TData, TCommand> OnUnhandled(
17-
Action<TTrigger, TState, TData> handler)
16+
public UnhandledConfiguration OnUnhandled()
1817
{
19-
_machine.OnUnhandled(handler);
20-
return this;
18+
return new UnhandledConfiguration(this);
19+
}
20+
21+
public sealed class UnhandledConfiguration
22+
{
23+
private readonly StateMachineBuilder<TState, TTrigger, TData, TCommand> _builder;
24+
25+
internal UnhandledConfiguration(StateMachineBuilder<TState, TTrigger, TData, TCommand> builder)
26+
{
27+
_builder = builder;
28+
}
29+
30+
public StateMachineBuilder<TState, TTrigger, TData, TCommand> Ignore()
31+
{
32+
_builder._machine.OnUnhandled((_, _) => Array.Empty<TCommand>());
33+
return _builder;
34+
}
35+
36+
public StateMachineBuilder<TState, TTrigger, TData, TCommand> Execute(
37+
Func<TTrigger, TState, IEnumerable<TCommand>> handler)
38+
{
39+
_builder._machine.OnUnhandled(handler);
40+
return _builder;
41+
}
2142
}
2243

2344
public StateConfiguration For(TState state)
@@ -1062,11 +1083,39 @@ public StateMachineBuilder<TState, TTrigger, TCommand> StartWith(TState state)
10621083
return this;
10631084
}
10641085

1065-
public StateMachineBuilder<TState, TTrigger, TCommand> OnUnhandled(
1066-
Action<TTrigger, TState> handler)
1086+
public UnhandledConfiguration OnUnhandled()
10671087
{
1068-
_machine.OnUnhandled(handler);
1069-
return this;
1088+
return new UnhandledConfiguration(this);
1089+
}
1090+
1091+
public sealed class UnhandledConfiguration
1092+
{
1093+
private readonly StateMachineBuilder<TState, TTrigger, TCommand> _builder;
1094+
1095+
internal UnhandledConfiguration(StateMachineBuilder<TState, TTrigger, TCommand> builder)
1096+
{
1097+
_builder = builder;
1098+
}
1099+
1100+
public StateMachineBuilder<TState, TTrigger, TCommand> Ignore()
1101+
{
1102+
_builder._machine.OnUnhandled((_, _) => Array.Empty<TCommand>());
1103+
return _builder;
1104+
}
1105+
1106+
public StateMachineBuilder<TState, TTrigger, TCommand> Execute(
1107+
Func<TTrigger, TState, IEnumerable<TCommand>> handler)
1108+
{
1109+
_builder._machine.OnUnhandled(handler);
1110+
return _builder;
1111+
}
1112+
1113+
public StateMachineBuilder<TState, TTrigger, TCommand> Execute(
1114+
Func<TTrigger, TState, TCommand> handler)
1115+
{
1116+
_builder._machine.OnUnhandled((trigger, state) => new[] { handler(trigger, state) });
1117+
return _builder;
1118+
}
10701119
}
10711120

10721121
public StateConfiguration For(TState state)

FunctionalStateMachine.Samples/LightSwitchSample.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ public static class LightSwitchSample
99
[StateMachineDiagram("diagrams/LightSwitch.md")]
1010
public static StateMachine<LightState, LightTrigger, LightCommand> Build() =>
1111
StateMachine<LightState, LightTrigger, LightCommand>.Create()
12+
.OnUnhandled()
13+
.Ignore()
1214
.StartWith(LightState.Off)
1315
.For(LightState.Off)
1416
.On<LightTrigger.ToggleTrigger>()

docs/ignore-unhandled.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ You can explicitly ignore triggers or handle them with a global callback.
1212

1313
```csharp
1414
var machine = StateMachine<State, Trigger, Data, Command>.Create()
15+
.OnUnhandled()
16+
.Ignore()
1517
.For(State.Ready)
1618
.On(Trigger.Ping)
1719
.Ignore()
@@ -22,8 +24,9 @@ var machine = StateMachine<State, Trigger, Data, Command>.Create()
2224

2325
```csharp
2426
var machine = StateMachine<State, Trigger, Data, Command>.Create()
25-
.OnUnhandled((trigger, state, data) =>
26-
data.Log.Add($"Unhandled {trigger} in {state}"))
27+
.OnUnhandled()
28+
.Execute((trigger, state) =>
29+
new Command.LogUnhandled(trigger, state))
2730
.For(State.Active)
2831
.On(Trigger.Stop)
2932
.TransitionTo(State.Stopped)

0 commit comments

Comments
 (0)