finite_machine 0.3.0 → 0.4.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.
@@ -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