Skip to content

Instantly share code, notes, and snippets.

@drewmccormack
Last active June 9, 2021 09:43
Show Gist options
  • Save drewmccormack/d60ded1f0e39e1e1d37529b4f0330ecc to your computer and use it in GitHub Desktop.
Save drewmccormack/d60ded1f0e39e1e1d37529b4f0330ecc to your computer and use it in GitHub Desktop.
//
// 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