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.
@@ -7,33 +7,51 @@ module Stealth
7
7
 
8
8
  include Comparable
9
9
 
10
- attr_accessor :name, :events, :meta, :on_entry, :on_exit
11
- attr_reader :spec
10
+ attr_accessor :name
11
+ attr_reader :spec, :fails_to
12
12
 
13
- def initialize(name, spec, meta = {})
14
- @name, @spec, @events, @meta = name, spec, EventCollection.new, meta
13
+ def initialize(name, spec, fails_to = nil)
14
+ if fails_to.present? && !fails_to.is_a?(Stealth::Flow::State)
15
+ raise(ArgumentError, 'fails_to state should be a Stealth::Flow::State')
16
+ end
17
+
18
+ @name, @spec, @fails_to = name, spec, fails_to
15
19
  end
16
20
 
17
- def draw(graph)
18
- defaults = {
19
- :label => to_s,
20
- :width => '1',
21
- :height => '1',
22
- :shape => 'ellipse'
23
- }
21
+ def <=>(other_state)
22
+ state_position(self) <=> state_position(other_state)
23
+ end
24
24
 
25
- node = graph.add_nodes(to_s, defaults.merge(meta))
25
+ def +(steps)
26
+ if steps < 0
27
+ new_position = state_position(self) + steps
26
28
 
27
- # Add open arrow for initial state
28
- # graph.add_edge(graph.add_node('starting_state', :shape => 'point'), node) if initial?
29
+ # we don't want to allow the array index to wrap here so we return
30
+ # the first state instead
31
+ if new_position < 0
32
+ new_state = spec.states.keys.first
33
+ else
34
+ new_state = spec.states.keys.at(new_position)
35
+ end
36
+ else
37
+ new_state = spec.states.keys[state_position(self) + steps]
29
38
 
30
- node
39
+ # we may have been told to access an out-of-bounds state
40
+ # return the last state
41
+ if new_state.blank?
42
+ new_state = spec.states.keys.last
43
+ end
44
+ end
45
+
46
+ new_state
31
47
  end
32
48
 
33
- def <=>(other_state)
34
- states = spec.states.keys
35
- raise ArgumentError, "state `#{other_state}' does not exist" unless states.include?(other_state.to_sym)
36
- states.index(self.to_sym) <=> states.index(other_state.to_sym)
49
+ def -(steps)
50
+ if steps < 0
51
+ return self + steps.abs
52
+ else
53
+ return self + (-steps)
54
+ end
37
55
  end
38
56
 
39
57
  def to_s
@@ -43,6 +61,18 @@ module Stealth
43
61
  def to_sym
44
62
  name.to_sym
45
63
  end
64
+
65
+ private
66
+
67
+ def state_position(state)
68
+ states = spec.states.keys
69
+
70
+ unless states.include?(state.to_sym)
71
+ raise(ArgumentError, "state `#{state}' does not exist")
72
+ end
73
+
74
+ states.index(state.to_sym)
75
+ end
46
76
  end
47
77
  end
48
78
  end
@@ -6,12 +6,14 @@ module Stealth
6
6
 
7
7
  SLUG_SEPARATOR = '->'
8
8
 
9
- attr_reader :session, :flow, :state, :user_id
9
+ attr_reader :flow, :state, :user_id, :previous
10
+ attr_accessor :session
10
11
 
11
- def initialize(user_id:)
12
+ def initialize(user_id:, previous: false)
12
13
  @user_id = user_id
14
+ @previous = previous
13
15
 
14
- unless defined?($redis)
16
+ unless defined?($redis) && $redis.present?
15
17
  raise(Stealth::Errors::RedisNotConfigured, "Please make sure REDIS_URL is configured before using sessions")
16
18
  end
17
19
 
@@ -27,7 +29,9 @@ module Stealth
27
29
  end
28
30
 
29
31
  def flow
30
- @flow = begin
32
+ return nil if flow_string.blank?
33
+
34
+ @flow ||= begin
31
35
  flow_klass = [flow_string, 'flow'].join('_').classify.constantize
32
36
  flow = flow_klass.new.init_state(state_string)
33
37
  flow
@@ -35,7 +39,7 @@ module Stealth
35
39
  end
36
40
 
37
41
  def state
38
- flow.current_state
42
+ flow&.current_state
39
43
  end
40
44
 
41
45
  def flow_string
@@ -47,12 +51,18 @@ module Stealth
47
51
  end
48
52
 
49
53
  def get
50
- @session ||= $redis.get(user_id)
54
+ if previous?
55
+ @session ||= $redis.get(previous_session_key(user_id: user_id))
56
+ else
57
+ @session ||= $redis.get(user_id)
58
+ end
51
59
  end
52
60
 
53
61
  def set(flow:, state:)
62
+ store_current_to_previous
63
+
64
+ @flow = nil
54
65
  @session = canonical_session_slug(flow: flow, state: state)
55
- flow
56
66
  $redis.set(user_id, session)
57
67
  end
58
68
 
@@ -64,11 +74,44 @@ module Stealth
64
74
  !present?
65
75
  end
66
76
 
77
+ def previous?
78
+ @previous
79
+ end
80
+
81
+ def +(steps)
82
+ return nil if flow.blank?
83
+ return self if steps.zero?
84
+
85
+ new_state = self.state + steps
86
+ new_session = Stealth::Session.new(user_id: self.user_id)
87
+ new_session.session = canonical_session_slug(flow: self.flow_string, state: new_state)
88
+
89
+ new_session
90
+ end
91
+
92
+ def -(steps)
93
+ return nil if flow.blank?
94
+
95
+ if steps < 0
96
+ return self + steps.abs
97
+ else
98
+ return self + (-steps)
99
+ end
100
+ end
101
+
67
102
  private
68
103
 
69
104
  def canonical_session_slug(flow:, state:)
70
105
  [flow, state].join(SLUG_SEPARATOR)
71
106
  end
72
107
 
108
+ def previous_session_key(user_id:)
109
+ [user_id, 'previous'].join('-')
110
+ end
111
+
112
+ def store_current_to_previous
113
+ $redis.set(previous_session_key(user_id: user_id), session)
114
+ end
115
+
73
116
  end
74
117
  end
@@ -5,7 +5,7 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
5
5
  describe "Stealth::Configuration" do
6
6
 
7
7
  describe "accessing via method calling" do
8
- let(:services_yml) { File.read(File.join(File.dirname(__FILE__), 'sample_services_yml', 'services.yml')) }
8
+ let(:services_yml) { File.read(File.join(File.dirname(__FILE__), 'support', 'services.yml')) }
9
9
  let(:parsed_config) { YAML.load(ERB.new(services_yml).result)[Stealth.env] }
10
10
  let(:config) { Stealth.load_services_config!(services_yml) }
11
11
 
@@ -31,7 +31,7 @@ describe "Stealth::Configuration" do
31
31
  end
32
32
 
33
33
  describe "config files with ERB" do
34
- let(:services_yml) { File.read(File.join(File.dirname(__FILE__), 'sample_services_yml', 'services_with_erb.yml')) }
34
+ let(:services_yml) { File.read(File.join(File.dirname(__FILE__), 'support', 'services_with_erb.yml')) }
35
35
  let(:config) { Stealth.load_services_config!(services_yml) }
36
36
 
37
37
  it "should replace available embedded env vars" do
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '/spec_helper'))
4
+
5
+ class BotController < Stealth::Controller
6
+ before_action :fetch_user_name
7
+
8
+ attr_accessor :record
9
+
10
+ def some_action
11
+ @record = []
12
+ step_to flow: 'flow_tester', state: 'my_action'
13
+ end
14
+
15
+ def other_action
16
+ @record = []
17
+ step_to flow: 'other_flow_tester', state: 'other_action'
18
+ end
19
+
20
+ def halted_action
21
+ @record = []
22
+ step_to flow: 'flow_tester', state: 'my_action2'
23
+ end
24
+
25
+ def filtered_action
26
+ @record = []
27
+ step_to flow: 'flow_tester', state: 'my_action3'
28
+ end
29
+
30
+ def some_other_action2
31
+ @record = []
32
+ step_to flow: 'other_flow_tester', state: 'other_action2'
33
+ end
34
+
35
+ def some_other_action3
36
+ @record = []
37
+ step_to flow: 'other_flow_tester', state: 'other_action3'
38
+ end
39
+
40
+ def some_other_action4
41
+ @record = []
42
+ step_to flow: 'other_flow_tester', state: 'other_action4'
43
+ end
44
+
45
+ def some_other_action5
46
+ @record = []
47
+ step_to flow: 'other_flow_tester', state: 'other_action5'
48
+ end
49
+
50
+ private
51
+
52
+ def fetch_user_name
53
+ @record << "fetched user name"
54
+ end
55
+ end
56
+
57
+ class FlowTestersController < BotController
58
+ before_action :test_before_halting, only: :my_action2
59
+ before_action :test_action
60
+ before_action :test_filtering, except: [:my_action, :my_action2]
61
+
62
+ attr_reader :action_ran
63
+
64
+ def my_action
65
+
66
+ end
67
+
68
+ def my_action2
69
+
70
+ end
71
+
72
+ def my_action3
73
+
74
+ end
75
+
76
+ protected
77
+
78
+ def test_action
79
+ @record << "tested action"
80
+ end
81
+
82
+ def test_before_halting
83
+ throw(:abort)
84
+ end
85
+
86
+ def test_filtering
87
+ @record << "filtered"
88
+ end
89
+
90
+ def test_after_halting
91
+ @record << "after action ran"
92
+ end
93
+ end
94
+
95
+ class OtherFlowTestersController < BotController
96
+ after_action :after_action1, only: :other_action2
97
+ after_action :after_action2, only: :other_action2
98
+
99
+ before_action :run_halt, only: [:other_action3, :other_action5]
100
+ after_action :after_action3, only: :other_action3
101
+
102
+ around_action :run_around_filter, only: [:other_action4, :other_action5]
103
+
104
+ def other_action
105
+
106
+ end
107
+
108
+ def other_action2
109
+
110
+ end
111
+
112
+ def other_action3
113
+
114
+ end
115
+
116
+ def other_action4
117
+
118
+ end
119
+
120
+ def other_action5
121
+
122
+ end
123
+
124
+ private
125
+
126
+ def after_action1
127
+ @record << "after action 1"
128
+ end
129
+
130
+ def after_action2
131
+ @record << "after action 2"
132
+ end
133
+
134
+ def run_halt
135
+ throw(:abort)
136
+ end
137
+
138
+ def run_around_filter
139
+ @record << "around before"
140
+ yield
141
+ @record << "around after"
142
+ end
143
+ end
144
+
145
+ class FlowTesterFlow
146
+ include Stealth::Flow
147
+
148
+ flow do
149
+ state :my_action
150
+ state :my_action2
151
+ state :my_action3
152
+ end
153
+ end
154
+
155
+ class OtherFlowTesterFlow
156
+ include Stealth::Flow
157
+
158
+ flow do
159
+ state :other_action
160
+ state :other_action2
161
+ state :other_action3
162
+ state :other_action4
163
+ state :other_action5
164
+ end
165
+ end
166
+
167
+ describe "Stealth::Controller callbacks" do
168
+
169
+ let(:facebook_message) { SampleMessage.new(service: 'facebook') }
170
+
171
+ describe "before_action" do
172
+ it "should fire the callback on the parent class" do
173
+ controller = BotController.new(service_message: facebook_message.message_with_text)
174
+ controller.other_action
175
+ expect(controller.record).to eq ["fetched user name"]
176
+ end
177
+
178
+ it "should fire the callback on a child class" do
179
+ controller = FlowTestersController.new(service_message: facebook_message.message_with_text)
180
+ controller.some_action
181
+ expect(controller.record).to eq ["fetched user name", "tested action"]
182
+ end
183
+
184
+ it "should halt the callback chain when :abort is thrown" do
185
+ controller = FlowTestersController.new(service_message: facebook_message.message_with_text)
186
+ controller.halted_action
187
+ expect(controller.record).to eq ["fetched user name"]
188
+ end
189
+
190
+ it "should respect 'unless' filter" do
191
+ controller = FlowTestersController.new(service_message: facebook_message.message_with_text)
192
+ controller.filtered_action
193
+ expect(controller.record).to eq ["fetched user name", "tested action", "filtered"]
194
+ end
195
+ end
196
+
197
+ describe "after_action" do
198
+ it "should fire the after callbacks in reverse order" do
199
+ controller = OtherFlowTestersController.new(service_message: facebook_message.message_with_text)
200
+ controller.some_other_action2
201
+ expect(controller.record).to eq ["fetched user name", "after action 2", "after action 1"]
202
+ end
203
+
204
+ it "should not fire after callbacks if a before callback throws an :abort" do
205
+ controller = OtherFlowTestersController.new(service_message: facebook_message.message_with_text)
206
+ controller.some_other_action3
207
+ expect(controller.record).to eq ["fetched user name"]
208
+ end
209
+ end
210
+
211
+ describe "around_action" do
212
+ it "should fire the around callback before and after" do
213
+ controller = OtherFlowTestersController.new(service_message: facebook_message.message_with_text)
214
+ controller.some_other_action4
215
+ expect(controller.record).to eq ["fetched user name", "around before", "around after"]
216
+ end
217
+
218
+ it "should not fire the around callback if a before callback throws abort" do
219
+ controller = OtherFlowTestersController.new(service_message: facebook_message.message_with_text)
220
+ controller.some_other_action5
221
+ expect(controller.record).to eq ["fetched user name"]
222
+ end
223
+ end
224
+
225
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '/spec_helper'))
4
+
5
+ describe "Stealth::Controller state transitions" do
6
+
7
+ class MrRobotsController < BotController
8
+ def my_action
9
+ [:success, :my_action]
10
+ end
11
+
12
+ def my_action2
13
+ [:success, :my_action2]
14
+ end
15
+
16
+ def my_action3
17
+ [:success, :my_action3]
18
+ end
19
+ end
20
+
21
+ class MrTronsController < BotController
22
+ def other_action
23
+
24
+ end
25
+
26
+ def other_action2
27
+
28
+ end
29
+
30
+ def other_action3
31
+
32
+ end
33
+ end
34
+
35
+ class MrRobotFlow
36
+ include Stealth::Flow
37
+
38
+ flow do
39
+ state :my_action
40
+ state :my_action2
41
+ state :my_action3
42
+ end
43
+ end
44
+
45
+ class MrTronFlow
46
+ include Stealth::Flow
47
+
48
+ flow do
49
+ state :other_action
50
+ state :other_action2
51
+ state :other_action3
52
+ end
53
+ end
54
+
55
+ let(:facebook_message) { SampleMessage.new(service: 'facebook') }
56
+
57
+ describe "step_to" do
58
+ it "should raise an ArgumentError if a session, flow, or state is not specified" do
59
+ controller = MrTronsController.new(service_message: facebook_message.message_with_text)
60
+ expect {
61
+ controller.step_to
62
+ }.to raise_error(ArgumentError)
63
+ end
64
+
65
+ it "should call the flow's first state's controller action when only a flow is provided" do
66
+
67
+ end
68
+
69
+ it "should call a controller's corresponding action when only a state is provided" do
70
+
71
+ end
72
+
73
+ it "should call a controller's corresponding action when a state and flow is provided" do
74
+
75
+ end
76
+
77
+ it "should call a controller's corresponding action when a session is provided" do
78
+
79
+ end
80
+ end
81
+
82
+ describe "update_session_to" do
83
+ it "should raise an ArgumentError if a session, flow, or state is not specified" do
84
+ controller = MrTronsController.new(service_message: facebook_message.message_with_text)
85
+ expect {
86
+ controller.update_session_to
87
+ }.to raise_error(ArgumentError)
88
+ end
89
+ end
90
+
91
+ describe "step_to_in" do
92
+ it "should raise an ArgumentError if a session, flow, or state is not specified" do
93
+ controller = MrTronsController.new(service_message: facebook_message.message_with_text)
94
+ expect {
95
+ controller.step_to_in
96
+ }.to raise_error(ArgumentError)
97
+ end
98
+ end
99
+
100
+ describe "step_to_next" do
101
+
102
+ end
103
+
104
+ describe "update_session_to_next" do
105
+
106
+ end
107
+
108
+ end
@@ -8,67 +8,37 @@ describe Stealth::Flow do
8
8
  include Stealth::Flow
9
9
 
10
10
  flow do
11
- state :new do
12
- event :submit_todo, :transitions_to => :get_due_date
13
- event :error_in_input, :transitions_to => :error
14
- end
11
+ state :new
15
12
 
16
- state :get_due_date do
17
- event :submit_due_date, :transitions_to => :created
18
- end
13
+ state :get_due_date
19
14
 
20
15
  state :created
21
16
 
22
- state :error do
23
- event :submit_todo, :transitions_to => :get_due_date
24
- event :error_in_input, :transitions_to => :error
25
- end
17
+ state :error
26
18
  end
27
19
  end
28
20
 
29
21
  let(:flow) { NewTodoFlow.new }
30
22
 
31
- describe "state transitions" do
32
- it "should start out in the 'new' state" do
33
- expect(flow.current_state).to eq :new
34
- end
35
-
36
- it "should transition into the 'get_due_date' state after submit" do
37
- flow.submit_todo!
38
- expect(flow.current_state).to eq :get_due_date
39
- end
40
-
41
- it "should transition into the 'error' state after error_in_input" do
42
- flow.error_in_input!
43
- expect(flow.current_state).to eq :error
44
- end
45
-
46
- it "should transition through multiple states" do
47
- flow.submit_todo!
48
- flow.submit_due_date!
23
+ describe "inititating with states" do
24
+ it "should init a state given a state name" do
25
+ flow.init_state(:created)
49
26
  expect(flow.current_state).to eq :created
50
- end
51
27
 
52
- it "should remain in the error state" do
53
- flow.error_in_input!
54
- expect(flow.current_state).to eq :error
55
- flow.error_in_input!
28
+ flow.init_state('error')
56
29
  expect(flow.current_state).to eq :error
57
30
  end
58
31
 
59
- it "should be false when checking the possibility of a non-valid transition" do
60
- expect(flow.can_submit_due_date?).to be false
61
- end
62
-
63
- it "should be false when checking the possibility of a valid transition" do
64
- flow.submit_todo!
65
- expect(flow.can_submit_due_date?).to be true
32
+ it "should raise an error if an invalid state is specified" do
33
+ expect {
34
+ flow.init_state(:invalid)
35
+ }.to raise_error(Stealth::Errors::InvalidStateTransition)
66
36
  end
67
37
  end
68
38
 
69
39
  describe "accessing states" do
70
- it "should start out in the 'new' state" do
71
- expect(flow.new?).to be true
40
+ it "should start out in the initial state" do
41
+ expect(flow.current_state).to eq :new
72
42
  end
73
43
 
74
44
  it "should support comparing states" do
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
4
+
5
+ describe Stealth::Flow::State do
6
+
7
+ class NewTodoFlow
8
+ include Stealth::Flow
9
+
10
+ flow do
11
+ state :new
12
+
13
+ state :get_due_date
14
+
15
+ state :created, fails_to: :new
16
+
17
+ state :error
18
+ end
19
+ end
20
+
21
+ let(:flow) { NewTodoFlow.new }
22
+
23
+ describe "flow states" do
24
+ it "should convert itself to a string" do
25
+ expect(flow.current_state.to_s).to be_a(String)
26
+ end
27
+
28
+ it "should convert itself to a symbol" do
29
+ expect(flow.current_state.to_sym).to be_a(Symbol)
30
+ end
31
+ end
32
+
33
+ describe "fails_to" do
34
+ it "should be nil for a state that has not specified a fails_to" do
35
+ expect(flow.current_state.fails_to).to be_nil
36
+ end
37
+
38
+ it "should return the fail_state if a fails_to was specified" do
39
+ flow.init_state(:created)
40
+ expect(flow.current_state.fails_to).to be_a(Stealth::Flow::State)
41
+ expect(flow.current_state.fails_to).to eq :new
42
+ end
43
+ end
44
+
45
+ describe "state incrementing and decrementing" do
46
+ it "should increment the state" do
47
+ flow.init_state(:get_due_date)
48
+ new_state = flow.current_state + 1.state
49
+ expect(new_state).to eq(:created)
50
+ end
51
+
52
+ it "should decrement the state" do
53
+ flow.init_state(:error)
54
+ new_state = flow.current_state - 2.states
55
+ expect(new_state).to eq(:get_due_date)
56
+ end
57
+
58
+ it "should return the first state if the decrement is out of bounds" do
59
+ flow.init_state(:get_due_date)
60
+ new_state = flow.current_state - 5.states
61
+ expect(new_state).to eq(:new)
62
+ end
63
+
64
+ it "should return the last state if the increment is out of bounds" do
65
+ flow.init_state(:created)
66
+ new_state = flow.current_state + 5.states
67
+ expect(new_state).to eq(:error)
68
+ end
69
+ end
70
+
71
+ end