Skip to content
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
23 changes: 23 additions & 0 deletions Code/TKStateMachine.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@
*/
@property (nonatomic, strong, readonly) TKState *currentState;

/**
The set of states which are terminal, and will cause the state machine to terminate

Once an event transitions the machine into one of the states in this set, the machine will return YES from -terminated and will refuse to accept any further events.
*/
@property (nonatomic, copy) NSSet *terminalStates;

/**
Adds a state to the receiver.

Expand Down Expand Up @@ -176,6 +183,15 @@
*/
- (BOOL)isActive;

///------------------------------------
/// @name Terminal State of the Machine
///------------------------------------

/**
Returns a Boolean value that indicates if the receiver has transitioned into one of the states in `terminalStates`.
*/
@property (nonatomic, readonly) BOOL terminated;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably be isTerminated

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly. I deliberated
this a bit myself, but I felt like if it wasn't just "terminated" that it
should really be "hasTerminated" but that doesn't really go with the naming
convention. I guess terminated isn't a great word... maybe finished would
be better... shrug


///--------------------
/// @name Firing Events
///--------------------
Expand Down Expand Up @@ -236,10 +252,17 @@ extern NSString *const TKStateMachineDidChangeStateEventUserInfoKey;
*/
extern NSString *const TKStateMachineIsImmutableException;

/**
A Notification posted when the `terminated` state of a `TKStateMachine` changes to YES
*/
extern NSString *const TKStateMachineDidTerminateNotification;


/**
Error Codes
*/
typedef enum {
TKInvalidTransitionError = 1000, // An invalid transition was attempted.
TKTransitionDeclinedError = 1001, // The transition was declined by the `shouldFireEvent` guard block.
TKStateMachineTerminatedError = 1002, // The transition failed because the state machine has reached a terminal state.
} TKErrorCode;
69 changes: 65 additions & 4 deletions Code/TKStateMachine.m
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ @interface TKState ()
NSString *const TKStateMachineDidChangeStateOldStateUserInfoKey = @"old";
NSString *const TKStateMachineDidChangeStateNewStateUserInfoKey = @"new";
NSString *const TKStateMachineDidChangeStateEventUserInfoKey = @"event";
NSString *const TKStateMachineDidTerminateNotification = @"TKStateMachineDidTerminateNotification";

NSString *const TKStateMachineIsImmutableException = @"TKStateMachineIsImmutableException";

Expand All @@ -56,6 +57,7 @@ @interface TKStateMachine ()
@property (nonatomic, strong) NSMutableSet *mutableStates;
@property (nonatomic, strong) NSMutableSet *mutableEvents;
@property (nonatomic, assign, getter = isActive) BOOL active;
@property (nonatomic) BOOL terminated;
@property (nonatomic, strong, readwrite) TKState *currentState;
@end

Expand All @@ -73,6 +75,10 @@ + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
NSSet *affectingKey = [NSSet setWithObject:@"mutableEvents"];
keyPaths = [keyPaths setByAddingObjectsFromSet:affectingKey];
return keyPaths;
} else if ([key isEqualToString:@"terminated"]) {
NSSet *affectingKey = [NSSet setWithObject:@"currentState"];
keyPaths = [keyPaths setByAddingObjectsFromSet:affectingKey];
return keyPaths;
}

return keyPaths;
Expand All @@ -82,8 +88,9 @@ - (id)init
{
self = [super init];
if (self) {
self.mutableStates = [NSMutableSet set];
self.mutableEvents = [NSMutableSet set];
_mutableStates = [NSMutableSet set];
_mutableEvents = [NSMutableSet set];
_terminalStates = [NSSet set];
}
return self;
}
Expand All @@ -98,9 +105,35 @@ - (NSString *)description
- (void)setInitialState:(TKState *)initialState
{
TKRaiseIfActive();
if (! [initialState isKindOfClass:[TKState class]]) [NSException raise:NSInvalidArgumentException format:@"Expected a `TKState` object, instead got a `%@` (%@)", [initialState class], initialState];
_initialState = initialState;
}

- (void)setTerminalStates:(NSSet*)terminalStates
{
TKRaiseIfActive();

if (terminalStates && ! [terminalStates isKindOfClass: [NSSet class]]) [NSException raise:NSInvalidArgumentException format:@"Expected an `NSSet` object specifying the terminal states, instead got a `%@` (%@)", [terminalStates class], terminalStates];

for (TKState* state in terminalStates)
{
if (! [state isKindOfClass:[TKState class]]) [NSException raise:NSInvalidArgumentException format:@"Expected an `NSSet` of `TKState` objects, but the set contains a `%@` (%@)", [state class], state];
}

[self.mutableStates unionSet: terminalStates];

_terminalStates = terminalStates ? [NSSet setWithSet: terminalStates] : [NSSet set];
}

- (void)setCurrentState: (TKState*)newCurrentState
{
_currentState = newCurrentState;
if ([self.terminalStates containsObject: newCurrentState])
{
self.terminated = YES;
}
}

- (NSSet *)states
{
return [NSSet setWithSet:self.mutableStates];
Expand All @@ -109,7 +142,7 @@ - (NSSet *)states
- (void)addState:(TKState *)state
{
TKRaiseIfActive();
if (! [state isKindOfClass:[TKState class]]) [NSException raise:NSInvalidArgumentException format:@"Expected a `TKState` object, instead got a `%@` (%@)", [state class], state];
if (! [state isKindOfClass:[TKState class]]) [NSException raise:NSInvalidArgumentException format:@"Expected a `TKState` object or `NSString` object specifying the name of a state, instead got a `%@` (%@)", [state class], state];
if (self.initialState == nil) self.initialState = state;
[self.mutableStates addObject:state];
}
Expand Down Expand Up @@ -190,7 +223,7 @@ - (BOOL)canFireEvent:(id)eventOrEventName
if (! [eventOrEventName isKindOfClass:[TKEvent class]] && ![eventOrEventName isKindOfClass:[NSString class]]) [NSException raise:NSInvalidArgumentException format:@"Expected a `TKEvent` object or `NSString` object specifying the name of an event, instead got a `%@` (%@)", [eventOrEventName class], eventOrEventName];
TKEvent *event = [eventOrEventName isKindOfClass:[TKEvent class]] ? eventOrEventName : [self eventNamed:eventOrEventName];
if (! event) [NSException raise:NSInvalidArgumentException format:@"Cannot find an Event named '%@'", eventOrEventName];
return [event.sourceStates containsObject:self.currentState];
return !self.terminated && (event.sourceStates == nil || [event.sourceStates containsObject:self.currentState]);
}

- (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:(NSError *__autoreleasing *)error
Expand All @@ -200,6 +233,17 @@ - (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:(
TKEvent *event = [eventOrEventName isKindOfClass:[TKEvent class]] ? eventOrEventName : [self eventNamed:eventOrEventName];
if (! event) [NSException raise:NSInvalidArgumentException format:@"Cannot find an Event named '%@'", eventOrEventName];

if (self.terminated)
{
if (error)
{
NSString *failureReason = [NSString stringWithFormat:@"An attempt was made to fire the '%@' event after the state machine has terminated.", event.name];
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"The event cannot be fired because the state machine has reached a terminal state", NSLocalizedFailureReasonErrorKey: failureReason };
*error = [NSError errorWithDomain:TKErrorDomain code:TKStateMachineTerminatedError userInfo:userInfo];
}
return NO;
}

// Check that this transition is permitted
if (event.sourceStates != nil && ![event.sourceStates containsObject:self.currentState]) {
NSString *failureReason = [NSString stringWithFormat:@"An attempt was made to fire the '%@' event while in the '%@' state, but the event can only be fired from the following states: %@", event.name, self.currentState.name, [[event.sourceStates valueForKey:@"name"] componentsJoinedByString:@", "]];
Expand Down Expand Up @@ -237,6 +281,11 @@ - (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:(
TKStateMachineDidChangeStateEventUserInfoKey: event }];
[[NSNotificationCenter defaultCenter] postNotificationName:TKStateMachineDidChangeStateNotification object:self userInfo:notificationInfo];

if(self.terminated)
{
[[NSNotificationCenter defaultCenter] postNotificationName:TKStateMachineDidTerminateNotification object:self userInfo:userInfo];
}

return YES;
}

Expand All @@ -254,6 +303,8 @@ - (id)initWithCoder:(NSCoder *)aDecoder
self.mutableStates = [[aDecoder decodeObjectForKey:@"states"] mutableCopy];
self.mutableEvents = [[aDecoder decodeObjectForKey:@"events"] mutableCopy];
self.active = [aDecoder decodeBoolForKey:@"isActive"];
self.terminalStates = [aDecoder decodeObjectForKey: @"terminalStates"];
self.terminated = [aDecoder decodeBoolForKey: @"terminated"];
return self;
}

Expand All @@ -264,6 +315,8 @@ - (void)encodeWithCoder:(NSCoder *)aCoder
[aCoder encodeObject:self.states forKey:@"states"];
[aCoder encodeObject:self.events forKey:@"events"];
[aCoder encodeBool:self.isActive forKey:@"isActive"];
[aCoder encodeObject:self.terminalStates forKey: @"terminalStates"];
[aCoder encodeBool:self.terminated forKey:@"terminated"];
}

#pragma mark - NSCopying
Expand All @@ -272,13 +325,20 @@ - (id)copyWithZone:(NSZone *)zone
{
TKStateMachine *copiedStateMachine = [[[self class] allocWithZone:zone] init];
copiedStateMachine.active = NO;
copiedStateMachine.terminated = NO;
copiedStateMachine.currentState = nil;
copiedStateMachine.initialState = self.initialState;

for (TKState *state in self.states) {
[copiedStateMachine addState:[state copy]];
}

NSMutableSet* terminalStates = [NSMutableSet setWithCapacity: self.terminalStates.count];
for (TKState *state in self.terminalStates) {
[terminalStates addObject: [copiedStateMachine stateNamed: state.name]];
}
copiedStateMachine.terminalStates = terminalStates;

for (TKEvent *event in self.events) {
NSMutableArray *sourceStates = [NSMutableArray arrayWithCapacity:[event.sourceStates count]];
for (TKState *sourceState in event.sourceStates) {
Expand All @@ -288,6 +348,7 @@ - (id)copyWithZone:(NSZone *)zone
TKEvent *copiedEvent = [TKEvent eventWithName:event.name transitioningFromStates:sourceStates toState:destinationState];
[copiedStateMachine addEvent:copiedEvent];
}

return copiedStateMachine;
}

Expand Down
Loading