stealth 0.9.1

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.
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