finite_machine 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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