finite_machine 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +85 -14
- data/examples/atm.rb +45 -0
- data/examples/bug_system.rb +145 -0
- data/lib/finite_machine.rb +16 -4
- data/lib/finite_machine/async_call.rb +10 -1
- data/lib/finite_machine/dsl.rb +27 -8
- data/lib/finite_machine/event_queue.rb +12 -10
- data/lib/finite_machine/logger.rb +23 -0
- data/lib/finite_machine/observer.rb +11 -3
- data/lib/finite_machine/state_machine.rb +2 -4
- data/lib/finite_machine/subscribers.rb +36 -2
- data/lib/finite_machine/thread_context.rb +3 -1
- data/lib/finite_machine/transition.rb +32 -6
- data/lib/finite_machine/version.rb +1 -1
- data/spec/unit/async_events_spec.rb +4 -4
- data/spec/unit/callbacks_spec.rb +48 -6
- data/spec/unit/events_spec.rb +15 -0
- data/spec/unit/if_unless_spec.rb +90 -60
- data/spec/unit/initialize_spec.rb +48 -1
- data/spec/unit/inspect_spec.rb +25 -0
- data/spec/unit/logger_spec.rb +33 -0
- data/spec/unit/subscribers_spec.rb +31 -0
- metadata +11 -2
@@ -13,6 +13,13 @@ module FiniteMachine
|
|
13
13
|
|
14
14
|
attr_threadsafe :block
|
15
15
|
|
16
|
+
# Create an AsynCall
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
def initialize
|
20
|
+
@mutex = Mutex.new
|
21
|
+
end
|
22
|
+
|
16
23
|
# Build asynchronous call instance
|
17
24
|
#
|
18
25
|
# @param [Object] context
|
@@ -41,7 +48,9 @@ module FiniteMachine
|
|
41
48
|
#
|
42
49
|
# @api private
|
43
50
|
def dispatch
|
44
|
-
|
51
|
+
@mutex.synchronize do
|
52
|
+
callable.call(context, *arguments, block)
|
53
|
+
end
|
45
54
|
end
|
46
55
|
end # AsyncCall
|
47
56
|
end # FiniteMachine
|
data/lib/finite_machine/dsl.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module FiniteMachine
|
4
|
-
|
5
4
|
# A generic DSL for describing the state machine
|
6
5
|
class GenericDSL
|
7
6
|
include Threadable
|
@@ -32,7 +31,6 @@ module FiniteMachine
|
|
32
31
|
end # GenericDSL
|
33
32
|
|
34
33
|
class DSL < GenericDSL
|
35
|
-
|
36
34
|
attr_threadsafe :defer
|
37
35
|
|
38
36
|
attr_threadsafe :initial_event
|
@@ -48,12 +46,20 @@ module FiniteMachine
|
|
48
46
|
|
49
47
|
# Define initial state
|
50
48
|
#
|
49
|
+
# @example
|
50
|
+
# initial :green
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# initial state: green, defer: true
|
54
|
+
#
|
51
55
|
# @param [String, Hash] value
|
52
56
|
#
|
57
|
+
# @return [StateMachine]
|
58
|
+
#
|
53
59
|
# @api public
|
54
60
|
def initial(value)
|
55
61
|
state, name, self.defer = parse(value)
|
56
|
-
|
62
|
+
machine.state = state unless defer
|
57
63
|
event = proc { event name, from: FiniteMachine::DEFAULT_STATE, to: state }
|
58
64
|
machine.events.call(&event)
|
59
65
|
end
|
@@ -70,6 +76,11 @@ module FiniteMachine
|
|
70
76
|
|
71
77
|
# Define terminal state
|
72
78
|
#
|
79
|
+
# @example
|
80
|
+
# terminal :red
|
81
|
+
#
|
82
|
+
# @return [StateMachine]
|
83
|
+
#
|
73
84
|
# @api public
|
74
85
|
def terminal(value)
|
75
86
|
machine.final_state = value
|
@@ -100,19 +111,28 @@ module FiniteMachine
|
|
100
111
|
|
101
112
|
# Parse initial options
|
102
113
|
#
|
103
|
-
# @param [
|
114
|
+
# @param [Object] value
|
104
115
|
#
|
105
116
|
# @return [Array[Symbol,String]]
|
106
117
|
#
|
107
118
|
# @api private
|
108
119
|
def parse(value)
|
109
|
-
|
120
|
+
unless value.is_a?(Hash)
|
110
121
|
[value, FiniteMachine::DEFAULT_EVENT_NAME, false]
|
111
122
|
else
|
112
|
-
[value
|
113
|
-
|
123
|
+
[value.fetch(:state) { raise_missing_state },
|
124
|
+
value.fetch(:event) { FiniteMachine::DEFAULT_EVENT_NAME },
|
125
|
+
value.fetch(:defer) { false }]
|
114
126
|
end
|
115
127
|
end
|
128
|
+
|
129
|
+
# Raises missing state error
|
130
|
+
#
|
131
|
+
# @api private
|
132
|
+
def raise_missing_state
|
133
|
+
raise MissingInitialStateError,
|
134
|
+
'Provide state to transition :to for the initial event'
|
135
|
+
end
|
116
136
|
end # DSL
|
117
137
|
|
118
138
|
class EventsDSL < GenericDSL
|
@@ -151,6 +171,5 @@ module FiniteMachine
|
|
151
171
|
def handle(*exceptions, &block)
|
152
172
|
machine.handle(*exceptions, &block)
|
153
173
|
end
|
154
|
-
|
155
174
|
end # ErrorsDSL
|
156
175
|
end # FiniteMachine
|
@@ -16,7 +16,10 @@ module FiniteMachine
|
|
16
16
|
@queue = Queue.new
|
17
17
|
@mutex = Mutex.new
|
18
18
|
@dead = false
|
19
|
-
|
19
|
+
|
20
|
+
@thread = Thread.new do
|
21
|
+
process_events
|
22
|
+
end
|
20
23
|
end
|
21
24
|
|
22
25
|
# Retrieve the next event
|
@@ -104,20 +107,19 @@ module FiniteMachine
|
|
104
107
|
|
105
108
|
private
|
106
109
|
|
107
|
-
#
|
110
|
+
# Process all the events
|
108
111
|
#
|
109
112
|
# @return [Thread]
|
110
113
|
#
|
111
114
|
# @api private
|
112
|
-
def
|
113
|
-
@
|
114
|
-
|
115
|
-
|
116
|
-
event = next_event
|
117
|
-
Thread.exit unless event
|
118
|
-
event.dispatch
|
119
|
-
end
|
115
|
+
def process_events
|
116
|
+
until(@dead) do
|
117
|
+
event = next_event
|
118
|
+
event.dispatch
|
120
119
|
end
|
120
|
+
rescue Exception => ex
|
121
|
+
Logger.error "Error while running event: #{ex}"
|
121
122
|
end
|
123
|
+
|
122
124
|
end # EventQueue
|
123
125
|
end # FiniteMachine
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
module Logger
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def debug(message)
|
8
|
+
FiniteMachine.logger.debug(message)
|
9
|
+
end
|
10
|
+
|
11
|
+
def info(message)
|
12
|
+
FiniteMachine.logger.info(message)
|
13
|
+
end
|
14
|
+
|
15
|
+
def warn(message)
|
16
|
+
FiniteMachine.logger.warn(message)
|
17
|
+
end
|
18
|
+
|
19
|
+
def error(message)
|
20
|
+
FiniteMachine.logger.error(message)
|
21
|
+
end
|
22
|
+
end # Logger
|
23
|
+
end # FiniteMachine
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
require 'set'
|
4
|
+
|
3
5
|
module FiniteMachine
|
4
6
|
|
5
7
|
# A class responsible for observing state changes
|
@@ -98,14 +100,20 @@ module FiniteMachine
|
|
98
100
|
end
|
99
101
|
end
|
100
102
|
|
103
|
+
# Run callback
|
104
|
+
#
|
105
|
+
# @api private
|
101
106
|
def run_callback(hook, event)
|
102
107
|
trans_event = TransitionEvent.new
|
103
108
|
trans_event.build(event.transition)
|
104
109
|
data = event.data
|
105
|
-
|
110
|
+
transition = event.transition
|
111
|
+
deferred_hook = proc do |_trans_event, *_data|
|
106
112
|
machine.instance_exec(_trans_event, *_data, &hook)
|
107
|
-
|
108
|
-
|
113
|
+
end
|
114
|
+
callable = Callable.new(deferred_hook)
|
115
|
+
result = callable.call(trans_event, *data)
|
116
|
+
transition.cancelled = (result == CANCELLED)
|
109
117
|
end
|
110
118
|
|
111
119
|
def trigger(event, *args, &block)
|
@@ -48,11 +48,9 @@ module FiniteMachine
|
|
48
48
|
@observer = Observer.new(self)
|
49
49
|
@transitions = Hash.new { |hash, name| hash[name] = Hash.new }
|
50
50
|
@env = Environment.new(target: self)
|
51
|
+
@dsl = DSL.new(self)
|
51
52
|
|
52
|
-
@dsl = DSL.new self
|
53
53
|
@dsl.call(&block) if block_given?
|
54
|
-
send(:"#{@dsl.initial_event}") unless @dsl.defer
|
55
|
-
self.event_queue = FiniteMachine::EventQueue.new
|
56
54
|
end
|
57
55
|
|
58
56
|
# @example
|
@@ -213,7 +211,7 @@ module FiniteMachine
|
|
213
211
|
return CANCELLED if valid_state?(_transition)
|
214
212
|
|
215
213
|
return CANCELLED unless _transition.conditions.all? do |condition|
|
216
|
-
condition.call(env.target)
|
214
|
+
condition.call(env.target, *args)
|
217
215
|
end
|
218
216
|
return NOTRANSITION if state == _transition.to
|
219
217
|
|
@@ -3,38 +3,72 @@
|
|
3
3
|
require 'monitor'
|
4
4
|
|
5
5
|
module FiniteMachine
|
6
|
-
|
7
6
|
# A class responsibile for storage of event subscribers
|
8
7
|
class Subscribers
|
9
8
|
include Enumerable
|
10
9
|
include MonitorMixin
|
11
10
|
|
11
|
+
# Initialize a subscribers collection
|
12
|
+
#
|
13
|
+
# @api public
|
12
14
|
def initialize(machine)
|
13
15
|
super()
|
14
16
|
@machine = machine
|
15
17
|
@subscribers = []
|
16
18
|
end
|
17
19
|
|
20
|
+
# Iterate over subscribers
|
21
|
+
#
|
22
|
+
# @api public
|
18
23
|
def each(&block)
|
19
24
|
@subscribers.each(&block)
|
20
25
|
end
|
21
26
|
|
27
|
+
# Return index of the subscriber
|
28
|
+
#
|
29
|
+
# @api public
|
22
30
|
def index(subscriber)
|
23
31
|
@subscribers.index(subscriber)
|
24
32
|
end
|
25
33
|
|
34
|
+
# Check if anyone is subscribed
|
35
|
+
#
|
36
|
+
# @return [Boolean]
|
37
|
+
#
|
38
|
+
# @api public
|
26
39
|
def empty?
|
27
40
|
@subscribers.empty?
|
28
41
|
end
|
29
42
|
|
43
|
+
# Add listener to subscribers
|
44
|
+
#
|
45
|
+
# @param [Array[#trigger]] observers
|
46
|
+
#
|
47
|
+
# @return [undefined]
|
48
|
+
#
|
49
|
+
# @api public
|
30
50
|
def subscribe(*observers)
|
31
|
-
|
51
|
+
synchronize do
|
52
|
+
observers.each { |observer| @subscribers << observer }
|
53
|
+
end
|
32
54
|
end
|
33
55
|
|
56
|
+
# Visit subscribers and notify
|
57
|
+
#
|
58
|
+
# @param [FiniteMachine::Event] event
|
59
|
+
#
|
60
|
+
# @return [undefined]
|
61
|
+
#
|
62
|
+
# @api public
|
34
63
|
def visit(event)
|
35
64
|
each { |subscriber| synchronize { event.notify subscriber } }
|
36
65
|
end
|
37
66
|
|
67
|
+
# Reset subscribers
|
68
|
+
#
|
69
|
+
# @return [self]
|
70
|
+
#
|
71
|
+
# @api public
|
38
72
|
def reset
|
39
73
|
@subscribers.clear
|
40
74
|
self
|
@@ -5,10 +5,12 @@ module FiniteMachine
|
|
5
5
|
# A mixin to allow sharing of thread context
|
6
6
|
module ThreadContext
|
7
7
|
|
8
|
+
# @api public
|
8
9
|
def event_queue
|
9
|
-
Thread.current[:finite_machine_event_queue]
|
10
|
+
Thread.current[:finite_machine_event_queue] ||= FiniteMachine::EventQueue.new
|
10
11
|
end
|
11
12
|
|
13
|
+
# @api public
|
12
14
|
def event_queue=(value)
|
13
15
|
Thread.current[:finite_machine_event_queue] = value
|
14
16
|
end
|
@@ -22,6 +22,9 @@ module FiniteMachine
|
|
22
22
|
# The original from state
|
23
23
|
attr_threadsafe :from_state
|
24
24
|
|
25
|
+
# Check if transition should be cancelled
|
26
|
+
attr_threadsafe :cancelled
|
27
|
+
|
25
28
|
# Initialize a Transition
|
26
29
|
#
|
27
30
|
# @param [StateMachine] machine
|
@@ -36,6 +39,7 @@ module FiniteMachine
|
|
36
39
|
@if = Array(attrs.fetch(:if, []))
|
37
40
|
@unless = Array(attrs.fetch(:unless, []))
|
38
41
|
@conditions = make_conditions
|
42
|
+
@cancelled = false
|
39
43
|
end
|
40
44
|
|
41
45
|
# Reduce conditions
|
@@ -97,17 +101,37 @@ module FiniteMachine
|
|
97
101
|
#
|
98
102
|
# @api private
|
99
103
|
def define_event
|
100
|
-
_transition = self
|
101
104
|
_name = name
|
105
|
+
bang_name = "#{_name}!"
|
102
106
|
|
103
107
|
machine.singleton_class.class_eval do
|
104
|
-
undef_method(_name)
|
108
|
+
undef_method(_name) if method_defined?(_name)
|
109
|
+
undef_method(bang_name) if method_defined?(bang_name)
|
105
110
|
end
|
111
|
+
define_transition(name)
|
112
|
+
define_event_bang(name)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Define transition event
|
116
|
+
#
|
117
|
+
# @api private
|
118
|
+
def define_transition(name)
|
119
|
+
_transition = self
|
106
120
|
machine.send(:define_singleton_method, name) do |*args, &block|
|
107
121
|
transition(_transition, *args, &block)
|
108
122
|
end
|
109
123
|
end
|
110
124
|
|
125
|
+
# Define event that skips validations
|
126
|
+
#
|
127
|
+
# @api private
|
128
|
+
def define_event_bang(name)
|
129
|
+
machine.send(:define_singleton_method, "#{name}!") do
|
130
|
+
transitions = machine.transitions[name]
|
131
|
+
machine.state = transitions.values[0]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
111
135
|
# Execute current transition
|
112
136
|
#
|
113
137
|
# @return [nil]
|
@@ -115,9 +139,10 @@ module FiniteMachine
|
|
115
139
|
# @api private
|
116
140
|
def call
|
117
141
|
sync_exclusive do
|
118
|
-
|
142
|
+
return if cancelled
|
143
|
+
transitions = machine.transitions[name]
|
119
144
|
self.from_state = machine.state
|
120
|
-
machine.state
|
145
|
+
machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
|
121
146
|
end
|
122
147
|
end
|
123
148
|
|
@@ -125,7 +150,7 @@ module FiniteMachine
|
|
125
150
|
#
|
126
151
|
# @api public
|
127
152
|
def to_s
|
128
|
-
@name
|
153
|
+
@name.to_s
|
129
154
|
end
|
130
155
|
|
131
156
|
# Return string representation
|
@@ -150,7 +175,8 @@ module FiniteMachine
|
|
150
175
|
#
|
151
176
|
# @api private
|
152
177
|
def raise_not_enough_transitions(attrs)
|
153
|
-
|
178
|
+
fail NotEnoughTransitionsError, "please provide state transitions for" \
|
179
|
+
" '#{attrs.inspect}'"
|
154
180
|
end
|
155
181
|
end # Transition
|
156
182
|
end # FiniteMachine
|
@@ -56,10 +56,10 @@ describe FiniteMachine, 'async_events' do
|
|
56
56
|
on_enter :yellow do |event, a| called << "(bar)on_enter_yellow_#{a}" end
|
57
57
|
}
|
58
58
|
end
|
59
|
-
fsmFoo.slow(:foo)
|
60
|
-
fsmBar.slow(:bar)
|
61
|
-
|
62
|
-
fsmBar.event_queue.join 0.
|
59
|
+
foo_thread = Thread.new { fsmFoo.async.slow(:foo) }
|
60
|
+
bar_thread = Thread.new { fsmBar.async.slow(:bar) }
|
61
|
+
[foo_thread, bar_thread].each(&:join)
|
62
|
+
[fsmFoo, fsmBar].each { |fsm| fsm.event_queue.join 0.02 }
|
63
63
|
expect(called).to include('(foo)on_enter_yellow_foo')
|
64
64
|
expect(called).to include('(bar)on_enter_yellow_bar')
|
65
65
|
expect(fsmFoo.current).to eql(:yellow)
|
data/spec/unit/callbacks_spec.rb
CHANGED
@@ -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 :green
|
10
|
+
initial state: :green, defer: true
|
11
11
|
|
12
12
|
events {
|
13
13
|
event :slow, :green => :yellow
|
@@ -47,8 +47,8 @@ describe FiniteMachine, 'callbacks' do
|
|
47
47
|
}
|
48
48
|
end
|
49
49
|
|
50
|
-
expect(fsm.current).to eql(:
|
51
|
-
|
50
|
+
expect(fsm.current).to eql(:none)
|
51
|
+
fsm.init
|
52
52
|
expect(called).to eql([
|
53
53
|
'on_exit_none',
|
54
54
|
'on_exit',
|
@@ -570,12 +570,54 @@ describe FiniteMachine, 'callbacks' do
|
|
570
570
|
fsm.slow
|
571
571
|
expect(fsm.current).to eql(:yellow)
|
572
572
|
expect(called).to eql([
|
573
|
-
'once_on_transition_green',
|
574
|
-
'once_on_enter_green',
|
575
573
|
'once_on_exit_green',
|
576
574
|
'once_on_transition_yellow',
|
577
575
|
'once_on_enter_yellow',
|
578
|
-
'once_on_exit_yellow'
|
576
|
+
'once_on_exit_yellow',
|
577
|
+
'once_on_transition_green',
|
578
|
+
'once_on_enter_green'
|
579
579
|
])
|
580
580
|
end
|
581
|
+
|
582
|
+
it "cancels transition on state callback" do
|
583
|
+
fsm = FiniteMachine.define do
|
584
|
+
initial :green
|
585
|
+
|
586
|
+
events {
|
587
|
+
event :slow, :green => :yellow
|
588
|
+
event :go, :yellow => :green
|
589
|
+
}
|
590
|
+
|
591
|
+
callbacks {
|
592
|
+
on_exit :green do |event| FiniteMachine::CANCELLED end
|
593
|
+
}
|
594
|
+
end
|
595
|
+
|
596
|
+
expect(fsm.current).to eql(:green)
|
597
|
+
fsm.slow
|
598
|
+
expect(fsm.current).to eql(:green)
|
599
|
+
end
|
600
|
+
|
601
|
+
it "cancels transition on event callback" do
|
602
|
+
fsm = FiniteMachine.define do
|
603
|
+
initial :green
|
604
|
+
|
605
|
+
events {
|
606
|
+
event :slow, :green => :yellow
|
607
|
+
event :go, :yellow => :green
|
608
|
+
}
|
609
|
+
|
610
|
+
callbacks {
|
611
|
+
on_enter :slow do |event|
|
612
|
+
FiniteMachine::CANCELLED
|
613
|
+
end
|
614
|
+
}
|
615
|
+
end
|
616
|
+
|
617
|
+
expect(fsm.current).to eql(:green)
|
618
|
+
fsm.slow
|
619
|
+
expect(fsm.current).to eql(:green)
|
620
|
+
end
|
621
|
+
|
622
|
+
xit "groups callbacks"
|
581
623
|
end
|