finite_machine 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b69fcd7446203267dc8130e4c986ea334e4b26ec
4
- data.tar.gz: 3a4bca63a58bf4a3c0a509cc99778d66771a535e
3
+ metadata.gz: 714f488440a58102f64c2c9911b1d55f3f759df5
4
+ data.tar.gz: 10005291036b23eee4654d2a81eeab01383126da
5
5
  SHA512:
6
- metadata.gz: d5c2636ea7607b138737e263771b5269d950f1403079e67dfd41d4940241f41cf62d1118cfe7ae177cfe3a716985cad9f33f76313ada9ce4424ba3fcbbd89511
7
- data.tar.gz: c40fda5ae25615ca3d2ee239dc59a952596c3860f309c22b8d01e8b4b8acd10f0d7aeb7ddb2f1dc80aecd46c9bd4b3b6e0fd456b28b14dfc9f34144b7ef80fac
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 (TODO - only sync)
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 Cancelling inside callbacks](#410-cancelling-inside-callbacks)
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 :ready { |event| ... }
96
- on_enter :go { |event| ... }
97
- on_enter :stop { |event| ... }
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 Cancelling inside callbacks
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
 
@@ -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)
@@ -1,7 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
-
5
4
  # A mixin to allow for specifying error handlers
6
5
  module Catchable
7
6
 
@@ -12,7 +12,13 @@ module FiniteMachine
12
12
 
13
13
  attr_threadsafe :machine
14
14
 
15
- def initialize(machine)
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 = true
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
- machine.state = state unless defer
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. This allows state machine
68
- # to initiate events in the context of a particular object.
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
- machine.env.target = object
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
@@ -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.event_name }
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 = Queue.new
17
- @mutex = Mutex.new
18
- @dead = false
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(@dead) do
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
@@ -1,7 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
-
5
4
  # A class reponsible for registering callbacks
6
5
  class Hooks
7
6
  include Threadable
@@ -71,6 +70,5 @@ module FiniteMachine
71
70
  yield hook
72
71
  end
73
72
  end
74
-
75
73
  end # Hooks
76
74
  end # FiniteMachine
@@ -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 [Symbol] name
37
- # @param [Proc] callback
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 = ANY_EVENT, name = ANY_STATE, &callback)
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
- hooks.register event_type, name, callback
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
- if machine.states.include?(name) || name == ANY_STATE_HOOK
62
- events << :"#{type}state"
63
- elsif machine.event_names.include?(name) || name == ANY_EVENT_HOOK
64
- events << :"#{type}action"
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 event, *args, &callback }
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
- TransitionEvent = Struct.new(:from, :to, :name) do
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 private
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
- run_callback(hook, event)
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
- @callback_names = Set.new
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(InvalidCallbackNameError, "#{name} is not a valid callback name." +
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
@@ -73,6 +73,5 @@ module FiniteMachine
73
73
  @subscribers.clear
74
74
  self
75
75
  end
76
-
77
76
  end # Subscribers
78
77
  end # FiniteMachine
@@ -1,7 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
-
5
4
  # A mixin to allow sharing of thread context
6
5
  module ThreadContext
7
6
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -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.stop(:bar)
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 eql([
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 include('(foo)on_enter_yellow_foo')
64
- expect(called).to include('(bar)on_enter_yellow_bar')
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
@@ -4,28 +4,51 @@ require 'spec_helper'
4
4
 
5
5
  describe FiniteMachine, 'define' do
6
6
 
7
- it "creates system state machine" do
8
- fsm = FiniteMachine.define do
9
- initial :green
10
-
11
- events {
12
- event :slow, :green => :yellow
13
- event :stop, :yellow => :red
14
- event :ready, :red => :yellow
15
- event :go, :yellow => :green
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
- expect(fsm.current).to eql(:green)
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
- fsm.slow
22
- expect(fsm.current).to eql(:yellow)
23
- fsm.stop
24
- expect(fsm.current).to eql(:red)
25
- fsm.ready
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
@@ -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
@@ -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
@@ -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
@@ -4,7 +4,7 @@ desc 'Load gem inside irb console'
4
4
  task :console do
5
5
  require 'irb'
6
6
  require 'irb/completion'
7
- require File.join(__FILE__, '../lib/finite_machine')
7
+ require File.join(__FILE__, '../../lib/finite_machine')
8
8
  ARGV.clear
9
9
  IRB.start
10
10
  end
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.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-13 00:00:00.000000000 Z
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