finite_machine 0.4.0 → 0.5.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 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