finite_machine 0.8.1 → 0.9.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/Gemfile +1 -1
  4. data/README.md +150 -35
  5. data/lib/finite_machine.rb +1 -0
  6. data/lib/finite_machine/catchable.rb +1 -1
  7. data/lib/finite_machine/definition.rb +61 -0
  8. data/lib/finite_machine/dsl.rb +45 -10
  9. data/lib/finite_machine/event.rb +17 -1
  10. data/lib/finite_machine/hook_event.rb +66 -7
  11. data/lib/finite_machine/observer.rb +3 -3
  12. data/lib/finite_machine/state_machine.rb +36 -28
  13. data/lib/finite_machine/version.rb +1 -1
  14. data/spec/spec_helper.rb +2 -2
  15. data/spec/unit/async_events_spec.rb +1 -1
  16. data/spec/unit/callbacks_spec.rb +23 -23
  17. data/spec/unit/can_spec.rb +22 -22
  18. data/spec/unit/event/eql_spec.rb +37 -0
  19. data/spec/unit/event/initialize_spec.rb +38 -0
  20. data/spec/unit/event/inspect_spec.rb +1 -1
  21. data/spec/unit/event_queue_spec.rb +2 -2
  22. data/spec/unit/events_chain/check_choice_conditions_spec.rb +2 -2
  23. data/spec/unit/events_chain/clear_spec.rb +1 -1
  24. data/spec/unit/events_chain/insert_spec.rb +1 -1
  25. data/spec/unit/events_spec.rb +17 -20
  26. data/spec/unit/hook_event/eql_spec.rb +37 -0
  27. data/spec/unit/hook_event/initialize_spec.rb +22 -0
  28. data/spec/unit/if_unless_spec.rb +6 -6
  29. data/spec/unit/initialize_spec.rb +6 -6
  30. data/spec/unit/is_spec.rb +12 -12
  31. data/spec/unit/logger_spec.rb +1 -1
  32. data/spec/unit/respond_to_spec.rb +2 -2
  33. data/spec/unit/standalone_spec.rb +72 -0
  34. data/spec/unit/subscribers_spec.rb +2 -2
  35. data/spec/unit/target_spec.rb +59 -10
  36. data/spec/unit/{finished_spec.rb → terminated_spec.rb} +38 -8
  37. metadata +15 -4
@@ -55,19 +55,38 @@ module FiniteMachine
55
55
 
56
56
  # Define initial state
57
57
  #
58
+ # @param [Symbol] value
59
+ # The initial state name.
60
+ # @param [Hash[Symbol]] options
61
+ # @option options [Symbol] :event
62
+ # The event name.
63
+ # @option options [Symbol] :defer
64
+ # Set to true to defer initial state transition.
65
+ # Default false.
66
+ # @option options [Symbol] :silent
67
+ # Set to true to disable callbacks.
68
+ # Default true.
69
+ #
58
70
  # @example
59
71
  # initial :green
60
72
  #
61
- # @example
73
+ # @example Defer initial event
62
74
  # initial state: green, defer: true
63
75
  #
76
+ # @example Trigger callbacks
77
+ # initial :green, silent: false
78
+ #
79
+ # @example Redefine event name
80
+ # initial :green, event: :start
81
+ #
64
82
  # @param [String, Hash] value
65
83
  #
66
84
  # @return [StateMachine]
67
85
  #
68
86
  # @api public
69
- def initial(value)
70
- state, name, self.defer, silent = parse(value)
87
+ def initial(value, options = {})
88
+ state = (value && !value.is_a?(Hash)) ? value : raise_missing_state
89
+ name, self.defer, silent = parse(options)
71
90
  self.initial_event = name
72
91
  machine.event(name, FiniteMachine::DEFAULT_STATE => state, silent: silent)
73
92
  end
@@ -112,12 +131,17 @@ module FiniteMachine
112
131
  # @return [FiniteMachine::StateMachine]
113
132
  #
114
133
  # @api public
115
- def terminal(value)
116
- machine.final_state = value
134
+ def terminal(*values)
135
+ machine.final_state = values
117
136
  end
118
137
 
119
138
  # Define state machine events
120
139
  #
140
+ # @example
141
+ # events do
142
+ # event :start, :red => :green
143
+ # end
144
+ #
121
145
  # @return [FiniteMachine::StateMachine]
122
146
  #
123
147
  # @api public
@@ -127,6 +151,13 @@ module FiniteMachine
127
151
 
128
152
  # Define state machine callbacks
129
153
  #
154
+ # @example
155
+ # callbacks do
156
+ # on_enter :green do |event| ... end
157
+ # end
158
+ #
159
+ # @return [FiniteMachine::Observer]
160
+ #
130
161
  # @api public
131
162
  def callbacks(&block)
132
163
  machine.observer.call(&block)
@@ -159,21 +190,25 @@ module FiniteMachine
159
190
  # @api private
160
191
  def parse(value)
161
192
  if value.is_a?(Hash)
162
- [value.fetch(:state) { raise_missing_state },
163
- value.fetch(:event) { FiniteMachine::DEFAULT_EVENT_NAME },
193
+ [value.fetch(:event) { FiniteMachine::DEFAULT_EVENT_NAME },
164
194
  value.fetch(:defer) { false },
165
195
  value.fetch(:silent) { true }]
166
196
  else
167
- [value, FiniteMachine::DEFAULT_EVENT_NAME, false, true]
197
+ [FiniteMachine::DEFAULT_EVENT_NAME, false, true]
168
198
  end
169
199
  end
170
200
 
171
201
  # Raises missing state error
172
202
  #
203
+ # @raise [MissingInitialStateError]
204
+ # Raised when state name is not provided for initial.
205
+ #
206
+ # @return [nil]
207
+ #
173
208
  # @api private
174
209
  def raise_missing_state
175
- raise MissingInitialStateError,
176
- 'Provide state to transition :to for the initial event'
210
+ fail MissingInitialStateError,
211
+ 'Provide state to transition :to for the initial event'
177
212
  end
178
213
  end # DSL
179
214
 
@@ -3,6 +3,7 @@
3
3
  module FiniteMachine
4
4
  # A class representing event with transitions
5
5
  class Event
6
+ include Comparable
6
7
  include Threadable
7
8
 
8
9
  # The name of this event
@@ -26,8 +27,11 @@ module FiniteMachine
26
27
  @silent = attrs.fetch(:silent, false)
27
28
  @state_transitions = []
28
29
  # TODO: add event conditions
30
+ freeze
29
31
  end
30
32
 
33
+ protected :machine
34
+
31
35
  # Add transition for this event
32
36
  #
33
37
  # @param [FiniteMachine::Transition] transition
@@ -109,7 +113,19 @@ module FiniteMachine
109
113
  #
110
114
  # @api public
111
115
  def inspect
112
- "<##{self.class} @name=#{@name}, @transitions=#{state_transitions.inspect}>"
116
+ "<##{self.class} @name=#{name}, @silent=#{silent}, " \
117
+ "@transitions=#{state_transitions.inspect}>"
118
+ end
119
+
120
+ # Compare whether the instance is greater, less then or equal to other
121
+ #
122
+ # @return [-1 0 1]
123
+ #
124
+ # @api public
125
+ def <=>(other)
126
+ other.is_a?(self.class) && [name, silent, state_transitions] <=>
127
+ [other.name, other.silent, other.state_transitions]
113
128
  end
129
+ alias_method :eql?, :==
114
130
  end # Event
115
131
  end # FiniteMachine
@@ -4,11 +4,12 @@ module FiniteMachine
4
4
  # A class responsible for event notification
5
5
  class HookEvent
6
6
  include Threadable
7
+ include Comparable
7
8
 
8
9
  MESSAGE = :trigger
9
10
 
10
- # HookEvent state
11
- attr_threadsafe :state
11
+ # HookEvent name
12
+ attr_threadsafe :name
12
13
 
13
14
  # HookEvent type
14
15
  attr_threadsafe :type
@@ -19,13 +20,50 @@ module FiniteMachine
19
20
  # Transition associated with the event
20
21
  attr_threadsafe :transition
21
22
 
22
- def initialize(state, transition, *data, &block)
23
- @state = state
23
+ # Instantiate a new HookEvent object
24
+ #
25
+ # @param [Symbol] name
26
+ # The action or state name
27
+ # @param [FiniteMachine::Transition]
28
+ # The transition associated with this event.
29
+ # @param [Array[Object]] data
30
+ #
31
+ # @example
32
+ # HookEvent.new(:green, ...)
33
+ #
34
+ # @return [Object]
35
+ #
36
+ # @api public
37
+ def initialize(name, transition, *data)
38
+ @name = name
24
39
  @transition = transition
25
- @data = *data
26
- @type = self.class
40
+ @data = *data
41
+ @type = self.class
42
+ freeze
27
43
  end
28
44
 
45
+ # Build event hook
46
+ #
47
+ # @param [Symbol] :state
48
+ # The state name.
49
+ # @param [FiniteMachine::Transition] :event_transition
50
+ # The transition associted with this hook.
51
+ # @param [Array[Object]] :data
52
+ # The data associated with this hook
53
+ #
54
+ # @return [self]
55
+ #
56
+ # @api public
57
+ def self.build(state, event_transition, *data)
58
+ state_or_action = self < Anystate ? state : event_transition.name
59
+ new(state_or_action, event_transition, *data)
60
+ end
61
+
62
+ # Notify subscriber about this event
63
+ #
64
+ # @return [nil]
65
+ #
66
+ # @api public
29
67
  def notify(subscriber, *args, &block)
30
68
  if subscriber.respond_to? MESSAGE
31
69
  subscriber.public_send(MESSAGE, self, *args, &block)
@@ -48,13 +86,34 @@ module FiniteMachine
48
86
 
49
87
  EVENTS = Anystate, Enter, Transition, Exit, Anyaction, Before, After
50
88
 
89
+ # Extract event name
90
+ #
91
+ # @return [String] the event name
92
+ #
93
+ # @api public
51
94
  def self.event_name
52
95
  name.split('::').last.downcase.to_sym
53
96
  end
54
97
 
98
+ # String representation
99
+ #
100
+ # @return [String] the event name
101
+ #
102
+ # @api public
55
103
  def self.to_s
56
- event_name
104
+ event_name.to_s
105
+ end
106
+
107
+ # Compare whether the instance is greater, less then or equal to other
108
+ #
109
+ # @return [-1 0 1]
110
+ #
111
+ # @api public
112
+ def <=>(other)
113
+ other.is_a?(type) &&
114
+ [name, transition, data] <=> [other.name, other.transition, other.data]
57
115
  end
116
+ alias_method :eql?, :==
58
117
 
59
118
  EVENTS.each do |event|
60
119
  (class << self; self; end).class_eval do
@@ -108,10 +108,10 @@ module FiniteMachine
108
108
  def trigger(event, *args, &block)
109
109
  sync_exclusive do
110
110
  [event.type, ANY_EVENT].each do |event_type|
111
- [event.state, ANY_STATE].each do |event_state|
112
- hooks.call(event_type, event_state) do |hook|
111
+ [event.name, ANY_STATE].each do |event_name|
112
+ hooks.call(event_type, event_name) do |hook|
113
113
  handle_callback(hook, event)
114
- off(event_type, event_state, &hook) if hook.is_a?(Once)
114
+ off(event_type, event_name, &hook) if hook.is_a?(Once)
115
115
  end
116
116
  end
117
117
  end
@@ -75,14 +75,22 @@ module FiniteMachine
75
75
  end
76
76
 
77
77
  # TODO: use trigger to actually fire state machine events!
78
- # Notify about event
78
+ # Notify about event all the subscribers
79
+ #
80
+ # @param [FiniteMachine::HookEvent] :event_type
81
+ # The hook event type.
82
+ # @param [FiniteMachine::Transition] :event_transition
83
+ # The event transition.
84
+ # @param [Array[Object]] :data
85
+ # The data associated with the hook event.
86
+ #
87
+ # @return [nil]
79
88
  #
80
89
  # @api public
81
- def notify(event_type, _transition, *data)
90
+ def notify(event_type, event_transition, *data)
82
91
  sync_shared do
83
- state_or_action = event_type < HookEvent::Anystate ? state : _transition.name
84
- _event = event_type.new(state_or_action, _transition, *data)
85
- subscribers.visit(_event)
92
+ hook_event = event_type.build(state, event_transition, *data)
93
+ subscribers.visit(hook_event)
86
94
  end
87
95
  end
88
96
 
@@ -177,7 +185,7 @@ module FiniteMachine
177
185
  event = args.shift
178
186
  valid_state = transitions[event].key?(current)
179
187
  valid_state ||= transitions[event].key?(ANY_STATE)
180
- valid_state &&= events_chain.valid_event?(event, *args, &block)
188
+ valid_state && events_chain.valid_event?(event, *args, &block)
181
189
  end
182
190
 
183
191
  # Checks if event cannot be triggered
@@ -199,7 +207,7 @@ module FiniteMachine
199
207
  # @return [Boolean]
200
208
  #
201
209
  # @api public
202
- def finished?
210
+ def terminated?
203
211
  is?(final_state)
204
212
  end
205
213
 
@@ -230,47 +238,51 @@ module FiniteMachine
230
238
 
231
239
  # Check if state is reachable
232
240
  #
241
+ # @param [FiniteMachine::Transition]
242
+ #
243
+ # @return [Boolean]
244
+ #
233
245
  # @api private
234
- def valid_state?(_transition)
235
- current_states = transitions[_transition.name].keys
246
+ def valid_state?(event_transition)
247
+ current_states = transitions[event_transition.name].keys
236
248
  if !current_states.include?(state) && !current_states.include?(ANY_STATE)
237
249
  exception = InvalidStateError
238
250
  catch_error(exception) ||
239
- raise(exception, "inappropriate current state '#{state}'")
251
+ fail(exception, "inappropriate current state '#{state}'")
240
252
  true
241
253
  end
242
254
  end
243
255
 
244
256
  # Performs transition
245
257
  #
246
- # @param [Transition] _transition
258
+ # @param [Transition] event_transition
247
259
  # @param [Array] args
248
260
  #
249
261
  # @return [Integer]
250
262
  # the status code for the transition
251
263
  #
252
264
  # @api private
253
- def transition(_transition, *args, &block)
265
+ def transition(event_transition, *args, &block)
254
266
  sync_exclusive do
255
- notify HookEvent::Before, _transition, *args
267
+ notify HookEvent::Before, event_transition, *args
256
268
 
257
- return CANCELLED if valid_state?(_transition)
258
- return CANCELLED unless _transition.valid?(*args, &block)
269
+ return CANCELLED if valid_state?(event_transition)
270
+ return CANCELLED unless event_transition.valid?(*args, &block)
259
271
 
260
- notify HookEvent::Exit, _transition, *args
272
+ notify HookEvent::Exit, event_transition, *args
261
273
 
262
274
  begin
263
- _transition.call(*args)
275
+ event_transition.call(*args)
264
276
 
265
- notify HookEvent::Transition, _transition, *args
277
+ notify HookEvent::Transition, event_transition, *args
266
278
  rescue Exception => e
267
279
  catch_error(e) || raise_transition_error(e)
268
280
  end
269
281
 
270
- notify HookEvent::Enter, _transition, *args
271
- notify HookEvent::After, _transition, *args
282
+ notify HookEvent::Enter, event_transition, *args
283
+ notify HookEvent::After, event_transition, *args
272
284
 
273
- _transition.same?(state) ? NOTRANSITION : SUCCEEDED
285
+ event_transition.same?(state) ? NOTRANSITION : SUCCEEDED
274
286
  end
275
287
  end
276
288
 
@@ -287,7 +299,7 @@ module FiniteMachine
287
299
  "occured at #{error.backtrace.join("\n")}")
288
300
  end
289
301
 
290
- # Forward the message to target, observer or self
302
+ # Forward the message to observer or self
291
303
  #
292
304
  # @param [String] method_name
293
305
  #
@@ -297,9 +309,7 @@ module FiniteMachine
297
309
  #
298
310
  # @api private
299
311
  def method_missing(method_name, *args, &block)
300
- if target.respond_to?(method_name.to_sym)
301
- target.public_send(method_name.to_sym, *args, &block)
302
- elsif observer.respond_to?(method_name.to_sym)
312
+ if observer.respond_to?(method_name.to_sym)
303
313
  observer.public_send(method_name.to_sym, *args, &block)
304
314
  else
305
315
  super
@@ -316,9 +326,7 @@ module FiniteMachine
316
326
  #
317
327
  # @api private
318
328
  def respond_to_missing?(method_name, include_private = false)
319
- env.target.respond_to?(method_name.to_sym) ||
320
- observer.respond_to?(method_name.to_sym) ||
321
- super
329
+ observer.respond_to?(method_name.to_sym) || super
322
330
  end
323
331
  end # StateMachine
324
332
  end # FiniteMachine
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
- VERSION = "0.8.1"
4
+ VERSION = "0.9.0"
5
5
  end
data/spec/spec_helper.rb CHANGED
@@ -18,9 +18,9 @@ end
18
18
  require 'finite_machine'
19
19
 
20
20
  RSpec.configure do |config|
21
- config.treat_symbols_as_metadata_keys_with_true_values = true
22
21
  config.run_all_when_everything_filtered = true
23
22
  config.filter_run :focus
23
+ config.raise_errors_for_deprecations!
24
24
 
25
25
  # Run specs in random order to surface order dependencies. If you find an
26
26
  # order dependency and want to debug it, you can fix the order by providing
@@ -30,7 +30,7 @@ RSpec.configure do |config|
30
30
 
31
31
  # Remove defined constants
32
32
  config.before :each do
33
- [:Car, :Logger, :Bug, :User].each do |class_name|
33
+ [:Car, :Logger, :Bug, :User, :Engine].each do |class_name|
34
34
  if Object.const_defined?(class_name)
35
35
  Object.send(:remove_const, class_name)
36
36
  end
@@ -71,7 +71,7 @@ describe FiniteMachine, 'async_events' do
71
71
  it "permits async callback" do
72
72
  called = []
73
73
  fsm = FiniteMachine.define do
74
- initial state: :green, silent: false
74
+ initial :green, silent: false
75
75
 
76
76
  events {
77
77
  event :slow, :green => :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 state: :green, defer: true, silent: false
10
+ initial :green, defer: true, silent: false
11
11
 
12
12
  callbacks {
13
13
  # generic state callbacks
@@ -54,7 +54,7 @@ describe FiniteMachine, 'callbacks' do
54
54
  it "executes callbacks in order" do
55
55
  called = []
56
56
  fsm = FiniteMachine.define do
57
- initial state: :green, silent: false
57
+ initial :green, silent: false
58
58
 
59
59
  events {
60
60
  event :slow, :green => :yellow
@@ -173,7 +173,7 @@ describe FiniteMachine, 'callbacks' do
173
173
  it "maintains transition execution sequence from UML statechart" do
174
174
  called = []
175
175
  fsm = FiniteMachine.define do
176
- initial state: :previous, silent: false
176
+ initial :previous, silent: false
177
177
 
178
178
  events {
179
179
  event :go, :previous => :next, if: -> { called << 'guard'; true}
@@ -207,7 +207,7 @@ describe FiniteMachine, 'callbacks' do
207
207
  it "allows multiple callbacks for the same state" do
208
208
  called = []
209
209
  fsm = FiniteMachine.define do
210
- initial state: :green, silent: false
210
+ initial :green, silent: false
211
211
 
212
212
  events {
213
213
  event :slow, :green => :yellow
@@ -359,12 +359,12 @@ describe FiniteMachine, 'callbacks' do
359
359
  expected = {name: :init, from: :none, to: :green, a: nil, b: nil, c: nil }
360
360
 
361
361
  callback = Proc.new { |event, a, b, c|
362
- expect(event.from).to eql(expected[:from])
363
- expect(event.to).to eql(expected[:to])
364
- expect(event.name).to eql(expected[:name])
365
- expect(a).to eql(expected[:a])
366
- expect(b).to eql(expected[:b])
367
- expect(c).to eql(expected[:c])
362
+ target.expect(event.from).to target.eql(expected[:from])
363
+ target.expect(event.to).to target.eql(expected[:to])
364
+ target.expect(event.name).to target.eql(expected[:name])
365
+ target.expect(a).to target.eql(expected[:a])
366
+ target.expect(b).to target.eql(expected[:b])
367
+ target.expect(c).to target.eql(expected[:c])
368
368
  }
369
369
  context = self
370
370
 
@@ -435,7 +435,7 @@ describe FiniteMachine, 'callbacks' do
435
435
  initial :green
436
436
 
437
437
  events {
438
- event :slow, :green => :yellow
438
+ event :slow, :green => :yellow
439
439
  }
440
440
 
441
441
  callbacks {
@@ -458,7 +458,7 @@ describe FiniteMachine, 'callbacks' do
458
458
  it "doesn't allow to mix event callback with state name" do
459
459
  expect {
460
460
  FiniteMachine.define do
461
- events { event :slow, :green => :yellow }
461
+ events { event :slow, :green => :yellow }
462
462
 
463
463
  callbacks { on_before_green do |event| end }
464
464
  end
@@ -469,7 +469,7 @@ describe FiniteMachine, 'callbacks' do
469
469
  fsm = FiniteMachine.define do
470
470
  initial :green
471
471
 
472
- events { event :slow, :green => :yellow }
472
+ events { event :slow, :green => :yellow }
473
473
 
474
474
  callbacks { on_enter(:yellow) { raise RuntimeError } }
475
475
  end
@@ -549,11 +549,11 @@ describe FiniteMachine, 'callbacks' do
549
549
  it "triggers callbacks only once" do
550
550
  called = []
551
551
  fsm = FiniteMachine.define do
552
- initial state: :green, silent: false
552
+ initial :green, silent: false
553
553
 
554
554
  events {
555
- event :slow, :green => :yellow
556
- event :go, :yellow => :green
555
+ event :slow, :green => :yellow
556
+ event :go, :yellow => :green
557
557
  }
558
558
 
559
559
  callbacks {
@@ -606,8 +606,8 @@ describe FiniteMachine, 'callbacks' do
606
606
  initial :green
607
607
 
608
608
  events {
609
- event :slow, :green => :yellow
610
- event :go, :yellow => :green
609
+ event :slow, :green => :yellow
610
+ event :go, :yellow => :green
611
611
  }
612
612
 
613
613
  callbacks {
@@ -625,8 +625,8 @@ describe FiniteMachine, 'callbacks' do
625
625
  initial :green
626
626
 
627
627
  events {
628
- event :slow, :green => :yellow
629
- event :go, :yellow => :green
628
+ event :slow, :green => :yellow
629
+ event :go, :yellow => :green
630
630
  }
631
631
 
632
632
  callbacks {
@@ -646,7 +646,7 @@ describe FiniteMachine, 'callbacks' do
646
646
  it "groups states from separate events with the same name" do
647
647
  callbacks = []
648
648
  fsm = FiniteMachine.define do
649
- initial state: :initial, silent: false
649
+ initial :initial, silent: false
650
650
 
651
651
  events {
652
652
  event :bump, :initial => :low
@@ -720,7 +720,7 @@ describe FiniteMachine, 'callbacks' do
720
720
  it "groups states under event name" do
721
721
  callbacks = []
722
722
  fsm = FiniteMachine.define do
723
- initial state: :initial, silent: false
723
+ initial :initial, silent: false
724
724
 
725
725
  events {
726
726
  event :bump, :initial => :low,
@@ -770,7 +770,7 @@ describe FiniteMachine, 'callbacks' do
770
770
  it "permits state and event with the same name" do
771
771
  called = []
772
772
  fsm = FiniteMachine.define do
773
- initial state: :on_hook, silent: false
773
+ initial :on_hook, silent: false
774
774
 
775
775
  events {
776
776
  event :off_hook, :on_hook => :off_hook