colloquy 1.0.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 +15 -0
- data/README.md +160 -0
- data/TODO.md +256 -0
- data/bin/colloquy +14 -0
- data/examples/config/flows.yaml +22 -0
- data/examples/config/logger.yaml +2 -0
- data/examples/config/messages.yaml +3 -0
- data/examples/config/mysql.yaml +18 -0
- data/examples/config/redis.yaml +9 -0
- data/examples/config/scribe.yaml +2 -0
- data/examples/config/settings.yaml +2 -0
- data/examples/config/test.yaml +8 -0
- data/examples/config/urls.yaml +18 -0
- data/examples/flows/active_record_flow.rb +42 -0
- data/examples/flows/art_of_war_flow.rb +27 -0
- data/examples/flows/calculator_flow.rb +71 -0
- data/examples/flows/crossover_flow.rb +17 -0
- data/examples/flows/database_flow.rb +33 -0
- data/examples/flows/hangman_flow.rb +82 -0
- data/examples/flows/metadata_flow.rb +11 -0
- data/examples/flows/pagination_flow.rb +23 -0
- data/examples/flows/pass_flow.rb +29 -0
- data/examples/flows/prefix_menu_flow.rb +24 -0
- data/examples/flows/scribe_flow.rb +26 -0
- data/examples/flows/settings_flow.rb +23 -0
- data/examples/flows/special/special_redis_flow.rb +28 -0
- data/examples/flows/url_flow.rb +27 -0
- data/examples/log/renderer.log +198381 -0
- data/examples/log/urls.log +3269 -0
- data/examples/messages/active_record.yaml +2 -0
- data/examples/messages/art_of_war.yaml +1 -0
- data/examples/messages/calculator.yaml +2 -0
- data/examples/messages/database.yaml +1 -0
- data/examples/messages/hangman.yaml +0 -0
- data/examples/messages/prefix_menu.yaml +3 -0
- data/examples/models/activations.rb +5 -0
- data/lib/colloquy.rb +39 -0
- data/lib/colloquy/exceptions.rb +64 -0
- data/lib/colloquy/flow_parser.rb +315 -0
- data/lib/colloquy/flow_pool.rb +21 -0
- data/lib/colloquy/helpers.rb +15 -0
- data/lib/colloquy/helpers/mysql.rb +110 -0
- data/lib/colloquy/helpers/redis.rb +103 -0
- data/lib/colloquy/helpers/scribe.rb +103 -0
- data/lib/colloquy/helpers/settings.rb +111 -0
- data/lib/colloquy/helpers/url.rb +10 -0
- data/lib/colloquy/input.rb +14 -0
- data/lib/colloquy/logger.rb +11 -0
- data/lib/colloquy/menu.rb +128 -0
- data/lib/colloquy/message_builder.rb +54 -0
- data/lib/colloquy/node.rb +67 -0
- data/lib/colloquy/paginator.rb +59 -0
- data/lib/colloquy/paginator/menu.rb +79 -0
- data/lib/colloquy/paginator/prompt.rb +32 -0
- data/lib/colloquy/prompt.rb +35 -0
- data/lib/colloquy/renderer.rb +423 -0
- data/lib/colloquy/response.rb +4 -0
- data/lib/colloquy/runner.rb +93 -0
- data/lib/colloquy/server.rb +157 -0
- data/lib/colloquy/session_store.rb +46 -0
- data/lib/colloquy/session_store/memory.rb +14 -0
- data/lib/colloquy/session_store/redis.rb +24 -0
- data/lib/colloquy/simulator.rb +114 -0
- data/lib/colloquy/spec_helpers.rb +21 -0
- metadata +459 -0
@@ -0,0 +1 @@
|
|
1
|
+
calculator: "Switch to Calculator flow"
|
@@ -0,0 +1 @@
|
|
1
|
+
more: "Next Page"
|
File without changes
|
data/lib/colloquy.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
require 'pathname'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Colloquy
|
6
|
+
class << self
|
7
|
+
attr_reader :root
|
8
|
+
attr_accessor :logger
|
9
|
+
attr_writer :maximum_message_length, :maximum_unicode_length
|
10
|
+
|
11
|
+
def root=(path)
|
12
|
+
@root = Pathname.new("#{File.expand_path(path || Dir.pwd)}").realdirpath
|
13
|
+
end
|
14
|
+
|
15
|
+
def maximum_message_length
|
16
|
+
@maximum_message_length || 160
|
17
|
+
end
|
18
|
+
|
19
|
+
def maximum_unicode_length
|
20
|
+
@maximum_unicode_length || 70
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
require_relative 'colloquy/logger'
|
26
|
+
require_relative 'colloquy/message_builder'
|
27
|
+
require_relative 'colloquy/session_store'
|
28
|
+
require_relative 'colloquy/response'
|
29
|
+
require_relative 'colloquy/renderer'
|
30
|
+
require_relative 'colloquy/input'
|
31
|
+
require_relative 'colloquy/paginator'
|
32
|
+
require_relative 'colloquy/prompt'
|
33
|
+
require_relative 'colloquy/menu'
|
34
|
+
require_relative 'colloquy/flow_parser'
|
35
|
+
require_relative 'colloquy/node'
|
36
|
+
require_relative 'colloquy/simulator'
|
37
|
+
require_relative 'colloquy/server'
|
38
|
+
require_relative 'colloquy/exceptions'
|
39
|
+
require_relative 'colloquy/flow_pool'
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class Colloquy::JumpException < Exception
|
2
|
+
attr_accessor :payload
|
3
|
+
end
|
4
|
+
|
5
|
+
class Colloquy::NotifyJump < Colloquy::JumpException
|
6
|
+
end
|
7
|
+
|
8
|
+
class Colloquy::SwitchJump < Colloquy::JumpException
|
9
|
+
end
|
10
|
+
|
11
|
+
class Colloquy::SwitchBackJump < Colloquy::JumpException
|
12
|
+
end
|
13
|
+
|
14
|
+
class Colloquy::SwitchFlowJump < Colloquy::JumpException
|
15
|
+
end
|
16
|
+
|
17
|
+
class Colloquy::PassJump < Colloquy::JumpException
|
18
|
+
end
|
19
|
+
|
20
|
+
class Colloquy::IndexNodeNotFoundException < Exception
|
21
|
+
end
|
22
|
+
|
23
|
+
class Colloquy::NodeNotFoundException < Exception
|
24
|
+
end
|
25
|
+
|
26
|
+
class Colloquy::DuplicateNodeException < Exception
|
27
|
+
end
|
28
|
+
|
29
|
+
class Colloquy::ConfigurationFolderNotFound < Exception
|
30
|
+
end
|
31
|
+
|
32
|
+
class Colloquy::FlowConfigurationNotFound < Exception
|
33
|
+
end
|
34
|
+
|
35
|
+
class Colloquy::LoggerConfigurationNotFound < Exception
|
36
|
+
end
|
37
|
+
|
38
|
+
class Colloquy::MessagesConfigurationNotFound < Exception
|
39
|
+
end
|
40
|
+
|
41
|
+
class Colloquy::FlowNotFound < Exception
|
42
|
+
end
|
43
|
+
|
44
|
+
class Colloquy::FlowStateInconsistent < Exception
|
45
|
+
end
|
46
|
+
|
47
|
+
class Colloquy::MSISDNParameterEmpty < Exception
|
48
|
+
end
|
49
|
+
|
50
|
+
class Colloquy::SessionIDParameterEmpty < Exception
|
51
|
+
end
|
52
|
+
|
53
|
+
class Colloquy::InputParameterEmpty < Exception
|
54
|
+
end
|
55
|
+
|
56
|
+
class Colloquy::JumpInvalidException < Exception
|
57
|
+
end
|
58
|
+
|
59
|
+
class Colloquy::FlowPoolEmpty < Exception
|
60
|
+
end
|
61
|
+
|
62
|
+
class Colloquy::LogDirectoryNotPresent < Exception
|
63
|
+
end
|
64
|
+
|
@@ -0,0 +1,315 @@
|
|
1
|
+
|
2
|
+
require_relative 'helpers'
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
4
|
+
|
5
|
+
module Colloquy::FlowParser
|
6
|
+
def self.included(klass)
|
7
|
+
klass.class_eval do
|
8
|
+
include InstanceMethods
|
9
|
+
extend ClassMethods
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
STATE_DEFAULT = { node: :index, flow_state: :init, previous: {} }
|
15
|
+
|
16
|
+
include Colloquy::Helpers
|
17
|
+
|
18
|
+
attr_accessor :nodes
|
19
|
+
attr_accessor :logger, :session, :messages, :headers
|
20
|
+
|
21
|
+
# Initializes the flow
|
22
|
+
# @param [Hash] options A list of options passed on to the flow.
|
23
|
+
def initialize(flow_name = nil, options = {})
|
24
|
+
@flow_name = flow_name.to_sym if flow_name
|
25
|
+
@logger = options[:logger] || Logger.new(STDOUT)
|
26
|
+
@session = HashWithIndifferentAccess.new(options[:session] || {})
|
27
|
+
@messages = options[:messages] || {}
|
28
|
+
@headers = {}
|
29
|
+
|
30
|
+
self.state = options[:state]
|
31
|
+
@nodes = options[:nodes] || []
|
32
|
+
|
33
|
+
setup if self.class.method_defined?(:setup)
|
34
|
+
|
35
|
+
create_nodes_from_definitions! if @nodes.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
def flow_name
|
39
|
+
@flow_name ||= "#{self.class}".underscore.gsub(/_flow/, '').split('/').last.to_sym
|
40
|
+
end
|
41
|
+
|
42
|
+
# Do not set @state directly, because it has many checks to ensure that @state is never inconsistent
|
43
|
+
def state=(state)
|
44
|
+
@state = STATE_DEFAULT.merge(flow_name: flow_name).merge(state || {})
|
45
|
+
end
|
46
|
+
|
47
|
+
def node_add(identifier, options = {}, &payload)
|
48
|
+
if node_by_id(identifier)
|
49
|
+
raise Colloquy::DuplicateNodeException, "A node named #{identifier} is already present in the flow"
|
50
|
+
end
|
51
|
+
|
52
|
+
options.merge!(flow: self)
|
53
|
+
|
54
|
+
@nodes << Colloquy::Node.new(identifier, options, &payload)
|
55
|
+
end
|
56
|
+
|
57
|
+
def apply(input = nil)
|
58
|
+
reset_nodes!
|
59
|
+
input = sanitize_input(input)
|
60
|
+
|
61
|
+
store_previous_state!
|
62
|
+
|
63
|
+
case state[:flow_state].to_sym
|
64
|
+
when :init
|
65
|
+
store_back_state!
|
66
|
+
|
67
|
+
apply_request(input)
|
68
|
+
when :request
|
69
|
+
apply_process(input)
|
70
|
+
else
|
71
|
+
raise Colloquy::FlowStateInconsistent, "An unexpected flow state: #{state[:flow_state]} was found"
|
72
|
+
end
|
73
|
+
rescue Colloquy::NotifyJump => e
|
74
|
+
notify! e.payload
|
75
|
+
rescue Colloquy::SwitchJump => e
|
76
|
+
switch! e.payload
|
77
|
+
rescue Colloquy::SwitchBackJump => e
|
78
|
+
switch_back!
|
79
|
+
rescue Colloquy::PassJump => e
|
80
|
+
pass! input
|
81
|
+
end
|
82
|
+
|
83
|
+
def reset!(include_messages = false)
|
84
|
+
@session = HashWithIndifferentAccess.new
|
85
|
+
@state = {}
|
86
|
+
self.state = {}
|
87
|
+
@headers = {}
|
88
|
+
|
89
|
+
@messages = {} if include_messages
|
90
|
+
|
91
|
+
reset_nodes!
|
92
|
+
end
|
93
|
+
|
94
|
+
def reset_nodes!
|
95
|
+
@nodes.each do |node|
|
96
|
+
node.reset!
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def _(message_emergent)
|
101
|
+
Colloquy::MessageBuilder.to_message(message_emergent, :flow => self)
|
102
|
+
end
|
103
|
+
|
104
|
+
def notify(message)
|
105
|
+
raise_jump_exception(Colloquy::NotifyJump, message)
|
106
|
+
end
|
107
|
+
|
108
|
+
def switch(node, options = {})
|
109
|
+
if node == :back
|
110
|
+
raise_jump_exception(Colloquy::SwitchBackJump)
|
111
|
+
end
|
112
|
+
|
113
|
+
if options[:flow]
|
114
|
+
raise_jump_exception(Colloquy::SwitchFlowJump, { :node => node, :flow => options[:flow] })
|
115
|
+
end
|
116
|
+
|
117
|
+
raise_jump_exception(Colloquy::SwitchJump, node)
|
118
|
+
end
|
119
|
+
|
120
|
+
def pass
|
121
|
+
raise_jump_exception(Colloquy::PassJump)
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
def raise_jump_exception(type, payload = nil)
|
126
|
+
jump_exception = type.new
|
127
|
+
jump_exception.payload = payload
|
128
|
+
raise jump_exception
|
129
|
+
end
|
130
|
+
|
131
|
+
def notify!(message)
|
132
|
+
state.merge!(flow_state: :notify)
|
133
|
+
Colloquy::MessageBuilder.to_message(message, messages: messages)
|
134
|
+
end
|
135
|
+
|
136
|
+
def switch!(node)
|
137
|
+
state.merge!(:node => node.to_sym, :flow_state => :init, :switched_from => state.dup)
|
138
|
+
apply
|
139
|
+
end
|
140
|
+
|
141
|
+
def switch_back!
|
142
|
+
if state[:back][:flow_name].to_sym != state[:flow_name].to_sym
|
143
|
+
raise_jump_exception(Colloquy::SwitchFlowJump, { :node => state[:back][:node], :flow => state[:back][:flow_name].to_sym })
|
144
|
+
end
|
145
|
+
|
146
|
+
if state[:back][:flow_name] == state[:flow_name] and state[:back][:node] == state[:node]
|
147
|
+
raise Colloquy::JumpInvalidException, "No previous switch found to jump back to"
|
148
|
+
end
|
149
|
+
|
150
|
+
state.merge!(:node => state[:back][:node], :flow_state => :init)
|
151
|
+
apply
|
152
|
+
end
|
153
|
+
|
154
|
+
def pass!(input)
|
155
|
+
unless state[:flow_state] == :request
|
156
|
+
raise Colloquy::JumpInvalidException, "The instruction pass can only be called from within the request block"
|
157
|
+
end
|
158
|
+
|
159
|
+
# This is a direct initial input
|
160
|
+
input.direct = true
|
161
|
+
|
162
|
+
apply(input)
|
163
|
+
end
|
164
|
+
|
165
|
+
def state
|
166
|
+
@state
|
167
|
+
end
|
168
|
+
|
169
|
+
def create_nodes_from_definitions!
|
170
|
+
@nodes = []
|
171
|
+
|
172
|
+
return unless self.class.node_definitions
|
173
|
+
|
174
|
+
self.class.node_definitions.each do |node_definition|
|
175
|
+
node_add(node_definition[:identifier], node_definition[:options], &node_definition[:payload])
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def store_back_state!
|
180
|
+
state[:back] = {
|
181
|
+
node: state[:previous][:node],
|
182
|
+
flow_name: state[:previous][:flow_name]
|
183
|
+
}
|
184
|
+
end
|
185
|
+
|
186
|
+
def store_previous_state!
|
187
|
+
state_actual = state[:switched_from] || state
|
188
|
+
state[:previous] = state_actual.except(:previous, :switched_from, :back)
|
189
|
+
|
190
|
+
state.delete(:switched_from)
|
191
|
+
end
|
192
|
+
|
193
|
+
def state_delete_supplements!
|
194
|
+
state.delete(:menu)
|
195
|
+
state.delete(:page)
|
196
|
+
state.delete(:prompt)
|
197
|
+
end
|
198
|
+
|
199
|
+
def apply_request(input)
|
200
|
+
state[:flow_state] = :request
|
201
|
+
|
202
|
+
current_node = node_by_id(state[:node])
|
203
|
+
apply_node_not_found_exception unless current_node
|
204
|
+
|
205
|
+
current_node.request!(input)
|
206
|
+
|
207
|
+
# let's find the prompt if the request chain goes through without any jumps
|
208
|
+
output = current_node.render
|
209
|
+
|
210
|
+
# and store it in the state (we might use it later in the flow)
|
211
|
+
store_state_from_node(current_node)
|
212
|
+
|
213
|
+
output
|
214
|
+
end
|
215
|
+
|
216
|
+
def apply_process(input)
|
217
|
+
previous_menu = generate_previous_menu
|
218
|
+
previous_prompt = generate_previous_prompt
|
219
|
+
|
220
|
+
if previous_prompt && (previous_prompt.key(input) == :more) && (previous_prompt.total_pages > previous_prompt.page)
|
221
|
+
state[:page] = state[:previous][:page] + 1
|
222
|
+
state[:prompt] = state[:previous][:prompt]
|
223
|
+
|
224
|
+
headers[:page] = state[:page]
|
225
|
+
previous_prompt.page = state[:page]
|
226
|
+
previous_prompt.render
|
227
|
+
elsif previous_menu && (previous_menu.key(input) == :more) && (previous_menu.total_pages > previous_menu.page)
|
228
|
+
state[:page] = state[:previous][:page] + 1
|
229
|
+
state[:menu] = state[:previous][:menu]
|
230
|
+
|
231
|
+
headers[:page] = state[:page]
|
232
|
+
previous_menu.page = state[:page]
|
233
|
+
previous_menu.render
|
234
|
+
elsif previous_menu && (previous_menu.key(input) == :previous) && (previous_menu.page > 1)
|
235
|
+
state[:page] = state[:previous][:page] - 1
|
236
|
+
state[:menu] = state[:previous][:menu]
|
237
|
+
|
238
|
+
headers[:page] = state[:page]
|
239
|
+
previous_menu.page = state[:page]
|
240
|
+
previous_menu.render
|
241
|
+
else
|
242
|
+
state[:flow_state] = :process
|
243
|
+
state_delete_supplements!
|
244
|
+
|
245
|
+
current_node = node_by_id(state[:node])
|
246
|
+
apply_node_not_found_exception(:process) unless current_node
|
247
|
+
|
248
|
+
current_node.instance_variable_set(:@menu, previous_menu)
|
249
|
+
current_node.instance_variable_set(:@prompt, previous_prompt)
|
250
|
+
current_node.process!(input)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def store_state_from_node(node)
|
255
|
+
state[:menu] = { :pages => node.menu.pages } if node.menu?
|
256
|
+
state[:prompt] = node.instance_variable_get(:@prompt).pages if node.prompt?
|
257
|
+
state[:page] = 1
|
258
|
+
end
|
259
|
+
|
260
|
+
def generate_previous_menu
|
261
|
+
return false unless state[:previous][:menu]
|
262
|
+
|
263
|
+
menu = node_by_id(state[:node]).menu
|
264
|
+
menu.page = state[:page]
|
265
|
+
menu.pages = state[:previous][:menu][:pages]
|
266
|
+
|
267
|
+
menu.freeze
|
268
|
+
menu
|
269
|
+
end
|
270
|
+
|
271
|
+
def generate_previous_prompt
|
272
|
+
return false unless state[:previous][:prompt]
|
273
|
+
|
274
|
+
prompt = Colloquy::Prompt.new(flow: self, page: state[:previous][:page])
|
275
|
+
prompt.page = state[:page]
|
276
|
+
prompt.pages = state[:previous][:prompt]
|
277
|
+
prompt.freeze
|
278
|
+
|
279
|
+
prompt
|
280
|
+
end
|
281
|
+
|
282
|
+
def apply_node_not_found_exception(from = :request)
|
283
|
+
if state[:node] == :index and from == :request
|
284
|
+
raise Colloquy::IndexNodeNotFoundException
|
285
|
+
else
|
286
|
+
raise Colloquy::NodeNotFoundException, "The node #{state[:node]} was not found in the #{from} state."
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def node_by_id(identifier)
|
291
|
+
@nodes.find { |node| node.identifier == identifier.to_sym }
|
292
|
+
end
|
293
|
+
|
294
|
+
def sanitize_input(input)
|
295
|
+
Colloquy::Input.new(input)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
module ClassMethods
|
300
|
+
attr_accessor :node_definitions
|
301
|
+
|
302
|
+
def method_missing(method, *arguments, &block)
|
303
|
+
@node_definitions ||= []
|
304
|
+
|
305
|
+
identifier = method
|
306
|
+
options = arguments.first || {}
|
307
|
+
payload = block
|
308
|
+
|
309
|
+
@node_definitions << { :identifier => method, :options => options, :payload => payload }
|
310
|
+
end
|
311
|
+
|
312
|
+
private
|
313
|
+
def to_ary; end
|
314
|
+
end
|
315
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Colloquy::Renderer::FlowPool
|
2
|
+
|
3
|
+
def self.create_flows(flow_name, flow_class, pool_size, options)
|
4
|
+
pool_size.times do |i|
|
5
|
+
flow_pool[flow_name.to_sym] << flow_class.constantize.new(flow_name, options)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.flow_pool
|
10
|
+
@flow_pool ||= Hash.new { |hash, key| hash[key] = [] }
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.pop_flow(flow_name)
|
14
|
+
@flow_pool[flow_name.to_sym].pop
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.add_flow(flow_name, flow)
|
18
|
+
@flow_pool[flow_name.to_sym] << flow
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|