Skip to content
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

Swift 3 Support #35

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
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
20 changes: 10 additions & 10 deletions StateMachine/GraphableStateMachineSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ public struct GraphableStateMachineSchema<A: DOTLabelable, B: DOTLabelable, C>:
public typealias Subject = C

public let initialState: State
public let transitionLogic: (State, Event) -> (State, (Subject -> ())?)?
public let transitionLogic: (State, Event) -> (State, ((Subject) -> ())?)?
public let DOTDigraph: String

public init(initialState: State, transitionLogic: (State, Event) -> (State, (Subject -> ())?)?) {
public init(initialState: State, transitionLogic: @escaping (State, Event) -> (State, ((Subject) -> ())?)?) {
self.initialState = initialState
self.transitionLogic = transitionLogic
self.DOTDigraph = GraphableStateMachineSchema.DOTDigraphGivenInitialState(initialState, transitionLogic: transitionLogic)
Expand Down Expand Up @@ -84,24 +84,24 @@ public struct GraphableStateMachineSchema<A: DOTLabelable, B: DOTLabelable, C>:
///
/// [DOT]: http://en.wikipedia.org/wiki/DOT_%28graph_description_language%29
/// [Graphviz]: http://www.graphviz.org/
public func saveDOTDigraphIfRunningInSimulator(filepathRelativeToCurrentFile filepathRelativeToCurrentFile: String, file: String = #file) throws {
public func saveDOTDigraphIfRunningInSimulator(filepathRelativeToCurrentFile: String, file: String = #file) throws {
if TARGET_IPHONE_SIMULATOR == 1 {
let filepath = ((file as NSString).stringByDeletingLastPathComponent as NSString).stringByAppendingPathComponent(filepathRelativeToCurrentFile)
try DOTDigraph.writeToFile(filepath, atomically: true, encoding: NSUTF8StringEncoding)
let filepath = ((file as NSString).deletingLastPathComponent as NSString).appendingPathComponent(filepathRelativeToCurrentFile)
try DOTDigraph.write(toFile: filepath, atomically: true, encoding: String.Encoding.utf8)
}
}
#endif

private static func DOTDigraphGivenInitialState(initialState: State, transitionLogic: (State, Event) -> (State, (Subject -> ())?)?) -> String {
fileprivate static func DOTDigraphGivenInitialState(_ initialState: State, transitionLogic: (State, Event) -> (State, ((Subject) -> ())?)?) -> String {
let states = State.DOTLabelableItems
let events = Event.DOTLabelableItems

var stateIndexesByLabel: [String: Int] = [:]
for (i, state) in states.enumerate() {
for (i, state) in states.enumerated() {
stateIndexesByLabel[label(state)] = i + 1
}

func index(state: State) -> Int {
func index(_ state: State) -> Int {
return stateIndexesByLabel[label(state)]!
}

Expand Down Expand Up @@ -129,6 +129,6 @@ public struct GraphableStateMachineSchema<A: DOTLabelable, B: DOTLabelable, C>:


/// Helper function used when generating DOT digraph strings.
private func label<T: DOTLabelable>(x: T) -> String {
return x.DOTLabel.stringByReplacingOccurrencesOfString("\"", withString: "\\\"", options: .LiteralSearch, range: nil)
private func label<T: DOTLabelable>(_ x: T) -> String {
return x.DOTLabel.replacingOccurrences(of: "\"", with: "\\\"", options: .literal, range: nil)
}
21 changes: 11 additions & 10 deletions StateMachine/StateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,24 @@ public protocol StateMachineSchemaType {
associatedtype Subject

var initialState: State { get }
var transitionLogic: (State, Event) -> (State, (Subject -> ())?)? { get }
var transitionLogic: (State, Event) -> (State, ((Subject) -> ())?)? { get }

init(initialState: State, transitionLogic: (State, Event) -> (State, (Subject -> ())?)?)
init(initialState: State, transitionLogic: @escaping (State, Event) -> (State, ((Subject) -> ())?)?)
}


/// A state machine schema conforming to the `StateMachineSchemaType`
/// protocol. See protocol documentation for more information.
public struct StateMachineSchema<A, B, C>: StateMachineSchemaType {

public typealias State = A
public typealias Event = B
public typealias Subject = C

public let initialState: State
public let transitionLogic: (State, Event) -> (State, (Subject -> ())?)?
public let transitionLogic: (State, Event) -> (State, ((Subject) -> ())?)?

public init(initialState: State, transitionLogic: (State, Event) -> (State, (Subject -> ())?)?) {
public init(initialState: State, transitionLogic: @escaping (State, Event) -> (State, ((Subject) -> ())?)?) {
self.initialState = initialState
self.transitionLogic = transitionLogic
}
Expand All @@ -66,7 +67,7 @@ public struct StateMachineSchema<A, B, C>: StateMachineSchemaType {
/// and the state after the transition.
public final class StateMachine<Schema: StateMachineSchemaType> {
/// The current state of the machine.
public private(set) var state: Schema.State
public fileprivate(set) var state: Schema.State

/// An optional block called after a transition with three arguments:
/// the state before the transition, the event causing the transition,
Expand All @@ -75,13 +76,13 @@ public final class StateMachine<Schema: StateMachineSchemaType> {

/// The schema of the state machine. See `StateMachineSchemaType`
/// documentation for more information.
private let schema: Schema
fileprivate let schema: Schema

/// Object associated with the state machine. Can be accessed in
/// transition blocks. Closure used to allow for weak references.
private let subject: () -> Schema.Subject?
fileprivate let subject: () -> Schema.Subject?

private init(schema: Schema, subject: () -> Schema.Subject?) {
fileprivate init(schema: Schema, subject: @escaping () -> Schema.Subject?) {
self.state = schema.initialState
self.schema = schema
self.subject = subject
Expand All @@ -92,10 +93,10 @@ public final class StateMachine<Schema: StateMachineSchemaType> {
/// becomes `nil`. If the transition logic of the schema defines a transition
/// for current state and given event, the state is changed, the optional
/// transition block is executed, and `didTransitionCallback` is called.
public func handleEvent(event: Schema.Event) {
public func handleEvent(_ event: Schema.Event) {
guard let
subject = subject(),
(newState, transition) = schema.transitionLogic(state, event)
let (newState, transition) = schema.transitionLogic(state, event)
else {
return
}
Expand Down
120 changes: 60 additions & 60 deletions StateMachineTests/StateMachineSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,45 +10,45 @@ private struct NumberKeeper {


private enum Number {
case One, Two, Three
case one, two, three
}

private enum Operation {
case Increment, Decrement
case increment, decrement
}


extension Number: DOTLabelable {
static var DOTLabelableItems: [Number] {
return [.One, .Two, .Three]
return [.one, .two, .three]
}
}

extension Operation: DOTLabelable {
static var DOTLabelableItems: [Operation] {
return [.Increment, .Decrement]
return [.increment, .decrement]
}
}


private enum SimpleState { case S1, S2 }
private enum SimpleEvent { case E }
private enum SimpleState { case s1, s2 }
private enum SimpleEvent { case e }

private func createSimpleSchema<T>(forward forward: (T -> ())? = nil, backward: (T -> ())? = nil) -> StateMachineSchema<SimpleState, SimpleEvent, T> {
return StateMachineSchema(initialState: .S1) { (state, event) in
private func createSimpleSchema<T>(forward: ((T) -> ())? = nil, backward: ((T) -> ())? = nil) -> StateMachineSchema<SimpleState, SimpleEvent, T> {
return StateMachineSchema(initialState: .s1) { (state, event) in
switch state {
case .S1: switch event {
case .E: return (.S2, { forward?($0) })
case .s1: switch event {
case .e: return (.s2, { forward?($0) })
}

case .S2: switch event {
case .E: return (.S1, { backward?($0) })
case .s2: switch event {
case .e: return (.s1, { backward?($0) })
}
}
}
}

private func createSimpleMachine(forward forward: (() -> ())? = nil, backward: (() -> ())? = nil) -> StateMachine<StateMachineSchema<SimpleState, SimpleEvent, Void>> {
private func createSimpleMachine(forward: (() -> ())? = nil, backward: (() -> ())? = nil) -> StateMachine<StateMachineSchema<SimpleState, SimpleEvent, Void>> {
return StateMachine(schema: createSimpleSchema(forward: { _ in forward?() }, backward: { _ in backward?() }), subject: ())
}

Expand All @@ -75,25 +75,25 @@ class StateMachineSpec: QuickSpec {

beforeEach {
keeper = NumberKeeper(n: 1)

let schema: StateMachineSchema<Number, Operation, NumberKeeper> = StateMachineSchema(initialState: .One) { (state, event) in
let decrement: NumberKeeper -> () = { _ in keeper.n -= 1 }
let increment: NumberKeeper -> () = { _ in keeper.n += 1 }
let schema = StateMachineSchema<Number, Operation, NumberKeeper>(initialState: .one) { (state, event) in
let decrement: (NumberKeeper) -> () = { _ in keeper.n -= 1 }
let increment: (NumberKeeper) -> () = { _ in keeper.n += 1 }

switch state {
case .One: switch event {
case .Decrement: return nil
case .Increment: return (.Two, increment)
case .one: switch event {
case .decrement: return nil
case .increment: return (.two, increment)
}

case .Two: switch event {
case .Decrement: return (.One, decrement)
case .Increment: return (.Three, increment)
case .two: switch event {
case .decrement: return (.one, decrement)
case .increment: return (.three, increment)
}

case .Three: switch event {
case .Decrement: return (.Two, decrement)
case .Increment: return nil
case .three: switch event {
case .decrement: return (.two, decrement)
case .increment: return nil
}
}
}
Expand All @@ -103,28 +103,28 @@ class StateMachineSpec: QuickSpec {

it("can be associated with a subject") {
expect(keeper.n) == 1
keeperMachine.handleEvent(.Increment)
keeperMachine.handleEvent(.increment)
expect(keeper.n) == 2
}

it("doesn't have to be associated with a subject") {
let machine = createSimpleMachine()

expect(machine.state) == SimpleState.S1
machine.handleEvent(.E)
expect(machine.state) == SimpleState.S2
expect(machine.state) == SimpleState.s1
machine.handleEvent(.e)
expect(machine.state) == SimpleState.s2
}

it("changes state on correct event") {
expect(keeperMachine.state) == Number.One
keeperMachine.handleEvent(.Increment)
expect(keeperMachine.state) == Number.Two
expect(keeperMachine.state) == Number.one
keeperMachine.handleEvent(.increment)
expect(keeperMachine.state) == Number.two
}

it("doesn't change state on ignored event") {
expect(keeperMachine.state) == Number.One
keeperMachine.handleEvent(.Decrement)
expect(keeperMachine.state) == Number.One
expect(keeperMachine.state) == Number.one
keeperMachine.handleEvent(.decrement)
expect(keeperMachine.state) == Number.one
}

it("executes transition block on transition") {
Expand All @@ -133,7 +133,7 @@ class StateMachineSpec: QuickSpec {
let machine = createSimpleMachine(forward: { didExecuteBlock = true })
expect(didExecuteBlock) == false

machine.handleEvent(.E)
machine.handleEvent(.e)
expect(didExecuteBlock) == true
}

Expand All @@ -142,20 +142,20 @@ class StateMachineSpec: QuickSpec {

var callbackWasCalledCorrectly = false
machine.didTransitionCallback = { (oldState: SimpleState, event: SimpleEvent, newState: SimpleState) in
callbackWasCalledCorrectly = oldState == .S1 && event == .E && newState == .S2
callbackWasCalledCorrectly = oldState == .s1 && event == .e && newState == .s2
}

machine.handleEvent(.E)
machine.handleEvent(.e)
expect(callbackWasCalledCorrectly) == true
}

it("can trigger transition from within transition") {
let subject = Subject(schema: createSimpleSchema(forward: {
$0.machine.handleEvent(.E)
$0.machine.handleEvent(.e)
}))

subject.machine.handleEvent(.E)
expect(subject.machine.state) == SimpleState.S1
subject.machine.handleEvent(.e)
expect(subject.machine.state) == SimpleState.s1
}

it("doesn't cause machine-subject reference cycles") {
Expand All @@ -164,7 +164,7 @@ class StateMachineSpec: QuickSpec {

init() {
machine = StateMachine(
schema: StateMachineSchema(initialState: .S1) { _ in nil },
schema: StateMachineSchema(initialState: .s1) { _ in nil },
subject: self)
}
}
Expand All @@ -179,32 +179,32 @@ class StateMachineSpec: QuickSpec {
describe("Graphable State Machine") {

it("has representation in DOT format") {
let schema: GraphableStateMachineSchema<Number, Operation, Void> = GraphableStateMachineSchema(initialState: .One) { (state, event) in
let schema: GraphableStateMachineSchema<Number, Operation, Void> = GraphableStateMachineSchema(initialState: .one) { (state, event) in
switch state {
case .One: switch event {
case .Decrement: return nil
case .Increment: return (.Two, nil)
case .one: switch event {
case .decrement: return nil
case .increment: return (.two, nil)
}

case .Two: switch event {
case .Decrement: return (.One, nil)
case .Increment: return (.Three, nil)
case .two: switch event {
case .decrement: return (.one, nil)
case .increment: return (.three, nil)
}

case .Three: switch event {
case .Decrement: return (.Two, nil)
case .Increment: return nil
case .three: switch event {
case .decrement: return (.two, nil)
case .increment: return nil
}
}
}

expect(schema.DOTDigraph) == "digraph {\n graph [rankdir=LR]\n\n 0 [label=\"\", shape=plaintext]\n 0 -> 1 [label=\"START\"]\n\n 1 [label=\"One\"]\n 2 [label=\"Two\"]\n 3 [label=\"Three\"]\n\n 1 -> 2 [label=\"Increment\"]\n 2 -> 3 [label=\"Increment\"]\n 2 -> 1 [label=\"Decrement\"]\n 3 -> 2 [label=\"Decrement\"]\n}"
expect(schema.DOTDigraph) == "digraph {\n graph [rankdir=LR]\n\n 0 [label=\"\", shape=plaintext]\n 0 -> 1 [label=\"START\"]\n\n 1 [label=\"one\"]\n 2 [label=\"two\"]\n 3 [label=\"three\"]\n\n 1 -> 2 [label=\"increment\"]\n 2 -> 3 [label=\"increment\"]\n 2 -> 1 [label=\"decrement\"]\n 3 -> 2 [label=\"decrement\"]\n}"
}

it("escapes double quotes in labels") {

let schema = GraphableStateMachineSchema<State, Event, Void>(initialState: .S) { _ in
(.S, nil)
let schema = GraphableStateMachineSchema<State, Event, Void>(initialState: .s) { _ in
(.s, nil)
}

expect(schema.DOTDigraph) == "digraph {\n graph [rankdir=LR]\n\n 0 [label=\"\", shape=plaintext]\n 0 -> 1 [label=\"START\"]\n\n 1 [label=\"An \\\"awesome\\\" state\"]\n\n 1 -> 1 [label=\"An \\\"awesome\\\" event\"]\n}"
Expand All @@ -216,13 +216,13 @@ class StateMachineSpec: QuickSpec {


enum State: DOTLabelable {
case S
case s
var DOTLabel: String { return "An \"awesome\" state" }
static var DOTLabelableItems: [State] { return [.S] }
static var DOTLabelableItems: [State] { return [.s] }
}

enum Event: DOTLabelable {
case E
case e
var DOTLabel: String { return "An \"awesome\" event" }
static var DOTLabelableItems: [Event] { return [.E] }
static var DOTLabelableItems: [Event] { return [.e] }
}
2 changes: 1 addition & 1 deletion SwiftyStateMachine.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SwiftyStateMachine"
s.version = "0.3.0"
s.version = "0.4.0"
s.summary = "A Swift µframework for creating finite-state machines, designed for clarity and maintainability."
s.homepage = "https://github.com/macoscope/SwiftyStateMachine"
s.license = "MIT"
Expand Down
Loading