mtrudel-adhearsion 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. data/CHANGELOG +26 -0
  2. data/EVENTS +11 -0
  3. data/LICENSE +456 -0
  4. data/Rakefile +127 -0
  5. data/adhearsion.gemspec +149 -0
  6. data/app_generators/ahn/USAGE +5 -0
  7. data/app_generators/ahn/ahn_generator.rb +91 -0
  8. data/app_generators/ahn/templates/.ahnrc +34 -0
  9. data/app_generators/ahn/templates/README +8 -0
  10. data/app_generators/ahn/templates/Rakefile +25 -0
  11. data/app_generators/ahn/templates/components/ami_remote/ami_remote.rb +15 -0
  12. data/app_generators/ahn/templates/components/disabled/HOW_TO_ENABLE +7 -0
  13. data/app_generators/ahn/templates/components/disabled/restful_rpc/README.markdown +11 -0
  14. data/app_generators/ahn/templates/components/disabled/restful_rpc/example-client.rb +48 -0
  15. data/app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.rb +87 -0
  16. data/app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.yml +34 -0
  17. data/app_generators/ahn/templates/components/disabled/restful_rpc/spec/restful_rpc_spec.rb +263 -0
  18. data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.rb +104 -0
  19. data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.yml +2 -0
  20. data/app_generators/ahn/templates/components/disabled/stomp_gateway/README.markdown +47 -0
  21. data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.rb +34 -0
  22. data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.yml +12 -0
  23. data/app_generators/ahn/templates/components/simon_game/simon_game.rb +56 -0
  24. data/app_generators/ahn/templates/config/startup.rb +50 -0
  25. data/app_generators/ahn/templates/dialplan.rb +3 -0
  26. data/app_generators/ahn/templates/events.rb +32 -0
  27. data/bin/ahn +28 -0
  28. data/bin/ahnctl +68 -0
  29. data/bin/jahn +42 -0
  30. data/examples/asterisk_manager_interface/standalone.rb +51 -0
  31. data/lib/adhearsion.rb +37 -0
  32. data/lib/adhearsion/cli.rb +223 -0
  33. data/lib/adhearsion/component_manager.rb +207 -0
  34. data/lib/adhearsion/component_manager/component_tester.rb +55 -0
  35. data/lib/adhearsion/component_manager/spec_framework.rb +24 -0
  36. data/lib/adhearsion/events_support.rb +84 -0
  37. data/lib/adhearsion/foundation/all.rb +9 -0
  38. data/lib/adhearsion/foundation/blank_slate.rb +5 -0
  39. data/lib/adhearsion/foundation/custom_daemonizer.rb +45 -0
  40. data/lib/adhearsion/foundation/event_socket.rb +203 -0
  41. data/lib/adhearsion/foundation/future_resource.rb +36 -0
  42. data/lib/adhearsion/foundation/global.rb +1 -0
  43. data/lib/adhearsion/foundation/metaprogramming.rb +17 -0
  44. data/lib/adhearsion/foundation/numeric.rb +13 -0
  45. data/lib/adhearsion/foundation/pseudo_guid.rb +10 -0
  46. data/lib/adhearsion/foundation/relationship_properties.rb +42 -0
  47. data/lib/adhearsion/foundation/string.rb +26 -0
  48. data/lib/adhearsion/foundation/synchronized_hash.rb +96 -0
  49. data/lib/adhearsion/foundation/thread_safety.rb +7 -0
  50. data/lib/adhearsion/host_definitions.rb +67 -0
  51. data/lib/adhearsion/initializer.rb +373 -0
  52. data/lib/adhearsion/initializer/asterisk.rb +81 -0
  53. data/lib/adhearsion/initializer/configuration.rb +254 -0
  54. data/lib/adhearsion/initializer/database.rb +50 -0
  55. data/lib/adhearsion/initializer/drb.rb +31 -0
  56. data/lib/adhearsion/initializer/freeswitch.rb +22 -0
  57. data/lib/adhearsion/initializer/rails.rb +41 -0
  58. data/lib/adhearsion/logging.rb +92 -0
  59. data/lib/adhearsion/tasks.rb +16 -0
  60. data/lib/adhearsion/tasks/database.rb +5 -0
  61. data/lib/adhearsion/tasks/deprecations.rb +59 -0
  62. data/lib/adhearsion/tasks/generating.rb +20 -0
  63. data/lib/adhearsion/tasks/lint.rb +4 -0
  64. data/lib/adhearsion/tasks/testing.rb +37 -0
  65. data/lib/adhearsion/version.rb +9 -0
  66. data/lib/adhearsion/voip/asterisk.rb +4 -0
  67. data/lib/adhearsion/voip/asterisk/agi_server.rb +84 -0
  68. data/lib/adhearsion/voip/asterisk/commands.rb +1314 -0
  69. data/lib/adhearsion/voip/asterisk/config_generators/agents.conf.rb +140 -0
  70. data/lib/adhearsion/voip/asterisk/config_generators/config_generator.rb +101 -0
  71. data/lib/adhearsion/voip/asterisk/config_generators/queues.conf.rb +250 -0
  72. data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +240 -0
  73. data/lib/adhearsion/voip/asterisk/config_manager.rb +71 -0
  74. data/lib/adhearsion/voip/asterisk/manager_interface.rb +597 -0
  75. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +1589 -0
  76. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +286 -0
  77. data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +78 -0
  78. data/lib/adhearsion/voip/asterisk/manager_interface/ami_protocol_lexer_machine.rl +87 -0
  79. data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +80 -0
  80. data/lib/adhearsion/voip/asterisk/super_manager.rb +19 -0
  81. data/lib/adhearsion/voip/call.rb +453 -0
  82. data/lib/adhearsion/voip/call_routing.rb +64 -0
  83. data/lib/adhearsion/voip/commands.rb +9 -0
  84. data/lib/adhearsion/voip/constants.rb +39 -0
  85. data/lib/adhearsion/voip/conveniences.rb +18 -0
  86. data/lib/adhearsion/voip/dial_plan.rb +218 -0
  87. data/lib/adhearsion/voip/dsl/dialing_dsl.rb +151 -0
  88. data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +37 -0
  89. data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +27 -0
  90. data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +124 -0
  91. data/lib/adhearsion/voip/dsl/dialplan/parser.rb +71 -0
  92. data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +16 -0
  93. data/lib/adhearsion/voip/dsl/numerical_string.rb +117 -0
  94. data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +48 -0
  95. data/lib/adhearsion/voip/freeswitch/event_handler.rb +58 -0
  96. data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +129 -0
  97. data/lib/adhearsion/voip/freeswitch/inbound_connection_manager.rb +38 -0
  98. data/lib/adhearsion/voip/freeswitch/oes_server.rb +195 -0
  99. data/lib/adhearsion/voip/menu_state_machine/calculated_match.rb +80 -0
  100. data/lib/adhearsion/voip/menu_state_machine/matchers.rb +123 -0
  101. data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +58 -0
  102. data/lib/adhearsion/voip/menu_state_machine/menu_class.rb +149 -0
  103. data/lib/theatre.rb +151 -0
  104. data/lib/theatre/README.markdown +64 -0
  105. data/lib/theatre/callback_definition_loader.rb +84 -0
  106. data/lib/theatre/guid.rb +23 -0
  107. data/lib/theatre/invocation.rb +121 -0
  108. data/lib/theatre/namespace_manager.rb +153 -0
  109. data/lib/theatre/version.rb +2 -0
  110. metadata +182 -0
@@ -0,0 +1,80 @@
1
+ module Adhearsion
2
+ class DialPlan
3
+ class ConfirmationManager
4
+
5
+ class << self
6
+
7
+ def encode_hash_for_dial_macro_argument(options)
8
+ options = options.clone
9
+ macro_name = options.delete :macro
10
+ options[:play] &&= options[:play].kind_of?(Array) ? options[:play].join('++') : options[:play]
11
+ encoded_options = URI.escape options.map { |key,value| "#{key}:#{value}" }.join('!')
12
+ returning "M(#{macro_name}^#{encoded_options})" do |str|
13
+ if str.rindex('^') != str.index('^')
14
+ raise ArgumentError, "You seem to have supplied a :confirm option with a caret (^) in it!" +
15
+ " Please remove it. This will blow Asterisk up."
16
+ end
17
+ end
18
+ end
19
+
20
+ def handle(call)
21
+ new(call).handle
22
+ end
23
+
24
+ def confirmation_call?(call)
25
+ call.variables.has_key?(:network_script) && call.variables[:network_script].starts_with?('confirm!')
26
+ end
27
+
28
+ def decode_hash(encoded_hash)
29
+ encoded_hash = encoded_hash =~ /^M\((.+)\)$/ ? $1 : encoded_hash
30
+ encoded_hash = encoded_hash =~ /^([^:]+\^)?(.+)$/ ? $2 : encoded_hash # Remove the macro name if it's there
31
+ unencoded = URI.unescape(encoded_hash).split('!')
32
+ unencoded.shift unless unencoded.first.include?(':')
33
+ unencoded = unencoded.map { |pair| key, value = pair.split(':'); [key.to_sym ,value] }.flatten
34
+ returning Hash[*unencoded] do |hash|
35
+ hash[:timeout] &&= hash[:timeout].to_i
36
+ hash[:play] &&= hash[:play].split('++')
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ attr_reader :call
43
+ def initialize(call)
44
+ @call = call
45
+ extend Adhearsion::VoIP::Commands.for(call.originating_voip_platform)
46
+ end
47
+
48
+ def handle
49
+ variables = self.class.decode_hash call.variables[:network_script]
50
+
51
+ answer
52
+ loop do
53
+ response = interruptable_play(*variables[:play])
54
+ if response && response.to_s == variables[:key].to_s
55
+ # Don't set a variable to pass through to dial()
56
+ break
57
+ elsif response && response.to_s != variables[:key].to_s
58
+ next
59
+ else
60
+ response = wait_for_digit variables[:timeout]
61
+ if response
62
+ if response.to_s == variables[:key].to_s
63
+ # Don't set a variable to pass through to dial()
64
+ break
65
+ else
66
+ next
67
+ end
68
+ else
69
+ # By setting MACRO_RESULT to CONTINUE, we cancel the dial.
70
+ variable 'MACRO_RESULT' => "CONTINUE"
71
+ break
72
+ end
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,19 @@
1
+ module Adhearsion
2
+ module VoIP
3
+ module Asterisk
4
+ module Manager
5
+
6
+ ##
7
+ # Higher level abstraction of the Asterisk Manager Interface.
8
+ #
9
+ class SuperManager
10
+
11
+ def initialize
12
+ raise NotImplementedError
13
+ end
14
+
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,453 @@
1
+ require 'uri'
2
+ #TODO Some of this is asterisk-specific
3
+ module Adhearsion
4
+ class << self
5
+ def active_calls
6
+ @calls ||= Calls.new
7
+ end
8
+
9
+ def receive_call_from(io, &block)
10
+ active_calls << (call = Call.receive_from(io, &block))
11
+ call
12
+ end
13
+
14
+ def remove_inactive_call(call)
15
+ active_calls.remove_inactive_call(call)
16
+ end
17
+ end
18
+
19
+ class Hangup < Exception
20
+ # At the moment, we'll just use this to end a call-handling Thread.
21
+ end
22
+
23
+ ##
24
+ # This manages the list of calls the Adhearsion service receives
25
+ class Calls
26
+ def initialize
27
+ @semaphore = Monitor.new
28
+ @calls = {}
29
+ end
30
+
31
+ def <<(call)
32
+ atomically do
33
+ calls[call.unique_identifier] = call
34
+ end
35
+ end
36
+
37
+ def any?
38
+ atomically do
39
+ !calls.empty?
40
+ end
41
+ end
42
+
43
+ def size
44
+ atomically do
45
+ calls.size
46
+ end
47
+ end
48
+
49
+ def remove_inactive_call(call)
50
+ atomically do
51
+ calls.delete call.unique_identifier
52
+ end
53
+ end
54
+
55
+ # Searches all active calls by their unique_identifier. See Call#unique_identifier.
56
+ def find(id)
57
+ atomically do
58
+ return calls[id]
59
+ end
60
+ end
61
+
62
+ def clear!
63
+ atomically do
64
+ calls.clear
65
+ end
66
+ end
67
+
68
+ def with_tag(tag)
69
+ atomically do
70
+ calls.inject(Array.new) do |calls_with_tag,(key,call)|
71
+ call.tagged_with?(tag) ? calls_with_tag << call : calls_with_tag
72
+ end
73
+ end
74
+ end
75
+
76
+ def to_a
77
+ calls.values
78
+ end
79
+
80
+ private
81
+ attr_reader :semaphore, :calls
82
+
83
+ def atomically(&block)
84
+ semaphore.synchronize(&block)
85
+ end
86
+
87
+ end
88
+
89
+ class UselessCallException < Exception; end
90
+
91
+ class MetaAgiCallException < Exception
92
+ attr_reader :call
93
+ def initialize(call)
94
+ super()
95
+ @call = call
96
+ end
97
+ end
98
+
99
+ class FailedExtensionCallException < MetaAgiCallException; end
100
+
101
+ class HungupExtensionCallException < MetaAgiCallException; end
102
+
103
+ ##
104
+ # Encapsulates call-related data and behavior.
105
+ # For example, variables passed in on call initiation are
106
+ # accessible here as attributes
107
+ class Call
108
+
109
+ # This is basically a translation of ast_channel_reason2str() from main/channel.c and
110
+ # ast_control_frame_type in include/asterisk/frame.h in the Asterisk source code. When
111
+ # Asterisk jumps to the 'failed' extension, it sets a REASON channel variable to a number.
112
+ # The indexes of these symbols represent the possible numbers REASON could be.
113
+ ASTERISK_FRAME_STATES = [
114
+ :failure, # "Call Failure (not BUSY, and not NO_ANSWER, maybe Circuit busy or down?)"
115
+ :hangup, # Other end has hungup
116
+ :ring, # Local ring
117
+ :ringing, # Remote end is ringing
118
+ :answer, # Remote end has answered
119
+ :busy, # Remote end is busy
120
+ :takeoffhook, # Make it go off hook
121
+ :offhook, # Line is off hook
122
+ :congestion, # Congestion (circuits busy)
123
+ :flash, # Flash hook
124
+ :wink, # Wink
125
+ :option, # Set a low-level option
126
+ :radio_key, # Key Radio
127
+ :radio_unkey, # Un-Key Radio
128
+ :progress, # Indicate PROGRESS
129
+ :proceeding, # Indicate CALL PROCEEDING
130
+ :hold, # Indicate call is placed on hold
131
+ :unhold, # Indicate call is left from hold
132
+ :vidupdate # Indicate video frame update
133
+ ]
134
+
135
+
136
+ class << self
137
+ ##
138
+ # The primary public interface for creating a Call instance.
139
+ # Given an IO (probably a socket accepted from an Asterisk service),
140
+ # creates a Call instance which encapsulates everything we know about that call.
141
+ def receive_from(io, &block)
142
+ returning new(io, variable_parser_for(io).variables) do |call|
143
+ block.call(call) if block
144
+ end
145
+ end
146
+
147
+ private
148
+ def variable_parser_for(io)
149
+ Variables::Parser.parse(io)
150
+ end
151
+
152
+ end
153
+
154
+ attr_accessor :io, :type, :variables, :originating_voip_platform, :inbox
155
+ def initialize(io, variables)
156
+ @io, @variables = io, variables.symbolize_keys
157
+ check_if_valid_call
158
+ define_variable_accessors
159
+ set_originating_voip_platform!
160
+ @tag_mutex = Mutex.new
161
+ @tags = []
162
+ end
163
+
164
+ def tags
165
+ @tag_mutex.synchronize do
166
+ return @tags.clone
167
+ end
168
+ end
169
+
170
+ def tag(symbol)
171
+ raise ArgumentError, "tag must be a Symbol" unless symbol.is_a? Symbol
172
+ @tag_mutex.synchronize do
173
+ @tags << symbol
174
+ end
175
+ end
176
+
177
+ def remove_tag(symbol)
178
+ @tag_mutex.synchronize do
179
+ @tags.reject! { |tag| tag == symbol }
180
+ end
181
+ end
182
+
183
+ def tagged_with?(symbol)
184
+ @tag_mutex.synchronize do
185
+ @tags.include? symbol
186
+ end
187
+ end
188
+
189
+ def deliver_message(message)
190
+ inbox << message
191
+ end
192
+ alias << deliver_message
193
+
194
+ def inbox
195
+ @inbox ||= Queue.new
196
+ end
197
+
198
+ def hangup!
199
+ io.close
200
+ Adhearsion.remove_inactive_call self
201
+ end
202
+
203
+ def closed?
204
+ io.closed?
205
+ end
206
+
207
+ # Asterisk sometimes uses the "failed" extension to indicate a failed dial attempt.
208
+ # Since it may be important to handle these, this flag helps the dialplan Manager
209
+ # figure that out.
210
+ def failed_call?
211
+ @failed_call
212
+ end
213
+
214
+ def hungup_call?
215
+ @hungup_call
216
+ end
217
+
218
+ # Adhearsion indexes calls by this identifier so they may later be found and manipulated. For calls from Asterisk, this
219
+ # method uses the following properties for uniqueness, falling back to the next if one is for some reason unavailable:
220
+ #
221
+ # Asterisk channel ID -> unique ID -> Call#object_id
222
+ # (e.g. SIP/mytrunk-jb12c88a) -> (e.g. 1215039989.47033) -> (e.g. 2792080)
223
+ #
224
+ # Note: channel is used over unique ID because channel may be used to bridge two channels together.
225
+ def unique_identifier
226
+ case originating_voip_platform
227
+ when :asterisk
228
+ variables[:channel] || variables[:uniqueid] || object_id
229
+ else
230
+ raise NotImplementedError
231
+ end
232
+ end
233
+
234
+ def define_variable_accessors(recipient=self)
235
+ variables.each do |key, value|
236
+ define_singleton_accessor_with_pair(key, value, recipient)
237
+ end
238
+ end
239
+
240
+ def extract_failed_reason_from(environment)
241
+ if originating_voip_platform == :asterisk
242
+ failed_reason = environment.variable 'REASON'
243
+ failed_reason &&= ASTERISK_FRAME_STATES[failed_reason.to_i]
244
+ define_singleton_accessor_with_pair(:failed_reason, failed_reason, environment)
245
+ end
246
+ end
247
+
248
+ private
249
+
250
+ def define_singleton_accessor_with_pair(key, value, recipient=self)
251
+ recipient.metaclass.send :attr_accessor, key unless recipient.class.respond_to?("#{key}=")
252
+ recipient.send "#{key}=", value
253
+ end
254
+
255
+ def check_if_valid_call
256
+ extension = variables[:extension]
257
+ @failed_call = true if extension == 'failed'
258
+ @hungup_call = true if extension == 'h'
259
+ raise UselessCallException if extension == 't' # TODO: Move this whole method to Manager
260
+ end
261
+
262
+ def set_originating_voip_platform!
263
+ # TODO: we can make this determination programatically at some point,
264
+ # but it will probably involve a bit more engineering than just a case statement (like
265
+ # subclasses of Call for the various platforms), so we'll be totally cheap for now.
266
+ self.originating_voip_platform = :asterisk
267
+ end
268
+
269
+ module Variables
270
+
271
+ module Coercions
272
+
273
+ COERCION_ORDER = %w{
274
+ remove_agi_prefixes_from_keys_and_strip_whitespace
275
+ coerce_keys_into_symbols
276
+ coerce_extension_into_phone_number_object
277
+ coerce_numerical_values_to_numerics
278
+ replace_unknown_values_with_nil
279
+ replace_yes_no_answers_with_booleans
280
+ coerce_request_into_uri_object
281
+ decompose_uri_query_into_hash
282
+ override_variables_with_query_params
283
+ remove_dashes_from_context_name
284
+ coerce_type_of_number_into_symbol
285
+ }
286
+
287
+ class << self
288
+
289
+ def remove_agi_prefixes_from_keys_and_strip_whitespace(variables)
290
+ variables.inject({}) do |new_variables,(key,value)|
291
+ returning new_variables do
292
+ stripped_name = key.kind_of?(String) ? key[/^(agi_)?(.+)$/,2] : key
293
+ new_variables[stripped_name] = value.kind_of?(String) ? value.strip : value
294
+ end
295
+ end
296
+ end
297
+
298
+ def coerce_keys_into_symbols(variables)
299
+ variables.inject({}) do |new_variables,(key,value)|
300
+ returning new_variables do
301
+ new_variables[key.to_sym] = value
302
+ end
303
+ end
304
+ end
305
+
306
+ def coerce_extension_into_phone_number_object(variables)
307
+ returning variables do
308
+ variables[:extension] = Adhearsion::VoIP::DSL::PhoneNumber.new(variables[:extension])
309
+ end
310
+ end
311
+
312
+ def coerce_numerical_values_to_numerics(variables)
313
+ variables.inject({}) do |vars,(key,value)|
314
+ returning vars do
315
+ is_numeric = value =~ /^-?\d+(?:(\.)\d+)?$/
316
+ is_float = $1
317
+ vars[key] = if is_numeric
318
+ if Adhearsion::VoIP::DSL::NumericalString.starts_with_leading_zero?(value)
319
+ Adhearsion::VoIP::DSL::NumericalString.new(value)
320
+ else
321
+ if is_float
322
+ if key == :uniqueid
323
+ value
324
+ else
325
+ value.to_f
326
+ end
327
+ else
328
+ value.to_i
329
+ end
330
+ end
331
+ else
332
+ value
333
+ end
334
+ end
335
+ end
336
+ end
337
+
338
+ def replace_unknown_values_with_nil(variables)
339
+ variables.each do |key,value|
340
+ variables[key] = nil if value == 'unknown'
341
+ end
342
+ end
343
+
344
+ def replace_yes_no_answers_with_booleans(variables)
345
+ variables.each do |key,value|
346
+ case value
347
+ when 'yes' : variables[key] = true
348
+ when 'no' : variables[key] = false
349
+ end
350
+ end
351
+ end
352
+
353
+ def coerce_request_into_uri_object(variables)
354
+ if variables[:request]
355
+ variables[:request] = URI.parse(variables[:request]) unless variables[:request].kind_of? URI
356
+ end
357
+ variables
358
+ end
359
+
360
+ def coerce_type_of_number_into_symbol(variables)
361
+ returning variables do
362
+ variables[:type_of_calling_number] = Adhearsion::VoIP::Constants::Q931_TYPE_OF_NUMBER[variables.delete(:callington).to_i]
363
+ end
364
+ end
365
+
366
+ def decompose_uri_query_into_hash(variables)
367
+ returning variables do
368
+ if variables[:request] && variables[:request].query
369
+ variables[:query] = variables[:request].query.split('&').inject({}) do |query_string_parameters, key_value_pair|
370
+ parameter_name, parameter_value = *key_value_pair.match(/(.+)=(.*)/).captures
371
+ query_string_parameters[parameter_name] = parameter_value
372
+ query_string_parameters
373
+ end
374
+ else
375
+ variables[:query] = {}
376
+ end
377
+ end
378
+ end
379
+
380
+ def override_variables_with_query_params(variables)
381
+ returning variables do
382
+ if variables[:query]
383
+ variables[:query].each do |key, value|
384
+ variables[key.to_sym] = value
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ def remove_dashes_from_context_name(variables)
391
+ returning variables do
392
+ variables[:context].gsub!('-', '_')
393
+ end
394
+ end
395
+
396
+ end
397
+ end
398
+
399
+ class Parser
400
+
401
+ class << self
402
+ def parse(*args, &block)
403
+ returning new(*args, &block) do |parser|
404
+ parser.parse
405
+ end
406
+ end
407
+
408
+ def coerce_variables(variables)
409
+ Coercions::COERCION_ORDER.inject(variables) do |tmp_variables, coercing_method_name|
410
+ Coercions.send(coercing_method_name, tmp_variables)
411
+ end
412
+ end
413
+
414
+ def separate_line_into_key_value_pair(line)
415
+ line.match(/^([^:]+):\s?(.+)/).captures
416
+ end
417
+ end
418
+
419
+ attr_reader :io, :variables, :lines
420
+ def initialize(io)
421
+ @io = io
422
+ @lines = []
423
+ end
424
+
425
+ def parse
426
+ extract_variable_lines_from_io
427
+ initialize_variables_as_hash_from_lines
428
+ @variables = self.class.coerce_variables(variables)
429
+ end
430
+
431
+ private
432
+
433
+ def initialize_variables_as_hash_from_lines
434
+ @variables = lines.inject({}) do |new_variables,line|
435
+ returning new_variables do
436
+ key, value = self.class.separate_line_into_key_value_pair line
437
+ new_variables[key] = value
438
+ end
439
+ end
440
+ end
441
+
442
+ def extract_variable_lines_from_io
443
+ while line = io.readline.chomp
444
+ break if line.empty?
445
+ @lines << line
446
+ end
447
+ end
448
+
449
+ end
450
+
451
+ end
452
+ end
453
+ end