Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add terminal states to TKStateMachine. #15

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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 TKStateMachineDidChangeStateNotification;
Copy link
Owner

Choose a reason for hiding this comment

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

Shouldn't this be TKStateMachineDidTerminateNotification ?

Copy link
Author

Choose a reason for hiding this comment

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

Yes indeed.



/**
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 was declines because the state machine has already terminated.
Copy link
Owner

Choose a reason for hiding this comment

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

"The transition failed because the state machine has been terminated." or "The transition failed because the state machine has reached a terminal state."

Copy link
Author

Choose a reason for hiding this comment

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

PR updated.

} 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