Last active
June 9, 2021 09:43
-
-
Save drewmccormack/d60ded1f0e39e1e1d37529b4f0330ecc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Lightweight but powerful state machine. | |
// | |
// Usage: | |
// enum TrafficLight: State { | |
// case red, orange, green | |
// } | |
// | |
// var trafficLights = StateMachine<TrafficLight>(initialState: .red) | |
// trafficLights.addRoute(from: .red, to: .green) | |
// trafficLights.addRoute(from: .green, to: .orange) | |
// trafficLights.addRoute(from: .orange, to: .red) | |
// | |
// trafficLights.handle(transitionsFrom: .any, to: .one(.green)) { route in | |
// print("Go Dog Go!") | |
// } | |
// | |
// trafficLights.handle(transitionsFrom: .any, to: .one(.red)) { route in | |
// print("Stop Dog Stop!") | |
// } | |
// | |
// try trafficLights.transition(to: .green) | |
// | |
public protocol State: Equatable {} | |
public struct StateMachine<StateType: State> { | |
public enum Error: Swift.Error { | |
case invalidStateTransition(from: StateType, to: StateType) | |
} | |
public struct Route: Equatable { | |
var fromState: StateType | |
var toState: StateType | |
} | |
public enum StateMatch { | |
case any | |
case one(StateType) | |
case anyIn([StateType]) | |
case notIn([StateType]) | |
func matches(state: StateType) -> Bool { | |
switch self { | |
case .any: | |
return true | |
case let .one(s): | |
return s == state | |
case let .anyIn(states): | |
return states.contains(state) | |
case let .notIn(excluded): | |
return !excluded.contains(state) | |
} | |
} | |
} | |
private struct Transition { | |
var fromMatch: StateMatch | |
var toMatch: StateMatch | |
var handler: (_ route: Route)->Void | |
} | |
public private(set) var state: StateType | |
public private(set) var routes: [Route] = [] | |
private var transitions: [Transition] = [] | |
public init(initialState: StateType) { | |
self.state = initialState | |
} | |
public mutating func addRoute(from fromState: StateType, to toState: StateType) { | |
let route = Route(fromState: fromState, toState: toState) | |
routes.removeAll { route == $0 } | |
routes.append(route) | |
} | |
public mutating func handle(transitionsFrom fromMatch: StateMatch, to toMatch: StateMatch, with handler: @escaping (_ route: Route)->Void) { | |
let transition = Transition(fromMatch: fromMatch, toMatch: toMatch, handler: handler) | |
transitions.append(transition) | |
} | |
public mutating func transition(to newState: StateType) throws { | |
guard routes.contains(Route(fromState: state, toState: newState)) else { | |
throw Error.invalidStateTransition(from: state, to: newState) | |
} | |
transitions | |
.filter { $0.fromMatch.matches(state: state) && $0.toMatch.matches(state: newState) } | |
.forEach { $0.handler(Route(fromState: state, toState: newState)) } | |
state = newState | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment