finite_machine 0.2.0 → 0.3.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/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +12 -1
- data/README.md +130 -73
- data/Rakefile +3 -36
- data/finite_machine.gemspec +1 -3
- data/lib/finite_machine.rb +4 -1
- data/lib/finite_machine/async_call.rb +47 -0
- data/lib/finite_machine/async_proxy.rb +28 -0
- data/lib/finite_machine/callable.rb +5 -5
- data/lib/finite_machine/catchable.rb +2 -2
- data/lib/finite_machine/dsl.rb +16 -9
- data/lib/finite_machine/event.rb +1 -1
- data/lib/finite_machine/event_queue.rb +123 -0
- data/lib/finite_machine/hooks.rb +33 -0
- data/lib/finite_machine/observer.rb +53 -11
- data/lib/finite_machine/state_machine.rb +70 -2
- data/lib/finite_machine/subscribers.rb +5 -2
- data/lib/finite_machine/thread_context.rb +16 -0
- data/lib/finite_machine/threadable.rb +36 -3
- data/lib/finite_machine/transition.rb +47 -4
- data/lib/finite_machine/version.rb +1 -1
- data/spec/spec_helper.rb +15 -0
- data/spec/unit/async_events_spec.rb +68 -0
- data/spec/unit/callable/call_spec.rb +25 -3
- data/spec/unit/callbacks_spec.rb +110 -4
- data/spec/unit/events_spec.rb +5 -0
- data/spec/unit/handlers_spec.rb +53 -0
- data/spec/unit/if_unless_spec.rb +1 -1
- data/spec/unit/is_spec.rb +22 -0
- data/spec/unit/target_spec.rb +17 -0
- data/tasks/console.rake +10 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- metadata +16 -32
@@ -6,6 +6,7 @@ module FiniteMachine
|
|
6
6
|
class StateMachine
|
7
7
|
include Threadable
|
8
8
|
include Catchable
|
9
|
+
include ThreadContext
|
9
10
|
|
10
11
|
# Initial state, defaults to :none
|
11
12
|
attr_threadsafe :initial_state
|
@@ -51,8 +52,13 @@ module FiniteMachine
|
|
51
52
|
@dsl = DSL.new self
|
52
53
|
@dsl.call(&block) if block_given?
|
53
54
|
send(:"#{@dsl.initial_event}") unless @dsl.defer
|
55
|
+
self.event_queue = FiniteMachine::EventQueue.new
|
54
56
|
end
|
55
57
|
|
58
|
+
# @example
|
59
|
+
# machine.subscribe(Observer.new(machine))
|
60
|
+
#
|
61
|
+
# @api public
|
56
62
|
def subscribe(*observers)
|
57
63
|
@subscribers.subscribe(*observers)
|
58
64
|
end
|
@@ -70,6 +76,30 @@ module FiniteMachine
|
|
70
76
|
end
|
71
77
|
end
|
72
78
|
|
79
|
+
# Help to mark the event as synchronous
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# fsm.sync.go
|
83
|
+
#
|
84
|
+
# @return [self]
|
85
|
+
#
|
86
|
+
# @api public
|
87
|
+
alias_method :sync, :method_missing
|
88
|
+
|
89
|
+
# Explicitly invoke event on proxy or delegate to proxy
|
90
|
+
#
|
91
|
+
# @return [AsyncProxy]
|
92
|
+
#
|
93
|
+
# @api public
|
94
|
+
def async(method_name = nil, *args, &block)
|
95
|
+
@async_proxy = AsyncProxy.new(self)
|
96
|
+
if method_name
|
97
|
+
@async_proxy.method_missing method_name, *args, &block
|
98
|
+
else
|
99
|
+
@async_proxy
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
73
103
|
# Get current state
|
74
104
|
#
|
75
105
|
# @return [String]
|
@@ -79,7 +109,10 @@ module FiniteMachine
|
|
79
109
|
state
|
80
110
|
end
|
81
111
|
|
82
|
-
# Check if current state
|
112
|
+
# Check if current state matches provided state
|
113
|
+
#
|
114
|
+
# @example
|
115
|
+
# fsm.is?(:green) # => true
|
83
116
|
#
|
84
117
|
# @param [String, Array[String]] state
|
85
118
|
#
|
@@ -96,6 +129,9 @@ module FiniteMachine
|
|
96
129
|
|
97
130
|
# Retrieve all states
|
98
131
|
#
|
132
|
+
# @example
|
133
|
+
# fsm.states # => [:yellow, :green, :red]
|
134
|
+
#
|
99
135
|
# @return [Array[Symbol]]
|
100
136
|
#
|
101
137
|
# @api public
|
@@ -114,6 +150,9 @@ module FiniteMachine
|
|
114
150
|
|
115
151
|
# Checks if event can be triggered
|
116
152
|
#
|
153
|
+
# @example
|
154
|
+
# fsm.can?(:go) # => true
|
155
|
+
#
|
117
156
|
# @param [String] event
|
118
157
|
#
|
119
158
|
# @return [Boolean]
|
@@ -125,6 +164,9 @@ module FiniteMachine
|
|
125
164
|
|
126
165
|
# Checks if event cannot be triggered
|
127
166
|
#
|
167
|
+
# @example
|
168
|
+
# fsm.cannot?(:go) # => false
|
169
|
+
#
|
128
170
|
# @param [String] event
|
129
171
|
#
|
130
172
|
# @return [Boolean]
|
@@ -160,6 +202,12 @@ module FiniteMachine
|
|
160
202
|
|
161
203
|
# Performs transition
|
162
204
|
#
|
205
|
+
# @param [Transition] _transition
|
206
|
+
# @param [Array] args
|
207
|
+
#
|
208
|
+
# @return [Integer]
|
209
|
+
# the status code for the transition
|
210
|
+
#
|
163
211
|
# @api private
|
164
212
|
def transition(_transition, *args, &block)
|
165
213
|
return CANCELLED if valid_state?(_transition)
|
@@ -191,14 +239,34 @@ module FiniteMachine
|
|
191
239
|
SUCCEEDED
|
192
240
|
end
|
193
241
|
|
242
|
+
# Forward the message to target, observer or self
|
243
|
+
#
|
244
|
+
# @param [String] method_name
|
245
|
+
#
|
246
|
+
# @param [Array] args
|
247
|
+
#
|
248
|
+
# @return [self]
|
249
|
+
#
|
250
|
+
# @api private
|
194
251
|
def method_missing(method_name, *args, &block)
|
195
252
|
if env.target.respond_to?(method_name.to_sym)
|
196
|
-
env.target.
|
253
|
+
env.target.public_send(method_name.to_sym, *args, &block)
|
254
|
+
elsif observer.respond_to?(method_name.to_sym)
|
255
|
+
observer.public_send(method_name.to_sym, *args, &block)
|
197
256
|
else
|
198
257
|
super
|
199
258
|
end
|
200
259
|
end
|
201
260
|
|
261
|
+
# Test if a message can be handled by state machine
|
262
|
+
#
|
263
|
+
# @param [String] method_name
|
264
|
+
#
|
265
|
+
# @param [Boolean] include_private
|
266
|
+
#
|
267
|
+
# @return [Boolean]
|
268
|
+
#
|
269
|
+
# @api private
|
202
270
|
def respond_to_missing?(method_name, include_private = false)
|
203
271
|
env.target.respond_to?(method_name.to_sym)
|
204
272
|
end
|
@@ -1,15 +1,18 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
require 'monitor'
|
4
|
+
|
3
5
|
module FiniteMachine
|
4
6
|
|
5
7
|
# A class responsibile for storage of event subscribers
|
6
8
|
class Subscribers
|
7
9
|
include Enumerable
|
10
|
+
include MonitorMixin
|
8
11
|
|
9
12
|
def initialize(machine)
|
13
|
+
super()
|
10
14
|
@machine = machine
|
11
15
|
@subscribers = []
|
12
|
-
@mutex = Mutex.new
|
13
16
|
end
|
14
17
|
|
15
18
|
def each(&block)
|
@@ -29,7 +32,7 @@ module FiniteMachine
|
|
29
32
|
end
|
30
33
|
|
31
34
|
def visit(event)
|
32
|
-
each { |subscriber| event.notify subscriber }
|
35
|
+
each { |subscriber| synchronize { event.notify subscriber } }
|
33
36
|
end
|
34
37
|
|
35
38
|
def reset
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
|
5
|
+
# A mixin to allow sharing of thread context
|
6
|
+
module ThreadContext
|
7
|
+
|
8
|
+
def event_queue
|
9
|
+
Thread.current[:finite_machine_event_queue]
|
10
|
+
end
|
11
|
+
|
12
|
+
def event_queue=(value)
|
13
|
+
Thread.current[:finite_machine_event_queue] = value
|
14
|
+
end
|
15
|
+
end # ThreadContext
|
16
|
+
end # FiniteMachine
|
@@ -7,15 +7,30 @@ module FiniteMachine
|
|
7
7
|
module InstanceMethods
|
8
8
|
@@sync = Sync.new
|
9
9
|
|
10
|
+
# Exclusive lock
|
11
|
+
#
|
12
|
+
# @return [nil]
|
13
|
+
#
|
14
|
+
# @api public
|
10
15
|
def sync_exclusive(&block)
|
11
16
|
@@sync.synchronize(:EX, &block)
|
12
17
|
end
|
13
18
|
|
19
|
+
# Shared lock
|
20
|
+
#
|
21
|
+
# @return [nil]
|
22
|
+
#
|
23
|
+
# @api public
|
14
24
|
def sync_shared(&block)
|
15
25
|
@@sync.synchronize(:SH, &block)
|
16
26
|
end
|
17
27
|
end
|
18
28
|
|
29
|
+
# Module hook
|
30
|
+
#
|
31
|
+
# @return [nil]
|
32
|
+
#
|
33
|
+
# @api private
|
19
34
|
def self.included(base)
|
20
35
|
base.extend ClassMethods
|
21
36
|
base.module_eval do
|
@@ -23,17 +38,35 @@ module FiniteMachine
|
|
23
38
|
end
|
24
39
|
end
|
25
40
|
|
41
|
+
private_class_method :included
|
42
|
+
|
26
43
|
module ClassMethods
|
27
44
|
include InstanceMethods
|
28
45
|
|
46
|
+
# Defines threadsafe attributes for a class
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
# attr_threadable :errors, :events
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# attr_threadable :errors, default: []
|
53
|
+
#
|
54
|
+
# @return [nil]
|
55
|
+
#
|
56
|
+
# @api public
|
29
57
|
def attr_threadsafe(*attrs)
|
58
|
+
opts = attrs.last.is_a?(::Hash) ? attrs.pop : {}
|
59
|
+
default = opts.fetch(:default, nil)
|
30
60
|
attrs.flatten.each do |attr|
|
31
61
|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
32
62
|
def #{attr}(*args)
|
33
|
-
|
63
|
+
value = args.shift
|
64
|
+
if value
|
65
|
+
self.#{attr} = value
|
66
|
+
elsif instance_variables.include?(:@#{attr})
|
34
67
|
sync_shared { @#{attr} }
|
35
|
-
|
36
|
-
|
68
|
+
elsif #{!default.nil?}
|
69
|
+
sync_shared { instance_variable_set(:@#{attr}, #{default}) }
|
37
70
|
end
|
38
71
|
end
|
39
72
|
alias_method '#{attr}?', '#{attr}'
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module FiniteMachine
|
4
|
-
|
5
4
|
# Class describing a transition associated with a given event
|
6
5
|
class Transition
|
7
6
|
include Threadable
|
@@ -25,6 +24,9 @@ module FiniteMachine
|
|
25
24
|
|
26
25
|
# Initialize a Transition
|
27
26
|
#
|
27
|
+
# @param [StateMachine] machine
|
28
|
+
# @param [Hash] attrs
|
29
|
+
#
|
28
30
|
# @api public
|
29
31
|
def initialize(machine, attrs = {})
|
30
32
|
@machine = machine
|
@@ -36,6 +38,9 @@ module FiniteMachine
|
|
36
38
|
@conditions = make_conditions
|
37
39
|
end
|
38
40
|
|
41
|
+
# Reduce conditions
|
42
|
+
#
|
43
|
+
# @api private
|
39
44
|
def make_conditions
|
40
45
|
@if.map { |c| Callable.new(c) } +
|
41
46
|
@unless.map { |c| Callable.new(c).invert }
|
@@ -43,6 +48,8 @@ module FiniteMachine
|
|
43
48
|
|
44
49
|
# Extract states from attributes
|
45
50
|
#
|
51
|
+
# @param [Hash] attrs
|
52
|
+
#
|
46
53
|
# @api private
|
47
54
|
def parse_states(attrs)
|
48
55
|
_attrs = attrs.dup
|
@@ -67,19 +74,44 @@ module FiniteMachine
|
|
67
74
|
end
|
68
75
|
end
|
69
76
|
|
77
|
+
# Define helper state mehods for the transition states
|
78
|
+
#
|
79
|
+
# @api private
|
80
|
+
def define_state_methods
|
81
|
+
from.concat([to]).each { |state| define_state_method(state) }
|
82
|
+
end
|
83
|
+
|
84
|
+
# Define state helper method
|
85
|
+
#
|
86
|
+
# @param [Symbol] state
|
87
|
+
#
|
88
|
+
# @api private
|
89
|
+
def define_state_method(state)
|
90
|
+
return if machine.respond_to?("#{state}?")
|
91
|
+
machine.send(:define_singleton_method, "#{state}?") do
|
92
|
+
machine.is?(state.to_sym)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
70
96
|
# Define event on the machine
|
71
97
|
#
|
72
98
|
# @api private
|
73
99
|
def define_event
|
74
100
|
_transition = self
|
75
|
-
|
76
|
-
|
101
|
+
_name = name
|
102
|
+
|
103
|
+
machine.singleton_class.class_eval do
|
104
|
+
undef_method(_name) if method_defined?(_name)
|
105
|
+
end
|
106
|
+
machine.send(:define_singleton_method, name) do |*args, &block|
|
77
107
|
transition(_transition, *args, &block)
|
78
108
|
end
|
79
109
|
end
|
80
110
|
|
81
111
|
# Execute current transition
|
82
112
|
#
|
113
|
+
# @return [nil]
|
114
|
+
#
|
83
115
|
# @api private
|
84
116
|
def call
|
85
117
|
sync_exclusive do
|
@@ -96,6 +128,11 @@ module FiniteMachine
|
|
96
128
|
@name
|
97
129
|
end
|
98
130
|
|
131
|
+
# Return string representation
|
132
|
+
#
|
133
|
+
# @return [String]
|
134
|
+
#
|
135
|
+
# @api public
|
99
136
|
def inspect
|
100
137
|
"<#{self.class} name: #{@name}, transitions: #{@from} => #{@to}, when: #{@conditions}>"
|
101
138
|
end
|
@@ -104,10 +141,16 @@ module FiniteMachine
|
|
104
141
|
|
105
142
|
# Raise error when not enough transitions are provided
|
106
143
|
#
|
144
|
+
# @param [Hash] attrs
|
145
|
+
#
|
146
|
+
# @raise [NotEnoughTransitionsError]
|
147
|
+
# if the event has not enough transition arguments
|
148
|
+
#
|
149
|
+
# @return [nil]
|
150
|
+
#
|
107
151
|
# @api private
|
108
152
|
def raise_not_enough_transitions(attrs)
|
109
153
|
raise NotEnoughTransitionsError, "please provide state transitions for '#{attrs.inspect}'"
|
110
154
|
end
|
111
|
-
|
112
155
|
end # Transition
|
113
156
|
end # FiniteMachine
|
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
if RUBY_VERSION > '1.9' and (ENV['COVERAGE'] || ENV['TRAVIS'])
|
4
|
+
require 'simplecov'
|
5
|
+
require 'coveralls'
|
6
|
+
|
7
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
8
|
+
SimpleCov::Formatter::HTMLFormatter,
|
9
|
+
Coveralls::SimpleCov::Formatter
|
10
|
+
]
|
11
|
+
|
12
|
+
SimpleCov.start do
|
13
|
+
command_name 'spec'
|
14
|
+
add_filter 'spec'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
3
18
|
require 'finite_machine'
|
4
19
|
|
5
20
|
RSpec.configure do |config|
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe FiniteMachine, 'async_events' do
|
6
|
+
|
7
|
+
it 'runs events asynchronously' do
|
8
|
+
called = []
|
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
|
+
|
19
|
+
callbacks {
|
20
|
+
on_enter :yellow do |event, a| called << "on_enter_yellow_#{a}" end
|
21
|
+
on_enter :red do |event, a| called << "on_enter_red_#{a}" end
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
expect(fsm.current).to eql(:green)
|
26
|
+
fsm.async.slow(:foo)
|
27
|
+
fsm.event_queue.join 0.01
|
28
|
+
expect(fsm.current).to eql(:yellow)
|
29
|
+
expect(called).to eql([
|
30
|
+
'on_enter_yellow_foo'
|
31
|
+
])
|
32
|
+
fsm.async.stop(:bar)
|
33
|
+
fsm.event_queue.join 0.01
|
34
|
+
expect(fsm.current).to eql(:red)
|
35
|
+
expect(called).to eql([
|
36
|
+
'on_enter_yellow_foo',
|
37
|
+
'on_enter_red_bar'
|
38
|
+
])
|
39
|
+
end
|
40
|
+
|
41
|
+
it "ensure queue per thread" do
|
42
|
+
called = []
|
43
|
+
fsmFoo = FiniteMachine.define do
|
44
|
+
initial :green
|
45
|
+
events { event :slow, :green => :yellow }
|
46
|
+
|
47
|
+
callbacks {
|
48
|
+
on_enter :yellow do |event, a| called << "(foo)on_enter_yellow_#{a}" end
|
49
|
+
}
|
50
|
+
end
|
51
|
+
fsmBar = FiniteMachine.define do
|
52
|
+
initial :green
|
53
|
+
events { event :slow, :green => :yellow }
|
54
|
+
|
55
|
+
callbacks {
|
56
|
+
on_enter :yellow do |event, a| called << "(bar)on_enter_yellow_#{a}" end
|
57
|
+
}
|
58
|
+
end
|
59
|
+
fsmFoo.slow(:foo)
|
60
|
+
fsmBar.slow(:bar)
|
61
|
+
fsmFoo.event_queue.join 0.01
|
62
|
+
fsmBar.event_queue.join 0.01
|
63
|
+
expect(called).to include('(foo)on_enter_yellow_foo')
|
64
|
+
expect(called).to include('(bar)on_enter_yellow_bar')
|
65
|
+
expect(fsmFoo.current).to eql(:yellow)
|
66
|
+
expect(fsmBar.current).to eql(:yellow)
|
67
|
+
end
|
68
|
+
end
|