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,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