stealth 0.9.8 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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