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.
@@ -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 machtes provided 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.send(method_name.to_sym, *args, &block)
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
- if args.empty?
63
+ value = args.shift
64
+ if value
65
+ self.#{attr} = value
66
+ elsif instance_variables.include?(:@#{attr})
34
67
  sync_shared { @#{attr} }
35
- else
36
- self.#{attr} = args.shift
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
- # TODO check if event is already defined and raise error
76
- machine.class.__send__(:define_method, name) do |*args, &block|
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
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
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