finite_machine 0.3.0 → 0.4.0
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 +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
|