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.
Files changed (65) hide show
  1. checksums.yaml +15 -0
  2. data/README.md +160 -0
  3. data/TODO.md +256 -0
  4. data/bin/colloquy +14 -0
  5. data/examples/config/flows.yaml +22 -0
  6. data/examples/config/logger.yaml +2 -0
  7. data/examples/config/messages.yaml +3 -0
  8. data/examples/config/mysql.yaml +18 -0
  9. data/examples/config/redis.yaml +9 -0
  10. data/examples/config/scribe.yaml +2 -0
  11. data/examples/config/settings.yaml +2 -0
  12. data/examples/config/test.yaml +8 -0
  13. data/examples/config/urls.yaml +18 -0
  14. data/examples/flows/active_record_flow.rb +42 -0
  15. data/examples/flows/art_of_war_flow.rb +27 -0
  16. data/examples/flows/calculator_flow.rb +71 -0
  17. data/examples/flows/crossover_flow.rb +17 -0
  18. data/examples/flows/database_flow.rb +33 -0
  19. data/examples/flows/hangman_flow.rb +82 -0
  20. data/examples/flows/metadata_flow.rb +11 -0
  21. data/examples/flows/pagination_flow.rb +23 -0
  22. data/examples/flows/pass_flow.rb +29 -0
  23. data/examples/flows/prefix_menu_flow.rb +24 -0
  24. data/examples/flows/scribe_flow.rb +26 -0
  25. data/examples/flows/settings_flow.rb +23 -0
  26. data/examples/flows/special/special_redis_flow.rb +28 -0
  27. data/examples/flows/url_flow.rb +27 -0
  28. data/examples/log/renderer.log +198381 -0
  29. data/examples/log/urls.log +3269 -0
  30. data/examples/messages/active_record.yaml +2 -0
  31. data/examples/messages/art_of_war.yaml +1 -0
  32. data/examples/messages/calculator.yaml +2 -0
  33. data/examples/messages/database.yaml +1 -0
  34. data/examples/messages/hangman.yaml +0 -0
  35. data/examples/messages/prefix_menu.yaml +3 -0
  36. data/examples/models/activations.rb +5 -0
  37. data/lib/colloquy.rb +39 -0
  38. data/lib/colloquy/exceptions.rb +64 -0
  39. data/lib/colloquy/flow_parser.rb +315 -0
  40. data/lib/colloquy/flow_pool.rb +21 -0
  41. data/lib/colloquy/helpers.rb +15 -0
  42. data/lib/colloquy/helpers/mysql.rb +110 -0
  43. data/lib/colloquy/helpers/redis.rb +103 -0
  44. data/lib/colloquy/helpers/scribe.rb +103 -0
  45. data/lib/colloquy/helpers/settings.rb +111 -0
  46. data/lib/colloquy/helpers/url.rb +10 -0
  47. data/lib/colloquy/input.rb +14 -0
  48. data/lib/colloquy/logger.rb +11 -0
  49. data/lib/colloquy/menu.rb +128 -0
  50. data/lib/colloquy/message_builder.rb +54 -0
  51. data/lib/colloquy/node.rb +67 -0
  52. data/lib/colloquy/paginator.rb +59 -0
  53. data/lib/colloquy/paginator/menu.rb +79 -0
  54. data/lib/colloquy/paginator/prompt.rb +32 -0
  55. data/lib/colloquy/prompt.rb +35 -0
  56. data/lib/colloquy/renderer.rb +423 -0
  57. data/lib/colloquy/response.rb +4 -0
  58. data/lib/colloquy/runner.rb +93 -0
  59. data/lib/colloquy/server.rb +157 -0
  60. data/lib/colloquy/session_store.rb +46 -0
  61. data/lib/colloquy/session_store/memory.rb +14 -0
  62. data/lib/colloquy/session_store/redis.rb +24 -0
  63. data/lib/colloquy/simulator.rb +114 -0
  64. data/lib/colloquy/spec_helpers.rb +21 -0
  65. metadata +459 -0
@@ -0,0 +1,2 @@
1
+ activation:
2
+ success: "Thank you! %{count} mobile numbers in total!"
@@ -0,0 +1 @@
1
+ calculator: "Switch to Calculator flow"
@@ -0,0 +1,2 @@
1
+ add: Add
2
+ subtract: Subtract
@@ -0,0 +1 @@
1
+ more: "Next Page"
File without changes
@@ -0,0 +1,3 @@
1
+ a: "Abracadabra"
2
+ b: "Boyish"
3
+ c: "Cow Female"
@@ -0,0 +1,5 @@
1
+
2
+ class Activation < ActiveRecord::Base
3
+
4
+ end
5
+
@@ -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
+