finite_machine 0.2.0 → 0.3.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/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +12 -1
- data/README.md +130 -73
- data/Rakefile +3 -36
- data/finite_machine.gemspec +1 -3
- data/lib/finite_machine.rb +4 -1
- data/lib/finite_machine/async_call.rb +47 -0
- data/lib/finite_machine/async_proxy.rb +28 -0
- data/lib/finite_machine/callable.rb +5 -5
- data/lib/finite_machine/catchable.rb +2 -2
- data/lib/finite_machine/dsl.rb +16 -9
- data/lib/finite_machine/event.rb +1 -1
- data/lib/finite_machine/event_queue.rb +123 -0
- data/lib/finite_machine/hooks.rb +33 -0
- data/lib/finite_machine/observer.rb +53 -11
- data/lib/finite_machine/state_machine.rb +70 -2
- data/lib/finite_machine/subscribers.rb +5 -2
- data/lib/finite_machine/thread_context.rb +16 -0
- data/lib/finite_machine/threadable.rb +36 -3
- data/lib/finite_machine/transition.rb +47 -4
- data/lib/finite_machine/version.rb +1 -1
- data/spec/spec_helper.rb +15 -0
- data/spec/unit/async_events_spec.rb +68 -0
- data/spec/unit/callable/call_spec.rb +25 -3
- data/spec/unit/callbacks_spec.rb +110 -4
- data/spec/unit/events_spec.rb +5 -0
- data/spec/unit/handlers_spec.rb +53 -0
- data/spec/unit/if_unless_spec.rb +1 -1
- data/spec/unit/is_spec.rb +22 -0
- data/spec/unit/target_spec.rb +17 -0
- data/tasks/console.rake +10 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- metadata +16 -32
data/finite_machine.gemspec
CHANGED
@@ -18,7 +18,5 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_development_dependency "bundler", "~> 1.
|
22
|
-
spec.add_development_dependency "rake"
|
23
|
-
spec.add_development_dependency "rspec"
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
24
22
|
end
|
data/lib/finite_machine.rb
CHANGED
@@ -5,9 +5,13 @@ require "sync"
|
|
5
5
|
|
6
6
|
require "finite_machine/version"
|
7
7
|
require "finite_machine/threadable"
|
8
|
+
require "finite_machine/thread_context"
|
8
9
|
require "finite_machine/callable"
|
9
10
|
require "finite_machine/catchable"
|
11
|
+
require "finite_machine/async_proxy"
|
12
|
+
require "finite_machine/async_call"
|
10
13
|
require "finite_machine/event"
|
14
|
+
require "finite_machine/event_queue"
|
11
15
|
require "finite_machine/hooks"
|
12
16
|
require "finite_machine/transition"
|
13
17
|
require "finite_machine/dsl"
|
@@ -59,5 +63,4 @@ module FiniteMachine
|
|
59
63
|
def self.define(*args, &block)
|
60
64
|
StateMachine.new(*args, &block)
|
61
65
|
end
|
62
|
-
|
63
66
|
end # FiniteMachine
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
# An asynchronouse call representation
|
5
|
+
class AsyncCall
|
6
|
+
include Threadable
|
7
|
+
|
8
|
+
attr_threadsafe :context
|
9
|
+
|
10
|
+
attr_threadsafe :callable
|
11
|
+
|
12
|
+
attr_threadsafe :arguments
|
13
|
+
|
14
|
+
attr_threadsafe :block
|
15
|
+
|
16
|
+
# Build asynchronous call instance
|
17
|
+
#
|
18
|
+
# @param [Object] context
|
19
|
+
# @param [Callable] callable
|
20
|
+
# @param [Array] args
|
21
|
+
# @param [#call] block
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# AsyncCall.build(self, Callable.new(:method), :a, :b)
|
25
|
+
#
|
26
|
+
# @return [self]
|
27
|
+
#
|
28
|
+
# @api public
|
29
|
+
def self.build(context, callable, *args, &block)
|
30
|
+
instance = new
|
31
|
+
instance.context = context
|
32
|
+
instance.callable = callable
|
33
|
+
instance.arguments = *args
|
34
|
+
instance.block = block
|
35
|
+
instance
|
36
|
+
end
|
37
|
+
|
38
|
+
# Dispatch the event to the context
|
39
|
+
#
|
40
|
+
# @return [nil]
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
def dispatch
|
44
|
+
callable.call(context, *arguments, block)
|
45
|
+
end
|
46
|
+
end # AsyncCall
|
47
|
+
end # FiniteMachine
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
# An asynchronous messages proxy
|
5
|
+
class AsyncProxy
|
6
|
+
include Threadable
|
7
|
+
include ThreadContext
|
8
|
+
|
9
|
+
attr_threadsafe :context
|
10
|
+
|
11
|
+
# Initialize an AsynxProxy
|
12
|
+
#
|
13
|
+
# @param [Object] context
|
14
|
+
# the context this proxy is associated with
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
def initialize(context)
|
18
|
+
self.context = context
|
19
|
+
end
|
20
|
+
|
21
|
+
# Delegate asynchronous event to event queue
|
22
|
+
#
|
23
|
+
# @api private
|
24
|
+
def method_missing(method_name, *args, &block)
|
25
|
+
event_queue << AsyncCall.build(context, Callable.new(method_name), *args, &block)
|
26
|
+
end
|
27
|
+
end # AsyncProxy
|
28
|
+
end # FiniteMachine
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module FiniteMachine
|
4
|
-
|
5
4
|
# A generic interface for executing strings, symbol methods or procs.
|
6
5
|
class Callable
|
7
6
|
|
@@ -21,7 +20,7 @@ module FiniteMachine
|
|
21
20
|
#
|
22
21
|
# @api public
|
23
22
|
def invert
|
24
|
-
lambda { |*args, &block| !
|
23
|
+
lambda { |*args, &block| !call(*args, &block) }
|
25
24
|
end
|
26
25
|
|
27
26
|
# Execute action
|
@@ -32,14 +31,15 @@ module FiniteMachine
|
|
32
31
|
def call(target, *args, &block)
|
33
32
|
case object
|
34
33
|
when Symbol
|
35
|
-
target.
|
34
|
+
target.public_send(object.to_sym, *args, &block)
|
36
35
|
when String
|
37
|
-
|
36
|
+
string = args.empty? ? "-> { #{object} }" : "-> { #{object}(*#{args}) }"
|
37
|
+
value = eval string
|
38
38
|
target.instance_exec(&value)
|
39
39
|
when ::Proc
|
40
40
|
object.arity.zero? ? object.call : object.call(target, *args)
|
41
41
|
else
|
42
|
-
raise ArgumentError, "Unknown callable #{
|
42
|
+
raise ArgumentError, "Unknown callable #{object}"
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end # Callable
|
data/lib/finite_machine/dsl.rb
CHANGED
@@ -48,7 +48,7 @@ module FiniteMachine
|
|
48
48
|
|
49
49
|
# Define initial state
|
50
50
|
#
|
51
|
-
# @
|
51
|
+
# @param [String, Hash] value
|
52
52
|
#
|
53
53
|
# @api public
|
54
54
|
def initial(value)
|
@@ -58,8 +58,14 @@ module FiniteMachine
|
|
58
58
|
machine.events.call(&event)
|
59
59
|
end
|
60
60
|
|
61
|
-
|
62
|
-
|
61
|
+
# Attach state machine to an object. This allows state machine
|
62
|
+
# to initiate events in the context of a particular object.
|
63
|
+
#
|
64
|
+
# @param [Object] object
|
65
|
+
#
|
66
|
+
# @api public
|
67
|
+
def target(object)
|
68
|
+
machine.env.target = object
|
63
69
|
end
|
64
70
|
|
65
71
|
# Define terminal state
|
@@ -94,7 +100,7 @@ module FiniteMachine
|
|
94
100
|
|
95
101
|
# Parse initial options
|
96
102
|
#
|
97
|
-
# @
|
103
|
+
# @param [String, Hash] value
|
98
104
|
#
|
99
105
|
# @return [Array[Symbol,String]]
|
100
106
|
#
|
@@ -124,6 +130,7 @@ module FiniteMachine
|
|
124
130
|
sync_exclusive do
|
125
131
|
_transition = Transition.new(machine, attrs.merge!(name: name))
|
126
132
|
_transition.define
|
133
|
+
_transition.define_state_methods
|
127
134
|
_transition.define_event
|
128
135
|
end
|
129
136
|
end
|
@@ -131,15 +138,15 @@ module FiniteMachine
|
|
131
138
|
|
132
139
|
class ErrorsDSL < GenericDSL
|
133
140
|
|
134
|
-
def initialize(machine)
|
135
|
-
super(machine)
|
136
|
-
machine.error_handlers = []
|
137
|
-
end
|
138
|
-
|
139
141
|
# Add error handler
|
140
142
|
#
|
141
143
|
# @param [Array] exceptions
|
142
144
|
#
|
145
|
+
# @example
|
146
|
+
# handle InvalidStateError, with: :log_errors
|
147
|
+
#
|
148
|
+
# @return [Array[Exception]]
|
149
|
+
#
|
143
150
|
# @api public
|
144
151
|
def handle(*exceptions, &block)
|
145
152
|
machine.handle(*exceptions, &block)
|
data/lib/finite_machine/event.rb
CHANGED
@@ -0,0 +1,123 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
|
5
|
+
# A class responsible for running asynchronous events
|
6
|
+
class EventQueue
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# Initialize an event queue
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# EventQueue.new
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
def initialize
|
16
|
+
@queue = Queue.new
|
17
|
+
@mutex = Mutex.new
|
18
|
+
@dead = false
|
19
|
+
run
|
20
|
+
end
|
21
|
+
|
22
|
+
# Retrieve the next event
|
23
|
+
#
|
24
|
+
# @return [AsyncCall]
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
def next_event
|
28
|
+
@queue.pop
|
29
|
+
end
|
30
|
+
|
31
|
+
# Add asynchronous event to the event queue
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# event_queue << AsyncCall.build(...)
|
35
|
+
#
|
36
|
+
# @param [AsyncCall] event
|
37
|
+
#
|
38
|
+
# @return [nil]
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
def <<(event)
|
42
|
+
@mutex.lock
|
43
|
+
begin
|
44
|
+
@queue << event
|
45
|
+
ensure
|
46
|
+
@mutex.unlock rescue nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Check if there are any events to handle
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# event_queue.empty?
|
54
|
+
#
|
55
|
+
# @api public
|
56
|
+
def empty?
|
57
|
+
@queue.empty?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Check if the event queue is alive
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
# event_queue.alive?
|
64
|
+
#
|
65
|
+
# @return [Boolean]
|
66
|
+
#
|
67
|
+
# @api public
|
68
|
+
def alive?
|
69
|
+
!@dead
|
70
|
+
end
|
71
|
+
|
72
|
+
# Join the event queue from current thread
|
73
|
+
#
|
74
|
+
# @param [Fixnum] timeout
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
# event_queue.join
|
78
|
+
#
|
79
|
+
# @return [nil, Thread]
|
80
|
+
#
|
81
|
+
# @api public
|
82
|
+
def join(timeout)
|
83
|
+
@thread.join timeout
|
84
|
+
end
|
85
|
+
|
86
|
+
# Shut down this event queue and clean it up
|
87
|
+
#
|
88
|
+
# @example
|
89
|
+
# event_queue.shutdown
|
90
|
+
#
|
91
|
+
# @return [Boolean]
|
92
|
+
#
|
93
|
+
# @api public
|
94
|
+
def shutdown
|
95
|
+
@mutex.lock
|
96
|
+
begin
|
97
|
+
@queue.clear
|
98
|
+
@dead = true
|
99
|
+
ensure
|
100
|
+
@mutex.unlock rescue nil
|
101
|
+
end
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Run all the events
|
108
|
+
#
|
109
|
+
# @return [Thread]
|
110
|
+
#
|
111
|
+
# @api private
|
112
|
+
def run
|
113
|
+
@thread = Thread.new do
|
114
|
+
Thread.current.abort_on_exception = true
|
115
|
+
until(@dead) do
|
116
|
+
event = next_event
|
117
|
+
Thread.exit unless event
|
118
|
+
event.dispatch
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end # EventQueue
|
123
|
+
end # FiniteMachine
|
data/lib/finite_machine/hooks.rb
CHANGED
@@ -10,6 +10,9 @@ module FiniteMachine
|
|
10
10
|
|
11
11
|
# Initialize a collection of hoooks
|
12
12
|
#
|
13
|
+
# @example
|
14
|
+
# Hoosk.new(machine)
|
15
|
+
#
|
13
16
|
# @api public
|
14
17
|
def initialize(machine)
|
15
18
|
@collection = Hash.new do |events_hash, event_type|
|
@@ -25,13 +28,43 @@ module FiniteMachine
|
|
25
28
|
# @param [String] name
|
26
29
|
# @param [Proc] callback
|
27
30
|
#
|
31
|
+
# @example
|
32
|
+
# hooks.register :enterstate, :green do ... end
|
33
|
+
#
|
34
|
+
# @return [Hash]
|
35
|
+
#
|
28
36
|
# @api public
|
29
37
|
def register(event_type, name, callback)
|
30
38
|
@collection[event_type][name] << callback
|
31
39
|
end
|
32
40
|
|
41
|
+
# Unregister callback
|
42
|
+
#
|
43
|
+
# @param [String] event_type
|
44
|
+
# @param [String] name
|
45
|
+
# @param [Proc] callback
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# hooks.unregister :enterstate, :green do ... end
|
49
|
+
#
|
50
|
+
# @return [Hash]
|
51
|
+
#
|
52
|
+
# @api public
|
53
|
+
def unregister(event_type, name, callback)
|
54
|
+
@collection[event_type][name].shift
|
55
|
+
end
|
56
|
+
|
33
57
|
# Return all hooks matching event and state
|
34
58
|
#
|
59
|
+
# @param [String] event_type
|
60
|
+
# @param [String] event_state
|
61
|
+
# @param [Event] event
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# hooks.call(:entersate, :green, Event.new)
|
65
|
+
#
|
66
|
+
# @return [Hash]
|
67
|
+
#
|
35
68
|
# @api public
|
36
69
|
def call(event_type, event_state, event)
|
37
70
|
@collection[event_type][event_state].each do |hook|
|
@@ -28,7 +28,7 @@ module FiniteMachine
|
|
28
28
|
instance_eval(&block)
|
29
29
|
end
|
30
30
|
|
31
|
-
# Register callback for a given event
|
31
|
+
# Register callback for a given event
|
32
32
|
#
|
33
33
|
# @param [Symbol] event_type
|
34
34
|
# @param [Symbol] name
|
@@ -42,6 +42,17 @@ module FiniteMachine
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
+
# Unregister callback for a given event
|
46
|
+
#
|
47
|
+
# @api public
|
48
|
+
def off(event_type = ANY_EVENT, name = ANY_STATE, &callback)
|
49
|
+
sync_exclusive do
|
50
|
+
hooks.unregister event_type, name, callback
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module Once; end
|
55
|
+
|
45
56
|
def listen_on(type, *args, &callback)
|
46
57
|
name = args.first
|
47
58
|
events = []
|
@@ -67,18 +78,16 @@ module FiniteMachine
|
|
67
78
|
listen_on :exit, *args, &callback
|
68
79
|
end
|
69
80
|
|
70
|
-
def
|
71
|
-
|
72
|
-
if callback_names.include?(callback_name.to_sym)
|
73
|
-
send(event_name, callback_name.to_sym, *args, &block)
|
74
|
-
else
|
75
|
-
super
|
76
|
-
end
|
81
|
+
def once_on_enter(*args, &callback)
|
82
|
+
listen_on :enter, *args, &callback.extend(Once)
|
77
83
|
end
|
78
84
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
85
|
+
def once_on_transition(*args, &callback)
|
86
|
+
listen_on :transition, *args, &callback.extend(Once)
|
87
|
+
end
|
88
|
+
|
89
|
+
def once_on_exit(*args, &callback)
|
90
|
+
listen_on :exit, *args, &callback.extend(Once)
|
82
91
|
end
|
83
92
|
|
84
93
|
TransitionEvent = Struct.new(:from, :to, :name) do
|
@@ -106,6 +115,7 @@ module FiniteMachine
|
|
106
115
|
ANY_STATE_HOOK, ANY_EVENT_HOOK].each do |event_state|
|
107
116
|
hooks.call(event_type, event_state, event) do |hook|
|
108
117
|
run_callback(hook, event)
|
118
|
+
off(event_type, event_state, &hook) if hook.is_a?(Once)
|
109
119
|
end
|
110
120
|
end
|
111
121
|
end
|
@@ -131,5 +141,37 @@ module FiniteMachine
|
|
131
141
|
end
|
132
142
|
end
|
133
143
|
|
144
|
+
# Forward the message to observer
|
145
|
+
#
|
146
|
+
# @param [String] method_name
|
147
|
+
#
|
148
|
+
# @param [Array] args
|
149
|
+
#
|
150
|
+
# @return [self]
|
151
|
+
#
|
152
|
+
# @api private
|
153
|
+
def method_missing(method_name, *args, &block)
|
154
|
+
_, event_name, callback_name = *method_name.to_s.match(/^(\w*?on_\w+?)_(\w+)$/)
|
155
|
+
if callback_names.include?(callback_name.to_sym)
|
156
|
+
public_send(event_name, :"#{callback_name}", *args, &block)
|
157
|
+
else
|
158
|
+
super
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Test if a message can be handled by observer
|
163
|
+
#
|
164
|
+
# @param [String] method_name
|
165
|
+
#
|
166
|
+
# @param [Boolean] include_private
|
167
|
+
#
|
168
|
+
# @return [Boolean]
|
169
|
+
#
|
170
|
+
# @api private
|
171
|
+
def respond_to_missing?(method_name, include_private = false)
|
172
|
+
*_, callback_name = *method_name.to_s.match(/^(\w*?on_\w+?)_(\w+)$/)
|
173
|
+
callback_names.include?(:"#{callback_name}")
|
174
|
+
end
|
175
|
+
|
134
176
|
end # Observer
|
135
177
|
end # FiniteMachine
|