stealth 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +58 -0
  3. data/.gitignore +12 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +81 -0
  6. data/LICENSE +20 -0
  7. data/README.md +1 -0
  8. data/VERSION +1 -0
  9. data/bin/stealth +5 -0
  10. data/lib/stealth/base.rb +87 -0
  11. data/lib/stealth/cli.rb +82 -0
  12. data/lib/stealth/cli_base.rb +25 -0
  13. data/lib/stealth/commands/command.rb +14 -0
  14. data/lib/stealth/commands/console.rb +75 -0
  15. data/lib/stealth/commands/server.rb +20 -0
  16. data/lib/stealth/configuration.rb +54 -0
  17. data/lib/stealth/controller.rb +190 -0
  18. data/lib/stealth/dispatcher.rb +48 -0
  19. data/lib/stealth/errors.rb +32 -0
  20. data/lib/stealth/flow/base.rb +256 -0
  21. data/lib/stealth/flow/errors.rb +25 -0
  22. data/lib/stealth/flow/event.rb +43 -0
  23. data/lib/stealth/flow/event_collection.rb +41 -0
  24. data/lib/stealth/flow/specification.rb +67 -0
  25. data/lib/stealth/flow/state.rb +48 -0
  26. data/lib/stealth/jobs.rb +10 -0
  27. data/lib/stealth/logger.rb +16 -0
  28. data/lib/stealth/reply.rb +19 -0
  29. data/lib/stealth/server.rb +38 -0
  30. data/lib/stealth/service_message.rb +17 -0
  31. data/lib/stealth/service_reply.rb +30 -0
  32. data/lib/stealth/services/base_client.rb +28 -0
  33. data/lib/stealth/services/base_message_handler.rb +28 -0
  34. data/lib/stealth/services/base_reply_handler.rb +65 -0
  35. data/lib/stealth/services/facebook/client.rb +35 -0
  36. data/lib/stealth/services/facebook/events/message_event.rb +59 -0
  37. data/lib/stealth/services/facebook/events/postback_event.rb +36 -0
  38. data/lib/stealth/services/facebook/message_handler.rb +84 -0
  39. data/lib/stealth/services/facebook/reply_handler.rb +471 -0
  40. data/lib/stealth/services/facebook/setup.rb +25 -0
  41. data/lib/stealth/services/jobs/handle_message_job.rb +22 -0
  42. data/lib/stealth/session.rb +74 -0
  43. data/lib/stealth/version.rb +12 -0
  44. data/lib/stealth.rb +1 -0
  45. data/spec/configuration_spec.rb +52 -0
  46. data/spec/flow/custom_transitions_spec.rb +99 -0
  47. data/spec/flow/flow_spec.rb +91 -0
  48. data/spec/flow/transition_callbacks_spec.rb +228 -0
  49. data/spec/replies/nested_reply_with_erb.yml +16 -0
  50. data/spec/sample_services_yml/services.yml +31 -0
  51. data/spec/sample_services_yml/services_with_erb.yml +31 -0
  52. data/spec/service_reply_spec.rb +34 -0
  53. data/spec/spec_helper.rb +13 -0
  54. data/spec/version_spec.rb +16 -0
  55. data/stealth.gemspec +30 -0
  56. metadata +247 -0
@@ -0,0 +1,190 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ class Controller
6
+
7
+ include ActiveSupport::Callbacks
8
+
9
+ define_callbacks :action
10
+
11
+ attr_reader :current_message, :current_user_id, :current_flow,
12
+ :current_service, :flow_controller
13
+
14
+ def initialize(service_message:, current_flow: nil)
15
+ @current_message = service_message
16
+ @current_service = service_message.service
17
+ @current_user_id = service_message.sender_id
18
+ @current_flow = current_flow
19
+ end
20
+
21
+ def has_location?
22
+ current_message.location.present?
23
+ end
24
+
25
+ def has_attachments?
26
+ current_message.attachments.present?
27
+ end
28
+
29
+ def route
30
+ raise(Stealth::Errors::ControllerRoutingNotImplemented, "Please implement `route` method in BotController.")
31
+ end
32
+
33
+ def send_replies
34
+ service_reply = Stealth::ServiceReply.new(
35
+ recipient_id: current_user_id,
36
+ yaml_reply: action_replies,
37
+ context: binding
38
+ )
39
+
40
+ for reply in service_reply.replies do
41
+ handler = reply_handler.new(
42
+ recipient_id: current_user_id,
43
+ reply: reply
44
+ )
45
+
46
+ translated_reply = handler.send(reply.reply_type)
47
+ client = service_client.new(reply: translated_reply)
48
+ client.transmit
49
+
50
+ # If this was a 'delay' type of reply, let's respect the delay
51
+ if reply.reply_type == 'delay'
52
+ begin
53
+ sleep_duration = Float(reply["duration"])
54
+ sleep(sleep_duration)
55
+ rescue ArgumentError, TypeError
56
+ raise(ArgumentError, 'Invalid duration specified. Duration must be a float.')
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def flow_controller
63
+ @flow_controller = begin
64
+ flow_controller = [current_session.flow_string.pluralize, 'controller'].join('_').classify.constantize
65
+ flow_controller.new(
66
+ service_message: @current_message,
67
+ current_flow: current_flow
68
+ )
69
+ end
70
+ end
71
+
72
+ def current_session
73
+ @current_session ||= Stealth::Session.new(user_id: current_user_id)
74
+ end
75
+
76
+ def action(action: nil)
77
+ run_callbacks :action do
78
+ action ||= current_session.state_string
79
+ flow_controller.send(action)
80
+ end
81
+ end
82
+
83
+ def step_to(session: nil, flow: nil, state: nil)
84
+ flow, state = get_flow_and_state(session: session, flow: flow, state: state)
85
+ step(flow: flow, state: state)
86
+ end
87
+
88
+ def update_session_to(session: nil, flow: nil, state: nil)
89
+ flow, state = get_flow_and_state(session: session, flow: flow, state: state)
90
+ update_session(flow: flow, state: state)
91
+ end
92
+
93
+ def step_to_next
94
+ flow, state = get_next_state
95
+ step(flow: flow, state: state)
96
+ end
97
+
98
+ def update_session_to_next
99
+ flow, state = get_next_state
100
+ update_session(flow: flow, state: state)
101
+ end
102
+
103
+ def self.before_action(*args, &block)
104
+ set_callback(:action, :before, *args, &block)
105
+ end
106
+
107
+ def self.around_action(*args, &block)
108
+ set_callback(:action, :around, *args, &block)
109
+ end
110
+
111
+ def self.after_action(*args, &block)
112
+ set_callback(:action, :after, *args, &block)
113
+ end
114
+
115
+ private
116
+
117
+ def reply_handler
118
+ begin
119
+ Kernel.const_get("Stealth::Services::#{current_service.capitalize}::ReplyHandler")
120
+ rescue NameError
121
+ raise(Stealth::Errors::ServiceNotRecognized, "The service '#{current_service}' was not recognized.")
122
+ end
123
+ end
124
+
125
+ def service_client
126
+ begin
127
+ Kernel.const_get("Stealth::Services::#{current_service.capitalize}::Client")
128
+ rescue NameError
129
+ raise(Stealth::Errors::ServiceNotRecognized, "The service '#{current_service}' was not recognized.")
130
+ end
131
+ end
132
+
133
+ def replies_folder
134
+ current_session.flow_string.underscore.pluralize
135
+ end
136
+
137
+ def action_replies
138
+ File.read(File.join(Stealth.root, 'bot', 'replies', replies_folder, "#{current_session.state_string}.yml"))
139
+ end
140
+
141
+ def update_session(flow:, state:)
142
+ @current_session = Stealth::Session.new(user_id: current_user_id)
143
+ @current_session.set(flow: flow, state: state)
144
+ end
145
+
146
+ def step(flow:, state:)
147
+ update_session(flow: flow, state: state)
148
+ @current_flow = current_session.flow
149
+
150
+ action(action: state)
151
+ end
152
+
153
+ def get_flow_and_state(session: nil, flow: nil, state: nil)
154
+ if session.nil? && flow.nil? && state.nil?
155
+ raise(ArgumentError, "A session, flow, or state must be specified.")
156
+ end
157
+
158
+ if session.present?
159
+ return session.flow_string, session.state_string
160
+ end
161
+
162
+ if flow.present?
163
+ if state.blank?
164
+ flow_klass = [flow, 'flow'].join('_').classify.constantize
165
+ state = flow_klass.flow_spec.states.keys.first
166
+ end
167
+
168
+ return flow, state
169
+ end
170
+
171
+ if state.present?
172
+ return current_session.flow_string, state
173
+ end
174
+ end
175
+
176
+ def get_next_state
177
+ current_state_index = current_session.flow.states.index(current_session.state_string.to_sym)
178
+ next_state = current_session.flow.states[current_state_index + 1]
179
+ if next_state.nil?
180
+ raise(
181
+ Stealth::Errors::InvalidStateTransitions,
182
+ "The next state after #{current_session.state_string} has not yet been defined."
183
+ )
184
+ end
185
+
186
+ return current_session.flow_string, next_state
187
+ end
188
+
189
+ end
190
+ end
@@ -0,0 +1,48 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+
6
+ # Responsible for coordinating incoming messages
7
+ # 1. Receives incoming request params
8
+ # 2. Initializes respective service request handler
9
+ # 3. Processes params through service request handler (might be async)
10
+ # 4. Inits base StealthController with state params returned from the service
11
+ # request handler
12
+ # 5. Returns an HTTP response to be returned to the requestor
13
+ class Dispatcher
14
+
15
+ attr_reader :service, :params, :headers, :message_handler
16
+
17
+ def initialize(service:, params:, headers:)
18
+ @service = service
19
+ @params = params
20
+ @headers = headers
21
+ @message_handler = message_handler_klass.new(
22
+ params: params,
23
+ headers: headers
24
+ )
25
+ end
26
+
27
+ def coordinate
28
+ message_handler.coordinate
29
+ end
30
+
31
+ def process
32
+ service_message = message_handler.process
33
+ bot_controller = BotController.new(service_message: service_message)
34
+ bot_controller.route
35
+ end
36
+
37
+ private
38
+
39
+ def message_handler_klass
40
+ begin
41
+ Kernel.const_get("Stealth::Services::#{service.capitalize}::MessageHandler")
42
+ rescue NameError
43
+ raise(Stealth::Errors::ServiceNotRecognized, "The service '#{service}' was not recognized.")
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ class Errors < StandardError
6
+
7
+ class ConfigurationError < Errors
8
+ end
9
+
10
+ class ReplyFormatNotSupported < Errors
11
+ end
12
+
13
+ class ServiceImpaired < Errors
14
+ end
15
+
16
+ class ServiceNotRecognized < Errors
17
+ end
18
+
19
+ class ControllerRoutingNotImplemented < Errors
20
+ end
21
+
22
+ class UndefinedVariable < Errors
23
+ end
24
+
25
+ class RedisNotConfigured < Errors
26
+ end
27
+
28
+ class InvalidStateTransition < Errors
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,256 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'stealth/flow/errors'
5
+ require 'stealth/flow/specification'
6
+ require 'stealth/flow/event_collection'
7
+ require 'stealth/flow/event'
8
+ require 'stealth/flow/state'
9
+
10
+ module Stealth
11
+ module Flow
12
+
13
+ module ClassMethods
14
+ attr_reader :flow_spec
15
+
16
+ def flow(&specification)
17
+ assign_flow Specification.new(Hash.new, &specification)
18
+ end
19
+
20
+ private
21
+
22
+ # Creates the convinience methods like `my_transition!`
23
+ def assign_flow(specification_object)
24
+
25
+ # Merging two workflow specifications can **not** be done automically, so
26
+ # just make the latest specification win. Same for inheritance -
27
+ # definition in the subclass wins.
28
+ if respond_to? :inherited_flow_spec # undefine methods defined by the old flow_spec
29
+ inherited_flow_spec.states.values.each do |state|
30
+ state_name = state.name
31
+ module_eval do
32
+ undef_method "#{state_name}?"
33
+ end
34
+
35
+ state.events.flat.each do |event|
36
+ event_name = event.name
37
+ module_eval do
38
+ undef_method "#{event_name}!".to_sym
39
+ undef_method "can_#{event_name}?"
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ @flow_spec = specification_object
46
+ @flow_spec.states.values.each do |state|
47
+ state_name = state.name
48
+ module_eval do
49
+ define_method "#{state_name}?" do
50
+ state_name == current_state.name
51
+ end
52
+ end
53
+
54
+ state.events.flat.each do |event|
55
+ event_name = event.name
56
+ module_eval do
57
+ define_method "#{event_name}!".to_sym do |*args|
58
+ process_event!(event_name, *args)
59
+ end
60
+
61
+ define_method "can_#{event_name}?" do
62
+ return !!current_state.events.first_applicable(event_name, self)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ module InstanceMethods
71
+ attr_accessor :flow_state, :user_id
72
+
73
+ def current_state
74
+ loaded_state = load_flow_state
75
+ res = spec.states[loaded_state.to_sym] if loaded_state
76
+ res || spec.initial_state
77
+ end
78
+
79
+ # Return true if the last transition was halted by one of the transition callbacks.
80
+ def halted?
81
+ @halted
82
+ end
83
+
84
+ # Return the reason of the last transition abort as set by the previous
85
+ # call of `halt` or `halt!` method.
86
+ def halted_because
87
+ @halted_because
88
+ end
89
+
90
+ def process_event!(name, *args)
91
+ event = current_state.events.first_applicable(name, self)
92
+ raise NoTransitionAllowed.new(
93
+ "There is no event #{name.to_sym} defined for the #{current_state} state") \
94
+ if event.nil?
95
+ @halted_because = nil
96
+ @halted = false
97
+
98
+ check_transition(event)
99
+
100
+ from = current_state
101
+ to = spec.states[event.transitions_to]
102
+
103
+ run_before_transition(from, to, name, *args)
104
+ return false if @halted
105
+
106
+ begin
107
+ return_value = run_action(event.action, *args) || run_action_callback(event.name, *args)
108
+ rescue StandardError => e
109
+ run_on_error(e, from, to, name, *args)
110
+ end
111
+
112
+ return false if @halted
113
+
114
+ run_on_transition(from, to, name, *args)
115
+
116
+ run_on_exit(from, to, name, *args)
117
+
118
+ transition_value = persist_flow_state(to.to_s)
119
+
120
+ run_on_entry(to, from, name, *args)
121
+
122
+ run_after_transition(from, to, name, *args)
123
+
124
+ return_value.nil? ? transition_value : return_value
125
+ end
126
+
127
+ def halt(reason = nil)
128
+ @halted_because = reason
129
+ @halted = true
130
+ end
131
+
132
+ def halt!(reason = nil)
133
+ @halted_because = reason
134
+ @halted = true
135
+ raise TransitionHalted.new(reason)
136
+ end
137
+
138
+ def spec
139
+ # check the singleton class first
140
+ class << self
141
+ return flow_spec if flow_spec
142
+ end
143
+
144
+ c = self.class
145
+ # using a simple loop instead of class_inheritable_accessor to avoid
146
+ # dependency on Rails' ActiveSupport
147
+ until c.flow_spec || !(c.include? Stealth::Flow)
148
+ c = c.superclass
149
+ end
150
+ c.flow_spec
151
+ end
152
+
153
+ def states
154
+ self.spec.states.keys
155
+ end
156
+
157
+ def init_state(state)
158
+ res = spec.states[state.to_sym] if state
159
+ @flow_state = res || spec.initial_state
160
+ self
161
+ end
162
+
163
+ private
164
+
165
+ def check_transition(event)
166
+ # Create a meaningful error message instead of
167
+ # "undefined method `on_entry' for nil:NilClass"
168
+ # Reported by Kyle Burton
169
+ if !spec.states[event.transitions_to]
170
+ raise StealthFlowError.new("Event[#{event.name}]'s " +
171
+ "transitions_to[#{event.transitions_to}] is not a declared state.")
172
+ end
173
+ end
174
+
175
+ def run_before_transition(from, to, event, *args)
176
+ instance_exec(from.name, to.name, event, *args, &spec.before_transition_proc) if spec.before_transition_proc
177
+ end
178
+
179
+ def run_on_error(error, from, to, event, *args)
180
+ if spec.on_error_proc
181
+ instance_exec(error, from.name, to.name, event, *args, &spec.on_error_proc)
182
+ halt(error.message)
183
+ else
184
+ raise error
185
+ end
186
+ end
187
+
188
+ def run_on_transition(from, to, event, *args)
189
+ instance_exec(from.name, to.name, event, *args, &spec.on_transition_proc) if spec.on_transition_proc
190
+ end
191
+
192
+ def run_after_transition(from, to, event, *args)
193
+ instance_exec(from.name, to.name, event, *args, &spec.after_transition_proc) if spec.after_transition_proc
194
+ end
195
+
196
+ def run_action(action, *args)
197
+ instance_exec(*args, &action) if action
198
+ end
199
+
200
+ def has_callback?(action)
201
+ # 1. public callback method or
202
+ # 2. protected method somewhere in the class hierarchy or
203
+ # 3. private in the immediate class (parent classes ignored)
204
+ action = action.to_sym
205
+ self.respond_to?(action) or
206
+ self.class.protected_method_defined?(action) or
207
+ self.private_methods(false).map(&:to_sym).include?(action)
208
+ end
209
+
210
+ def run_action_callback(action_name, *args)
211
+ action = action_name.to_sym
212
+ self.send(action, *args) if has_callback?(action)
213
+ end
214
+
215
+ def run_on_entry(state, prior_state, triggering_event, *args)
216
+ if state.on_entry
217
+ instance_exec(prior_state.name, triggering_event, *args, &state.on_entry)
218
+ else
219
+ hook_name = "on_#{state}_entry"
220
+ self.send(hook_name, prior_state, triggering_event, *args) if has_callback?(hook_name)
221
+ end
222
+ end
223
+
224
+ def run_on_exit(state, new_state, triggering_event, *args)
225
+ if state
226
+ if state.on_exit
227
+ instance_exec(new_state.name, triggering_event, *args, &state.on_exit)
228
+ else
229
+ hook_name = "on_#{state}_exit"
230
+ self.send(hook_name, new_state, triggering_event, *args) if has_callback?(hook_name)
231
+ end
232
+ end
233
+ end
234
+
235
+ def load_flow_state
236
+ @flow_state
237
+ end
238
+
239
+ def persist_flow_state(new_value)
240
+ @flow_state = new_value
241
+ if defined?($redis)
242
+ $redis.set(user_id, flow_and_state)
243
+ end
244
+ end
245
+
246
+ def flow_and_state
247
+ [self.class.to_s, current_state].join("->")
248
+ end
249
+ end
250
+
251
+ def self.included(klass)
252
+ klass.send :include, InstanceMethods
253
+ klass.extend ClassMethods
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ module Flow
6
+ class Error < StandardError; end
7
+
8
+ class TransitionHalted < Error
9
+
10
+ attr_reader :halted_because
11
+
12
+ def initialize(msg = nil)
13
+ @halted_because = msg
14
+ super msg
15
+ end
16
+
17
+ end
18
+
19
+ class NoTransitionAllowed < Error; end
20
+
21
+ class StealthFlowError < Error; end
22
+
23
+ class StealthFlowDefinitionError < Error; end
24
+ end
25
+ end
@@ -0,0 +1,43 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ module Flow
6
+ class Event
7
+
8
+ attr_accessor :name, :transitions_to, :meta, :action, :condition
9
+
10
+ def initialize(name, transitions_to, condition = nil, meta = {}, &action)
11
+ @name = name
12
+ @transitions_to = transitions_to.to_sym
13
+ @meta = meta
14
+ @action = action
15
+ @condition = if condition.nil? || condition.is_a?(Symbol) || condition.respond_to?(:call)
16
+ condition
17
+ else
18
+ raise TypeError, 'condition must be nil, an instance method name symbol or a callable (eg. a proc or lambda)'
19
+ end
20
+ end
21
+
22
+ def condition_applicable?(object)
23
+ if condition
24
+ if condition.is_a?(Symbol)
25
+ object.send(condition)
26
+ else
27
+ condition.call(object)
28
+ end
29
+ else
30
+ true
31
+ end
32
+ end
33
+
34
+ def draw(graph, from_state)
35
+ graph.add_edges(from_state.name.to_s, transitions_to.to_s, meta.merge(:label => to_s))
36
+ end
37
+
38
+ def to_s
39
+ @name.to_s
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ module Flow
6
+ class EventCollection < Hash
7
+
8
+ def [](name)
9
+ super name.to_sym # Normalize to symbol
10
+ end
11
+
12
+ def push(name, event)
13
+ key = name.to_sym
14
+ self[key] ||= []
15
+ self[key] << event
16
+ end
17
+
18
+ def flat
19
+ self.values.flatten.uniq do |event|
20
+ [:name, :transitions_to, :meta, :action].map { |m| event.send(m) }
21
+ end
22
+ end
23
+
24
+ def include?(name_or_obj)
25
+ case name_or_obj
26
+ when Event
27
+ flat.include? name_or_obj
28
+ else
29
+ !(self[name_or_obj].nil?)
30
+ end
31
+ end
32
+
33
+ def first_applicable(name, object_context)
34
+ (self[name] || []).detect do |event|
35
+ event.condition_applicable?(object_context) && event
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,67 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ module Flow
6
+ class Specification
7
+ attr_accessor :states, :initial_state, :meta,
8
+ :on_transition_proc, :before_transition_proc, :after_transition_proc, :on_error_proc
9
+
10
+ def initialize(meta = {}, &specification)
11
+ @states = Hash.new
12
+ @meta = meta
13
+ instance_eval(&specification)
14
+ end
15
+
16
+ def state_names
17
+ states.keys
18
+ end
19
+
20
+ private
21
+
22
+ def state(name, meta = {:meta => {}}, &events_and_etc)
23
+ # meta[:meta] to keep the API consistent..., gah
24
+ new_state = Stealth::Flow::State.new(name, self, meta[:meta])
25
+ @initial_state = new_state if @states.empty?
26
+ @states[name.to_sym] = new_state
27
+ @scoped_state = new_state
28
+ instance_eval(&events_and_etc) if events_and_etc
29
+ end
30
+
31
+ def event(name, args = {}, &action)
32
+ target = args[:transitions_to] || args[:transition_to]
33
+ condition = args[:if]
34
+ raise StealthFlowDefinitionError.new(
35
+ "missing ':transitions_to' in workflow event definition for '#{name}'") \
36
+ if target.nil?
37
+ @scoped_state.events.push(
38
+ name, Stealth::Flow::Event.new(name, target, condition, (args[:meta] or {}), &action)
39
+ )
40
+ end
41
+
42
+ def on_entry(&proc)
43
+ @scoped_state.on_entry = proc
44
+ end
45
+
46
+ def on_exit(&proc)
47
+ @scoped_state.on_exit = proc
48
+ end
49
+
50
+ def after_transition(&proc)
51
+ @after_transition_proc = proc
52
+ end
53
+
54
+ def before_transition(&proc)
55
+ @before_transition_proc = proc
56
+ end
57
+
58
+ def on_transition(&proc)
59
+ @on_transition_proc = proc
60
+ end
61
+
62
+ def on_error(&proc)
63
+ @on_error_proc = proc
64
+ end
65
+ end
66
+ end
67
+ end