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,4 @@
1
+
2
+ class Colloquy::Response < String
3
+ attr_accessor :flow_state
4
+ end
@@ -0,0 +1,93 @@
1
+ require 'goliath/api'
2
+ require 'goliath/runner'
3
+
4
+ require 'goliath/rack/default_response_format'
5
+ require 'goliath/rack/heartbeat'
6
+ require 'goliath/rack/params'
7
+ require 'goliath/rack/render'
8
+ require 'goliath/rack/default_mime_type'
9
+ require 'goliath/rack/tracer'
10
+ require 'goliath/rack/formatters/json'
11
+ require 'goliath/rack/formatters/html'
12
+ require 'goliath/rack/formatters/xml'
13
+ require 'goliath/rack/jsonp'
14
+
15
+ require 'goliath/rack/validation/request_method'
16
+ require 'goliath/rack/validation/required_param'
17
+ require 'goliath/rack/validation/required_value'
18
+ require 'goliath/rack/validation/numeric_range'
19
+ require 'goliath/rack/validation/default_params'
20
+ require 'goliath/rack/validation/boolean_value'
21
+
22
+ class Colloquy::Runner
23
+ class << self
24
+ # Much of this is borrowed verbatim from Goliath internals so we
25
+ # can use our own class structure
26
+ def run!(argv)
27
+ options = {}
28
+ goliath_argv = []
29
+
30
+ option_parser = OptionParser.new do |opt|
31
+ opt.banner = 'Usage: colloquy [options] /path/to/flow/root'
32
+
33
+ opt.on('-a', '--address HOST', 'Hostname or IP address to bind to') do |host|
34
+ goliath_argv << '-a' << host
35
+ end
36
+
37
+ opt.on('-p', '---port PORT', 'Port to run the server on') do |port|
38
+ goliath_argv << '-p' << port
39
+ end
40
+
41
+ opt.on('-e', '---environment ENV', 'Rack environment to run the renderer') do |env|
42
+ goliath_argv << '-e' << env
43
+ options[:environment] = env
44
+ end
45
+
46
+ opt.on('-P', '--pidfile PATH_TO_FILE', 'Location to write the PID file to') do |pid_file|
47
+ goliath_argv << '-P' << pid_file
48
+ end
49
+
50
+ opt.on('-d', '--daemonize', 'Daemonize the server') do
51
+ goliath_argv << '-d'
52
+ end
53
+
54
+ opt.on('-v', '--verbose', 'Turn on debug logging') do
55
+ goliath_argv << '-v'
56
+ options[:verbose] = true
57
+ end
58
+
59
+ opt.on('-s', '--simulator', 'Run the flow simulator instead') do
60
+ options[:interactive] = true
61
+ end
62
+
63
+ opt.on( '-h', '--help', 'Display this screen' ) do
64
+ puts opt
65
+ exit!
66
+ end
67
+ end
68
+
69
+ option_parser.parse!(argv)
70
+
71
+ path_root = argv.pop
72
+ unless path_root
73
+ puts 'You have to provide a flow root directory. See colloquy --help'
74
+ exit!
75
+ end
76
+
77
+ goliath_argv << '-l' << Pathname.new(path_root).realpath.join('log', 'server.log').to_s
78
+
79
+ if options[:interactive]
80
+ simulator = Colloquy::Simulator.new(path_root: path_root, verbose: options[:verbose])
81
+ simulator.run
82
+ else
83
+ klass = Colloquy::Server
84
+ api = klass.new(path_root: path_root, verbose: options[:verbose])
85
+ runner = Goliath::Runner.new(goliath_argv, api)
86
+
87
+ runner.app = Goliath::Rack::Builder.build(klass, api)
88
+ runner.load_plugins(klass.plugins)
89
+ runner.run
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,157 @@
1
+ require 'goliath/api'
2
+ require 'goliath/runner'
3
+ require 'goliath/rack/params'
4
+
5
+ require 'em-synchrony'
6
+ require 'em-synchrony/em-http'
7
+ require 'yajl'
8
+
9
+ class Colloquy::Server < Goliath::API
10
+ use Goliath::Rack::Params # parse and merge query and body parameters
11
+
12
+ # Create an instance of Colloquy::Renderer with given options
13
+ # and set's it up
14
+ # @param options [Hash] The options hash.
15
+ def initialize(options = {})
16
+ @renderer = Colloquy::Renderer.new(options)
17
+ @renderer.prepare!
18
+ end
19
+
20
+ # This methods overrides #response of Goliath::API.
21
+ # So this is where parameters are validated and fed to an instance of Renderer.
22
+ # This methods returns response array with a http 200 status and body on successful execution.
23
+ #
24
+ # It validates the incoming request by checking presence of flow, msisdn and session_id. It sanitizes
25
+ # the parameters and obtains a hash containing flow, msisdn, session_id and input. Any exception raised
26
+ # at this point is rescued and a default error message is stored in response.
27
+ #
28
+ # After validation params are passed on the #apply method of instance of Renderer which was created and
29
+ # configured in Server constructor.
30
+ #
31
+ # @param [Goliath::Env] env The request environment.
32
+ # @return [Array] Array contains [Status code, Headers Hash, Body]
33
+ def response(env)
34
+ response = Colloquy::Response.new
35
+
36
+ begin
37
+ parameters = {}
38
+ parameters = validate_request(env)
39
+ parameters = sanitize_parameters(parameters)
40
+ logger.debug "REQUEST flow: #{parameters[:flow_name]}, msisdn: #{parameters[:msisdn]}, \
41
+ session_id: #{parameters[:session_id]}, input: #{parameters[:input]}, other: #{parameters[:params].inspect}"
42
+ rescue Exception => e
43
+ logger.error "Exception #{e.inspect} when trying to validate request flow: #{parameters[:flow_name]}, \
44
+ msisdn: #{parameters[:msisdn]}, session_id: #{parameters[:session_id]}, input: #{parameters[:input]}"
45
+ logger.debug "#{e.backtrace.inspect}"
46
+ logger.info 'Responding with default error message'
47
+
48
+ response = Colloquy::Response.new(Colloquy::Renderer::DEFAULT_ERROR_MESSAGE)
49
+ response.flow_state = :notify
50
+ end
51
+
52
+ response = @renderer.apply(parameters[:flow_name], parameters[:msisdn], parameters[:session_id], parameters[:input], parameters[:params]) if response.empty?
53
+
54
+ body = case parameters[:params][:accept]
55
+ when 'text/plain'
56
+ response.to_s
57
+ else
58
+ Yajl.dump({ response: response, flow_state: response.flow_state })
59
+ end
60
+
61
+ [200, {}, body]
62
+ end
63
+
64
+ private
65
+ def logger
66
+ @renderer.logger
67
+ end
68
+
69
+ # Validate request and return parameters
70
+ # @param [Goliath::Env] env The request environment
71
+ # @return [Hash] parameters The extracted parameters
72
+ def validate_request(env)
73
+ parameters = extract_request_parameters
74
+
75
+ validate_flow_presence!(parameters)
76
+ validate_msisdn!(parameters)
77
+ validate_session_id!(parameters)
78
+
79
+ parameters
80
+ end
81
+
82
+ # Create a hash with all request parameters combined
83
+ # @return [Hash] The request parameters
84
+ def extract_request_parameters
85
+ flow_name = env['REQUEST_PATH'][1..-1].to_s
86
+
87
+ # Use dup to preserve original env.
88
+ params = env['params'].dup.to_options
89
+ msisdn = params.delete(:msisdn).to_s
90
+ session_id = params.delete(:session_id).to_s
91
+ input = params.delete(:input).to_s
92
+
93
+ {
94
+ flow_name: flow_name,
95
+ params: params,
96
+ msisdn: msisdn,
97
+ session_id: session_id,
98
+ input: input
99
+ }
100
+ end
101
+
102
+ # Check if required flow is present. Raise FlowNotFound exception if not
103
+ # @param [Hash] parameters Extracted request parameters
104
+ # @return [nil]
105
+ # @raise [Colloquy::FlowNotFound]
106
+ # if flow is not present
107
+ def validate_flow_presence!(parameters)
108
+ unless @renderer.flow_exists?(parameters[:flow_name])
109
+ raise Colloquy::FlowNotFound, "Flow not found: #{parameters[:flow_name]}"
110
+ end
111
+ end
112
+
113
+ # Checks whether msisdn is present, raises MSISDNParameterEmpty if not
114
+ # @param [Hash] parameters The paramters hash
115
+ # @return [nil]
116
+ # @raise [Colloquy::MSISDNParameterEmpty]
117
+ # if msisdn parameter not present
118
+ def validate_msisdn!(parameters)
119
+ if parameters[:msisdn] == ''
120
+ raise Colloquy::MSISDNParameterEmpty, 'The msisdn parameter should not be empty.'
121
+ end
122
+ end
123
+
124
+ # Checks whether session_id is present
125
+ # @param [Hash] parameters The parameters hash
126
+ # @return [nil]
127
+ # @raise [Colloquy::SessionIDParameterEmpty]
128
+ # if session_id not present
129
+ def validate_session_id!(parameters)
130
+ if parameters[:session_id] == ''
131
+ raise Colloquy::SessionIDParameterEmpty, 'The session_id parameter should not be empty.'
132
+ end
133
+ end
134
+
135
+ # Clean parameter values(exact type, size etc)
136
+ # @param [Hash] parameters The Parameters hash
137
+ # @return [Hash] Parameters Sanitized parameters hash
138
+ def sanitize_parameters(parameters)
139
+ flow_name = parameters[:flow_name].to_sym
140
+ msisdn = parameters[:msisdn].to_i.to_s
141
+ session_id = parameters[:session_id].to_s[0..20]
142
+ input = parameters[:input].to_s[0..160]
143
+
144
+ #remove default proc so that the hash can be serialized using Marshal
145
+ params = parameters[:params].tap do |p|
146
+ p.default = nil
147
+ end
148
+
149
+ {
150
+ flow_name: flow_name,
151
+ params: params,
152
+ msisdn: msisdn,
153
+ session_id: session_id,
154
+ input: input
155
+ }
156
+ end
157
+ end
@@ -0,0 +1,46 @@
1
+ class Colloquy::SessionStore
2
+ KEY_PREFIX = 'ussd_renderer:'
3
+
4
+ class << self
5
+ # Returns a Memory store object according to the type of memory.
6
+ # @param [Symbol, Hash], Type and Options
7
+ # @return [Colloquy::SessionStore::Memory Colloquy::SessionStore::Redis]
8
+ def haystack(type = :memory, options = {})
9
+ case type.to_sym
10
+ when :memory
11
+ require_relative 'session_store/memory'
12
+
13
+ Colloquy::SessionStore::Memory.new(options)
14
+ else :redis
15
+ require_relative 'session_store/redis'
16
+
17
+ Colloquy::SessionStore::Redis.new(options)
18
+ end
19
+ end
20
+ end
21
+
22
+ def initialize(options = {})
23
+ @identifier = options[:identifier] || :sessions
24
+ end
25
+
26
+ private
27
+ def normalized_key_name(key)
28
+ KEY_PREFIX + @identifier.to_s + ":" + key.to_s
29
+ end
30
+
31
+ def encode_value(value)
32
+ Marshal.dump(value)
33
+ end
34
+
35
+ def decode_value(string)
36
+ if string
37
+ Marshal.load(string)
38
+ else
39
+ {}
40
+ end
41
+ rescue TypeError
42
+ {}
43
+ rescue ArgumentError
44
+ {}
45
+ end
46
+ end
@@ -0,0 +1,14 @@
1
+ class Colloquy::SessionStore::Memory < Colloquy::SessionStore
2
+
3
+ def []=(key, value)
4
+ @store ||= {}
5
+ @store[normalized_key_name(key)] = encode_value(value)
6
+ end
7
+
8
+ def [](key)
9
+ @store ||= {}
10
+ string = @store[normalized_key_name(key)]
11
+
12
+ decode_value(string)
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ class Colloquy::SessionStore::Redis < Colloquy::SessionStore
2
+ KEY_EXPIRY = 300 # 5 minutes
3
+
4
+ def []=(key, value)
5
+ @store ||= redis_connection
6
+ @store.set(normalized_key_name(key), encode_value(value), KEY_EXPIRY)
7
+ end
8
+
9
+ def [](key)
10
+ @store ||= redis_connection
11
+ string = @store.get(normalized_key_name(key))
12
+
13
+ decode_value(string)
14
+ end
15
+
16
+ private
17
+ def redis_connection
18
+ @redis = Colloquy::Helpers::Redis::RedisProxy.instance
19
+ @redis.configure
20
+
21
+ @redis[@identifier]
22
+ end
23
+
24
+ end
@@ -0,0 +1,114 @@
1
+ require 'em-synchrony'
2
+
3
+ class Colloquy::Simulator
4
+ def initialize(options = {})
5
+ @renderer = Colloquy::Renderer.new(options)
6
+ @renderer.prepare!
7
+ end
8
+
9
+ def construct_response
10
+ @renderer.apply(@flow_name, @msisdn, @session_id, @input)
11
+ end
12
+
13
+ def run_simulator(input)
14
+ response = construct_response
15
+ puts response
16
+
17
+ if response.flow_state == :notify
18
+ puts "\n---Flow complete---"
19
+ reset
20
+ else
21
+ read_input
22
+ end
23
+
24
+ run_simulator(@input)
25
+ end
26
+
27
+ def reset
28
+ puts "\n--Going back to beginning of flow--\n"
29
+
30
+ # Get a new session_id when the simulator is reset
31
+ @session_id = @session_id.to_i + 1
32
+
33
+ puts 'Initial input (for direct flow):'
34
+ read_input
35
+
36
+ run!
37
+ end
38
+
39
+ def ask_for_flow_parameters
40
+ puts 'Please enter flow name:'
41
+ @flow_name = EM::Synchrony.gets.strip
42
+ puts 'Please enter msisdn: '
43
+ @msisdn = EM::Synchrony.gets.strip
44
+ puts 'Please enter session_id: '
45
+ @session_id = EM::Synchrony.gets.strip
46
+ puts 'Initial input (for direct flow): '
47
+ read_input
48
+ end
49
+
50
+ # Run simulator inside EM.synchrony loop
51
+ def run
52
+ EM.synchrony do
53
+ ask_for_flow_parameters
54
+ run!
55
+ end
56
+ end
57
+
58
+ def run!
59
+ validate_request
60
+ sanitize_parameters!
61
+
62
+ run_simulator(@input)
63
+ end
64
+
65
+ private
66
+ def read_input
67
+ print '> '
68
+ @input = EM::Synchrony.gets.strip
69
+ sanitize_parameters!
70
+
71
+ case @input
72
+ when 'reset'
73
+ reset
74
+ when 'quit'
75
+ puts 'Bye!'
76
+ exit!
77
+ end
78
+ end
79
+
80
+ def logger
81
+ @renderer.logger
82
+ end
83
+
84
+ def validate_request
85
+ validate_flow_presence!
86
+ validate_msisdn!
87
+ validate_session_id!
88
+ end
89
+
90
+ def validate_flow_presence!
91
+ unless @renderer.flow_exists?(@flow_name)
92
+ raise Colloquy::FlowNotFound, "Flow not found: #{@flow_name}"
93
+ end
94
+ end
95
+
96
+ def validate_msisdn!
97
+ if @msisdn == ''
98
+ raise Colloquy::MSISDNParameterEmpty, 'The msisdn parameter should not be empty.'
99
+ end
100
+ end
101
+
102
+ def validate_session_id!
103
+ if @session_id == ''
104
+ raise Colloquy::SessionIDParameterEmpty, 'The session_id parameter should not be empty.'
105
+ end
106
+ end
107
+
108
+ def sanitize_parameters!
109
+ @flow_name = @flow_name.to_sym
110
+ @msisdn = @msisdn.to_i.to_s
111
+ @session_id = @session_id.to_s[0..20].strip
112
+ @input = @input.to_s[0..160].strip
113
+ end
114
+ end