finite_machine 0.9.1 → 0.9.2

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: 5ce0812460235da2deebb8aafc2d542b33392804
4
- data.tar.gz: 89bb6ab79f200d7efaf004ffe9704bbbeb86b5c0
3
+ metadata.gz: 64cf549e74a02a5036aec535a7be9784a10224bf
4
+ data.tar.gz: e7733f5805b8989033e229109200b7f69feebe3a
5
5
  SHA512:
6
- metadata.gz: ed665ca255296e317a683106444e9e4ebc8044f927ea9067c92709fe777787de8b26cfa969e5c42bb2089f16c184d69774729fb06cd6c26d93b645dd1d96b133
7
- data.tar.gz: 88538af514156c8a5e899b876e747a0716ab4439c271395418364830cc3372bafcbc772383aaeb3cbecf75c07b0e4087961092eec824c42709d1e40656cfd75f
6
+ metadata.gz: db31c094295a9ed657daca0960b3299d00ee131a3149ce99750d14b44555e0e5375e5f0e3b5c0d70ecf955a895cd4addafc1b9e99576dd03a21df9ca2b271547
7
+ data.tar.gz: bff4f5c620276cf7cc664662d0c3aa66e555d1e09fd242e9d22ab5577f0425ee94c4db7bf804e4102a2e99d759eb1180fc04988ccff3fca2368879d4cc02ab1d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ 0.9.2 (September 27, 2014)
2
+
3
+ * Removes use of class variable to share Sync by @reggieb
4
+ * Fix observer to differentiate between any state and any event
5
+ * [#23] Fix transition to correctly set :from and :to parameters for :any state
6
+ * [#25] Fix passing parameters to choice events with same named events
7
+ * Fix choice pseudostate to work with :any state
8
+
1
9
  0.9.1 (August 10, 2014)
2
10
 
3
11
  * Add TransitionBuilder to internally build transitions from states
data/Gemfile CHANGED
@@ -3,8 +3,8 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :development do
6
- gem 'rake', '~> 10.1.0'
7
- gem 'rspec', '~> 3.0.0'
6
+ gem 'rake', '~> 10.3.2'
7
+ gem 'rspec', '~> 3.1.0'
8
8
  gem 'yard', '~> 0.8.7'
9
9
  end
10
10
 
data/README.md CHANGED
@@ -1217,15 +1217,23 @@ car.reverse_lights_on? # => true
1217
1217
 
1218
1218
  ### 8.2 ActiveRecord
1219
1219
 
1220
- In order to integrate **FiniteMachine** with ActiveRecord use the `target` helper to reference the current class and call ActiveRecord methods inside the callbacks to persist the state.
1220
+ In order to integrate **FiniteMachine** with ActiveRecord simply add a method with state machine definition. You can also define the state machine in separate module to aid reusability. Once the state machine is defined use the `target` helper to reference the current class. Having defined `target` you call ActiveRecord methods inside the callbacks to persist the state.
1221
+
1222
+ You can use the `restore!` method to specify which state the **FininteMachine** should be put back into as follows:
1221
1223
 
1222
1224
  ```ruby
1223
1225
  class Account < ActiveRecord::Base
1224
1226
  validates :state, presence: true
1225
1227
 
1228
+ before_validation :set_initial_state, on: :create
1229
+
1230
+ def set_initial_state
1231
+ self.state = manage.current
1232
+ end
1233
+
1226
1234
  def initialize(attrs = {})
1227
1235
  super
1228
- @manage.restore!(state) if state
1236
+ manage.restore!(state.to_sym) if state.present?
1229
1237
  end
1230
1238
 
1231
1239
  def manage
@@ -1241,7 +1249,7 @@ class Account < ActiveRecord::Base
1241
1249
  }
1242
1250
 
1243
1251
  callbacks {
1244
- on_enter_state do |event|
1252
+ on_enter do |event|
1245
1253
  target.state = event.to
1246
1254
  target.save
1247
1255
  end
@@ -1258,6 +1266,8 @@ account.manage.authorize
1258
1266
  account.state # => :access
1259
1267
  ```
1260
1268
 
1269
+ Please note that you do not need to call `target.save` inside callback, it is enought to just set the state. It is much more prefereable to let the `ActiveRecord` object to persist when it makes sense for the application and thus keep the state machine focused on managing the state transitions.
1270
+
1261
1271
  ### 8.3 Transactions
1262
1272
 
1263
1273
  When using **FiniteMachine** with ActiveRecord it advisable to trigger state changes inside transactions to ensure integrity of the database. Given Account example from section 8.2 one can run event in transaction in the following way:
@@ -31,6 +31,7 @@ require "finite_machine/subscribers"
31
31
  require "finite_machine/state_parser"
32
32
  require "finite_machine/observer"
33
33
  require "finite_machine/listener"
34
+ require "finite_machine/two_phase_lock"
34
35
 
35
36
  module FiniteMachine
36
37
  # Default state name
@@ -43,7 +44,7 @@ module FiniteMachine
43
44
  ANY_STATE = :any
44
45
 
45
46
  # Describe any event name
46
- ANY_EVENT = :any
47
+ ANY_EVENT = :any_event
47
48
 
48
49
  # Returned when transition has successfully performed
49
50
  SUCCEEDED = 1
@@ -16,7 +16,7 @@ module FiniteMachine
16
16
 
17
17
  # Initialize a EventsChain
18
18
  #
19
- # @param [FiniteMachine::StateMachine] machine
19
+ # @param [StateMachine] machine
20
20
  # the state machine
21
21
  #
22
22
  # @api public
@@ -30,7 +30,7 @@ module FiniteMachine
30
30
  # @param [Symbol] name
31
31
  # the event name
32
32
  #
33
- # @param [FiniteMachine::Transition]
33
+ # @param [Transition]
34
34
  #
35
35
  # @return [nil]
36
36
  #
@@ -63,7 +63,7 @@ module FiniteMachine
63
63
  # @param [Symbol] name
64
64
  # the event name
65
65
  #
66
- # @return [FiniteMachine::Transition]
66
+ # @return [Transition]
67
67
  #
68
68
  # @api public
69
69
  def select_transition(name, *args)
@@ -72,12 +72,19 @@ module FiniteMachine
72
72
 
73
73
  # Examine choice transitions to find one matching condition
74
74
  #
75
- # @return [FiniteMachine::Transition]
75
+ # @param [Symbol] name
76
+ # the event name
77
+ #
78
+ # @param [Symbol] from_state
79
+ # the current context from_state
80
+ #
81
+ # @return [Transition]
76
82
  # The choice transition that matches
77
83
  #
78
84
  # @api public
79
- def select_choice_transition(name, *args, &block)
85
+ def select_choice_transition(name, from_state, *args, &block)
80
86
  chain[name].state_transitions.find do |trans|
87
+ [from_state, ANY_STATE].include?(trans.from_state) &&
81
88
  trans.check_conditions(*args, &block)
82
89
  end
83
90
  end
@@ -41,7 +41,9 @@ module FiniteMachine
41
41
  def on(event_type = HookEvent, *args, &callback)
42
42
  sync_exclusive do
43
43
  name, async, _ = args
44
- name = ANY_EVENT if name.nil?
44
+ if name.nil?
45
+ name = event_type < HookEvent::Anyaction ? ANY_EVENT : ANY_STATE
46
+ end
45
47
  async = false if async.nil?
46
48
  ensure_valid_callback_name!(event_type, name)
47
49
  callback.extend(Async) if async == :async
@@ -52,7 +54,7 @@ module FiniteMachine
52
54
  # Unregister callback for a given event
53
55
  #
54
56
  # @api public
55
- def off(event_type = ANY_EVENT, name = ANY_STATE, &callback)
57
+ def off(event_type, name = ANY_STATE, &callback)
56
58
  sync_exclusive do
57
59
  hooks.unregister event_type, name, callback
58
60
  end
@@ -107,8 +109,8 @@ module FiniteMachine
107
109
  # @api public
108
110
  def trigger(event, *args, &block)
109
111
  sync_exclusive do
110
- [event.type, ANY_EVENT].each do |event_type|
111
- [event.name, ANY_STATE].each do |event_name|
112
+ [event.type].each do |event_type|
113
+ [event.name, ANY_STATE, ANY_EVENT].each do |event_name|
112
114
  hooks.call(event_type, event_name) do |hook|
113
115
  handle_callback(hook, event)
114
116
  off(event_type, event_name, &hook) if hook.is_a?(Once)
@@ -163,7 +165,7 @@ module FiniteMachine
163
165
  #
164
166
  # @api private
165
167
  def callback_names
166
- machine.states + machine.event_names + [ANY_EVENT]
168
+ machine.states + machine.event_names + [ANY_EVENT, ANY_STATE]
167
169
  end
168
170
 
169
171
  # Forward the message to observer
@@ -38,6 +38,9 @@ module FiniteMachine
38
38
  # The state machine environment
39
39
  attr_threadsafe :env
40
40
 
41
+ # The previous state before transition
42
+ attr_threadsafe :previous_state
43
+
41
44
  # The state machine event definitions
42
45
  attr_threadsafe :events_chain
43
46
 
@@ -4,7 +4,6 @@ module FiniteMachine
4
4
  # A mixin to allow instance methods to be synchronized
5
5
  module Threadable
6
6
  module InstanceMethods
7
- @@sync = Sync.new
8
7
 
9
8
  # Exclusive lock
10
9
  #
@@ -12,7 +11,7 @@ module FiniteMachine
12
11
  #
13
12
  # @api public
14
13
  def sync_exclusive(&block)
15
- @@sync.synchronize(:EX, &block)
14
+ TwoPhaseLock.synchronize(:EX, &block)
16
15
  end
17
16
 
18
17
  # Shared lock
@@ -21,7 +20,7 @@ module FiniteMachine
21
20
  #
22
21
  # @api public
23
22
  def sync_shared(&block)
24
- @@sync.synchronize(:SH, &block)
23
+ TwoPhaseLock.synchronize(:SH, &block)
25
24
  end
26
25
  end
27
26
 
@@ -63,12 +63,12 @@ module FiniteMachine
63
63
  #
64
64
  # @api public
65
65
  def self.create(machine, attrs = {})
66
- _transition = new(machine, attrs)
67
- _transition.update_transitions
68
- _transition.define_state_methods
66
+ transition = new(machine, attrs)
67
+ transition.update_transitions
68
+ transition.define_state_methods
69
69
 
70
70
  builder = EventBuilder.new(machine)
71
- builder.call(_transition)
71
+ builder.call(transition)
72
72
  end
73
73
 
74
74
  # Decide :to state from available transitions for this event
@@ -78,10 +78,11 @@ module FiniteMachine
78
78
  # @api public
79
79
  def to_state(*args)
80
80
  if transition_choice?
81
- found_trans = machine.select_choice_transition(name, *args)
82
- found_trans.map[from_state]
81
+ found_trans = machine.select_choice_transition(name, from_state, *args)
82
+ found_trans.map[from_state] || found_trans.map[ANY_STATE]
83
83
  else
84
- machine.transitions[name][from_state]
84
+ available_trans = machine.transitions[name]
85
+ available_trans[from_state] || available_trans[ANY_STATE]
85
86
  end
86
87
  end
87
88
 
@@ -165,7 +166,7 @@ module FiniteMachine
165
166
  # @api private
166
167
  def update_transitions
167
168
  from_states.each do |from|
168
- if value = machine.transitions[name][from]
169
+ if (value = machine.transitions[name][from])
169
170
  machine.transitions[name][from] = [value, map[from]].flatten
170
171
  else
171
172
  machine.transitions[name][from] = map[from] || ANY_STATE
@@ -205,6 +206,20 @@ module FiniteMachine
205
206
  end
206
207
  end
207
208
 
209
+ # Find latest from state
210
+ #
211
+ # Note that for the exit hook the call hasn't happened yet so
212
+ # we need to find previous to state when the from is :any.
213
+ #
214
+ # @return [Object] from_state
215
+ #
216
+ # @api private
217
+ def latest_from_state
218
+ sync_shared do
219
+ from_state == ANY_STATE ? machine.previous_state : from_state
220
+ end
221
+ end
222
+
208
223
  # Execute current transition
209
224
  #
210
225
  # @return [nil]
@@ -215,6 +230,7 @@ module FiniteMachine
215
230
  return if cancelled
216
231
  self.from_state = machine.state
217
232
  update_state(*args)
233
+ machine.previous_state = machine.state
218
234
  machine.initial_state = machine.state if from_state == DEFAULT_STATE
219
235
  end
220
236
  end
@@ -2,12 +2,28 @@
2
2
 
3
3
  module FiniteMachine
4
4
  # A class representing a callback transition event
5
+ #
6
+ # Used internally by {Observer}
7
+ #
8
+ # @api private
5
9
  class TransitionEvent
6
-
10
+ # This event from state name
11
+ #
12
+ # @return [Object]
13
+ #
14
+ # @api public
7
15
  attr_accessor :from
8
16
 
17
+ # This event to state name
18
+ #
19
+ # @return [Object]
20
+ #
21
+ # @api public
9
22
  attr_accessor :to
10
23
 
24
+ # This event name
25
+ #
26
+ # @api public
11
27
  attr_accessor :name
12
28
 
13
29
  # Build a transition event
@@ -20,7 +36,7 @@ module FiniteMachine
20
36
  def self.build(transition, *data)
21
37
  instance = new
22
38
  instance.name = transition.name
23
- instance.from = transition.from_state
39
+ instance.from = transition.latest_from_state
24
40
  instance.to = transition.to_state(*data)
25
41
  instance
26
42
  end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+ # Mixin to provide lock to a {Threadable}
5
+ #
6
+ # @api private
7
+ module TwoPhaseLock
8
+ # Create synchronization lock
9
+ #
10
+ # @return [Sync]
11
+ #
12
+ # @api private
13
+ def sync
14
+ @sync ||= Sync.new
15
+ end
16
+
17
+ # Synchronize given block of code
18
+ #
19
+ # @param [Symbol] mode
20
+ # the synchronization mode out of :SH and :EX
21
+ #
22
+ # @return [nil]
23
+ #
24
+ # @api private
25
+ def synchronize(mode, &block)
26
+ sync.synchronize(mode, &block)
27
+ end
28
+
29
+ module_function :sync, :synchronize
30
+ end # TwoPhaseLock
31
+ end # FiniteMachine
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
- VERSION = "0.9.1"
4
+ VERSION = "0.9.2"
5
5
  end
@@ -429,6 +429,92 @@ describe FiniteMachine, 'callbacks' do
429
429
  fsm.go(nil, nil)
430
430
  end
431
431
 
432
+ it "sets callback parameters correctly for transition from :any state" do
433
+ expected = {name: :init, from: :none, to: :green, a: nil, b: nil, c: nil }
434
+
435
+ callback = Proc.new { |event, a, b, c|
436
+ target.expect(event.from).to target.eql(expected[:from])
437
+ target.expect(event.to).to target.eql(expected[:to])
438
+ target.expect(event.name).to target.eql(expected[:name])
439
+ target.expect(a).to target.eql(expected[:a])
440
+ target.expect(b).to target.eql(expected[:b])
441
+ target.expect(c).to target.eql(expected[:c])
442
+ }
443
+
444
+ context = self
445
+
446
+ fsm = FiniteMachine.define do
447
+ initial :red
448
+
449
+ target context
450
+
451
+ events {
452
+ event :power_on, :off => :red
453
+ event :power_off, :any => :off
454
+ event :go, :red => :green
455
+ event :slow, :green => :yellow
456
+ event :stop, :yellow => :red
457
+ }
458
+
459
+ callbacks {
460
+ # generic state callbacks
461
+ on_enter(&callback)
462
+ on_transition(&callback)
463
+ on_exit(&callback)
464
+
465
+ # generic event callbacks
466
+ on_before(&callback)
467
+ on_after(&callback)
468
+
469
+ # state callbacks
470
+ on_enter :green, &callback
471
+ on_enter :yellow, &callback
472
+ on_enter :red, &callback
473
+ on_enter :off, &callback
474
+ on_enter :off, &callback
475
+
476
+ on_transition :green, &callback
477
+ on_transition :yellow, &callback
478
+ on_transition :red, &callback
479
+ on_transition :off, &callback
480
+ on_transition :off, &callback
481
+
482
+ on_exit :green, &callback
483
+ on_exit :yellow, &callback
484
+ on_exit :red, &callback
485
+ on_exit :off, &callback
486
+ on_exit :off, &callback
487
+
488
+ # event callbacks
489
+ on_before :power_on, &callback
490
+ on_before :power_off, &callback
491
+ on_before :go, &callback
492
+ on_before :slow, &callback
493
+ on_before :stop, &callback
494
+
495
+ on_after :power_on, &callback
496
+ on_after :power_off, &callback
497
+ on_after :go, &callback
498
+ on_after :slow, &callback
499
+ on_after :stop, &callback
500
+ }
501
+ end
502
+
503
+ expect(fsm.current).to eq(:red)
504
+
505
+ expected = {name: :go, from: :red, to: :green, a: 1, b: 2, c: 3 }
506
+ fsm.go(1, 2, 3)
507
+
508
+ expected = {name: :slow, from: :green, to: :yellow, a: 4, b: 5, c: 6}
509
+ fsm.slow(4, 5, 6)
510
+
511
+ expected = {name: :stop, from: :yellow, to: :red, a: 7, b: 8, c: 9}
512
+ fsm.stop(7, 8, 9)
513
+
514
+ expected = {name: :power_off, from: :red, to: :off, a: 10, b: 11, c: 12}
515
+ fsm.power_off(10, 11, 12)
516
+ end
517
+
432
518
  it "raises an error with invalid callback name" do
433
519
  expect {
434
520
  FiniteMachine.define do
@@ -184,4 +184,80 @@ describe FiniteMachine, '#choice' do
184
184
  fsm.next
185
185
  expect(fsm.current).to eq(:green)
186
186
  end
187
+
188
+ it "sets callback properties correctly" do
189
+ expected = {name: :init, from: :none, to: :red, a: nil, b: nil, c: nil }
190
+
191
+ callback = Proc.new { |event, a, b, c|
192
+ target.expect(event.from).to target.eql(expected[:from])
193
+ target.expect(event.to).to target.eql(expected[:to])
194
+ target.expect(event.name).to target.eql(expected[:name])
195
+ target.expect(a).to target.eql(expected[:a])
196
+ target.expect(b).to target.eql(expected[:b])
197
+ target.expect(c).to target.eql(expected[:c])
198
+ }
199
+
200
+ context = self
201
+
202
+ fsm = FiniteMachine.define do
203
+ initial :red
204
+
205
+ target context
206
+
207
+ events {
208
+ event :next, from: :red do
209
+ choice :green, if: -> { false }
210
+ choice :yellow
211
+ end
212
+
213
+ event :next, from: :yellow do
214
+ choice :green, if: -> { true }
215
+ choice :yellow
216
+ end
217
+
218
+ event :finish, from: :any do
219
+ choice :green, if: -> { false }
220
+ choice :red
221
+ end
222
+ }
223
+
224
+ callbacks {
225
+ # generic state callbacks
226
+ on_enter(&callback)
227
+ on_transition(&callback)
228
+ on_exit(&callback)
229
+
230
+ # generic event callbacks
231
+ on_before(&callback)
232
+ on_after(&callback)
233
+
234
+ # state callbacks
235
+ on_enter :green, &callback
236
+ on_enter :yellow, &callback
237
+ on_enter :red, &callback
238
+
239
+ on_transition :green, &callback
240
+ on_transition :yellow, &callback
241
+ on_transition :red, &callback
242
+
243
+ on_exit :green, &callback
244
+ on_exit :yellow, &callback
245
+ on_exit :red, &callback
246
+
247
+ # event callbacks
248
+ on_before :next, &callback
249
+ on_after :next, &callback
250
+ }
251
+ end
252
+ expect(fsm.current).to eq(:red)
253
+
254
+ expected = {name: :next, from: :red, to: :yellow, a: 1, b: 2, c: 3}
255
+ fsm.next(1, 2, 3)
256
+
257
+ expected = {name: :next, from: :yellow, to: :green, a: 4, b: 5, c: 6}
258
+ fsm.next(4, 5, 6)
259
+
260
+ expected = {name: :finish, from: :green, to: :red, a: 7, b: 8, c: 9}
261
+ fsm.finish(7, 8, 9)
262
+ end
187
263
  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.9.1
4
+ version: 0.9.2
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-08-10 00:00:00.000000000 Z
11
+ date: 2014-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -72,6 +72,7 @@ files:
72
72
  - lib/finite_machine/transition.rb
73
73
  - lib/finite_machine/transition_builder.rb
74
74
  - lib/finite_machine/transition_event.rb
75
+ - lib/finite_machine/two_phase_lock.rb
75
76
  - lib/finite_machine/version.rb
76
77
  - spec/spec_helper.rb
77
78
  - spec/unit/async_events_spec.rb