diff --git a/SwiftState.xcodeproj/project.pbxproj b/SwiftState.xcodeproj/project.pbxproj index df172a2..1101cdc 100644 --- a/SwiftState.xcodeproj/project.pbxproj +++ b/SwiftState.xcodeproj/project.pbxproj @@ -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 */; }; @@ -86,6 +90,8 @@ 1FA6202F199660CA00460108 /* StateTransitionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateTransitionTests.swift; sourceTree = ""; }; 1FB1EC89199E515B00ABD937 /* MyEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyEvent.swift; sourceTree = ""; }; 1FB1EC8E199E60F800ABD937 /* String+SwiftState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SwiftState.swift"; sourceTree = ""; }; + 1FD01B5F19EC2B5C00DA1C91 /* HierarchicalStateMachine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HierarchicalStateMachine.swift; sourceTree = ""; }; + 1FD01B6219EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HierarchicalStateMachineTests.swift; sourceTree = ""; }; 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 */ @@ -154,6 +160,7 @@ 1FA6201E1996606300460108 /* StateTransitionChain.swift */, 1FA6201B1996606300460108 /* StateRoute.swift */, 1FA6201C1996606300460108 /* StateRouteChain.swift */, + 1FD01B5F19EC2B5C00DA1C91 /* HierarchicalStateMachine.swift */, 1FA620031996601000460108 /* Supporting Files */, ); path = SwiftState; @@ -180,6 +187,7 @@ 1FA6202F199660CA00460108 /* StateTransitionTests.swift */, 1FA6202E199660CA00460108 /* StateTransitionChainTests.swift */, 1FA6202D199660CA00460108 /* StateRouteTests.swift */, + 1FD01B6219EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift */, 1FA6200D1996601000460108 /* Supporting Files */, ); path = SwiftStateTests; @@ -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 */, ); @@ -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 */, @@ -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 */, @@ -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 */, ); diff --git a/SwiftState/HierarchicalStateMachine.swift b/SwiftState/HierarchicalStateMachine.swift new file mode 100644 index 0000000..12bab6f --- /dev/null +++ b/SwiftState/HierarchicalStateMachine.swift @@ -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 + +// +// NOTE: +// When subclassing StateMachine, +// 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` +// +// To avoid above issue, we use `typealias HSM` instead. +// + +/// nestable StateMachine with StateType as String +public class HierarchicalStateMachine: StateMachine, Printable +{ + private var _submachines = [String : HSM]() + + public let name: String + + /// init with submachines + public init(name: String, submachines: [HSM]? = nil, state: State, initClosure: (StateMachine -> 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) + } +} \ No newline at end of file diff --git a/SwiftStateTests/HierarchicalStateMachineTests.swift b/SwiftStateTests/HierarchicalStateMachineTests.swift new file mode 100644 index 0000000..5f32252 --- /dev/null +++ b/SwiftStateTests/HierarchicalStateMachineTests.swift @@ -0,0 +1,180 @@ +// +// HierarchicalStateMachineTests.swift +// SwiftState +// +// Created by Yasuhiro Inami on 2014/10/13. +// Copyright (c) 2014年 Yasuhiro Inami. All rights reserved. +// + +import SwiftState +import XCTest + +class HierarchicalStateMachineTests: _TestCase +{ + var mainMachine: HSM? + var sub1Machine: HSM? + var sub2Machine: HSM? + + // + // set up hierarchical state machines as following: + // + // - mainMachine + // - sub1Machine + // - State1 (substate) + // - State2 (substate) + // - sub2Machine + // - State1 (substate) + // - State2 (substate) + // - MainState0 (state) + // + override func setUp() + { + super.setUp() + + let sub1Machine = HSM(name: "Sub1", state: "State1") + let sub2Machine = HSM(name: "Sub2", state: "State1") + + // [sub1] add 1-1 => 1-2 + sub1Machine.addRoute("State1" => "State2") + + // [sub2] add 2-1 => 2-2 + sub2Machine.addRoute("State1" => "State2") + + // create mainMachine with configuring submachines + // NOTE: accessing submachine's state will be of form: "\(submachine.name).\(substate)" + let mainMachine = HSM(name: "Main", submachines:[sub1Machine, sub2Machine], state: "Sub1.State1") + + // [main] add '1-2 => 2-1' & '2-2 => 0' & '0 => 1-1' (switching submachine) + // NOTE: MainState0 does not belong to any submachine's state + mainMachine.addRoute("Sub1.State2" => "Sub2.State1") + mainMachine.addRoute("Sub2.State2" => "MainState0") + mainMachine.addRoute("MainState0" => "Sub1.State1") + + // add logging handlers + sub1Machine.addHandler(nil => nil) { println("[Sub1] \($0.transition)") } + sub1Machine.addErrorHandler { println("[ERROR][Sub1] \($0.transition)") } + sub2Machine.addHandler(nil => nil) { println("[Sub2] \($0.transition)") } + sub2Machine.addErrorHandler { println("[ERROR][Sub2] \($0.transition)") } + mainMachine.addHandler(nil => nil) { println("[Main] \($0.transition)") } + mainMachine.addErrorHandler { println("[ERROR][Main] \($0.transition)") } + + self.mainMachine = mainMachine + self.sub1Machine = sub1Machine + self.sub2Machine = sub2Machine + } + + func testHasRoute_submachine_internal() + { + let mainMachine = self.mainMachine! + + // NOTE: mainMachine can check submachine's internal routes + + // sub1 internal routes + XCTAssertFalse(mainMachine.hasRoute("Sub1.State1" => "Sub1.State1")) + XCTAssertTrue(mainMachine.hasRoute("Sub1.State1" => "Sub1.State2")) // 1-1 => 1-2 + XCTAssertFalse(mainMachine.hasRoute("Sub1.State2" => "Sub1.State1")) + XCTAssertFalse(mainMachine.hasRoute("Sub1.State2" => "Sub1.State2")) + + // sub2 internal routes + XCTAssertFalse(mainMachine.hasRoute("Sub2.State1" => "Sub2.State1")) + XCTAssertTrue(mainMachine.hasRoute("Sub2.State1" => "Sub2.State2")) // 2-1 => 2-2 + XCTAssertFalse(mainMachine.hasRoute("Sub2.State2" => "Sub2.State1")) + XCTAssertFalse(mainMachine.hasRoute("Sub2.State2" => "Sub2.State2")) + } + + func testHasRoute_submachine_switching() + { + let mainMachine = self.mainMachine! + + // NOTE: mainMachine can check switchable submachines + // (external routes between submachines = Sub1, Sub2, or nil) + + XCTAssertFalse(mainMachine.hasRoute("Sub1.State1" => "Sub2.State1")) + XCTAssertFalse(mainMachine.hasRoute("Sub1.State1" => "Sub2.State2")) + XCTAssertFalse(mainMachine.hasRoute("Sub1.State1" => "MainState0")) + XCTAssertTrue(mainMachine.hasRoute("Sub1.State2" => "Sub2.State1")) // 1-2 => 2-1 + XCTAssertFalse(mainMachine.hasRoute("Sub1.State2" => "Sub2.State2")) + XCTAssertFalse(mainMachine.hasRoute("Sub1.State2" => "MainState0")) + + XCTAssertFalse(mainMachine.hasRoute("Sub2.State1" => "Sub1.State1")) + XCTAssertFalse(mainMachine.hasRoute("Sub2.State1" => "Sub1.State2")) + XCTAssertFalse(mainMachine.hasRoute("Sub2.State1" => "MainState0")) + XCTAssertFalse(mainMachine.hasRoute("Sub2.State2" => "Sub1.State1")) + XCTAssertFalse(mainMachine.hasRoute("Sub2.State2" => "Sub1.State2")) + XCTAssertTrue(mainMachine.hasRoute("Sub2.State2" => "MainState0")) // 2-2 => 0 + + XCTAssertTrue(mainMachine.hasRoute("MainState0" => "Sub1.State1")) // 0 => 1-1 + XCTAssertFalse(mainMachine.hasRoute("MainState0" => "Sub1.State2")) + XCTAssertFalse(mainMachine.hasRoute("MainState0" => "Sub2.State1")) + XCTAssertFalse(mainMachine.hasRoute("MainState0" => "Sub2.State2")) + } + + func testTryState() + { + let mainMachine = self.mainMachine! + let sub1Machine = self.sub1Machine! + + XCTAssertEqual(mainMachine.state, "Sub1.State1") + + // 1-1 => 1-2 (sub1 internal transition) + mainMachine <- "Sub1.State2" + XCTAssertEqual(mainMachine.state, "Sub1.State2") + + // dummy + mainMachine <- "DUMMY" + XCTAssertEqual(mainMachine.state, "Sub1.State2", "mainMachine.state should not be updated because there is no 1-2 => DUMMY route.") + + // 1-2 => 2-1 (sub1 => sub2 external transition) + mainMachine <- "Sub2.State1" + XCTAssertEqual(mainMachine.state, "Sub2.State1") + + // dummy + mainMachine <- "MainState0" + XCTAssertEqual(mainMachine.state, "Sub2.State1", "mainMachine.state should not be updated because there is no 2-1 => 0 route.") + + // 2-1 => 2-2 (sub1 internal transition) + mainMachine <- "Sub2.State2" + XCTAssertEqual(mainMachine.state, "Sub2.State2") + + // 2-2 => 0 (sub2 => main external transition) + mainMachine <- "MainState0" + XCTAssertEqual(mainMachine.state, "MainState0") + + // 0 => 1-1 (fail) + mainMachine <- "Sub1.State1" + XCTAssertEqual(mainMachine.state, "MainState0", "mainMachine.state should not be updated because current sub1Machine.state is State2, not State1.") + XCTAssertEqual(sub1Machine.state, "State2") + + // let's add resetting route for sub1Machine & reset to Sub1.State1 + sub1Machine.addRoute(nil => "State1") + sub1Machine <- "State1" + XCTAssertEqual(mainMachine.state, "MainState0") + XCTAssertEqual(sub1Machine.state, "State1") + + // 0 => 1-1 (retry, succeed) + mainMachine <- "Sub1.State1" + XCTAssertEqual(mainMachine.state, "Sub1.State1") + XCTAssertEqual(sub1Machine.state, "State1") + } + + func testAddHandler() + { + let mainMachine = self.mainMachine! + let sub1Machine = self.sub1Machine! + + var didPass = false + + // NOTE: this handler is added to mainMachine and doesn't make submachines dirty + mainMachine.addHandler("Sub1.State1" => "Sub1.State2") { context in + println("[Main] 1-1 => 1-2 (specific)") + didPass = true + } + + XCTAssertEqual(mainMachine.state, "Sub1.State1") + XCTAssertFalse(didPass) + + mainMachine <- "Sub1.State2" + XCTAssertEqual(mainMachine.state, "Sub1.State2") + XCTAssertTrue(didPass) + } +}