Skip to content

Commit 6219f82

Browse files
committed
Added wildcard transitions
1 parent aeeffb8 commit 6219f82

File tree

4 files changed

+151
-12
lines changed

4 files changed

+151
-12
lines changed

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,63 @@ do {
153153
}
154154
```
155155

156+
### Wildcard transitions
157+
158+
An event definition in Stately may define a single wildcard transition. A wildcard transition will transition from any from state to the specified to state. The following example adds a broken state to the above example:
159+
160+
```swift
161+
var stateClosed: State!
162+
var stateOpened: State!
163+
var stateBroken: State!
164+
var eventOpen: Event!
165+
var eventClose: Event!
166+
var eventBroken: Event!
167+
var stateMachine: StateMachine!
168+
169+
do {
170+
// Define the states that the state machine can be in.
171+
stateClosed = try State(name: "Closed") { (object: AnyObject?) -> StateChange? in
172+
// Log.
173+
print("Closed")
174+
175+
// Return, leaving state unchanged.
176+
return nil
177+
}
178+
stateOpened = try State(name: "Opened") { (object: AnyObject?) -> StateChange? in
179+
// Log.
180+
print("Opened")
181+
182+
// Return, leaving state unchanged.
183+
return nil
184+
}
185+
stateBroken = try State(name: "Broken") { (object: AnyObject?) -> StateChange? in
186+
// Log.
187+
print("Broken")
188+
189+
// Return, leaving state unchanged.
190+
return nil
191+
}
192+
193+
// Define the events that can be sent to the state machine.
194+
eventOpen = try Event(name: "Open", transitions: [(fromState: stateClosed, toState: stateOpened)])
195+
eventClose = try Event(name: "Close", transitions: [(fromState: stateOpened, toState: stateClosed)])
196+
eventBroken = try Event(name: "Broken", transitions: [(fromState: nil, toState: stateBroken)])
197+
198+
// Initialize the state machine.
199+
stateMachine = try StateMachine(name: "Door",
200+
defaultState: stateClosed,
201+
states: [stateClosed, stateOpened],
202+
events: [eventClose, eventOpen])
203+
204+
// Fire events to the state machine.
205+
try stateMachine.fireEvent(event: eventOpen)
206+
try stateMachine.fireEvent(event: eventClose)
207+
try stateMachine.fireEvent(event: eventBroken)
208+
} catch {
209+
// Handle errors.
210+
}
211+
```
212+
156213
## Example Project
157214

158215
The [StatelyExample](https://github.com/softwarenerd/StatelyExample) project provides a fairly complete example of using Stately to build a garage door simulator. Other examples are planned.

Stately/Code/Event.swift

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
import Foundation
1010

1111
// Types alias for a transition tuple.
12-
public typealias Transition = (fromState: State, toState: State)
12+
public typealias Transition = (fromState: State?, toState: State)
1313

1414
// Event error enumeration.
1515
public enum EventError: Error {
1616
case NameEmpty
1717
case NoTransitions
1818
case DuplicateTransition(fromState: State)
19+
case MultipleWildcardTransitions
1920
}
2021

2122
// Event class.
@@ -26,11 +27,16 @@ public class Event : Hashable {
2627
// The set of transitions for the event.
2728
let transitions: [Transition]
2829

30+
// The wildcard transition.
31+
let wildcardTransition: Transition?
32+
2933
/// Initializes a new instance of the Event class.
3034
///
3135
/// - Parameters:
3236
/// - name: The name of the event. Each event must have a unique name.
33-
/// - transitions: The the event transitions. Each event must define at least one transition.
37+
/// - transitions: The transitions for the event. An event must define at least one transition.
38+
/// A transition with a from state of nil is the whildcard transition and will match any from
39+
/// state. Only one wildcard transition may defined for an event.
3440
public init(name nameIn: String, transitions transitionsIn: [Transition]) throws {
3541
// Validate the event name.
3642
if nameIn.isEmpty {
@@ -42,31 +48,44 @@ public class Event : Hashable {
4248
throw EventError.NoTransitions
4349
}
4450

45-
// Ensure that there are no duplicate from state transitions defined. While this wouldn't strictly
51+
// Ensure that there are no duplicate from state transitions defined. (While this wouldn't strictly
4652
// be a bad thing, the presence of duplicate from state transitions more than likely indicates that
47-
// there is a bug in the definition of the state machine, so we don't allow it.
53+
// there is a bug in the definition of the state machine, so we don't allow it.) Also, there can be
54+
// only one wildcard transition (a transition with a nil from state) defined.
55+
var wildcardTransitionTemp: Transition? = nil
4856
var fromStatesTemp = Set<State>(minimumCapacity: transitionsIn.count)
4957
for transition in transitionsIn {
50-
if fromStatesTemp.contains(transition.fromState) {
51-
throw EventError.DuplicateTransition(fromState: transition.fromState)
58+
// See if there's a from state. If there is, ensure it's not a duplicate. If there isn't, then
59+
// ensure there is only one wildcard transition.
60+
if let fromState = transition.fromState {
61+
if fromStatesTemp.contains(fromState) {
62+
throw EventError.DuplicateTransition(fromState: fromState)
63+
} else {
64+
fromStatesTemp.insert(fromState)
65+
}
5266
} else {
53-
fromStatesTemp.insert(transition.fromState)
67+
if wildcardTransitionTemp != nil {
68+
throw EventError.MultipleWildcardTransitions
69+
} else {
70+
wildcardTransitionTemp = transition
71+
}
5472
}
5573
}
5674

5775
// Initialize.
5876
name = nameIn
5977
transitions = transitionsIn
78+
wildcardTransition = wildcardTransitionTemp
6079
}
6180

6281
/// Returns the transition with the specified from state, if one is found; otherwise, nil.
6382
///
6483
/// - Parameters:
6584
/// - fromState: The from state.
6685
func transition(fromState: State) -> State? {
67-
// Find the transition. If it cannot be found, return nil.
86+
// Find the transition. If it cannot be found, and there's a wildcard transition, return its to state.
6887
guard let transition = (transitions.first(where: { (transition: Transition) -> Bool in return transition.fromState === fromState })) else {
69-
return nil;
88+
return wildcardTransition?.toState
7089
}
7190

7291
// Return the to state.

Stately/Code/StateMachine.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,12 @@ public class StateMachine {
8787
} else {
8888
// Validate the transitions for the event.
8989
for transition in event.transitions {
90-
// Ensure that the from state is defined.
91-
if !statesTemp.contains(transition.fromState) {
92-
throw StateMachineError.TransitionFromStateNotDefined(fromState: transition.fromState)
90+
91+
// Ensure that the from state is defined, or is nil, indicating "wildcard".
92+
if let fromState = transition.fromState {
93+
if !statesTemp.contains(fromState) {
94+
throw StateMachineError.TransitionFromStateNotDefined(fromState: fromState)
95+
}
9396
}
9497

9598
// Ensure that the to state is defined.

StatelyTests/StatelyTests.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,66 @@ class StatelyTests: XCTestCase
214214
}
215215
}
216216

217+
// Tests multiple wildcard transitions.
218+
func testMultipleWildcardTransition() {
219+
do {
220+
// Setup.
221+
let stateA = try State(name: "A") { (object: AnyObject?) -> StateChange? in
222+
return nil
223+
}
224+
let stateB = try State(name: "B") { (object: AnyObject?) -> StateChange? in
225+
return nil
226+
}
227+
let stateC = try State(name: "C") { (object: AnyObject?) -> StateChange? in
228+
return nil
229+
}
230+
let event1 = try Event(name: "1", transitions: [(fromState: nil, toState: stateA),
231+
(fromState: nil, toState: stateC)])
232+
233+
// Test.
234+
let stateMachine = try StateMachine(name: "StateMachine", defaultState: stateA, states: [stateA, stateB, stateC], events: [event1])
235+
try stateMachine.fireEvent(event: event1)
236+
237+
// Assert.
238+
XCTFail("An error should have been thrown")
239+
} catch EventError.MultipleWildcardTransitions {
240+
// Expect to arrive here. This is success.
241+
} catch {
242+
XCTFail("Unexpeced error thrown: \(error)")
243+
}
244+
}
245+
246+
// Tests a wildcard transition.
247+
func testWildcardTransition() {
248+
do {
249+
// Setup.
250+
let stateA = try State(name: "A") { (object: AnyObject?) -> StateChange? in
251+
return nil
252+
}
253+
let stateB = try State(name: "B") { (object: AnyObject?) -> StateChange? in
254+
return nil
255+
}
256+
var stateCEntered = false
257+
let stateC = try State(name: "C") { (object: AnyObject?) -> StateChange? in
258+
stateCEntered = true
259+
return nil
260+
}
261+
let event1 = try Event(name: "1", transitions: [(fromState: stateB, toState: stateA),
262+
(fromState: nil, toState: stateC)])
263+
264+
// Test.
265+
let stateMachine = try StateMachine(name: "StateMachine", defaultState: stateA, states: [stateA, stateB, stateC], events: [event1])
266+
try stateMachine.fireEvent(event: event1)
267+
268+
// Assert.
269+
XCTAssertTrue(stateCEntered)
270+
}
271+
catch {
272+
// Assert.
273+
XCTFail("Unexpeced error thrown: \(error)")
274+
}
275+
}
276+
217277
// Tests multiple threads.
218278
func testMultipleThreads() {
219279
do {

0 commit comments

Comments
 (0)