Last active
May 17, 2023 12:27
-
-
Save shaps80/fb81ada03c3669a5df66be95ab60de04 to your computer and use it in GitHub Desktop.
Revisions
-
shaps80 revised this gist
Aug 18, 2018 . 1 changed file with 8 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -212,8 +212,14 @@ private extension StateMachine { func log(_ kind: Log, from: Delegate.StateType, to: Delegate.StateType) { guard isLoggingEnabled else { return } if #available(iOS 12.0, *) { os_log(.debug, log: .state, "%{public}@ transition from %{public}@ to %{public}@", kind.rawValue, String(describing: from), String(describing: to)) } else { NSLog("%@ transition from %@ to %@", kind.rawValue, String(describing: from), String(describing: to)) } } } -
shaps80 revised this gist
Aug 18, 2018 . 1 changed file with 4 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -195,6 +195,10 @@ open class StateMachine<Delegate> where Delegate: StateMachineDelegate { } private extension OSLog { static let state = OSLog(subsystem: "com.152percent", category: "state-machine") } private extension StateMachine { enum Log: String { -
shaps80 revised this gist
Aug 18, 2018 . 1 changed file with 6 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -37,6 +37,12 @@ final class StateMachine_Spec: QuickSpec, StateMachineDelegate { .to( throwError(StateMachineError.illegalTransition) ) expect(machine.currentState).to(equal(.initial)) } it("it should succeed when transitioning to the same state") { expect { try machine.transition(to: .initial) } .toNot( throwError() ) expect(machine.currentState).to(equal(.initial)) } } } -
shaps80 revised this gist
Aug 18, 2018 . 2 changed files with 14 additions and 29 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -150,8 +150,12 @@ open class StateMachine<Delegate> where Delegate: StateMachineDelegate { log(.request, from: fromState, to: toState) try validateTransition(from: fromState, to: toState) log(.will, from: fromState, to: toState) delegate?.willTransition(from: fromState, to: toState) currentState = toState log(.did, from: fromState, to: toState) delegate?.didTransition(from: fromState, to: toState) } @@ -172,6 +176,8 @@ open class StateMachine<Delegate> where Delegate: StateMachineDelegate { return } log(.rejected, from: fromState, to: toState) guard let delegate = delegate else { throw StateMachineError.illegalTransition } This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -5,6 +5,8 @@ import Quick import Nimble @testable import DataController final class StateMachine_Spec: QuickSpec, StateMachineDelegate { typealias StateType = State @@ -20,17 +22,20 @@ final class StateMachine_Spec: QuickSpec, StateMachineDelegate { it("it should succeed when performing an valid transition") { expect { try machine.transition(to: .preparing) } .toNot( throwError(StateMachineError.illegalTransition) ) expect(machine.currentState).to(equal(.preparing)) } it("it should throw when performing an invalid transition") { expect { try machine.transition(to: .refreshing) } .to( throwError(StateMachineError.illegalTransition) ) expect(machine.currentState).to(equal(.initial)) } it("it should throw when performing a valid transition that was rejected") { machine.delegate = self expect { try machine.transition(to: .preparing) } .to( throwError(StateMachineError.illegalTransition) ) expect(machine.currentState).to(equal(.initial)) } } } @@ -43,40 +48,14 @@ final class StateMachine_Spec: QuickSpec, StateMachineDelegate { extension StateMachine_Spec { enum State: String { case initial case preparing case refreshing static var validTransitions: [StateMachine_Spec.State: [StateMachine_Spec.State]] { return [ .initial: [.preparing] ] } } } -
shaps80 revised this gist
Aug 18, 2018 . No changes.There are no files selected for viewing
-
shaps80 revised this gist
Aug 18, 2018 . No changes.There are no files selected for viewing
-
shaps80 revised this gist
Aug 18, 2018 . 3 changed files with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes.File renamed without changes.File renamed without changes. -
shaps80 revised this gist
Aug 18, 2018 . 1 changed file with 19 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,19 @@ // MARK: - Locks public protocol Lock { func lock() func unlock() } public final class SpinLock: Lock { private var unfairLock = os_unfair_lock_s() public init() { } public func lock() { os_unfair_lock_lock(&unfairLock) } public func unlock() { os_unfair_lock_unlock(&unfairLock) } } -
shaps80 created this gist
Aug 18, 2018 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,209 @@ import Foundation import os.log public protocol StateMachineDelegate: class { associatedtype StateType: Hashable /// Invoked before a transition is about to occur, allowing you to reject even a valid transition. Defaults to true /// /// - Parameters: /// - source: The state before the transition /// - destination: The state after the transition /// - Returns: True if the transition is allowed, false otherwise func shouldTransition(from source: StateType, to destination: StateType) -> Bool /// Invoked before the state machine transitions from `source` to `destination` /// /// - Parameters: /// - source: The state before the transition /// - destination: The state after the transition func willTransition(from source: StateType, to destination: StateType) /// Invoked after the state machine transitions from `source` to `destination` /// /// - Parameters: /// - source: The state before the transition /// - destination: The state after the transition func didTransition(from source: StateType, to destination: StateType) /// Invoked if the transition was invalid or rejected. By default this always throws /// /// - Parameters: /// - source: The state before the transition /// - destination: The state after the transition /// - Throws: `illegalTransition` if the transition was invalid or rejected func missingTransition(from source: StateType, to destination: StateType) throws } public extension StateMachineDelegate { func shouldTransition(from source: StateType, to destination: StateType) -> Bool { return true } func willTransition(from source: StateType, to destination: StateType) { } func didTransition(from source: StateType, to destination: StateType) { } func missingTransition(from source: StateType, to destination: StateType) throws { throw StateMachineError.illegalTransition } } /// Defines the errors that can be thrown from a state machine /// /// - illegalTransition: An illegal transition attempt was made public enum StateMachineError: Error { /// An illegal transition attempt was made case illegalTransition } /** A generic state machine implementation. It is generally not necessary to subclass. Instead, set the delegate property and implement state transition methods as appropriate. Example: enum State { case initial case ready static var validTransitions: [State: [State]] { return [.initial: [.ready]] } } class StateDelegate { associatedtype StateType = State } let machine = StateMachine<StateDelegate>(initial: .initial, validTransitions: State.validTransitions) */ open class StateMachine<Delegate> where Delegate: StateMachineDelegate { /** If specified, the state machine invokes transition methods on this delegate instead of itself. */ public weak var delegate: Delegate? /** Uses OSLog to output state transitions; useful for debugging, but can be noisy. Defaults to true for DEBUG builds. False otherwise */ public var isLoggingEnabled: Bool = false /** The current state of the state machine */ public private(set) var currentState: Delegate.StateType { get { lock.lock() let state = _currentState lock.unlock() return state } set { lock.lock() _currentState = newValue lock.unlock() } } /** Definition of the valid transitions for this state machine. This is a dictionary where the keys are the state and the value for each key is an array of the valid next state. */ public let validTransitions: [Delegate.StateType: [Delegate.StateType]] private let lock: SpinLock private var _currentState: Delegate.StateType /** Makes a new state machine. - Parameters: - initial: The initial state of this machine - validTransitions: A dictionary of valid transitions that can be performed Example: StateMachine<StateDelegate>(initial: .initial, validTransitions: [ .initial: [.preparing], .preparing: [.ready, .empty, .error], .ready: [.refreshing], .refreshing: [.ready, .empty, .error] ]) */ public init(initial: Delegate.StateType, validTransitions: [Delegate.StateType: [Delegate.StateType]]) { self._currentState = initial self.validTransitions = validTransitions self.lock = SpinLock() #if DEBUG isLoggingEnabled = true #endif } /** Attempts to transitions to the specified state. This does not bypass `missingTransition(from:to:)` – if you invoke this with an invalid transition an illegal error will be thrown */ public func transition(to state: Delegate.StateType) throws { let fromState = currentState let toState = state log(.request, from: fromState, to: toState) try validateTransition(from: fromState, to: toState) delegate?.willTransition(from: fromState, to: toState) currentState = toState delegate?.didTransition(from: fromState, to: toState) } /// Validates the transition between two states /// /// Transitioning to the same state is always allowed. If its explicity defined as a valid transition, the standard methods calls will be invoked, otherwise if will succeed silently. /// /// - Parameters: /// - fromState: The state to transition from /// - toState: The state to transition to /// - Throws: An `illegalTransition` error is thrown if the transition is invalid or rejected open func validateTransition(from fromState: Delegate.StateType, to toState: Delegate.StateType) throws { let isValid = validTransitions[fromState]?.contains(toState) == true guard isValid else { if fromState == toState { log(.ignore, from: fromState, to: toState) return } guard let delegate = delegate else { throw StateMachineError.illegalTransition } try delegate.missingTransition(from: fromState, to: toState) return } guard delegate?.shouldTransition(from: fromState, to: toState) == true else { log(.rejected, from: fromState, to: toState) try delegate?.missingTransition(from: fromState, to: toState) return } } } private extension StateMachine { enum Log: String { case request = "Request" case will = "Will" case did = "Did" case ignore = "Ignoring" case rejected = "Rejected" case illegal = "Illegal" } func log(_ kind: Log, from: Delegate.StateType, to: Delegate.StateType) { guard isLoggingEnabled else { return } os_log(.debug, log: .state, "%{public}@ transition from %{public}@ to %{public}@", kind.rawValue, String(describing: from), String(describing: to)) } } This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,82 @@ /** Depends on Quick & Nimble */ import Quick import Nimble final class StateMachine_Spec: QuickSpec, StateMachineDelegate { typealias StateType = State override func spec() { var machine: StateMachine<StateMachine_Spec>! context("Given a state machine") { beforeEach { machine = StateMachine<StateMachine_Spec>(initial: .initial, validTransitions: State.validTransitions) } it("it should succeed when performing an valid transition") { expect { try machine.transition(to: .preparing) } .toNot( throwError(StateMachineError.illegalTransition) ) } it("it should throw when performing an invalid transition") { expect { try machine.transition(to: .refreshing) } .to( throwError(StateMachineError.illegalTransition) ) } it("it should throw when performing a valid transition that was rejected") { machine.delegate = self expect { try machine.transition(to: .preparing) } .to( throwError(StateMachineError.illegalTransition) ) } } } func shouldTransition(from source: StateMachine_Spec.State, to destination: StateMachine_Spec.State) -> Bool { return false } } extension StateMachine_Spec { /// Defines the various states our DataSource can be in. /// /// - initial: The initial state is before any data has been loaded /// - preparing: Content is being prepared for the first time /// - ready: Content is available and ready /// - refreshing: Content is being refreshed /// - empty: No content is available /// - error: An error occurred enum State: String { /// The initial state before any content has been loaded case initial /// Content is being prepared for the first time case preparing /// Content is available and ready case ready /// Content is being refreshed case refreshing /// No content is available case empty /// An error occurred case error } } extension StateMachine_Spec.State { static var validTransitions: [StateMachine_Spec.State: [StateMachine_Spec.State]] { return [ .initial: [.preparing], .preparing: [.ready, .empty, .error], .ready: [.refreshing], .refreshing: [.ready, .empty, .error] ] } }