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