finite_machine 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +62 -6
- data/lib/finite_machine.rb +6 -1
- data/lib/finite_machine/catchable.rb +0 -1
- data/lib/finite_machine/dsl.rb +46 -10
- data/lib/finite_machine/event.rb +5 -3
- data/lib/finite_machine/event_queue.rb +43 -8
- data/lib/finite_machine/hooks.rb +0 -2
- data/lib/finite_machine/listener.rb +22 -0
- data/lib/finite_machine/observer.rb +80 -45
- data/lib/finite_machine/state_machine.rb +8 -3
- data/lib/finite_machine/subscribers.rb +0 -1
- data/lib/finite_machine/thread_context.rb +0 -1
- data/lib/finite_machine/threadable.rb +0 -2
- data/lib/finite_machine/transition.rb +1 -0
- data/lib/finite_machine/transition_event.rb +26 -0
- data/lib/finite_machine/version.rb +1 -1
- data/spec/unit/async_events_spec.rb +34 -4
- data/spec/unit/define_spec.rb +42 -19
- data/spec/unit/event_queue_spec.rb +51 -0
- data/spec/unit/events_spec.rb +11 -0
- data/spec/unit/finished_spec.rb +14 -0
- data/spec/unit/initialize_spec.rb +42 -0
- data/spec/unit/target_spec.rb +23 -0
- data/tasks/console.rake +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 714f488440a58102f64c2c9911b1d55f3f759df5
|
4
|
+
data.tar.gz: 10005291036b23eee4654d2a81eeab01383126da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a16908a3c0d0c6fceeb196aac75249f5470593020c29658c4b688bbe7af8aa8a2342b8f42cae065ba19167698204061353b27bc12100508ba4bccfe66dcd58bd
|
7
|
+
data.tar.gz: 6aec10fde3d2e76645458a7b60a6aca87784f8193376944b2c0be40c05c4a7ad5facfe75d98c1b155fb9937da504c0a7903a5558caf3e1fd1edd909cfe0341c0
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
0.5.0 (April 28, 2014)
|
2
|
+
|
3
|
+
* Change to allow for machine to be constructed as plain object
|
4
|
+
* Allow for :initial, :terminal and :target to be machine parameters
|
5
|
+
* Add generic Listener interface
|
6
|
+
* Change EventQueue to allow for subscription
|
7
|
+
* Increase test coverage to 98%
|
8
|
+
* Change to allow access to target inside machine dsl
|
9
|
+
* Add ability to fire callbacks asynchronously
|
10
|
+
* Add initial state storage
|
11
|
+
|
1
12
|
0.4.0 (April 13, 2014)
|
2
13
|
|
3
14
|
* Change initial state to stop firing event notification
|
data/README.md
CHANGED
@@ -23,7 +23,7 @@ A minimal finite state machine with a straightforward and intuitive syntax. You
|
|
23
23
|
* ability to check for terminal state
|
24
24
|
* conditional transitions
|
25
25
|
* sync and async transitions
|
26
|
-
* sync and async callbacks
|
26
|
+
* sync and async callbacks
|
27
27
|
* nested/composable states (TODO)
|
28
28
|
|
29
29
|
## Installation
|
@@ -70,7 +70,8 @@ Or install it yourself as:
|
|
70
70
|
* [4.7 Fluid callbacks](#47-fluid-callbacks)
|
71
71
|
* [4.8 Executing methods inside callbacks](#48-executing-methods-inside-callbacks)
|
72
72
|
* [4.9 Defining callbacks](#49-defining-callbacks)
|
73
|
-
* [4.10
|
73
|
+
* [4.10 Asynchronous callbacks](#410-asynchronous-callbacks)
|
74
|
+
* [4.11 Cancelling inside callbacks](#411-cancelling-inside-callbacks)
|
74
75
|
* [5. Errors](#5-errors)
|
75
76
|
* [5.1 Using target](#51-using-target)
|
76
77
|
* [6. Integration](#6-integration)
|
@@ -92,15 +93,27 @@ fm = FiniteMachine.define do
|
|
92
93
|
}
|
93
94
|
|
94
95
|
callbacks {
|
95
|
-
on_enter
|
96
|
-
|
97
|
-
on_enter
|
96
|
+
on_enter(:ready) { |event| ... }
|
97
|
+
on_exit(:go) { |event| ... }
|
98
|
+
on_enter(:stop) { |event| ... }
|
98
99
|
}
|
99
100
|
end
|
100
101
|
```
|
101
102
|
|
102
103
|
As the example demonstrates, by calling the `define` method on **FiniteMachine** you create an instance of finite state machine. The `events` and `callbacks` scopes help to define the behaviour of the machine. Read [Transitions](#2-transitions) and [Callbacks](#4-callbacks) sections for more details.
|
103
104
|
|
105
|
+
Alternatively, you can construct the machine like a regular object without using the DSL. The same machine could be reimplemented as follows:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
fm = FiniteMachine.new initial: :red
|
109
|
+
fm.event(:ready, :red => :yellow)
|
110
|
+
fm.event(:go, :yellow => :green)
|
111
|
+
fm.event(:stop, :green => :red)
|
112
|
+
fm.on_enter(:ready) { |event| ... }
|
113
|
+
fm.on_exit(:go) { |event| ... }
|
114
|
+
fm.on_enter(:stop) { |event| ...}
|
115
|
+
```
|
116
|
+
|
104
117
|
### 1.1 current
|
105
118
|
|
106
119
|
The **FiniteMachine** allows you to query the current state by calling the `current` method.
|
@@ -144,6 +157,14 @@ end
|
|
144
157
|
fm.current # => :green
|
145
158
|
```
|
146
159
|
|
160
|
+
or by passing named argument `:initial` like so
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
fm = FiniteMachine.define initial: :green do
|
164
|
+
...
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
147
168
|
If you want to defer setting the initial state, pass the `:defer` option to the `initial` helper. By default **FiniteMachine** will create `init` event that will allow to transition from `:none` state to the new state.
|
148
169
|
|
149
170
|
```ruby
|
@@ -280,6 +301,25 @@ end
|
|
280
301
|
|
281
302
|
For more complex example see [Integration](#6-integration) section.
|
282
303
|
|
304
|
+
Finally, you can always reference an external context inside the **FiniteMachine** by simply calling `target`, for instance, to reference it inside a callback:
|
305
|
+
|
306
|
+
car = Car.new
|
307
|
+
|
308
|
+
fm = FiniteMachine.define do
|
309
|
+
initial :neutral
|
310
|
+
|
311
|
+
target car
|
312
|
+
|
313
|
+
events {
|
314
|
+
event :start, :neutral => :one, if: "engine_on?"
|
315
|
+
}
|
316
|
+
callbacks {
|
317
|
+
on_enter_start do |event|
|
318
|
+
target.turn_engine_on
|
319
|
+
end
|
320
|
+
}
|
321
|
+
end
|
322
|
+
|
283
323
|
## 2 Transitions
|
284
324
|
|
285
325
|
The `events` scope exposes the `event` helper to define possible state transitions.
|
@@ -687,7 +727,23 @@ fm.on_enter_yellow do |event|
|
|
687
727
|
end
|
688
728
|
```
|
689
729
|
|
690
|
-
### 4.10
|
730
|
+
### 4.10 Asynchronous callbacks
|
731
|
+
|
732
|
+
By default all callbacks are run synchronosuly. In order to add a callback that runs asynchronously, you need to pass second `:async` argument like so:
|
733
|
+
|
734
|
+
```ruby
|
735
|
+
on_enter :green, :async do |event| ... end
|
736
|
+
```
|
737
|
+
|
738
|
+
or
|
739
|
+
|
740
|
+
```ruby
|
741
|
+
on_enter_green(:async) { |event| }
|
742
|
+
```
|
743
|
+
|
744
|
+
This will ensure that when the callback is fired it will run in seperate thread outside of the main execution thread.
|
745
|
+
|
746
|
+
### 4.11 Cancelling inside callbacks
|
691
747
|
|
692
748
|
Preferred way to handle cancelling transitions is to use [3 Conditional transitions](#3-conditional-transitions). However if the logic is more than one liner you can cancel the event, hence the transition by returning `FiniteMachine::CANCELLED` constant from the callback scope. The two ways you can affect the event are
|
693
749
|
|
data/lib/finite_machine.rb
CHANGED
@@ -16,10 +16,12 @@ require "finite_machine/event_queue"
|
|
16
16
|
require "finite_machine/hooks"
|
17
17
|
require "finite_machine/logger"
|
18
18
|
require "finite_machine/transition"
|
19
|
+
require "finite_machine/transition_event"
|
19
20
|
require "finite_machine/dsl"
|
20
21
|
require "finite_machine/state_machine"
|
21
22
|
require "finite_machine/subscribers"
|
22
23
|
require "finite_machine/observer"
|
24
|
+
require "finite_machine/listener"
|
23
25
|
|
24
26
|
module FiniteMachine
|
25
27
|
|
@@ -61,6 +63,9 @@ module FiniteMachine
|
|
61
63
|
# Raised when initial event specified without state name
|
62
64
|
MissingInitialStateError = Class.new(::StandardError)
|
63
65
|
|
66
|
+
# Raised when event queue is already dead
|
67
|
+
EventQueueDeadError = Class.new(::StandardError)
|
68
|
+
|
64
69
|
Environment = Struct.new(:target)
|
65
70
|
|
66
71
|
class << self
|
@@ -71,8 +76,8 @@ module FiniteMachine
|
|
71
76
|
def define(*args, &block)
|
72
77
|
StateMachine.new(*args, &block)
|
73
78
|
end
|
79
|
+
alias_method :new, :define
|
74
80
|
end
|
75
|
-
|
76
81
|
end # FiniteMachine
|
77
82
|
|
78
83
|
FiniteMachine.logger = Logger.new(STDERR)
|
data/lib/finite_machine/dsl.rb
CHANGED
@@ -12,7 +12,13 @@ module FiniteMachine
|
|
12
12
|
|
13
13
|
attr_threadsafe :machine
|
14
14
|
|
15
|
-
|
15
|
+
attr_threadsafe :attrs
|
16
|
+
|
17
|
+
# Initialize a generic DSL
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
def initialize(machine, attrs = {})
|
21
|
+
self.attrs = attrs
|
16
22
|
self.machine = machine
|
17
23
|
end
|
18
24
|
|
@@ -30,6 +36,7 @@ module FiniteMachine
|
|
30
36
|
end
|
31
37
|
end # GenericDSL
|
32
38
|
|
39
|
+
# A class responsible for adding state machine specific dsl
|
33
40
|
class DSL < GenericDSL
|
34
41
|
attr_threadsafe :defer
|
35
42
|
|
@@ -38,10 +45,12 @@ module FiniteMachine
|
|
38
45
|
# Initialize top level DSL
|
39
46
|
#
|
40
47
|
# @api public
|
41
|
-
def initialize(machine)
|
42
|
-
super(machine)
|
48
|
+
def initialize(machine, attrs = {})
|
49
|
+
super(machine, attrs)
|
43
50
|
machine.state = FiniteMachine::DEFAULT_STATE
|
44
|
-
self.defer
|
51
|
+
self.defer = true
|
52
|
+
|
53
|
+
initialize_attrs
|
45
54
|
end
|
46
55
|
|
47
56
|
# Define initial state
|
@@ -59,19 +68,35 @@ module FiniteMachine
|
|
59
68
|
# @api public
|
60
69
|
def initial(value)
|
61
70
|
state, name, self.defer = parse(value)
|
62
|
-
|
71
|
+
unless defer
|
72
|
+
machine.state = state
|
73
|
+
machine.initial_state = state
|
74
|
+
end
|
63
75
|
event = proc { event name, from: FiniteMachine::DEFAULT_STATE, to: state }
|
64
76
|
machine.events.call(&event)
|
65
77
|
end
|
66
78
|
|
67
|
-
# Attach state machine to an object
|
68
|
-
#
|
79
|
+
# Attach state machine to an object
|
80
|
+
#
|
81
|
+
# This allows state machine to initiate events in the context
|
82
|
+
# of a particular object
|
83
|
+
#
|
84
|
+
# @example
|
85
|
+
# FiniteMachine.define do
|
86
|
+
# target :red
|
87
|
+
# end
|
69
88
|
#
|
70
89
|
# @param [Object] object
|
71
90
|
#
|
91
|
+
# @return [FiniteMachine::StateMachine]
|
92
|
+
#
|
72
93
|
# @api public
|
73
|
-
def target(object)
|
74
|
-
|
94
|
+
def target(object = nil)
|
95
|
+
if object.nil?
|
96
|
+
machine.env.target
|
97
|
+
else
|
98
|
+
machine.env.target = object
|
99
|
+
end
|
75
100
|
end
|
76
101
|
|
77
102
|
# Define terminal state
|
@@ -79,7 +104,7 @@ module FiniteMachine
|
|
79
104
|
# @example
|
80
105
|
# terminal :red
|
81
106
|
#
|
82
|
-
# @return [StateMachine]
|
107
|
+
# @return [FiniteMachine::StateMachine]
|
83
108
|
#
|
84
109
|
# @api public
|
85
110
|
def terminal(value)
|
@@ -88,6 +113,8 @@ module FiniteMachine
|
|
88
113
|
|
89
114
|
# Define state machine events
|
90
115
|
#
|
116
|
+
# @return [FiniteMachine::StateMachine]
|
117
|
+
#
|
91
118
|
# @api public
|
92
119
|
def events(&block)
|
93
120
|
machine.events.call(&block)
|
@@ -109,6 +136,15 @@ module FiniteMachine
|
|
109
136
|
|
110
137
|
private
|
111
138
|
|
139
|
+
# Initialize state machine properties based off attributes
|
140
|
+
#
|
141
|
+
# @api private
|
142
|
+
def initialize_attrs
|
143
|
+
attrs[:initial] and initial(attrs[:initial])
|
144
|
+
attrs[:target] and target(attrs[:target])
|
145
|
+
attrs[:terminal] and terminal(attrs[:terminal])
|
146
|
+
end
|
147
|
+
|
112
148
|
# Parse initial options
|
113
149
|
#
|
114
150
|
# @param [Object] value
|
data/lib/finite_machine/event.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module FiniteMachine
|
4
|
-
|
5
4
|
# A class responsible for event notification
|
6
5
|
class Event
|
7
6
|
include Threadable
|
@@ -56,11 +55,14 @@ module FiniteMachine
|
|
56
55
|
name.split('::').last.downcase.to_sym
|
57
56
|
end
|
58
57
|
|
58
|
+
def self.to_s
|
59
|
+
event_name
|
60
|
+
end
|
61
|
+
|
59
62
|
EVENTS.each do |event|
|
60
63
|
(class << self; self; end).class_eval do
|
61
|
-
define_method(event.event_name) { event
|
64
|
+
define_method(event.event_name) { event }
|
62
65
|
end
|
63
66
|
end
|
64
|
-
|
65
67
|
end # Event
|
66
68
|
end # FiniteMachine
|
@@ -1,11 +1,8 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module FiniteMachine
|
4
|
-
|
5
4
|
# A class responsible for running asynchronous events
|
6
5
|
class EventQueue
|
7
|
-
include Enumerable
|
8
|
-
|
9
6
|
# Initialize an event queue
|
10
7
|
#
|
11
8
|
# @example
|
@@ -13,9 +10,10 @@ module FiniteMachine
|
|
13
10
|
#
|
14
11
|
# @api public
|
15
12
|
def initialize
|
16
|
-
@queue
|
17
|
-
@mutex
|
18
|
-
@dead
|
13
|
+
@queue = Queue.new
|
14
|
+
@mutex = Mutex.new
|
15
|
+
@dead = false
|
16
|
+
@listeners = []
|
19
17
|
|
20
18
|
@thread = Thread.new do
|
21
19
|
process_events
|
@@ -48,6 +46,16 @@ module FiniteMachine
|
|
48
46
|
ensure
|
49
47
|
@mutex.unlock rescue nil
|
50
48
|
end
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
# Add listener to the queue to receive messages
|
53
|
+
#
|
54
|
+
# @api public
|
55
|
+
def subscribe(*args, &block)
|
56
|
+
listener = Listener.new
|
57
|
+
listener.on_delivery(&block)
|
58
|
+
@listeners << listener
|
51
59
|
end
|
52
60
|
|
53
61
|
# Check if there are any events to handle
|
@@ -95,31 +103,58 @@ module FiniteMachine
|
|
95
103
|
#
|
96
104
|
# @api public
|
97
105
|
def shutdown
|
106
|
+
raise EventQueueDeadError, "event queue already dead" if @dead
|
107
|
+
|
98
108
|
@mutex.lock
|
99
109
|
begin
|
110
|
+
queue = @queue
|
100
111
|
@queue.clear
|
101
112
|
@dead = true
|
102
113
|
ensure
|
103
114
|
@mutex.unlock rescue nil
|
104
115
|
end
|
116
|
+
while(!queue.empty?)
|
117
|
+
Logger.debug "Discarded message: #{queue.pop}"
|
118
|
+
end
|
105
119
|
true
|
106
120
|
end
|
107
121
|
|
122
|
+
# Get number of events waiting for processing
|
123
|
+
#
|
124
|
+
# @example
|
125
|
+
# event_queue.size
|
126
|
+
#
|
127
|
+
# @return [Integer]
|
128
|
+
#
|
129
|
+
# @api public
|
130
|
+
def size
|
131
|
+
@mutex.synchronize { @queue.size }
|
132
|
+
end
|
133
|
+
|
108
134
|
private
|
109
135
|
|
136
|
+
# Notify consumers about process event
|
137
|
+
#
|
138
|
+
# @param [FiniteMachine::AsyncCall] event
|
139
|
+
#
|
140
|
+
# @api private
|
141
|
+
def notify_listeners(event)
|
142
|
+
@listeners.each { |listener| listener.handle_delivery(event) }
|
143
|
+
end
|
144
|
+
|
110
145
|
# Process all the events
|
111
146
|
#
|
112
147
|
# @return [Thread]
|
113
148
|
#
|
114
149
|
# @api private
|
115
150
|
def process_events
|
116
|
-
until
|
151
|
+
until @dead
|
117
152
|
event = next_event
|
153
|
+
notify_listeners(event)
|
118
154
|
event.dispatch
|
119
155
|
end
|
120
156
|
rescue Exception => ex
|
121
157
|
Logger.error "Error while running event: #{ex}"
|
122
158
|
end
|
123
|
-
|
124
159
|
end # EventQueue
|
125
160
|
end # FiniteMachine
|
data/lib/finite_machine/hooks.rb
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
# A generic listener interface
|
5
|
+
class Listener
|
6
|
+
# Define event delivery handler
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
def on_delivery(&block)
|
10
|
+
@on_delivery = block
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
# Invoke event handler
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
def call(*args)
|
18
|
+
@on_delivery.call(*args) if @on_delivery
|
19
|
+
end
|
20
|
+
alias_method :handle_delivery, :call
|
21
|
+
end # Listener
|
22
|
+
end # FiniteMachine
|
@@ -3,7 +3,6 @@
|
|
3
3
|
require 'set'
|
4
4
|
|
5
5
|
module FiniteMachine
|
6
|
-
|
7
6
|
# A class responsible for observing state changes
|
8
7
|
class Observer
|
9
8
|
include Threadable
|
@@ -30,17 +29,21 @@ module FiniteMachine
|
|
30
29
|
instance_eval(&block)
|
31
30
|
end
|
32
31
|
|
33
|
-
# Register callback for a given event
|
32
|
+
# Register callback for a given event type
|
34
33
|
#
|
35
|
-
# @param [Symbol] event_type
|
36
|
-
# @param [
|
37
|
-
# @param [Proc]
|
34
|
+
# @param [Symbol, FiniteMachine::Event] event_type
|
35
|
+
# @param [Array] args
|
36
|
+
# @param [Proc] callback
|
38
37
|
#
|
39
38
|
# @api public
|
40
|
-
def on(event_type =
|
39
|
+
def on(event_type = Event, *args, &callback)
|
41
40
|
sync_exclusive do
|
41
|
+
name, async, _ = args
|
42
|
+
name = ANY_EVENT if name.nil?
|
43
|
+
async = false if async.nil?
|
42
44
|
ensure_valid_callback_name!(name)
|
43
|
-
|
45
|
+
callback.extend(Async) if async == :async
|
46
|
+
hooks.register event_type.to_s, name, callback
|
44
47
|
end
|
45
48
|
end
|
46
49
|
|
@@ -55,17 +58,17 @@ module FiniteMachine
|
|
55
58
|
|
56
59
|
module Once; end
|
57
60
|
|
61
|
+
module Async; end
|
62
|
+
|
58
63
|
def listen_on(type, *args, &callback)
|
59
64
|
name = args.first
|
60
65
|
events = []
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
else
|
66
|
-
events << :"#{type}state" << :"#{type}action"
|
66
|
+
case name
|
67
|
+
when *state_names then events << :"#{type}state"
|
68
|
+
when *event_names then events << :"#{type}action"
|
69
|
+
else events << :"#{type}state" << :"#{type}action"
|
67
70
|
end
|
68
|
-
events.each { |event| on
|
71
|
+
events.each { |event| on(Event.send(event), *args, &callback) }
|
69
72
|
end
|
70
73
|
|
71
74
|
def on_enter(*args, &callback)
|
@@ -92,37 +95,16 @@ module FiniteMachine
|
|
92
95
|
listen_on :exit, *args, &callback.extend(Once)
|
93
96
|
end
|
94
97
|
|
95
|
-
|
96
|
-
def build(_transition)
|
97
|
-
self.from = _transition.from_state
|
98
|
-
self.to = _transition.to
|
99
|
-
self.name = _transition.name
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
# Run callback
|
98
|
+
# Trigger all listeners
|
104
99
|
#
|
105
|
-
# @api
|
106
|
-
def run_callback(hook, event)
|
107
|
-
trans_event = TransitionEvent.new
|
108
|
-
trans_event.build(event.transition)
|
109
|
-
data = event.data
|
110
|
-
transition = event.transition
|
111
|
-
deferred_hook = proc do |_trans_event, *_data|
|
112
|
-
machine.instance_exec(_trans_event, *_data, &hook)
|
113
|
-
end
|
114
|
-
callable = Callable.new(deferred_hook)
|
115
|
-
result = callable.call(trans_event, *data)
|
116
|
-
transition.cancelled = (result == CANCELLED)
|
117
|
-
end
|
118
|
-
|
100
|
+
# @api public
|
119
101
|
def trigger(event, *args, &block)
|
120
102
|
sync_exclusive do
|
121
103
|
[event.type, ANY_EVENT].each do |event_type|
|
122
104
|
[event.state, ANY_STATE,
|
123
105
|
ANY_STATE_HOOK, ANY_EVENT_HOOK].each do |event_state|
|
124
106
|
hooks.call(event_type, event_state, event) do |hook|
|
125
|
-
|
107
|
+
handle_callback(hook, event)
|
126
108
|
off(event_type, event_state, &hook) if hook.is_a?(Once)
|
127
109
|
end
|
128
110
|
end
|
@@ -132,19 +114,73 @@ module FiniteMachine
|
|
132
114
|
|
133
115
|
private
|
134
116
|
|
117
|
+
# Defer callback execution
|
118
|
+
#
|
119
|
+
# @api private
|
120
|
+
def defer(callable, trans_event, *data)
|
121
|
+
async_call = AsyncCall.build(machine, callable, trans_event, *data)
|
122
|
+
machine.event_queue << async_call
|
123
|
+
end
|
124
|
+
|
125
|
+
# Create callable instance
|
126
|
+
#
|
127
|
+
# @api private
|
128
|
+
def create_callable(hook)
|
129
|
+
deferred_hook = proc do |_trans_event, *_data|
|
130
|
+
machine.instance_exec(_trans_event, *_data, &hook)
|
131
|
+
end
|
132
|
+
Callable.new(deferred_hook)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Handle callback and decide if run synchronously or asynchronously
|
136
|
+
#
|
137
|
+
# @api private
|
138
|
+
def handle_callback(hook, event)
|
139
|
+
trans_event = TransitionEvent.build(event.transition)
|
140
|
+
data = event.data
|
141
|
+
callable = create_callable(hook)
|
142
|
+
|
143
|
+
if hook.is_a?(Async)
|
144
|
+
defer(callable, trans_event, *data)
|
145
|
+
result = nil
|
146
|
+
else
|
147
|
+
result = callable.call(trans_event, *data)
|
148
|
+
end
|
149
|
+
|
150
|
+
event.transition.cancelled = (result == CANCELLED)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Set of all state names
|
154
|
+
#
|
155
|
+
# @return [Set]
|
156
|
+
#
|
157
|
+
# @api private
|
158
|
+
def state_names
|
159
|
+
@names = Set.new
|
160
|
+
@names.merge machine.states
|
161
|
+
@names.merge [ANY_STATE, ANY_STATE_HOOK]
|
162
|
+
end
|
163
|
+
|
164
|
+
# Set of all event names
|
165
|
+
#
|
166
|
+
# @return [Set]
|
167
|
+
#
|
168
|
+
# @api private
|
169
|
+
def event_names
|
170
|
+
@names = Set.new
|
171
|
+
@names.merge machine.event_names
|
172
|
+
@names.merge [ANY_EVENT, ANY_EVENT_HOOK]
|
173
|
+
end
|
174
|
+
|
135
175
|
def callback_names
|
136
|
-
|
137
|
-
@callback_names.merge machine.event_names
|
138
|
-
@callback_names.merge machine.states
|
139
|
-
@callback_names.merge [ANY_STATE, ANY_EVENT]
|
140
|
-
@callback_names.merge [ANY_STATE_HOOK, ANY_EVENT_HOOK]
|
176
|
+
state_names + event_names
|
141
177
|
end
|
142
178
|
|
143
179
|
def ensure_valid_callback_name!(name)
|
144
180
|
unless callback_names.include?(name)
|
145
181
|
exception = InvalidCallbackNameError
|
146
182
|
machine.catch_error(exception) ||
|
147
|
-
raise(
|
183
|
+
raise(exception, "#{name} is not a valid callback name." +
|
148
184
|
" Valid callback names are #{callback_names.to_a.inspect}")
|
149
185
|
end
|
150
186
|
end
|
@@ -180,6 +216,5 @@ module FiniteMachine
|
|
180
216
|
*_, callback_name = *method_name.to_s.match(/^(\w*?on_\w+?)_(\w+)$/)
|
181
217
|
callback_names.include?(:"#{callback_name}")
|
182
218
|
end
|
183
|
-
|
184
219
|
end # Observer
|
185
220
|
end # FiniteMachine
|
@@ -1,12 +1,12 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module FiniteMachine
|
4
|
-
|
5
4
|
# Base class for state machine
|
6
5
|
class StateMachine
|
7
6
|
include Threadable
|
8
7
|
include Catchable
|
9
8
|
include ThreadContext
|
9
|
+
extend Forwardable
|
10
10
|
|
11
11
|
# Initial state, defaults to :none
|
12
12
|
attr_threadsafe :initial_state
|
@@ -38,17 +38,23 @@ module FiniteMachine
|
|
38
38
|
# The state machine environment
|
39
39
|
attr_threadsafe :env
|
40
40
|
|
41
|
+
def_delegators :@dsl, :initial, :terminal, :target
|
42
|
+
|
43
|
+
def_delegator :@events, :event
|
44
|
+
|
41
45
|
# Initialize state machine
|
42
46
|
#
|
43
47
|
# @api private
|
44
48
|
def initialize(*args, &block)
|
49
|
+
attributes = args.last.is_a?(Hash) ? args.pop : {}
|
50
|
+
@initial_state = DEFAULT_STATE
|
45
51
|
@subscribers = Subscribers.new(self)
|
46
52
|
@events = EventsDSL.new(self)
|
47
53
|
@errors = ErrorsDSL.new(self)
|
48
54
|
@observer = Observer.new(self)
|
49
55
|
@transitions = Hash.new { |hash, name| hash[name] = Hash.new }
|
50
56
|
@env = Environment.new(target: self)
|
51
|
-
@dsl = DSL.new(self)
|
57
|
+
@dsl = DSL.new(self, attributes)
|
52
58
|
|
53
59
|
@dsl.call(&block) if block_given?
|
54
60
|
end
|
@@ -268,6 +274,5 @@ module FiniteMachine
|
|
268
274
|
def respond_to_missing?(method_name, include_private = false)
|
269
275
|
env.target.respond_to?(method_name.to_sym)
|
270
276
|
end
|
271
|
-
|
272
277
|
end # StateMachine
|
273
278
|
end # FiniteMachine
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module FiniteMachine
|
4
|
-
|
5
4
|
# A mixin to allow instance methods to be synchronized
|
6
5
|
module Threadable
|
7
6
|
module InstanceMethods
|
@@ -78,6 +77,5 @@ module FiniteMachine
|
|
78
77
|
end
|
79
78
|
end
|
80
79
|
end
|
81
|
-
|
82
80
|
end # Threadable
|
83
81
|
end # FiniteMachine
|
@@ -143,6 +143,7 @@ module FiniteMachine
|
|
143
143
|
transitions = machine.transitions[name]
|
144
144
|
self.from_state = machine.state
|
145
145
|
machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
|
146
|
+
machine.initial_state = machine.state if from_state == DEFAULT_STATE
|
146
147
|
end
|
147
148
|
end
|
148
149
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
# A class representing a callback transition event
|
5
|
+
class TransitionEvent
|
6
|
+
|
7
|
+
attr_accessor :from
|
8
|
+
|
9
|
+
attr_accessor :to
|
10
|
+
|
11
|
+
attr_accessor :name
|
12
|
+
|
13
|
+
# Build a transition event
|
14
|
+
#
|
15
|
+
# @return [self]
|
16
|
+
#
|
17
|
+
# @api private
|
18
|
+
def self.build(transition)
|
19
|
+
instance = new
|
20
|
+
instance.from = transition.from_state
|
21
|
+
instance.to = transition.to
|
22
|
+
instance.name = transition.name
|
23
|
+
instance
|
24
|
+
end
|
25
|
+
end # TransitionEvent
|
26
|
+
end # FiniteMachine
|
@@ -29,10 +29,10 @@ describe FiniteMachine, 'async_events' do
|
|
29
29
|
expect(called).to eql([
|
30
30
|
'on_enter_yellow_foo'
|
31
31
|
])
|
32
|
-
fsm.async
|
32
|
+
fsm.async(:stop, :bar) # execute directly
|
33
33
|
fsm.event_queue.join 0.01
|
34
34
|
expect(fsm.current).to eql(:red)
|
35
|
-
expect(called).to
|
35
|
+
expect(called).to match_array([
|
36
36
|
'on_enter_yellow_foo',
|
37
37
|
'on_enter_red_bar'
|
38
38
|
])
|
@@ -60,9 +60,39 @@ describe FiniteMachine, 'async_events' do
|
|
60
60
|
bar_thread = Thread.new { fsmBar.async.slow(:bar) }
|
61
61
|
[foo_thread, bar_thread].each(&:join)
|
62
62
|
[fsmFoo, fsmBar].each { |fsm| fsm.event_queue.join 0.02 }
|
63
|
-
expect(called).to
|
64
|
-
|
63
|
+
expect(called).to match_array([
|
64
|
+
'(foo)on_enter_yellow_foo',
|
65
|
+
'(bar)on_enter_yellow_bar'
|
66
|
+
])
|
65
67
|
expect(fsmFoo.current).to eql(:yellow)
|
66
68
|
expect(fsmBar.current).to eql(:yellow)
|
67
69
|
end
|
70
|
+
|
71
|
+
it "permits async callback" do
|
72
|
+
called = []
|
73
|
+
fsm = FiniteMachine.define do
|
74
|
+
initial :green
|
75
|
+
|
76
|
+
events {
|
77
|
+
event :slow, :green => :yellow
|
78
|
+
event :go, :yellow => :green
|
79
|
+
}
|
80
|
+
|
81
|
+
callbacks {
|
82
|
+
on_enter :green, :async do |event| called << 'on_enter_green' end
|
83
|
+
on_enter :slow, :async do |event| called << 'on_enter_slow' end
|
84
|
+
on_exit :yellow, :async do |event| called << 'on_exit_yellow' end
|
85
|
+
on_exit :go, :async do |event| called << 'on_exit_go' end
|
86
|
+
}
|
87
|
+
end
|
88
|
+
fsm.slow
|
89
|
+
fsm.go
|
90
|
+
sleep 0.1
|
91
|
+
expect(called).to match_array([
|
92
|
+
'on_enter_green',
|
93
|
+
'on_enter_slow',
|
94
|
+
'on_exit_go',
|
95
|
+
'on_exit_yellow'
|
96
|
+
])
|
97
|
+
end
|
68
98
|
end
|
data/spec/unit/define_spec.rb
CHANGED
@@ -4,28 +4,51 @@ require 'spec_helper'
|
|
4
4
|
|
5
5
|
describe FiniteMachine, 'define' do
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
7
|
+
context 'with block' do
|
8
|
+
it "creates system state machine" do
|
9
|
+
fsm = FiniteMachine.define do
|
10
|
+
initial :green
|
11
|
+
|
12
|
+
events {
|
13
|
+
event :slow, :green => :yellow
|
14
|
+
event :stop, :yellow => :red
|
15
|
+
event :ready, :red => :yellow
|
16
|
+
event :go, :yellow => :green
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
expect(fsm.current).to eql(:green)
|
21
|
+
|
22
|
+
fsm.slow
|
23
|
+
expect(fsm.current).to eql(:yellow)
|
24
|
+
fsm.stop
|
25
|
+
expect(fsm.current).to eql(:red)
|
26
|
+
fsm.ready
|
27
|
+
expect(fsm.current).to eql(:yellow)
|
28
|
+
fsm.go
|
29
|
+
expect(fsm.current).to eql(:green)
|
17
30
|
end
|
31
|
+
end
|
18
32
|
|
19
|
-
|
33
|
+
context 'without block' do
|
34
|
+
it "creates state machine" do
|
35
|
+
called = []
|
36
|
+
fsm = FiniteMachine.define
|
37
|
+
fsm.initial(:green)
|
38
|
+
fsm.event(:slow, :green => :yellow)
|
39
|
+
fsm.event(:stop, :yellow => :red)
|
40
|
+
fsm.event(:ready,:red => :yellow)
|
41
|
+
fsm.event(:go, :yellow => :green)
|
42
|
+
fsm.on_enter(:yellow) { |event| called << 'on_enter_yellow' }
|
43
|
+
fsm.handle(FiniteMachine::InvalidStateError) { |exception|
|
44
|
+
called << 'error_handler'
|
45
|
+
}
|
20
46
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
expect(fsm.current).to eql(:yellow)
|
27
|
-
fsm.go
|
28
|
-
expect(fsm.current).to eql(:green)
|
47
|
+
expect(fsm.current).to eql(:green)
|
48
|
+
fsm.slow
|
49
|
+
fsm.ready
|
50
|
+
expect(called).to match_array(['on_enter_yellow', 'error_handler'])
|
51
|
+
end
|
29
52
|
end
|
30
53
|
|
31
54
|
xit "creates multiple machines"
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe FiniteMachine::EventQueue do
|
6
|
+
|
7
|
+
subject(:event_queue) { described_class.new }
|
8
|
+
|
9
|
+
it "dispatches all events" do
|
10
|
+
called = []
|
11
|
+
event1 = double(:event1, dispatch: called << 'event1_dispatched')
|
12
|
+
event2 = double(:event2, dispatch: called << 'event2_dispatched')
|
13
|
+
expect(event_queue.size).to be_zero
|
14
|
+
event_queue << event1
|
15
|
+
event_queue << event2
|
16
|
+
sleep 0.001
|
17
|
+
expect(called).to match_array(['event1_dispatched', 'event2_dispatched'])
|
18
|
+
end
|
19
|
+
|
20
|
+
it "logs error" do
|
21
|
+
event = double(:event)
|
22
|
+
expect(FiniteMachine::Logger).to receive(:error)
|
23
|
+
event_queue << event
|
24
|
+
sleep 0.01
|
25
|
+
expect(event_queue).to be_empty
|
26
|
+
end
|
27
|
+
|
28
|
+
it "notifies listeners" do
|
29
|
+
called = []
|
30
|
+
event1 = double(:event1, dispatch: true)
|
31
|
+
event2 = double(:event2, dispatch: true)
|
32
|
+
event3 = double(:event3, dispatch: true)
|
33
|
+
event_queue.subscribe(:listener1) { |event| called << event }
|
34
|
+
event_queue << event1 << event2 << event3
|
35
|
+
sleep 0.01
|
36
|
+
expect(called).to match_array([event1, event2, event3])
|
37
|
+
end
|
38
|
+
|
39
|
+
it "allows to shutdown event queue" do
|
40
|
+
event1 = double(:event1, dispatch: true)
|
41
|
+
event2 = double(:event2, dispatch: true)
|
42
|
+
event3 = double(:event3, dispatch: true)
|
43
|
+
expect(event_queue.alive?).to be_true
|
44
|
+
event_queue << event1
|
45
|
+
event_queue << event2
|
46
|
+
event_queue.shutdown
|
47
|
+
event_queue << event3
|
48
|
+
sleep 0.001
|
49
|
+
expect(event_queue.alive?).to be_false
|
50
|
+
end
|
51
|
+
end
|
data/spec/unit/events_spec.rb
CHANGED
@@ -21,6 +21,17 @@ describe FiniteMachine, 'events' do
|
|
21
21
|
expect(fsm.current).to eql(:red)
|
22
22
|
end
|
23
23
|
|
24
|
+
it "allows to add event without events scope" do
|
25
|
+
fsm = FiniteMachine.define do
|
26
|
+
initial :green
|
27
|
+
|
28
|
+
event :slow, :green => :yellow
|
29
|
+
event :stop, :yellow => :red
|
30
|
+
end
|
31
|
+
|
32
|
+
expect(fsm.current).to eql(:green)
|
33
|
+
end
|
34
|
+
|
24
35
|
it "allows for (:from | :to) key pairs to describe transition" do
|
25
36
|
fsm = FiniteMachine.define do
|
26
37
|
initial :green
|
data/spec/unit/finished_spec.rb
CHANGED
@@ -27,6 +27,20 @@ describe FiniteMachine, 'finished?' do
|
|
27
27
|
expect(fsm.finished?).to be_true
|
28
28
|
end
|
29
29
|
|
30
|
+
it "allows to specify terminal state as parameter" do
|
31
|
+
fsm = FiniteMachine.define terminal: :red do
|
32
|
+
initial :green
|
33
|
+
|
34
|
+
events {
|
35
|
+
event :slow, :green => :yellow
|
36
|
+
event :stop, :yellow => :red
|
37
|
+
}
|
38
|
+
end
|
39
|
+
fsm.slow
|
40
|
+
fsm.stop
|
41
|
+
expect(fsm.finished?).to be_true
|
42
|
+
end
|
43
|
+
|
30
44
|
it "checks without terminal state" do
|
31
45
|
fsm = FiniteMachine.define do
|
32
46
|
initial :green
|
@@ -56,6 +56,16 @@ describe FiniteMachine, 'initialize' do
|
|
56
56
|
expect(called).to be_empty
|
57
57
|
end
|
58
58
|
|
59
|
+
it "allows to specify initial state through parameter" do
|
60
|
+
fsm = FiniteMachine.define initial: :green do
|
61
|
+
events {
|
62
|
+
event :slow, :green => :yellow
|
63
|
+
event :stop, :yellow => :red
|
64
|
+
}
|
65
|
+
end
|
66
|
+
expect(fsm.current).to eql(:green)
|
67
|
+
end
|
68
|
+
|
59
69
|
it "allows to specify deferred inital state" do
|
60
70
|
fsm = FiniteMachine.define do
|
61
71
|
initial state: :green, defer: true
|
@@ -143,4 +153,36 @@ describe FiniteMachine, 'initialize' do
|
|
143
153
|
fsm.b
|
144
154
|
expect(fsm.current).to eql(3)
|
145
155
|
end
|
156
|
+
|
157
|
+
it "allows to retrieve initial state" do
|
158
|
+
fsm = FiniteMachine.define do
|
159
|
+
initial :green
|
160
|
+
|
161
|
+
events {
|
162
|
+
event :slow, :green => :yellow
|
163
|
+
event :stop, :yellow => :red
|
164
|
+
}
|
165
|
+
end
|
166
|
+
expect(fsm.current).to eq(:green)
|
167
|
+
expect(fsm.initial_state).to eq(:green)
|
168
|
+
fsm.slow
|
169
|
+
expect(fsm.current).to eq(:yellow)
|
170
|
+
expect(fsm.initial_state).to eq(:green)
|
171
|
+
end
|
172
|
+
|
173
|
+
it "allows to retrieve initial state for deferred" do
|
174
|
+
fsm = FiniteMachine.define do
|
175
|
+
initial state: :green, defer: true
|
176
|
+
|
177
|
+
events {
|
178
|
+
event :slow, :green => :yellow
|
179
|
+
event :stop, :yellow => :red
|
180
|
+
}
|
181
|
+
end
|
182
|
+
expect(fsm.current).to eq(:none)
|
183
|
+
expect(fsm.initial_state).to eq(:none)
|
184
|
+
fsm.init
|
185
|
+
expect(fsm.current).to eq(:green)
|
186
|
+
expect(fsm.initial_state).to eq(:green)
|
187
|
+
end
|
146
188
|
end
|
data/spec/unit/target_spec.rb
CHANGED
@@ -151,4 +151,27 @@ describe FiniteMachine, '#target' do
|
|
151
151
|
'on_enter_forward with Piotr!'
|
152
152
|
])
|
153
153
|
end
|
154
|
+
|
155
|
+
it "allows to access target inside the callback" do
|
156
|
+
context = double(:context)
|
157
|
+
called = nil
|
158
|
+
fsm = FiniteMachine.define do
|
159
|
+
initial :green
|
160
|
+
|
161
|
+
target context
|
162
|
+
|
163
|
+
events {
|
164
|
+
event :slow, :green => :yellow
|
165
|
+
event :stop, :yellow => :red
|
166
|
+
}
|
167
|
+
callbacks {
|
168
|
+
on_enter_yellow do |event|
|
169
|
+
called = target
|
170
|
+
end
|
171
|
+
}
|
172
|
+
end
|
173
|
+
expect(fsm.current).to eql(:green)
|
174
|
+
fsm.slow
|
175
|
+
expect(called).to eq(context)
|
176
|
+
end
|
154
177
|
end
|
data/tasks/console.rake
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: finite_machine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Murach
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-04-
|
11
|
+
date: 2014-04-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -55,6 +55,7 @@ files:
|
|
55
55
|
- lib/finite_machine/event.rb
|
56
56
|
- lib/finite_machine/event_queue.rb
|
57
57
|
- lib/finite_machine/hooks.rb
|
58
|
+
- lib/finite_machine/listener.rb
|
58
59
|
- lib/finite_machine/logger.rb
|
59
60
|
- lib/finite_machine/observer.rb
|
60
61
|
- lib/finite_machine/state_machine.rb
|
@@ -62,6 +63,7 @@ files:
|
|
62
63
|
- lib/finite_machine/thread_context.rb
|
63
64
|
- lib/finite_machine/threadable.rb
|
64
65
|
- lib/finite_machine/transition.rb
|
66
|
+
- lib/finite_machine/transition_event.rb
|
65
67
|
- lib/finite_machine/version.rb
|
66
68
|
- spec/spec_helper.rb
|
67
69
|
- spec/unit/async_events_spec.rb
|
@@ -69,6 +71,7 @@ files:
|
|
69
71
|
- spec/unit/callbacks_spec.rb
|
70
72
|
- spec/unit/can_spec.rb
|
71
73
|
- spec/unit/define_spec.rb
|
74
|
+
- spec/unit/event_queue_spec.rb
|
72
75
|
- spec/unit/events_spec.rb
|
73
76
|
- spec/unit/finished_spec.rb
|
74
77
|
- spec/unit/handlers_spec.rb
|
@@ -115,6 +118,7 @@ test_files:
|
|
115
118
|
- spec/unit/callbacks_spec.rb
|
116
119
|
- spec/unit/can_spec.rb
|
117
120
|
- spec/unit/define_spec.rb
|
121
|
+
- spec/unit/event_queue_spec.rb
|
118
122
|
- spec/unit/events_spec.rb
|
119
123
|
- spec/unit/finished_spec.rb
|
120
124
|
- spec/unit/handlers_spec.rb
|