Skip to content

Add HierarchicalStateMachine. #10

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

Merged
merged 1 commit into from
Oct 18, 2014
Merged
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
12 changes: 12 additions & 0 deletions SwiftState.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
1FA62038199660CA00460108 /* StateTransitionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA6202F199660CA00460108 /* StateTransitionTests.swift */; };
1FB1EC8A199E515B00ABD937 /* MyEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB1EC89199E515B00ABD937 /* MyEvent.swift */; };
1FB1EC8F199E60F800ABD937 /* String+SwiftState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB1EC8E199E60F800ABD937 /* String+SwiftState.swift */; };
1FD01B6019EC2B5C00DA1C91 /* HierarchicalStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD01B5F19EC2B5C00DA1C91 /* HierarchicalStateMachine.swift */; };
1FD01B6119EC2B5C00DA1C91 /* HierarchicalStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD01B5F19EC2B5C00DA1C91 /* HierarchicalStateMachine.swift */; };
1FD01B6319EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD01B6219EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift */; };
1FD01B6419EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD01B6219EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift */; };
1FF692041996625900E3CE40 /* SwiftState.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FA620001996601000460108 /* SwiftState.framework */; };
4822F0A819D008E300F5F572 /* _TestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA62027199660CA00460108 /* _TestCase.swift */; };
4822F0A919D008E700F5F572 /* MyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA62029199660CA00460108 /* MyState.swift */; };
Expand Down Expand Up @@ -86,6 +90,8 @@
1FA6202F199660CA00460108 /* StateTransitionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateTransitionTests.swift; sourceTree = "<group>"; };
1FB1EC89199E515B00ABD937 /* MyEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyEvent.swift; sourceTree = "<group>"; };
1FB1EC8E199E60F800ABD937 /* String+SwiftState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SwiftState.swift"; sourceTree = "<group>"; };
1FD01B5F19EC2B5C00DA1C91 /* HierarchicalStateMachine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HierarchicalStateMachine.swift; sourceTree = "<group>"; };
1FD01B6219EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HierarchicalStateMachineTests.swift; sourceTree = "<group>"; };
4822F0A619D0085E00F5F572 /* SwiftStateTests-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SwiftStateTests-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
4872D5AC19B4211900F326B5 /* SwiftState.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftState.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -154,6 +160,7 @@
1FA6201E1996606300460108 /* StateTransitionChain.swift */,
1FA6201B1996606300460108 /* StateRoute.swift */,
1FA6201C1996606300460108 /* StateRouteChain.swift */,
1FD01B5F19EC2B5C00DA1C91 /* HierarchicalStateMachine.swift */,
1FA620031996601000460108 /* Supporting Files */,
);
path = SwiftState;
Expand All @@ -180,6 +187,7 @@
1FA6202F199660CA00460108 /* StateTransitionTests.swift */,
1FA6202E199660CA00460108 /* StateTransitionChainTests.swift */,
1FA6202D199660CA00460108 /* StateRouteTests.swift */,
1FD01B6219EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift */,
1FA6200D1996601000460108 /* Supporting Files */,
);
path = SwiftStateTests;
Expand Down Expand Up @@ -387,6 +395,7 @@
1FA620201996606300460108 /* StateEventType.swift in Sources */,
1FA620221996606300460108 /* StateRoute.swift in Sources */,
1FA620211996606300460108 /* StateMachine.swift in Sources */,
1FD01B6019EC2B5C00DA1C91 /* HierarchicalStateMachine.swift in Sources */,
1FA620261996606300460108 /* StateType.swift in Sources */,
1FA620231996606300460108 /* StateRouteChain.swift in Sources */,
);
Expand All @@ -398,6 +407,7 @@
files = (
1FB1EC8A199E515B00ABD937 /* MyEvent.swift in Sources */,
1F198C5C19972320001C3700 /* QiitaTests.swift in Sources */,
1FD01B6319EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift in Sources */,
1FA62038199660CA00460108 /* StateTransitionTests.swift in Sources */,
1FA62033199660CA00460108 /* StateMachineChainTests.swift in Sources */,
1FA62030199660CA00460108 /* _TestCase.swift in Sources */,
Expand All @@ -416,6 +426,7 @@
files = (
4822F0AC19D008EB00F5F572 /* QiitaTests.swift in Sources */,
4822F0AB19D008EB00F5F572 /* BasicTests.swift in Sources */,
1FD01B6419EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift in Sources */,
4822F0AF19D008EB00F5F572 /* StateMachineEventTests.swift in Sources */,
4822F0A819D008E300F5F572 /* _TestCase.swift in Sources */,
4822F0AA19D008E700F5F572 /* MyEvent.swift in Sources */,
Expand All @@ -438,6 +449,7 @@
48797D5F19B42CCE0085D80F /* StateTransition.swift in Sources */,
48797D6319B42CD40085D80F /* StateType.swift in Sources */,
48797D5E19B42CCE0085D80F /* StateMachine.swift in Sources */,
1FD01B6119EC2B5C00DA1C91 /* HierarchicalStateMachine.swift in Sources */,
48797D6119B42CCE0085D80F /* StateRoute.swift in Sources */,
48797D6419B42CD40085D80F /* StateEventType.swift in Sources */,
);
Expand Down
168 changes: 168 additions & 0 deletions SwiftState/HierarchicalStateMachine.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//
// HierarchicalStateMachine.swift
// SwiftState
//
// Created by Yasuhiro Inami on 2014/10/13.
// Copyright (c) 2014年 Yasuhiro Inami. All rights reserved.
//

import Foundation

public typealias HSM = HierarchicalStateMachine<String, String>

//
// NOTE:
// When subclassing StateMachine<StateType, StateEventType>,
// don't directly set String as a replacement of StateType (generic type)
// due to Xcode6.1-GM2 generics bug(?) causing EXC_I386_GPFLT when overriding method e.g. `hasRoute()`.
//
// Ideally, class `HierarchicalStateMachine` should be declared as following:
//
// `public class HierarchicalStateMachine: StateMachine<String, String>`
//
// To avoid above issue, we use `typealias HSM` instead.
//

/// nestable StateMachine with StateType as String
public class HierarchicalStateMachine<S: StateType, E: StateEventType>: StateMachine<S, E>, Printable
{
private var _submachines = [String : HSM]()

public let name: String

/// init with submachines
public init(name: String, submachines: [HSM]? = nil, state: State, initClosure: (StateMachine<State, Event> -> Void)? = nil)
{
self.name = name

if let submachines = submachines {
for submachine in submachines {
self._submachines[submachine.name] = submachine
}
}

super.init(state: state, initClosure: initClosure)
}

public var description: String
{
return self.name
}

///
/// Converts dot-chained state sent from mainMachine into (submachine, substate) tuple.
/// e.g.
///
/// - state="MainState1" will return (nil, "MainState1")
/// - state="SubMachine1.State1" will return (submachine1, "State1")
/// - state="" (nil) will return (nil, nil)
///
private func _submachineTupleForState(state: State) -> (HSM?, HSM.State)
{
assert(state is HSM.State, "HSM state must be String.")

let components = split(state as HSM.State, { $0 == "." }, maxSplit: 1)

switch components.count {
case 2:
let submachineName = components[0]
let substate = components[1]
return (self._submachines[submachineName], substate)

case 1:
let state = components[0]
return (nil, state)

default:
// NOTE: reaches here when state="" (empty) as AnyState
return (nil, nil)
}
}

public override var state: State
{
// NOTE: returning `substate` is not reliable (as a spec), so replace it with actual `submachine.state` instead
let (submachine, substate) = self._submachineTupleForState(self._state)

if let submachine = submachine {
self._state = "\(submachine.name).\(submachine.state)" as State
}

return self._state
}

public override func hasRoute(transition: Transition, forEvent event: Event = nil) -> Bool
{
let (fromSubmachine, fromSubstate) = self._submachineTupleForState(transition.fromState)
let (toSubmachine, toSubstate) = self._submachineTupleForState(transition.toState)

// check submachine-internal routes
if fromSubmachine != nil && toSubmachine != nil && fromSubmachine === toSubmachine {
return fromSubmachine!.hasRoute(fromSubstate => toSubstate, forEvent: nil)
}

return super.hasRoute(transition, forEvent: event)
}

internal override func _addRoute(var route: Route, forEvent event: Event = nil) -> RouteID
{
let originalCondition = route.condition

let condition: Condition = { transition -> Bool in

let (fromSubmachine, fromSubstate) = self._submachineTupleForState(route.transition.fromState)
let (toSubmachine, toSubstate) = self._submachineTupleForState(route.transition.toState)

//
// For external-route, don't let mainMachine switch to submachine.state=toSubstate
// when current submachine.state is not toSubstate.
//
// e.g. ignore `"MainState0" => "Sub1.State1"` transition when
// `mainMachine.state="MainState0"` but `submachine.state="State2"` (not "State1")
//
if toSubmachine != nil && toSubmachine!.state != toSubstate && fromSubmachine !== toSubmachine {
return false
}

return originalCondition?(transition: transition) ?? true
}

route = Route(transition: route.transition, condition: condition)

return super._addRoute(route, forEvent: event)
}

// TODO: apply mainMachine's events to submachines
internal override func _tryState(state: State, userInfo: Any? = nil, forEvent event: Event) -> Bool
{
assert(state is HSM.State, "HSM state must be String.")

let fromState = self.state
let toState = state
let transition = fromState => toState

let (fromSubmachine, fromSubstate) = self._submachineTupleForState(fromState)
let (toSubmachine, toSubstate) = self._submachineTupleForState(toState)

// try changing submachine-internal state
if fromSubmachine != nil && toSubmachine != nil && fromSubmachine === toSubmachine {

if toSubmachine!.canTryState(toSubstate, forEvent: event as HSM.Event) {

//
// NOTE:
// Change mainMachine's state first to invoke its handlers
// before changing toSubmachine's state because mainMachine.state relies on it.
//
super._tryState(toState, userInfo: userInfo, forEvent: event)

toSubmachine! <- (toSubstate as HSM.State)

return true
}

}

return super._tryState(toState, userInfo: userInfo, forEvent: event)
}
}
Loading