stealth 0.9.8 → 0.10.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 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