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 +4 -4
- data/Gemfile.lock +5 -3
- data/VERSION +1 -1
- data/lib/stealth/base.rb +3 -1
- data/lib/stealth/controller/callbacks.rb +64 -0
- data/lib/stealth/controller/catch_all.rb +54 -0
- data/lib/stealth/{controller.rb → controller/controller.rb} +28 -20
- data/lib/stealth/errors.rb +6 -0
- data/lib/stealth/flow/base.rb +17 -211
- data/lib/stealth/flow/core_ext.rb +11 -0
- data/lib/stealth/flow/specification.rb +12 -45
- data/lib/stealth/flow/state.rb +49 -19
- data/lib/stealth/session.rb +50 -7
- data/spec/configuration_spec.rb +2 -2
- data/spec/controller/callbacks_spec.rb +225 -0
- data/spec/controller/state_transitions_spec.rb +108 -0
- data/spec/flow/flow_spec.rb +13 -43
- data/spec/flow/state_spec.rb +71 -0
- data/spec/session_spec.rb +116 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/sample_messages.rb +65 -0
- data/spec/{sample_services_yml → support}/services.yml +0 -0
- data/spec/{sample_services_yml → support}/services_with_erb.yml +0 -0
- data/stealth.gemspec +1 -0
- metadata +35 -15
- data/lib/stealth/flow/errors.rb +0 -25
- data/lib/stealth/flow/event.rb +0 -43
- data/lib/stealth/flow/event_collection.rb +0 -41
- data/spec/flow/custom_transitions_spec.rb +0 -99
- data/spec/flow/transition_callbacks_spec.rb +0 -228
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
4
|
+
|
5
|
+
class NewTodoFlow
|
6
|
+
include Stealth::Flow
|
7
|
+
|
8
|
+
flow do
|
9
|
+
state :new
|
10
|
+
|
11
|
+
state :get_due_date
|
12
|
+
|
13
|
+
state :created, fails_to: :new
|
14
|
+
|
15
|
+
state :error
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "Stealth::Session" do
|
20
|
+
let(:user_id) { '0xDEADBEEF' }
|
21
|
+
|
22
|
+
it "should raise an error if $redis is not set" do
|
23
|
+
$redis = nil
|
24
|
+
|
25
|
+
expect {
|
26
|
+
Stealth::Session.new(user_id: user_id)
|
27
|
+
}.to raise_error(Stealth::Errors::RedisNotConfigured)
|
28
|
+
|
29
|
+
$redis = MockRedis.new
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "without a session" do
|
33
|
+
let(:session) { Stealth::Session.new(user_id: user_id) }
|
34
|
+
|
35
|
+
it "should have nil flow and state" do
|
36
|
+
expect(session.flow).to be_nil
|
37
|
+
expect(session.state).to be_nil
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should have nil flow_string and state_string" do
|
41
|
+
expect(session.flow_string).to be_nil
|
42
|
+
expect(session.state_string).to be_nil
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should respond to present? and blank?" do
|
46
|
+
expect(session.present?).to be false
|
47
|
+
expect(session.blank?).to be true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "with a session" do
|
52
|
+
class MarcoFlow
|
53
|
+
include Stealth::Flow
|
54
|
+
|
55
|
+
flow do
|
56
|
+
state :polo
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
let(:session) do
|
61
|
+
session = Stealth::Session.new(user_id: user_id)
|
62
|
+
session.set(flow: 'Marco', state: 'polo')
|
63
|
+
session
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should return the flow" do
|
67
|
+
expect(session.flow).to be_a(MarcoFlow)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should return the state" do
|
71
|
+
expect(session.state).to be_a(Stealth::Flow::State)
|
72
|
+
expect(session.state).to eq :polo
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should return the flow_string" do
|
76
|
+
expect(session.flow_string).to eq "Marco"
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should return the state_string" do
|
80
|
+
expect(session.state_string).to eq "polo"
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should respond to present? and blank?" do
|
84
|
+
expect(session.present?).to be true
|
85
|
+
expect(session.blank?).to be false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "incrementing and decrementing" do
|
90
|
+
let(:session) { Stealth::Session.new(user_id: user_id) }
|
91
|
+
|
92
|
+
it "should increment the state" do
|
93
|
+
session.set(flow: 'NewTodo', state: 'get_due_date')
|
94
|
+
new_session = session + 1.state
|
95
|
+
expect(new_session.state_string).to eq('created')
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should decrement the state" do
|
99
|
+
session.set(flow: 'NewTodo', state: 'error')
|
100
|
+
new_session = session - 2.states
|
101
|
+
expect(new_session.state_string).to eq('get_due_date')
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should return the first state if the decrement is out of bounds" do
|
105
|
+
session.set(flow: 'NewTodo', state: 'get_due_date')
|
106
|
+
new_session = session - 5.states
|
107
|
+
expect(new_session.state_string).to eq('new')
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should return the last state if the increment is out of bounds" do
|
111
|
+
session.set(flow: 'NewTodo', state: 'created')
|
112
|
+
new_session = session + 5.states
|
113
|
+
expect(new_session.state_string).to eq('error')
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -3,11 +3,14 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
3
3
|
require 'rspec'
|
4
4
|
|
5
5
|
require 'stealth'
|
6
|
+
require 'mock_redis'
|
6
7
|
|
7
8
|
# Requires supporting files with custom matchers and macros, etc,
|
8
9
|
# in ./support/ and its subdirectories.
|
9
10
|
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
10
11
|
|
12
|
+
$redis = MockRedis.new
|
13
|
+
|
11
14
|
RSpec.configure do |config|
|
12
15
|
|
13
16
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class SampleMessage
|
2
|
+
|
3
|
+
def initialize(service:)
|
4
|
+
@service = service
|
5
|
+
@base_message = Stealth::ServiceMessage.new(service: @service)
|
6
|
+
@base_message.sender_id = sender_id
|
7
|
+
@base_message.timestamp = timestamp
|
8
|
+
@base_message
|
9
|
+
end
|
10
|
+
|
11
|
+
def message_with_text
|
12
|
+
@base_message.message = message
|
13
|
+
@base_message
|
14
|
+
end
|
15
|
+
|
16
|
+
def message_with_payload
|
17
|
+
@base_message.payload = payload
|
18
|
+
@base_message
|
19
|
+
end
|
20
|
+
|
21
|
+
def message_with_location
|
22
|
+
@base_message.location = location
|
23
|
+
@base_message
|
24
|
+
end
|
25
|
+
|
26
|
+
def message_with_attachments
|
27
|
+
@base_message.attachments = attachments
|
28
|
+
@base_message
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def sender_id
|
34
|
+
if @service == 'twilio'
|
35
|
+
'+15554561212'
|
36
|
+
else
|
37
|
+
"8b3e0a3c-62f1-401e-8b0f-615c9d256b1f"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def timestamp
|
42
|
+
Time.now
|
43
|
+
end
|
44
|
+
|
45
|
+
def message
|
46
|
+
"Hello World!"
|
47
|
+
end
|
48
|
+
|
49
|
+
def payload
|
50
|
+
"some_payload"
|
51
|
+
end
|
52
|
+
|
53
|
+
def location
|
54
|
+
{ lat: '42.323724' , lng: '-83.047543' }
|
55
|
+
end
|
56
|
+
|
57
|
+
def attachments
|
58
|
+
[ { type: 'image', url: 'https://domain.none/image.jpg' } ]
|
59
|
+
end
|
60
|
+
|
61
|
+
def referral
|
62
|
+
{}
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
File without changes
|
File without changes
|
data/stealth.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |s|
|
|
22
22
|
s.add_development_dependency 'rspec', '~> 3.6'
|
23
23
|
s.add_development_dependency 'rspec_junit_formatter', '~> 0.3'
|
24
24
|
s.add_development_dependency 'rack-test', '~> 0.7'
|
25
|
+
s.add_development_dependency 'mock_redis', '~> 0.17'
|
25
26
|
|
26
27
|
s.files = `git ls-files`.split("\n")
|
27
28
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stealth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mauricio Gomes
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-01-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sinatra
|
@@ -136,6 +136,20 @@ dependencies:
|
|
136
136
|
- - "~>"
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0.7'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: mock_redis
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.17'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.17'
|
139
153
|
description: Ruby framework for building conversational bots.
|
140
154
|
email: mauricio@edge14.com
|
141
155
|
executables:
|
@@ -159,13 +173,13 @@ files:
|
|
159
173
|
- lib/stealth/commands/console.rb
|
160
174
|
- lib/stealth/commands/server.rb
|
161
175
|
- lib/stealth/configuration.rb
|
162
|
-
- lib/stealth/controller.rb
|
176
|
+
- lib/stealth/controller/callbacks.rb
|
177
|
+
- lib/stealth/controller/catch_all.rb
|
178
|
+
- lib/stealth/controller/controller.rb
|
163
179
|
- lib/stealth/dispatcher.rb
|
164
180
|
- lib/stealth/errors.rb
|
165
181
|
- lib/stealth/flow/base.rb
|
166
|
-
- lib/stealth/flow/
|
167
|
-
- lib/stealth/flow/event.rb
|
168
|
-
- lib/stealth/flow/event_collection.rb
|
182
|
+
- lib/stealth/flow/core_ext.rb
|
169
183
|
- lib/stealth/flow/specification.rb
|
170
184
|
- lib/stealth/flow/state.rb
|
171
185
|
- lib/stealth/jobs.rb
|
@@ -182,14 +196,17 @@ files:
|
|
182
196
|
- lib/stealth/session.rb
|
183
197
|
- lib/stealth/version.rb
|
184
198
|
- spec/configuration_spec.rb
|
185
|
-
- spec/
|
199
|
+
- spec/controller/callbacks_spec.rb
|
200
|
+
- spec/controller/state_transitions_spec.rb
|
186
201
|
- spec/flow/flow_spec.rb
|
187
|
-
- spec/flow/
|
202
|
+
- spec/flow/state_spec.rb
|
188
203
|
- spec/replies/nested_reply_with_erb.yml
|
189
|
-
- spec/sample_services_yml/services.yml
|
190
|
-
- spec/sample_services_yml/services_with_erb.yml
|
191
204
|
- spec/service_reply_spec.rb
|
205
|
+
- spec/session_spec.rb
|
192
206
|
- spec/spec_helper.rb
|
207
|
+
- spec/support/sample_messages.rb
|
208
|
+
- spec/support/services.yml
|
209
|
+
- spec/support/services_with_erb.yml
|
193
210
|
- spec/version_spec.rb
|
194
211
|
- stealth.gemspec
|
195
212
|
homepage: https://github.com/whoisblackops/stealth
|
@@ -212,18 +229,21 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
212
229
|
version: '0'
|
213
230
|
requirements: []
|
214
231
|
rubyforge_project:
|
215
|
-
rubygems_version: 2.6.
|
232
|
+
rubygems_version: 2.6.12
|
216
233
|
signing_key:
|
217
234
|
specification_version: 4
|
218
235
|
summary: Ruby framework for conversational bots
|
219
236
|
test_files:
|
220
237
|
- spec/configuration_spec.rb
|
221
|
-
- spec/
|
238
|
+
- spec/controller/callbacks_spec.rb
|
239
|
+
- spec/controller/state_transitions_spec.rb
|
222
240
|
- spec/flow/flow_spec.rb
|
223
|
-
- spec/flow/
|
241
|
+
- spec/flow/state_spec.rb
|
224
242
|
- spec/replies/nested_reply_with_erb.yml
|
225
|
-
- spec/sample_services_yml/services.yml
|
226
|
-
- spec/sample_services_yml/services_with_erb.yml
|
227
243
|
- spec/service_reply_spec.rb
|
244
|
+
- spec/session_spec.rb
|
228
245
|
- spec/spec_helper.rb
|
246
|
+
- spec/support/sample_messages.rb
|
247
|
+
- spec/support/services.yml
|
248
|
+
- spec/support/services_with_erb.yml
|
229
249
|
- spec/version_spec.rb
|
data/lib/stealth/flow/errors.rb
DELETED
@@ -1,25 +0,0 @@
|
|
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
|
data/lib/stealth/flow/event.rb
DELETED
@@ -1,43 +0,0 @@
|
|
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
|
@@ -1,41 +0,0 @@
|
|
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
|
@@ -1,99 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
4
|
-
|
5
|
-
describe "custom transitions" do
|
6
|
-
|
7
|
-
class FetchTodosFlow
|
8
|
-
include Stealth::Flow
|
9
|
-
|
10
|
-
flow do
|
11
|
-
state :todays_todos do
|
12
|
-
event :fetch_tomorrows_todos, :transitions_to => :tomorrows_todos
|
13
|
-
event :fetch_yesterdays_todos, :transitions_to => :yesterdays_todos
|
14
|
-
end
|
15
|
-
|
16
|
-
state :tomorrows_todos do
|
17
|
-
event :view_todo, :transitions_to => :show
|
18
|
-
event :edit_todo, :transitions_to => :edit
|
19
|
-
end
|
20
|
-
|
21
|
-
state :tomorrows_todos do
|
22
|
-
event :view_todo, :transitions_to => :show
|
23
|
-
event :edit_todo, :transitions_to => :edit
|
24
|
-
end
|
25
|
-
|
26
|
-
state :show
|
27
|
-
|
28
|
-
state :edit do
|
29
|
-
event :save_todo, :transitions_to => :show
|
30
|
-
event :error_in_input, :transitions_to => :error
|
31
|
-
end
|
32
|
-
|
33
|
-
state :error
|
34
|
-
end
|
35
|
-
|
36
|
-
def view_todo(todo_id)
|
37
|
-
unless todo_id > 0
|
38
|
-
halt('ID is not valid.')
|
39
|
-
end
|
40
|
-
|
41
|
-
:the_todo
|
42
|
-
end
|
43
|
-
|
44
|
-
def edit_todo(todo_id)
|
45
|
-
unless todo_id > 0
|
46
|
-
halt('ID is not valid.')
|
47
|
-
end
|
48
|
-
|
49
|
-
:edit_todo_view
|
50
|
-
end
|
51
|
-
|
52
|
-
def save_todo(params)
|
53
|
-
if params.nil?
|
54
|
-
halt!('Invalid todo params specified.')
|
55
|
-
end
|
56
|
-
|
57
|
-
:todo_saved
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
let(:flow) { FetchTodosFlow.new }
|
62
|
-
|
63
|
-
it "should transition via custom transition methods" do
|
64
|
-
flow.fetch_tomorrows_todos!
|
65
|
-
expect(flow.view_todo!(1)).to eq :the_todo
|
66
|
-
expect(flow.current_state).to eq :show
|
67
|
-
end
|
68
|
-
|
69
|
-
it "should follow multiple custom transitions" do
|
70
|
-
flow.fetch_tomorrows_todos!
|
71
|
-
expect(flow.edit_todo!(1)).to eq :edit_todo_view
|
72
|
-
expect(flow.current_state).to eq :edit
|
73
|
-
|
74
|
-
expect(flow.save_todo!({ task: 'test' })).to eq :todo_saved
|
75
|
-
expect(flow.current_state).to eq :show
|
76
|
-
end
|
77
|
-
|
78
|
-
describe "halting transitions" do
|
79
|
-
it "should halt the transition when halt() is called" do
|
80
|
-
flow.fetch_tomorrows_todos!
|
81
|
-
flow.view_todo!(-1)
|
82
|
-
expect(flow.current_state).to eq :tomorrows_todos
|
83
|
-
expect(flow.halted_because).to eq "ID is not valid."
|
84
|
-
end
|
85
|
-
|
86
|
-
it "should halt the transition when halt!() is called and raise Stealth::Flow::TransitionHalted" do
|
87
|
-
flow.fetch_tomorrows_todos!
|
88
|
-
flow.edit_todo!(1)
|
89
|
-
expect(flow.current_state).to eq :edit
|
90
|
-
|
91
|
-
expect {
|
92
|
-
flow.save_todo!(nil)
|
93
|
-
}.to raise_error(Stealth::Flow::TransitionHalted)
|
94
|
-
|
95
|
-
expect(flow.halted_because).to eq "Invalid todo params specified."
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
end
|