Skip to content

Commit

Permalink
Added wildcard transitions
Browse files Browse the repository at this point in the history
  • Loading branch information
softwarenerd committed Nov 25, 2016
1 parent aeeffb8 commit 6219f82
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 12 deletions.
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,63 @@ do {
}
```

### Wildcard transitions

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:

```swift
var stateClosed: State!
var stateOpened: State!
var stateBroken: State!
var eventOpen: Event!
var eventClose: Event!
var eventBroken: Event!
var stateMachine: StateMachine!

do {
// Define the states that the state machine can be in.
stateClosed = try State(name: "Closed") { (object: AnyObject?) -> StateChange? in
// Log.
print("Closed")

// Return, leaving state unchanged.
return nil
}
stateOpened = try State(name: "Opened") { (object: AnyObject?) -> StateChange? in
// Log.
print("Opened")

// Return, leaving state unchanged.
return nil
}
stateBroken = try State(name: "Broken") { (object: AnyObject?) -> StateChange? in
// Log.
print("Broken")

// Return, leaving state unchanged.
return nil
}

// Define the events that can be sent to the state machine.
eventOpen = try Event(name: "Open", transitions: [(fromState: stateClosed, toState: stateOpened)])
eventClose = try Event(name: "Close", transitions: [(fromState: stateOpened, toState: stateClosed)])
eventBroken = try Event(name: "Broken", transitions: [(fromState: nil, toState: stateBroken)])

// Initialize the state machine.
stateMachine = try StateMachine(name: "Door",
defaultState: stateClosed,
states: [stateClosed, stateOpened],
events: [eventClose, eventOpen])

// Fire events to the state machine.
try stateMachine.fireEvent(event: eventOpen)
try stateMachine.fireEvent(event: eventClose)
try stateMachine.fireEvent(event: eventBroken)
} catch {
// Handle errors.
}
```

## Example Project

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.
Expand Down
37 changes: 28 additions & 9 deletions Stately/Code/Event.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
import Foundation

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

// Event error enumeration.
public enum EventError: Error {
case NameEmpty
case NoTransitions
case DuplicateTransition(fromState: State)
case MultipleWildcardTransitions
}

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

// The wildcard transition.
let wildcardTransition: Transition?

/// Initializes a new instance of the Event class.
///
/// - Parameters:
/// - name: The name of the event. Each event must have a unique name.
/// - transitions: The the event transitions. Each event must define at least one transition.
/// - transitions: The transitions for the event. An event must define at least one transition.
/// A transition with a from state of nil is the whildcard transition and will match any from
/// state. Only one wildcard transition may defined for an event.
public init(name nameIn: String, transitions transitionsIn: [Transition]) throws {
// Validate the event name.
if nameIn.isEmpty {
Expand All @@ -42,31 +48,44 @@ public class Event : Hashable {
throw EventError.NoTransitions
}

// Ensure that there are no duplicate from state transitions defined. While this wouldn't strictly
// Ensure that there are no duplicate from state transitions defined. (While this wouldn't strictly
// be a bad thing, the presence of duplicate from state transitions more than likely indicates that
// there is a bug in the definition of the state machine, so we don't allow it.
// there is a bug in the definition of the state machine, so we don't allow it.) Also, there can be
// only one wildcard transition (a transition with a nil from state) defined.
var wildcardTransitionTemp: Transition? = nil
var fromStatesTemp = Set<State>(minimumCapacity: transitionsIn.count)
for transition in transitionsIn {
if fromStatesTemp.contains(transition.fromState) {
throw EventError.DuplicateTransition(fromState: transition.fromState)
// See if there's a from state. If there is, ensure it's not a duplicate. If there isn't, then
// ensure there is only one wildcard transition.
if let fromState = transition.fromState {
if fromStatesTemp.contains(fromState) {
throw EventError.DuplicateTransition(fromState: fromState)
} else {
fromStatesTemp.insert(fromState)
}
} else {
fromStatesTemp.insert(transition.fromState)
if wildcardTransitionTemp != nil {
throw EventError.MultipleWildcardTransitions
} else {
wildcardTransitionTemp = transition
}
}
}

// Initialize.
name = nameIn
transitions = transitionsIn
wildcardTransition = wildcardTransitionTemp
}

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

// Return the to state.
Expand Down
9 changes: 6 additions & 3 deletions Stately/Code/StateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,12 @@ public class StateMachine {
} else {
// Validate the transitions for the event.
for transition in event.transitions {
// Ensure that the from state is defined.
if !statesTemp.contains(transition.fromState) {
throw StateMachineError.TransitionFromStateNotDefined(fromState: transition.fromState)

// Ensure that the from state is defined, or is nil, indicating "wildcard".
if let fromState = transition.fromState {
if !statesTemp.contains(fromState) {
throw StateMachineError.TransitionFromStateNotDefined(fromState: fromState)
}
}

// Ensure that the to state is defined.
Expand Down
60 changes: 60 additions & 0 deletions StatelyTests/StatelyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,66 @@ class StatelyTests: XCTestCase
}
}

// Tests multiple wildcard transitions.
func testMultipleWildcardTransition() {
do {
// Setup.
let stateA = try State(name: "A") { (object: AnyObject?) -> StateChange? in
return nil
}
let stateB = try State(name: "B") { (object: AnyObject?) -> StateChange? in
return nil
}
let stateC = try State(name: "C") { (object: AnyObject?) -> StateChange? in
return nil
}
let event1 = try Event(name: "1", transitions: [(fromState: nil, toState: stateA),
(fromState: nil, toState: stateC)])

// Test.
let stateMachine = try StateMachine(name: "StateMachine", defaultState: stateA, states: [stateA, stateB, stateC], events: [event1])
try stateMachine.fireEvent(event: event1)

// Assert.
XCTFail("An error should have been thrown")
} catch EventError.MultipleWildcardTransitions {
// Expect to arrive here. This is success.
} catch {
XCTFail("Unexpeced error thrown: \(error)")
}
}

// Tests a wildcard transition.
func testWildcardTransition() {
do {
// Setup.
let stateA = try State(name: "A") { (object: AnyObject?) -> StateChange? in
return nil
}
let stateB = try State(name: "B") { (object: AnyObject?) -> StateChange? in
return nil
}
var stateCEntered = false
let stateC = try State(name: "C") { (object: AnyObject?) -> StateChange? in
stateCEntered = true
return nil
}
let event1 = try Event(name: "1", transitions: [(fromState: stateB, toState: stateA),
(fromState: nil, toState: stateC)])

// Test.
let stateMachine = try StateMachine(name: "StateMachine", defaultState: stateA, states: [stateA, stateB, stateC], events: [event1])
try stateMachine.fireEvent(event: event1)

// Assert.
XCTAssertTrue(stateCEntered)
}
catch {
// Assert.
XCTFail("Unexpeced error thrown: \(error)")
}
}

// Tests multiple threads.
func testMultipleThreads() {
do {
Expand Down

0 comments on commit 6219f82

Please sign in to comment.