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.
@@ -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.3"
22
- spec.add_development_dependency "rake"
23
- spec.add_development_dependency "rspec"
21
+ spec.add_development_dependency "bundler", "~> 1.5"
24
22
  end
@@ -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| !self.call(*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.__send__(@object.to_sym)
34
+ target.public_send(object.to_sym, *args, &block)
36
35
  when String
37
- value = eval "lambda { #{@object} }"
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 #{@object}"
42
+ raise ArgumentError, "Unknown callable #{object}"
43
43
  end
44
44
  end
45
45
  end # Callable
@@ -6,8 +6,8 @@ module FiniteMachine
6
6
  module Catchable
7
7
 
8
8
  def self.included(base)
9
- base.class_eval do
10
- attr_threadsafe :error_handlers
9
+ base.module_eval do
10
+ attr_threadsafe :error_handlers, default: []
11
11
  end
12
12
  end
13
13
 
@@ -48,7 +48,7 @@ module FiniteMachine
48
48
 
49
49
  # Define initial state
50
50
  #
51
- # @params [String, Hash] value
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
- def target(value)
62
- machine.env.target = value
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
- # @params [String, Hash] value
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)
@@ -29,7 +29,7 @@ module FiniteMachine
29
29
 
30
30
  def notify(subscriber, *args, &block)
31
31
  if subscriber.respond_to? MESSAGE
32
- subscriber.__send__(MESSAGE, self, *args, &block)
32
+ subscriber.public_send(MESSAGE, self, *args, &block)
33
33
  end
34
34
  end
35
35
 
@@ -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
@@ -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 method_missing(method_name, *args, &block)
71
- _, event_name, callback_name = *method_name.to_s.match(/^(on_\w+?)_(\w+)$/)
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 respond_to_missing?(method_name, include_private = false)
80
- _, callback_name = *method_name.to_s.match(/^(on_\w+?)_(\w+)$/)
81
- callback_names.include?(callback_name.to_sym)
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