finite_machine 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,6 +13,13 @@ module FiniteMachine
13
13
 
14
14
  attr_threadsafe :block
15
15
 
16
+ # Create an AsynCall
17
+ #
18
+ # @api private
19
+ def initialize
20
+ @mutex = Mutex.new
21
+ end
22
+
16
23
  # Build asynchronous call instance
17
24
  #
18
25
  # @param [Object] context
@@ -41,7 +48,9 @@ module FiniteMachine
41
48
  #
42
49
  # @api private
43
50
  def dispatch
44
- callable.call(context, *arguments, block)
51
+ @mutex.synchronize do
52
+ callable.call(context, *arguments, block)
53
+ end
45
54
  end
46
55
  end # AsyncCall
47
56
  end # FiniteMachine
@@ -1,7 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
-
5
4
  # A generic DSL for describing the state machine
6
5
  class GenericDSL
7
6
  include Threadable
@@ -32,7 +31,6 @@ module FiniteMachine
32
31
  end # GenericDSL
33
32
 
34
33
  class DSL < GenericDSL
35
-
36
34
  attr_threadsafe :defer
37
35
 
38
36
  attr_threadsafe :initial_event
@@ -48,12 +46,20 @@ module FiniteMachine
48
46
 
49
47
  # Define initial state
50
48
  #
49
+ # @example
50
+ # initial :green
51
+ #
52
+ # @example
53
+ # initial state: green, defer: true
54
+ #
51
55
  # @param [String, Hash] value
52
56
  #
57
+ # @return [StateMachine]
58
+ #
53
59
  # @api public
54
60
  def initial(value)
55
61
  state, name, self.defer = parse(value)
56
- self.initial_event = name
62
+ machine.state = state unless defer
57
63
  event = proc { event name, from: FiniteMachine::DEFAULT_STATE, to: state }
58
64
  machine.events.call(&event)
59
65
  end
@@ -70,6 +76,11 @@ module FiniteMachine
70
76
 
71
77
  # Define terminal state
72
78
  #
79
+ # @example
80
+ # terminal :red
81
+ #
82
+ # @return [StateMachine]
83
+ #
73
84
  # @api public
74
85
  def terminal(value)
75
86
  machine.final_state = value
@@ -100,19 +111,28 @@ module FiniteMachine
100
111
 
101
112
  # Parse initial options
102
113
  #
103
- # @param [String, Hash] value
114
+ # @param [Object] value
104
115
  #
105
116
  # @return [Array[Symbol,String]]
106
117
  #
107
118
  # @api private
108
119
  def parse(value)
109
- if value.is_a?(String) || value.is_a?(Symbol)
120
+ unless value.is_a?(Hash)
110
121
  [value, FiniteMachine::DEFAULT_EVENT_NAME, false]
111
122
  else
112
- [value[:state], value.fetch(:event, FiniteMachine::DEFAULT_EVENT_NAME),
113
- !!value[:defer]]
123
+ [value.fetch(:state) { raise_missing_state },
124
+ value.fetch(:event) { FiniteMachine::DEFAULT_EVENT_NAME },
125
+ value.fetch(:defer) { false }]
114
126
  end
115
127
  end
128
+
129
+ # Raises missing state error
130
+ #
131
+ # @api private
132
+ def raise_missing_state
133
+ raise MissingInitialStateError,
134
+ 'Provide state to transition :to for the initial event'
135
+ end
116
136
  end # DSL
117
137
 
118
138
  class EventsDSL < GenericDSL
@@ -151,6 +171,5 @@ module FiniteMachine
151
171
  def handle(*exceptions, &block)
152
172
  machine.handle(*exceptions, &block)
153
173
  end
154
-
155
174
  end # ErrorsDSL
156
175
  end # FiniteMachine
@@ -16,7 +16,10 @@ module FiniteMachine
16
16
  @queue = Queue.new
17
17
  @mutex = Mutex.new
18
18
  @dead = false
19
- run
19
+
20
+ @thread = Thread.new do
21
+ process_events
22
+ end
20
23
  end
21
24
 
22
25
  # Retrieve the next event
@@ -104,20 +107,19 @@ module FiniteMachine
104
107
 
105
108
  private
106
109
 
107
- # Run all the events
110
+ # Process all the events
108
111
  #
109
112
  # @return [Thread]
110
113
  #
111
114
  # @api private
112
- def run
113
- @thread = Thread.new do
114
- Thread.current.abort_on_exception = true
115
- until(@dead) do
116
- event = next_event
117
- Thread.exit unless event
118
- event.dispatch
119
- end
115
+ def process_events
116
+ until(@dead) do
117
+ event = next_event
118
+ event.dispatch
120
119
  end
120
+ rescue Exception => ex
121
+ Logger.error "Error while running event: #{ex}"
121
122
  end
123
+
122
124
  end # EventQueue
123
125
  end # FiniteMachine
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+ module Logger
5
+ module_function
6
+
7
+ def debug(message)
8
+ FiniteMachine.logger.debug(message)
9
+ end
10
+
11
+ def info(message)
12
+ FiniteMachine.logger.info(message)
13
+ end
14
+
15
+ def warn(message)
16
+ FiniteMachine.logger.warn(message)
17
+ end
18
+
19
+ def error(message)
20
+ FiniteMachine.logger.error(message)
21
+ end
22
+ end # Logger
23
+ end # FiniteMachine
@@ -1,5 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'set'
4
+
3
5
  module FiniteMachine
4
6
 
5
7
  # A class responsible for observing state changes
@@ -98,14 +100,20 @@ module FiniteMachine
98
100
  end
99
101
  end
100
102
 
103
+ # Run callback
104
+ #
105
+ # @api private
101
106
  def run_callback(hook, event)
102
107
  trans_event = TransitionEvent.new
103
108
  trans_event.build(event.transition)
104
109
  data = event.data
105
- deferred_hook = proc { |_trans_event, *_data|
110
+ transition = event.transition
111
+ deferred_hook = proc do |_trans_event, *_data|
106
112
  machine.instance_exec(_trans_event, *_data, &hook)
107
- }
108
- deferred_hook.call(trans_event, *data)
113
+ end
114
+ callable = Callable.new(deferred_hook)
115
+ result = callable.call(trans_event, *data)
116
+ transition.cancelled = (result == CANCELLED)
109
117
  end
110
118
 
111
119
  def trigger(event, *args, &block)
@@ -48,11 +48,9 @@ module FiniteMachine
48
48
  @observer = Observer.new(self)
49
49
  @transitions = Hash.new { |hash, name| hash[name] = Hash.new }
50
50
  @env = Environment.new(target: self)
51
+ @dsl = DSL.new(self)
51
52
 
52
- @dsl = DSL.new self
53
53
  @dsl.call(&block) if block_given?
54
- send(:"#{@dsl.initial_event}") unless @dsl.defer
55
- self.event_queue = FiniteMachine::EventQueue.new
56
54
  end
57
55
 
58
56
  # @example
@@ -213,7 +211,7 @@ module FiniteMachine
213
211
  return CANCELLED if valid_state?(_transition)
214
212
 
215
213
  return CANCELLED unless _transition.conditions.all? do |condition|
216
- condition.call(env.target)
214
+ condition.call(env.target, *args)
217
215
  end
218
216
  return NOTRANSITION if state == _transition.to
219
217
 
@@ -3,38 +3,72 @@
3
3
  require 'monitor'
4
4
 
5
5
  module FiniteMachine
6
-
7
6
  # A class responsibile for storage of event subscribers
8
7
  class Subscribers
9
8
  include Enumerable
10
9
  include MonitorMixin
11
10
 
11
+ # Initialize a subscribers collection
12
+ #
13
+ # @api public
12
14
  def initialize(machine)
13
15
  super()
14
16
  @machine = machine
15
17
  @subscribers = []
16
18
  end
17
19
 
20
+ # Iterate over subscribers
21
+ #
22
+ # @api public
18
23
  def each(&block)
19
24
  @subscribers.each(&block)
20
25
  end
21
26
 
27
+ # Return index of the subscriber
28
+ #
29
+ # @api public
22
30
  def index(subscriber)
23
31
  @subscribers.index(subscriber)
24
32
  end
25
33
 
34
+ # Check if anyone is subscribed
35
+ #
36
+ # @return [Boolean]
37
+ #
38
+ # @api public
26
39
  def empty?
27
40
  @subscribers.empty?
28
41
  end
29
42
 
43
+ # Add listener to subscribers
44
+ #
45
+ # @param [Array[#trigger]] observers
46
+ #
47
+ # @return [undefined]
48
+ #
49
+ # @api public
30
50
  def subscribe(*observers)
31
- observers.each { |observer| @subscribers << observer }
51
+ synchronize do
52
+ observers.each { |observer| @subscribers << observer }
53
+ end
32
54
  end
33
55
 
56
+ # Visit subscribers and notify
57
+ #
58
+ # @param [FiniteMachine::Event] event
59
+ #
60
+ # @return [undefined]
61
+ #
62
+ # @api public
34
63
  def visit(event)
35
64
  each { |subscriber| synchronize { event.notify subscriber } }
36
65
  end
37
66
 
67
+ # Reset subscribers
68
+ #
69
+ # @return [self]
70
+ #
71
+ # @api public
38
72
  def reset
39
73
  @subscribers.clear
40
74
  self
@@ -5,10 +5,12 @@ module FiniteMachine
5
5
  # A mixin to allow sharing of thread context
6
6
  module ThreadContext
7
7
 
8
+ # @api public
8
9
  def event_queue
9
- Thread.current[:finite_machine_event_queue]
10
+ Thread.current[:finite_machine_event_queue] ||= FiniteMachine::EventQueue.new
10
11
  end
11
12
 
13
+ # @api public
12
14
  def event_queue=(value)
13
15
  Thread.current[:finite_machine_event_queue] = value
14
16
  end
@@ -22,6 +22,9 @@ module FiniteMachine
22
22
  # The original from state
23
23
  attr_threadsafe :from_state
24
24
 
25
+ # Check if transition should be cancelled
26
+ attr_threadsafe :cancelled
27
+
25
28
  # Initialize a Transition
26
29
  #
27
30
  # @param [StateMachine] machine
@@ -36,6 +39,7 @@ module FiniteMachine
36
39
  @if = Array(attrs.fetch(:if, []))
37
40
  @unless = Array(attrs.fetch(:unless, []))
38
41
  @conditions = make_conditions
42
+ @cancelled = false
39
43
  end
40
44
 
41
45
  # Reduce conditions
@@ -97,17 +101,37 @@ module FiniteMachine
97
101
  #
98
102
  # @api private
99
103
  def define_event
100
- _transition = self
101
104
  _name = name
105
+ bang_name = "#{_name}!"
102
106
 
103
107
  machine.singleton_class.class_eval do
104
- undef_method(_name) if method_defined?(_name)
108
+ undef_method(_name) if method_defined?(_name)
109
+ undef_method(bang_name) if method_defined?(bang_name)
105
110
  end
111
+ define_transition(name)
112
+ define_event_bang(name)
113
+ end
114
+
115
+ # Define transition event
116
+ #
117
+ # @api private
118
+ def define_transition(name)
119
+ _transition = self
106
120
  machine.send(:define_singleton_method, name) do |*args, &block|
107
121
  transition(_transition, *args, &block)
108
122
  end
109
123
  end
110
124
 
125
+ # Define event that skips validations
126
+ #
127
+ # @api private
128
+ def define_event_bang(name)
129
+ machine.send(:define_singleton_method, "#{name}!") do
130
+ transitions = machine.transitions[name]
131
+ machine.state = transitions.values[0]
132
+ end
133
+ end
134
+
111
135
  # Execute current transition
112
136
  #
113
137
  # @return [nil]
@@ -115,9 +139,10 @@ module FiniteMachine
115
139
  # @api private
116
140
  def call
117
141
  sync_exclusive do
118
- transitions = machine.transitions[name]
142
+ return if cancelled
143
+ transitions = machine.transitions[name]
119
144
  self.from_state = machine.state
120
- machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
145
+ machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
121
146
  end
122
147
  end
123
148
 
@@ -125,7 +150,7 @@ module FiniteMachine
125
150
  #
126
151
  # @api public
127
152
  def to_s
128
- @name
153
+ @name.to_s
129
154
  end
130
155
 
131
156
  # Return string representation
@@ -150,7 +175,8 @@ module FiniteMachine
150
175
  #
151
176
  # @api private
152
177
  def raise_not_enough_transitions(attrs)
153
- raise NotEnoughTransitionsError, "please provide state transitions for '#{attrs.inspect}'"
178
+ fail NotEnoughTransitionsError, "please provide state transitions for" \
179
+ " '#{attrs.inspect}'"
154
180
  end
155
181
  end # Transition
156
182
  end # FiniteMachine
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -56,10 +56,10 @@ describe FiniteMachine, 'async_events' do
56
56
  on_enter :yellow do |event, a| called << "(bar)on_enter_yellow_#{a}" end
57
57
  }
58
58
  end
59
- fsmFoo.slow(:foo)
60
- fsmBar.slow(:bar)
61
- fsmFoo.event_queue.join 0.01
62
- fsmBar.event_queue.join 0.01
59
+ foo_thread = Thread.new { fsmFoo.async.slow(:foo) }
60
+ bar_thread = Thread.new { fsmBar.async.slow(:bar) }
61
+ [foo_thread, bar_thread].each(&:join)
62
+ [fsmFoo, fsmBar].each { |fsm| fsm.event_queue.join 0.02 }
63
63
  expect(called).to include('(foo)on_enter_yellow_foo')
64
64
  expect(called).to include('(bar)on_enter_yellow_bar')
65
65
  expect(fsmFoo.current).to eql(:yellow)
@@ -7,7 +7,7 @@ describe FiniteMachine, 'callbacks' do
7
7
  it "triggers default init event" do
8
8
  called = []
9
9
  fsm = FiniteMachine.define do
10
- initial :green
10
+ initial state: :green, defer: true
11
11
 
12
12
  events {
13
13
  event :slow, :green => :yellow
@@ -47,8 +47,8 @@ describe FiniteMachine, 'callbacks' do
47
47
  }
48
48
  end
49
49
 
50
- expect(fsm.current).to eql(:green)
51
-
50
+ expect(fsm.current).to eql(:none)
51
+ fsm.init
52
52
  expect(called).to eql([
53
53
  'on_exit_none',
54
54
  'on_exit',
@@ -570,12 +570,54 @@ describe FiniteMachine, 'callbacks' do
570
570
  fsm.slow
571
571
  expect(fsm.current).to eql(:yellow)
572
572
  expect(called).to eql([
573
- 'once_on_transition_green',
574
- 'once_on_enter_green',
575
573
  'once_on_exit_green',
576
574
  'once_on_transition_yellow',
577
575
  'once_on_enter_yellow',
578
- 'once_on_exit_yellow'
576
+ 'once_on_exit_yellow',
577
+ 'once_on_transition_green',
578
+ 'once_on_enter_green'
579
579
  ])
580
580
  end
581
+
582
+ it "cancels transition on state callback" do
583
+ fsm = FiniteMachine.define do
584
+ initial :green
585
+
586
+ events {
587
+ event :slow, :green => :yellow
588
+ event :go, :yellow => :green
589
+ }
590
+
591
+ callbacks {
592
+ on_exit :green do |event| FiniteMachine::CANCELLED end
593
+ }
594
+ end
595
+
596
+ expect(fsm.current).to eql(:green)
597
+ fsm.slow
598
+ expect(fsm.current).to eql(:green)
599
+ end
600
+
601
+ it "cancels transition on event callback" do
602
+ fsm = FiniteMachine.define do
603
+ initial :green
604
+
605
+ events {
606
+ event :slow, :green => :yellow
607
+ event :go, :yellow => :green
608
+ }
609
+
610
+ callbacks {
611
+ on_enter :slow do |event|
612
+ FiniteMachine::CANCELLED
613
+ end
614
+ }
615
+ end
616
+
617
+ expect(fsm.current).to eql(:green)
618
+ fsm.slow
619
+ expect(fsm.current).to eql(:green)
620
+ end
621
+
622
+ xit "groups callbacks"
581
623
  end