colloquy 1.0.0

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