stealth 0.9.8 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f8e354e371b86c32fbd88873d2c99fd81b1b2e7c
4
- data.tar.gz: d68dfa6ed766e034d43cf3c1ebbd1f8e237f633c
3
+ metadata.gz: 05a79205c1c753dc6ab5b49f1c23695cf13b7d5c
4
+ data.tar.gz: bff5e9b18f8b6fb5608f23834f65e2526460f440
5
5
  SHA512:
6
- metadata.gz: af5f5ceb58561a7697ba353eeca4c3bf7e5393016fc7e3edaa4178d89dc437c8f2d937208f46488ee67db110ba456f38e8e25a33fea95ed093a1bcd8c7ec46e3
7
- data.tar.gz: d3c6d97b1ec63eeca65ae9be3867e6d3a422bcf99c0110afca0ee5ede9caa55143b7e6c08d801a2fcf363797e90fecfc7ea838f7c22840755944ad3fb0e84289
6
+ metadata.gz: 8a0997a30591438682bc14fd7c5a61f45935cc0780572bef8a0bd3fc92e2d0beb7d88dd730dd3391088457a953150485b0c6670ade82dc58c869e621af43e1f3
7
+ data.tar.gz: 68973f3f68bd5c9c8f0b85405fe45f7f0d55e5b8f3a3aa4467d21fef2c6429ff715d144196ac934b4e1a00b76de90d0d48433b370be17914672723c1a62dcca3
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- stealth (0.9.5)
4
+ stealth (0.10.0)
5
5
  activesupport (~> 5.1)
6
6
  multi_json (~> 1.12)
7
7
  puma (~> 3.10)
@@ -23,10 +23,11 @@ GEM
23
23
  i18n (0.9.1)
24
24
  concurrent-ruby (~> 1.0)
25
25
  minitest (5.10.3)
26
+ mock_redis (0.17.3)
26
27
  multi_json (1.12.2)
27
28
  mustermann (1.0.1)
28
29
  oj (3.3.6)
29
- puma (3.10.0)
30
+ puma (3.11.0)
30
31
  rack (2.0.3)
31
32
  rack-protection (2.0.0)
32
33
  rack
@@ -68,6 +69,7 @@ PLATFORMS
68
69
  ruby
69
70
 
70
71
  DEPENDENCIES
72
+ mock_redis (~> 0.17)
71
73
  oj (~> 3.3)
72
74
  rack-test (~> 0.7)
73
75
  rspec (~> 3.6)
@@ -75,4 +77,4 @@ DEPENDENCIES
75
77
  stealth!
76
78
 
77
79
  BUNDLED WITH
78
- 1.15.3
80
+ 1.16.1
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.8
1
+ 0.10.0
data/lib/stealth/base.rb CHANGED
@@ -19,7 +19,9 @@ require 'stealth/scheduled_reply'
19
19
  require 'stealth/service_reply'
20
20
  require 'stealth/service_message'
21
21
  require 'stealth/session'
22
- require 'stealth/controller'
22
+ require 'stealth/controller/callbacks'
23
+ require 'stealth/controller/catch_all'
24
+ require 'stealth/controller/controller'
23
25
  require 'stealth/flow/base'
24
26
  require 'stealth/services/base_client'
25
27
 
@@ -0,0 +1,64 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ class Controller
6
+ module Callbacks
7
+
8
+ extend ActiveSupport::Concern
9
+
10
+ include ActiveSupport::Callbacks
11
+
12
+ included do
13
+ define_callbacks :action, skip_after_callbacks_if_terminated: true
14
+ end
15
+
16
+ module ClassMethods
17
+ def _normalize_callback_options(options)
18
+ _normalize_callback_option(options, :only, :if)
19
+ _normalize_callback_option(options, :except, :unless)
20
+ end
21
+
22
+ def _normalize_callback_option(options, from, to)
23
+ if from = options[from]
24
+ _from = Array(from).map(&:to_s).to_set
25
+ from = proc { |c| _from.include?(c.action_name) }
26
+ options[to] = Array(options[to]).unshift(from)
27
+ end
28
+ end
29
+
30
+ def _insert_callbacks(callbacks, block = nil)
31
+ options = callbacks.extract_options!
32
+ _normalize_callback_options(options)
33
+ callbacks.push(block) if block
34
+ callbacks.each do |callback|
35
+ yield callback, options
36
+ end
37
+ end
38
+
39
+ [:before, :after, :around].each do |callback|
40
+ define_method "#{callback}_action" do |*names, &blk|
41
+ _insert_callbacks(names, blk) do |name, options|
42
+ set_callback(:action, callback, name, options)
43
+ end
44
+ end
45
+
46
+ define_method "prepend_#{callback}_action" do |*names, &blk|
47
+ _insert_callbacks(names, blk) do |name, options|
48
+ set_callback(:action, callback, name, options.merge(prepend: true))
49
+ end
50
+ end
51
+
52
+ define_method "skip_#{callback}_action" do |*names|
53
+ _insert_callbacks(names) do |name, options|
54
+ skip_callback(:action, callback, name, options)
55
+ end
56
+ end
57
+
58
+ alias_method :"append_#{callback}_action", :"#{callback}_action"
59
+ end
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,54 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ class Controller
6
+ module CatchAll
7
+
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+
12
+ def run_catch_all(reason: nil)
13
+ error_level = fetch_error_level
14
+ Stealth::Logger.l(topic: "catch_all", message: "CatchAll #{catch_all_state(error_level)} triggered for #{error_slug}: #{reason}")
15
+
16
+ if defined?(CatchAllsController)
17
+ step_to flow: 'catch_all', state: catch_all_state(error_level)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def fetch_error_level
24
+ if fail_attempts = $redis.get(error_slug)
25
+ begin
26
+ fail_attempts = Integer(fail_attempts)
27
+ rescue ArgumentError
28
+ fail_attempts = 1
29
+ end
30
+
31
+ fail_attempts += 1
32
+ else
33
+ fail_attempts = 1
34
+ end
35
+
36
+ # Set the error with an expiration to avoid filling Redis
37
+ $redis.setex(error_slug, 15.minutes.to_i, fail_attempts)
38
+
39
+ fail_attempts
40
+ end
41
+
42
+ def error_slug
43
+ ['error', current_user_id, current_session.flow_string, current_session.state_string].join('-')
44
+ end
45
+
46
+ def catch_all_state(error_level)
47
+ "level#{error_level}"
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -4,18 +4,18 @@
4
4
  module Stealth
5
5
  class Controller
6
6
 
7
- include ActiveSupport::Callbacks
8
-
9
- define_callbacks :action
7
+ include Stealth::Controller::Callbacks
8
+ include Stealth::Controller::CatchAll
10
9
 
11
10
  attr_reader :current_message, :current_user_id, :current_flow,
12
- :current_service, :flow_controller
11
+ :current_service, :flow_controller, :action_name
13
12
 
14
13
  def initialize(service_message:, current_flow: nil)
15
14
  @current_message = service_message
16
15
  @current_service = service_message.service
17
16
  @current_user_id = service_message.sender_id
18
17
  @current_flow = current_flow
18
+ @progressed = false
19
19
  end
20
20
 
21
21
  def has_location?
@@ -26,6 +26,10 @@ module Stealth
26
26
  current_message.attachments.present?
27
27
  end
28
28
 
29
+ def progressed?
30
+ @progressed.present?
31
+ end
32
+
29
33
  def route
30
34
  raise(Stealth::Errors::ControllerRoutingNotImplemented, "Please implement `route` method in BotController")
31
35
  end
@@ -57,10 +61,12 @@ module Stealth
57
61
  end
58
62
  end
59
63
  end
64
+
65
+ @progressed = :sent_replies
60
66
  end
61
67
 
62
68
  def flow_controller
63
- @flow_controller = begin
69
+ @flow_controller ||= begin
64
70
  flow_controller = [current_session.flow_string.pluralize, 'controller'].join('_').classify.constantize
65
71
  flow_controller.new(
66
72
  service_message: @current_message,
@@ -73,10 +79,21 @@ module Stealth
73
79
  @current_session ||= Stealth::Session.new(user_id: current_user_id)
74
80
  end
75
81
 
82
+ def previous_session
83
+ @previous_session ||= Stealth::Session.new(user_id: current_user_id, previous: true)
84
+ end
85
+
76
86
  def action(action: nil)
87
+ @action_name = action
88
+ @action_name ||= current_session.state_string
89
+
77
90
  run_callbacks :action do
78
- action ||= current_session.state_string
79
- flow_controller.send(action)
91
+ begin
92
+ flow_controller.send(@action_name)
93
+ run_catch_all(reason: 'Did not send replies, update session, or step') unless progressed?
94
+ rescue StandardError => e
95
+ run_catch_all(reason: e.message)
96
+ end
80
97
  end
81
98
  end
82
99
 
@@ -84,7 +101,7 @@ module Stealth
84
101
  flow, state = get_flow_and_state(session: session, flow: flow, state: state)
85
102
 
86
103
  unless delay.is_a?(ActiveSupport::Duration)
87
- raise ArgumentError, "Please specify your schedule_step_to `in` parameter using ActiveSupport::Duration, e.g. `1.day` or `5.hours`"
104
+ raise ArgumentError, "Please specify your step_to_in `delay` parameter using ActiveSupport::Duration, e.g. `1.day` or `5.hours`"
88
105
  end
89
106
 
90
107
  Stealth::ScheduledReplyJob.perform_in(delay, current_service, current_user_id, flow, state)
@@ -110,18 +127,6 @@ module Stealth
110
127
  update_session(flow: flow, state: state)
111
128
  end
112
129
 
113
- def self.before_action(*args, &block)
114
- set_callback(:action, :before, *args, &block)
115
- end
116
-
117
- def self.around_action(*args, &block)
118
- set_callback(:action, :around, *args, &block)
119
- end
120
-
121
- def self.after_action(*args, &block)
122
- set_callback(:action, :after, *args, &block)
123
- end
124
-
125
130
  private
126
131
 
127
132
  def reply_handler
@@ -156,11 +161,14 @@ module Stealth
156
161
 
157
162
  def update_session(flow:, state:)
158
163
  @current_session = Stealth::Session.new(user_id: current_user_id)
164
+ @progressed = :updated_session
159
165
  @current_session.set(flow: flow, state: state)
160
166
  end
161
167
 
162
168
  def step(flow:, state:)
163
169
  update_session(flow: flow, state: state)
170
+ @progressed = :stepped
171
+ @flow_controller = nil
164
172
  @current_flow = current_session.flow
165
173
 
166
174
  action(action: state)
@@ -34,5 +34,11 @@ module Stealth
34
34
  class ReplyNotFound < Errors
35
35
  end
36
36
 
37
+ class FlowError < Errors
38
+ end
39
+
40
+ class FlowDefinitionError < Errors
41
+ end
42
+
37
43
  end
38
44
  end
@@ -1,153 +1,38 @@
1
1
  # coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'stealth/flow/errors'
4
+ require 'stealth/flow/core_ext'
5
5
  require 'stealth/flow/specification'
6
- require 'stealth/flow/event_collection'
7
- require 'stealth/flow/event'
8
6
  require 'stealth/flow/state'
9
7
 
10
8
  module Stealth
11
9
  module Flow
12
10
 
13
- module ClassMethods
11
+ extend ActiveSupport::Concern
12
+
13
+ class_methods do
14
14
  attr_reader :flow_spec
15
15
 
16
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
17
+ @flow_spec = Specification.new(&specification)
67
18
  end
68
19
  end
69
20
 
70
- module InstanceMethods
21
+ included do
71
22
  attr_accessor :flow_state, :user_id
72
23
 
73
24
  def current_state
74
- loaded_state = load_flow_state
75
- res = spec.states[loaded_state.to_sym] if loaded_state
25
+ res = spec.states[@flow_state.to_sym] if @flow_state
76
26
  res || spec.initial_state
77
27
  end
78
28
 
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
29
  def spec
139
30
  # check the singleton class first
140
31
  class << self
141
32
  return flow_spec if flow_spec
142
33
  end
143
34
 
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
35
+ self.class.flow_spec
151
36
  end
152
37
 
153
38
  def states
@@ -155,102 +40,23 @@ module Stealth
155
40
  end
156
41
 
157
42
  def init_state(state)
158
- res = spec.states[state.to_sym] if state
159
- @flow_state = res || spec.initial_state
160
- self
161
- end
43
+ raise(ArgumentError, 'No state was specified.') if state.blank?
162
44
 
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")
45
+ new_state = state.to_sym
46
+ unless states.include?(new_state)
47
+ raise(Stealth::Errors::InvalidStateTransition)
172
48
  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
49
+ @flow_state = new_state
178
50
 
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
51
+ self
233
52
  end
234
53
 
235
- def load_flow_state
236
- @flow_state
237
- end
54
+ private
238
55
 
239
- def persist_flow_state(new_value)
240
- @flow_state = new_value
241
- if defined?($redis)
242
- $redis.set(user_id, flow_and_state)
56
+ def flow_and_state
57
+ [self.class.to_s, current_state].join("->")
243
58
  end
244
- end
245
-
246
- def flow_and_state
247
- [self.class.to_s, current_state].join("->")
248
- end
249
59
  end
250
60
 
251
- def self.included(klass)
252
- klass.send :include, InstanceMethods
253
- klass.extend ClassMethods
254
- end
255
61
  end
256
62
  end
@@ -0,0 +1,11 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ class Numeric
5
+
6
+ def states
7
+ self
8
+ end
9
+ alias :state :states
10
+
11
+ end
@@ -4,12 +4,10 @@
4
4
  module Stealth
5
5
  module Flow
6
6
  class Specification
7
- attr_accessor :states, :initial_state, :meta,
8
- :on_transition_proc, :before_transition_proc, :after_transition_proc, :on_error_proc
7
+ attr_accessor :states, :initial_state
9
8
 
10
- def initialize(meta = {}, &specification)
9
+ def initialize(&specification)
11
10
  @states = Hash.new
12
- @meta = meta
13
11
  instance_eval(&specification)
14
12
  end
15
13
 
@@ -19,49 +17,18 @@ module Stealth
19
17
 
20
18
  private
21
19
 
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
20
+ def state(name, fails_to: nil)
21
+ fail_state = nil
22
+ if fails_to.present?
23
+ fail_state = Stealth::Flow::State.new(fails_to, self)
24
+ end
41
25
 
42
- def on_entry(&proc)
43
- @scoped_state.on_entry = proc
44
- end
26
+ new_state = Stealth::Flow::State.new(name, self, fail_state)
27
+ @initial_state = new_state if @states.empty?
28
+ @states[name.to_sym] = new_state
29
+ @scoped_state = new_state
30
+ end
45
31
 
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
32
  end
66
33
  end
67
34
  end