finite_machine 0.9.0 → 0.9.1
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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +65 -6
- data/lib/finite_machine.rb +1 -0
- data/lib/finite_machine/choice_merger.rb +2 -2
- data/lib/finite_machine/dsl.rb +6 -8
- data/lib/finite_machine/event.rb +8 -8
- data/lib/finite_machine/events_chain.rb +12 -0
- data/lib/finite_machine/state_machine.rb +2 -1
- data/lib/finite_machine/transition.rb +18 -2
- data/lib/finite_machine/transition_builder.rb +42 -0
- data/lib/finite_machine/version.rb +1 -1
- data/spec/unit/choice_spec.rb +50 -0
- data/spec/unit/event/next_transition_spec.rb +6 -19
- data/spec/unit/events_spec.rb +23 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ce0812460235da2deebb8aafc2d542b33392804
|
4
|
+
data.tar.gz: 89bb6ab79f200d7efaf004ffe9704bbbeb86b5c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed665ca255296e317a683106444e9e4ebc8044f927ea9067c92709fe777787de8b26cfa969e5c42bb2089f16c184d69774729fb06cd6c26d93b645dd1d96b133
|
7
|
+
data.tar.gz: 88538af514156c8a5e899b876e747a0716ab4439c271395418364830cc3372bafcbc772383aaeb3cbecf75c07b0e4087961092eec824c42709d1e40656cfd75f
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
0.9.1 (August 10, 2014)
|
2
|
+
|
3
|
+
* Add TransitionBuilder to internally build transitions from states
|
4
|
+
* Fix #choice to allow for multiple from states
|
5
|
+
* Add #current? to Transition to determine if matches from state
|
6
|
+
* Add #select_choice_transition to EventsChain to determine matching choice transition
|
7
|
+
* Fix #choice to work with same named events
|
8
|
+
|
1
9
|
0.9.0 (August 3, 2014)
|
2
10
|
|
3
11
|
* Add Definition class to allow to define standalone state machine
|
data/README.md
CHANGED
@@ -55,15 +55,18 @@ Or install it yourself as:
|
|
55
55
|
* [2.1 Performing transitions](#21-performing-transitions)
|
56
56
|
* [2.2 Forcing transitions](#22-forcing-transitions)
|
57
57
|
* [2.3 Asynchronous transitions](#23-asynchronous-transitions)
|
58
|
-
* [2.4
|
59
|
-
* [2.5
|
60
|
-
* [2.6
|
58
|
+
* [2.4 Multiple from states](#24-multiple-from-states)
|
59
|
+
* [2.5 From :any state](#25-from-any-state)
|
60
|
+
* [2.6 Grouping states under single event](#26-grouping-states-under-single-event)
|
61
|
+
* [2.7 Silent transitions](#27-silent-transitions)
|
61
62
|
* [3. Conditional transitions](#3-conditional-transitions)
|
62
63
|
* [3.1 Using a Proc](#31-using-a-proc)
|
63
64
|
* [3.2 Using a Symbol](#32-using-a-symbol)
|
64
65
|
* [3.3 Using a String](#33-using-a-string)
|
65
66
|
* [3.4 Combining transition conditions](#34-combining-transition-conditions)
|
66
67
|
* [4. Choice pseudostates](#4-choice-pseudostates)
|
68
|
+
* [4.1 Dynamic choice conditions](#41-dynamic-choice-conditions)
|
69
|
+
* [4.2 Multiple from states](#42-multiple-from-states)
|
67
70
|
* [5. Callbacks](#5-callbacks)
|
68
71
|
* [5.1 on_enter](#51-on_enter)
|
69
72
|
* [5.2 on_transition](#52-on_transition)
|
@@ -436,7 +439,7 @@ In order to fire the event transition asynchronously use the `async` scope like
|
|
436
439
|
fm.async.ready # => executes in separate Thread
|
437
440
|
```
|
438
441
|
|
439
|
-
### 2.4
|
442
|
+
### 2.4 Multiple from states
|
440
443
|
|
441
444
|
If an event transitions from multiple states to the same state then all the states can be grouped into an array.
|
442
445
|
Alternatively, you can create separate events under the same name for each transition that needs combining.
|
@@ -455,7 +458,29 @@ fm = FiniteMachine.define do
|
|
455
458
|
end
|
456
459
|
```
|
457
460
|
|
458
|
-
### 2.5
|
461
|
+
### 2.5 From :any state
|
462
|
+
|
463
|
+
The **FiniteMachine** offers few ways to transition out of any state. This is parrticularly useful when the machine already defines many states.
|
464
|
+
|
465
|
+
You can pass `:any` for the name of the state, for instance:
|
466
|
+
|
467
|
+
```ruby
|
468
|
+
event :run, from: :any, to: :green
|
469
|
+
|
470
|
+
or
|
471
|
+
|
472
|
+
event :run, :any => :green
|
473
|
+
```
|
474
|
+
|
475
|
+
Alternatively, you can skip the `:any` state definition and just specify `to` state:
|
476
|
+
|
477
|
+
```ruby
|
478
|
+
event :run, to: :green
|
479
|
+
```
|
480
|
+
|
481
|
+
All the above `run` event definitions will always transition the state machine into `:green` state.
|
482
|
+
|
483
|
+
### 2.6 Grouping states under single event
|
459
484
|
|
460
485
|
Another way to specify state transitions under single event name is to group all your state transitions into a single hash like so:
|
461
486
|
|
@@ -483,7 +508,7 @@ fm = FiniteMachine.define do
|
|
483
508
|
}
|
484
509
|
```
|
485
510
|
|
486
|
-
### 2.
|
511
|
+
### 2.7 Silent transitions
|
487
512
|
|
488
513
|
The **FiniteMachine** allows to selectively silence events and thus prevent any callbacks from firing. Using the `silent` option passed to event definition like so:
|
489
514
|
|
@@ -655,6 +680,8 @@ fsm.next
|
|
655
680
|
fsm.current # => :red
|
656
681
|
```
|
657
682
|
|
683
|
+
### 4.1 Dynamic choice conditions
|
684
|
+
|
658
685
|
Just as with event conditions you can make conditional logic dynamic and dependent on parameters passed in:
|
659
686
|
|
660
687
|
```ruby
|
@@ -677,6 +704,38 @@ fsm.current # => :yellow
|
|
677
704
|
|
678
705
|
If more than one of the conditions evaluates to true, a first matching one is chosen. If none of the conditions evaluate to true, then the `default` state is matched. However if default state is not present and non of the conditions match, no transition is performed. To avoid such situation always specify `default` choice.
|
679
706
|
|
707
|
+
### 4.2 Multiple from states
|
708
|
+
|
709
|
+
Similarly to event definitions, you can specify the event to transition from a group of states:
|
710
|
+
|
711
|
+
```ruby
|
712
|
+
FiniteMachine.define do
|
713
|
+
initial :red
|
714
|
+
|
715
|
+
events {
|
716
|
+
event :next, from: [:yellow, :red] do
|
717
|
+
choice :pink, if: -> { false }
|
718
|
+
choice :green
|
719
|
+
end
|
720
|
+
}
|
721
|
+
end
|
722
|
+
```
|
723
|
+
|
724
|
+
or from any state using the `:any` state name like so:
|
725
|
+
|
726
|
+
```ruby
|
727
|
+
FiniteMachine.define do
|
728
|
+
initial :red
|
729
|
+
|
730
|
+
events {
|
731
|
+
event :next, from: :any do
|
732
|
+
choice :pink, if: -> { false }
|
733
|
+
choice :green
|
734
|
+
end
|
735
|
+
}
|
736
|
+
end
|
737
|
+
```
|
738
|
+
|
680
739
|
## 5 Callbacks
|
681
740
|
|
682
741
|
You can watch state machine events and the information they provide by registering a callback. The following 5 types of callbacks are available in **FiniteMachine**:
|
data/lib/finite_machine.rb
CHANGED
@@ -22,6 +22,7 @@ require "finite_machine/events_chain"
|
|
22
22
|
require "finite_machine/hooks"
|
23
23
|
require "finite_machine/logger"
|
24
24
|
require "finite_machine/transition"
|
25
|
+
require "finite_machine/transition_builder"
|
25
26
|
require "finite_machine/transition_event"
|
26
27
|
require "finite_machine/dsl"
|
27
28
|
require "finite_machine/definition"
|
@@ -36,8 +36,8 @@ module FiniteMachine
|
|
36
36
|
def choice(to, attrs = {})
|
37
37
|
opts = options.dup
|
38
38
|
opts.merge!(attrs)
|
39
|
-
|
40
|
-
|
39
|
+
transition_builder = TransitionBuilder.new(context.machine, opts)
|
40
|
+
transition_builder.call(options[:from] => to)
|
41
41
|
end
|
42
42
|
alias_method :default, :choice
|
43
43
|
end # ChoiceMerger
|
data/lib/finite_machine/dsl.rb
CHANGED
@@ -226,14 +226,12 @@ module FiniteMachine
|
|
226
226
|
def event(name, attrs = {}, &block)
|
227
227
|
sync_exclusive do
|
228
228
|
attributes = attrs.merge!(name: name)
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
Transition.create(machine, attributes)
|
236
|
-
end
|
229
|
+
if block_given?
|
230
|
+
merger = ChoiceMerger.new(self, attributes)
|
231
|
+
merger.instance_eval(&block)
|
232
|
+
else
|
233
|
+
transition_builder = TransitionBuilder.new(machine, attributes)
|
234
|
+
transition_builder.call(attrs)
|
237
235
|
end
|
238
236
|
end
|
239
237
|
end
|
data/lib/finite_machine/event.rb
CHANGED
@@ -56,10 +56,8 @@ module FiniteMachine
|
|
56
56
|
# @api private
|
57
57
|
def next_transition
|
58
58
|
sync_shared do
|
59
|
-
state_transitions.find
|
60
|
-
|
61
|
-
transition.from_state == ANY_STATE
|
62
|
-
end || state_transitions.first
|
59
|
+
state_transitions.find { |transition| transition.current? } ||
|
60
|
+
state_transitions.first
|
63
61
|
end
|
64
62
|
end
|
65
63
|
|
@@ -72,7 +70,9 @@ module FiniteMachine
|
|
72
70
|
# @api private
|
73
71
|
def find_transition(*args)
|
74
72
|
sync_shared do
|
75
|
-
state_transitions.find
|
73
|
+
state_transitions.find do |trans|
|
74
|
+
trans.current? && trans.check_conditions(*args)
|
75
|
+
end
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
@@ -89,11 +89,11 @@ module FiniteMachine
|
|
89
89
|
# @api public
|
90
90
|
def call(*args, &block)
|
91
91
|
sync_exclusive do
|
92
|
-
|
92
|
+
event_transition = next_transition
|
93
93
|
if silent
|
94
|
-
|
94
|
+
event_transition.call(*args, &block)
|
95
95
|
else
|
96
|
-
machine.send(:transition,
|
96
|
+
machine.send(:transition, event_transition, *args, &block)
|
97
97
|
end
|
98
98
|
end
|
99
99
|
end
|
@@ -70,6 +70,18 @@ module FiniteMachine
|
|
70
70
|
chain[name].find_transition(*args)
|
71
71
|
end
|
72
72
|
|
73
|
+
# Examine choice transitions to find one matching condition
|
74
|
+
#
|
75
|
+
# @return [FiniteMachine::Transition]
|
76
|
+
# The choice transition that matches
|
77
|
+
#
|
78
|
+
# @api public
|
79
|
+
def select_choice_transition(name, *args, &block)
|
80
|
+
chain[name].state_transitions.find do |trans|
|
81
|
+
trans.check_conditions(*args, &block)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
73
85
|
# Check if any of the transition constraints passes
|
74
86
|
#
|
75
87
|
# @param [Symbol] name
|
@@ -45,7 +45,8 @@ module FiniteMachine
|
|
45
45
|
|
46
46
|
def_delegator :@events_dsl, :event
|
47
47
|
|
48
|
-
def_delegators :@events_chain, :check_choice_conditions, :select_transition
|
48
|
+
def_delegators :@events_chain, :check_choice_conditions, :select_transition,
|
49
|
+
:select_choice_transition
|
49
50
|
|
50
51
|
# Initialize state machine
|
51
52
|
#
|
@@ -78,7 +78,7 @@ module FiniteMachine
|
|
78
78
|
# @api public
|
79
79
|
def to_state(*args)
|
80
80
|
if transition_choice?
|
81
|
-
found_trans = machine.
|
81
|
+
found_trans = machine.select_choice_transition(name, *args)
|
82
82
|
found_trans.map[from_state]
|
83
83
|
else
|
84
84
|
machine.transitions[name][from_state]
|
@@ -116,13 +116,29 @@ module FiniteMachine
|
|
116
116
|
map[state] == state || (map[ANY_STATE] == state && from_state == state)
|
117
117
|
end
|
118
118
|
|
119
|
+
# Check if from matches current state
|
120
|
+
#
|
121
|
+
# @example
|
122
|
+
# transition.current? # => true
|
123
|
+
#
|
124
|
+
# @return [Boolean]
|
125
|
+
# Return true if match is found, false otherwise.
|
126
|
+
#
|
127
|
+
# @api public
|
128
|
+
def current?
|
129
|
+
[machine.current, ANY_STATE].any? { |state| state == from_state }
|
130
|
+
end
|
131
|
+
|
119
132
|
# Check if this transition has branching choice or not
|
120
133
|
#
|
121
134
|
# @return [Boolean]
|
122
135
|
#
|
123
136
|
# @api public
|
124
137
|
def transition_choice?
|
125
|
-
machine.transitions[name]
|
138
|
+
matching = machine.transitions[name]
|
139
|
+
[matching[from_state], matching[ANY_STATE]].any? do |match|
|
140
|
+
match.is_a?(Array)
|
141
|
+
end
|
126
142
|
end
|
127
143
|
|
128
144
|
# Check if transition can be performed according to constraints
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
# A class reponsible for building transition out of parsed states
|
5
|
+
class TransitionBuilder
|
6
|
+
include Threadable
|
7
|
+
|
8
|
+
# The current state machine
|
9
|
+
attr_threadsafe :machine
|
10
|
+
|
11
|
+
attr_threadsafe :attributes
|
12
|
+
|
13
|
+
# Initialize a TransitionBuilder
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# TransitionBuilder.new(machine, {})
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
def initialize(machine, attributes = {})
|
20
|
+
@machine = machine
|
21
|
+
@attributes = attributes
|
22
|
+
end
|
23
|
+
|
24
|
+
# Creates transitions for the states
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# transition_parser.call([:green, :yellow] => :red)
|
28
|
+
#
|
29
|
+
# @param [Hash[Symbol]] states
|
30
|
+
# The states to extract
|
31
|
+
#
|
32
|
+
# @return [nil]
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
def call(states)
|
36
|
+
FiniteMachine::StateParser.new(states).parse_states do |from, to|
|
37
|
+
attributes.merge!(parsed_states: { from => to })
|
38
|
+
Transition.create(machine, attributes)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end # TransitionBuilder
|
42
|
+
end # FiniteMachine
|
data/spec/unit/choice_spec.rb
CHANGED
@@ -134,4 +134,54 @@ describe FiniteMachine, '#choice' do
|
|
134
134
|
fsm.next
|
135
135
|
expect(fsm.current).to eq(:company_form)
|
136
136
|
end
|
137
|
+
|
138
|
+
it "allows to transition from multiple states to choice pseudostate" do
|
139
|
+
fsm = FiniteMachine.define do
|
140
|
+
initial :red
|
141
|
+
|
142
|
+
event :go, from: [:yellow, :red] do
|
143
|
+
choice :pink, if: -> { false }
|
144
|
+
choice :green
|
145
|
+
end
|
146
|
+
end
|
147
|
+
expect(fsm.current).to eq(:red)
|
148
|
+
fsm.go
|
149
|
+
expect(fsm.current).to eq(:green)
|
150
|
+
fsm.restore!(:yellow)
|
151
|
+
expect(fsm.current).to eq(:yellow)
|
152
|
+
fsm.go
|
153
|
+
expect(fsm.current).to eq(:green)
|
154
|
+
end
|
155
|
+
|
156
|
+
it "allows to transition from any state to choice pseudo state" do
|
157
|
+
fsm = FiniteMachine.define do
|
158
|
+
initial :red
|
159
|
+
|
160
|
+
event :go, from: :any do
|
161
|
+
choice :pink, if: -> { false }
|
162
|
+
choice :green
|
163
|
+
end
|
164
|
+
end
|
165
|
+
expect(fsm.current).to eq(:red)
|
166
|
+
fsm.go
|
167
|
+
expect(fsm.current).to eq(:green)
|
168
|
+
end
|
169
|
+
|
170
|
+
it "groups correctly events under the same name" do
|
171
|
+
fsm = FiniteMachine.define do
|
172
|
+
initial :red
|
173
|
+
|
174
|
+
event :next, from: :yellow, to: :green
|
175
|
+
|
176
|
+
event :next, from: :red do
|
177
|
+
choice :pink, if: -> { false }
|
178
|
+
choice :yellow
|
179
|
+
end
|
180
|
+
end
|
181
|
+
expect(fsm.current).to eq(:red)
|
182
|
+
fsm.next
|
183
|
+
expect(fsm.current).to eq(:yellow)
|
184
|
+
fsm.next
|
185
|
+
expect(fsm.current).to eq(:green)
|
186
|
+
end
|
137
187
|
end
|
@@ -8,11 +8,11 @@ describe FiniteMachine::Event, '#next_transition' do
|
|
8
8
|
subject(:event) { object.new(machine, name: :test) }
|
9
9
|
|
10
10
|
describe "matches transition by name" do
|
11
|
-
let(:machine) { double(:machine
|
11
|
+
let(:machine) { double(:machine) }
|
12
12
|
|
13
13
|
it "finds matching transition" do
|
14
|
-
transition_a = double(:transition_a,
|
15
|
-
transition_b = double(:transition_b,
|
14
|
+
transition_a = double(:transition_a, current?: false)
|
15
|
+
transition_b = double(:transition_b, current?: true)
|
16
16
|
event << transition_a
|
17
17
|
event << transition_b
|
18
18
|
|
@@ -20,25 +20,12 @@ describe FiniteMachine::Event, '#next_transition' do
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
describe "matches :any transition" do
|
24
|
-
let(:machine) { double(:machine, current: :any) }
|
25
|
-
|
26
|
-
it "finds matching transition" do
|
27
|
-
transition_a = double(:transition_a, from_state: :a)
|
28
|
-
transition_any = double(:transition_any, from_state: :any)
|
29
|
-
event << transition_a
|
30
|
-
event << transition_any
|
31
|
-
|
32
|
-
expect(event.next_transition).to eq(transition_any)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
23
|
describe "fails to find" do
|
37
|
-
let(:machine) { double(:machine
|
24
|
+
let(:machine) { double(:machine) }
|
38
25
|
|
39
26
|
it "choses first available transition" do
|
40
|
-
transition_a = double(:transition_a,
|
41
|
-
transition_b = double(:transition_b,
|
27
|
+
transition_a = double(:transition_a, current?: false)
|
28
|
+
transition_b = double(:transition_b, current?: false)
|
42
29
|
event << transition_a
|
43
30
|
event << transition_b
|
44
31
|
|
data/spec/unit/events_spec.rb
CHANGED
@@ -111,6 +111,29 @@ describe FiniteMachine, 'events' do
|
|
111
111
|
expect(fsm.current).to eql(:green)
|
112
112
|
end
|
113
113
|
|
114
|
+
it "permits event from any state for hash syntax" do
|
115
|
+
fsm = FiniteMachine.define do
|
116
|
+
initial :red
|
117
|
+
|
118
|
+
events {
|
119
|
+
event :start, :red => :yellow
|
120
|
+
event :run, :yellow => :green
|
121
|
+
event :stop, :green => :red
|
122
|
+
event :go, :any => :green
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
expect(fsm.current).to eql(:red)
|
127
|
+
|
128
|
+
fsm.go
|
129
|
+
expect(fsm.current).to eql(:green)
|
130
|
+
fsm.stop
|
131
|
+
fsm.start
|
132
|
+
expect(fsm.current).to eql(:yellow)
|
133
|
+
fsm.go
|
134
|
+
expect(fsm.current).to eql(:green)
|
135
|
+
end
|
136
|
+
|
114
137
|
it "permits event from any state without 'from'" do
|
115
138
|
fsm = FiniteMachine.define do
|
116
139
|
initial :green
|
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.
|
4
|
+
version: 0.9.1
|
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-
|
11
|
+
date: 2014-08-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -70,6 +70,7 @@ files:
|
|
70
70
|
- lib/finite_machine/thread_context.rb
|
71
71
|
- lib/finite_machine/threadable.rb
|
72
72
|
- lib/finite_machine/transition.rb
|
73
|
+
- lib/finite_machine/transition_builder.rb
|
73
74
|
- lib/finite_machine/transition_event.rb
|
74
75
|
- lib/finite_machine/version.rb
|
75
76
|
- spec/spec_helper.rb
|