adhearsion 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.travis.yml +4 -3
  2. data/CHANGELOG.md +30 -0
  3. data/README.markdown +1 -0
  4. data/adhearsion.gemspec +3 -4
  5. data/bin/ahn +0 -20
  6. data/features/cli_create.feature +1 -1
  7. data/features/cli_restart.feature +25 -1
  8. data/features/cli_start.feature +0 -2
  9. data/features/plugin_generator.feature +66 -15
  10. data/features/support/env.rb +0 -13
  11. data/lib/adhearsion.rb +26 -6
  12. data/lib/adhearsion/call.rb +42 -7
  13. data/lib/adhearsion/call_controller.rb +5 -2
  14. data/lib/adhearsion/call_controller/dial.rb +92 -50
  15. data/lib/adhearsion/call_controller/input.rb +19 -6
  16. data/lib/adhearsion/call_controller/menu_dsl/menu.rb +4 -0
  17. data/lib/adhearsion/call_controller/output.rb +143 -161
  18. data/lib/adhearsion/call_controller/output/abstract_player.rb +30 -0
  19. data/lib/adhearsion/call_controller/output/async_player.rb +26 -0
  20. data/lib/adhearsion/call_controller/output/formatter.rb +81 -0
  21. data/lib/adhearsion/call_controller/output/player.rb +25 -0
  22. data/lib/adhearsion/call_controller/record.rb +19 -2
  23. data/lib/adhearsion/events.rb +3 -0
  24. data/lib/adhearsion/foundation.rb +12 -6
  25. data/lib/adhearsion/foundation/exception_handler.rb +8 -6
  26. data/lib/adhearsion/generators/app/templates/README.md +13 -0
  27. data/lib/adhearsion/generators/app/templates/config/adhearsion.rb +7 -1
  28. data/lib/adhearsion/generators/plugin/plugin_generator.rb +1 -0
  29. data/lib/adhearsion/generators/plugin/templates/plugin-template.gemspec.tt +3 -7
  30. data/lib/adhearsion/generators/plugin/templates/spec/spec_helper.rb.tt +0 -1
  31. data/lib/adhearsion/outbound_call.rb +15 -5
  32. data/lib/adhearsion/punchblock_plugin.rb +13 -2
  33. data/lib/adhearsion/punchblock_plugin/initializer.rb +13 -12
  34. data/lib/adhearsion/router.rb +43 -2
  35. data/lib/adhearsion/router/evented_route.rb +15 -0
  36. data/lib/adhearsion/router/openended_route.rb +16 -0
  37. data/lib/adhearsion/router/route.rb +31 -13
  38. data/lib/adhearsion/router/unaccepting_route.rb +11 -0
  39. data/lib/adhearsion/version.rb +1 -1
  40. data/pre-commit +14 -1
  41. data/spec/adhearsion/call_controller/dial_spec.rb +105 -10
  42. data/spec/adhearsion/call_controller/input_spec.rb +19 -21
  43. data/spec/adhearsion/call_controller/output/async_player_spec.rb +67 -0
  44. data/spec/adhearsion/call_controller/output/formatter_spec.rb +90 -0
  45. data/spec/adhearsion/call_controller/output/player_spec.rb +65 -0
  46. data/spec/adhearsion/call_controller/output_spec.rb +436 -190
  47. data/spec/adhearsion/call_controller/record_spec.rb +49 -6
  48. data/spec/adhearsion/call_controller_spec.rb +10 -2
  49. data/spec/adhearsion/call_spec.rb +138 -0
  50. data/spec/adhearsion/calls_spec.rb +1 -1
  51. data/spec/adhearsion/outbound_call_spec.rb +48 -8
  52. data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +34 -23
  53. data/spec/adhearsion/router/evented_route_spec.rb +34 -0
  54. data/spec/adhearsion/router/openended_route_spec.rb +61 -0
  55. data/spec/adhearsion/router/route_spec.rb +26 -4
  56. data/spec/adhearsion/router/unaccepting_route_spec.rb +72 -0
  57. data/spec/adhearsion/router_spec.rb +107 -2
  58. data/spec/adhearsion_spec.rb +19 -0
  59. data/spec/capture_warnings.rb +28 -21
  60. data/spec/spec_helper.rb +2 -3
  61. data/spec/support/call_controller_test_helpers.rb +31 -30
  62. metadata +32 -29
@@ -4,7 +4,12 @@ module Adhearsion
4
4
  class Router
5
5
  extend ActiveSupport::Autoload
6
6
 
7
+ autoload :EventedRoute
8
+ autoload :OpenendedRoute
7
9
  autoload :Route
10
+ autoload :UnacceptingRoute
11
+
12
+ NoMatchError = Class.new Adhearsion::Error
8
13
 
9
14
  attr_reader :routes
10
15
 
@@ -24,9 +29,45 @@ module Adhearsion
24
29
  end
25
30
 
26
31
  def handle(call)
27
- return unless route = match(call)
32
+ raise NoMatchError unless route = match(call)
28
33
  logger.info "Call #{call.id} selected route \"#{route.name}\" (#{route.target})"
29
- route.dispatcher
34
+ route.dispatch call
35
+ rescue NoMatchError
36
+ logger.warn "Call #{call.id} could not find a matching route. Rejecting."
37
+ call.reject :error
38
+ end
39
+
40
+ module Filters
41
+ def evented(&block)
42
+ filtered_routes EventedRoute, &block
43
+ end
44
+
45
+ def unaccepting(&block)
46
+ filtered_routes UnacceptingRoute, &block
47
+ end
48
+
49
+ def openended(&block)
50
+ filtered_routes OpenendedRoute, &block
51
+ end
52
+
53
+ def filtered_routes(mixin, &block)
54
+ FilteredRouter.new(self, mixin).instance_exec(&block)
55
+ end
56
+ end
57
+
58
+ include Filters
59
+
60
+ class FilteredRouter < SimpleDelegator
61
+ include Filters
62
+
63
+ def initialize(delegate, mixin)
64
+ super delegate
65
+ @mixin = mixin
66
+ end
67
+
68
+ def route(*args, &block)
69
+ super.tap { |r| r.extend @mixin }
70
+ end
30
71
  end
31
72
  end
32
73
  end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ module Adhearsion
4
+ class Router
5
+ module EventedRoute
6
+ def evented?
7
+ true
8
+ end
9
+
10
+ def dispatch(call, callback = nil)
11
+ target.call call, callback
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+
3
+ module Adhearsion
4
+ class Router
5
+ module OpenendedRoute
6
+ def openended?
7
+ true
8
+ end
9
+
10
+ def dispatch(call, callback = nil)
11
+ call[:ahn_prevent_hangup] = true
12
+ super
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,5 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'has_guarded_handlers'
4
+
3
5
  module Adhearsion
4
6
  class Router
5
7
  class Route
@@ -21,22 +23,38 @@ module Adhearsion
21
23
  !guarded? guards, call
22
24
  end
23
25
 
24
- def dispatcher
25
- @dispatcher ||= lambda do |call, callback = nil|
26
- controller = if target.respond_to?(:call)
27
- CallController.new call, &target
28
- else
29
- target.new call
30
- end
26
+ def dispatch(call, callback = nil)
27
+ controller = if target.respond_to?(:call)
28
+ CallController.new call, &target
29
+ else
30
+ target.new call
31
+ end
31
32
 
32
- call.execute_controller controller, lambda { |call_actor|
33
- begin
33
+ call.accept if accepting?
34
+
35
+ call.execute_controller controller, lambda { |call_actor|
36
+ begin
37
+ if call_actor[:ahn_prevent_hangup]
38
+ logger.info "Call routing completed, keeping the call alive at controller/router request."
39
+ else
34
40
  call_actor.hangup
35
- rescue Call::Hangup
36
41
  end
37
- callback.call if callback
38
- }
39
- end
42
+ rescue Call::Hangup
43
+ end
44
+ callback.call if callback
45
+ }
46
+ end
47
+
48
+ def evented?
49
+ false
50
+ end
51
+
52
+ def accepting?
53
+ true
54
+ end
55
+
56
+ def openended?
57
+ false
40
58
  end
41
59
 
42
60
  def inspect
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ module Adhearsion
4
+ class Router
5
+ module UnacceptingRoute
6
+ def accepting?
7
+ false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Adhearsion
4
- VERSION = '2.0.1'
4
+ VERSION = '2.1.0'
5
5
  end
data/pre-commit CHANGED
@@ -1,2 +1,15 @@
1
- #!/bin/sh
1
+ #!/bin/bash
2
2
  rake encodeify
3
+
4
+ # avoid committing :focus on rspec examples
5
+ focus=', :focus'
6
+ result=$(git grep -n -a "$focus" -- */**/*_spec.rb)
7
+
8
+ if [ "$result" != '' ]; then
9
+ echo ""
10
+ echo "Refusing to commit a :focus tag in specs: "
11
+ echo ""
12
+ echo $result
13
+ echo ""
14
+ exit 1
15
+ fi
@@ -51,6 +51,16 @@ module Adhearsion
51
51
  dial_thread.join.should be_true
52
52
  end
53
53
 
54
+ let(:options) { { :foo => :bar } }
55
+
56
+ def dial_in_thread
57
+ Thread.new do
58
+ status = subject.dial to, options
59
+ latch.countdown!
60
+ status
61
+ end
62
+ end
63
+
54
64
  describe "without a block" do
55
65
  before do
56
66
  flexmock(other_mock_call).should_receive(:dial).once.with(to, options)
@@ -58,16 +68,6 @@ module Adhearsion
58
68
  flexmock(OutboundCall).should_receive(:new).and_return other_mock_call
59
69
  end
60
70
 
61
- let(:options) { { :foo => :bar } }
62
-
63
- def dial_in_thread
64
- Thread.new do
65
- status = subject.dial to, options
66
- latch.countdown!
67
- status
68
- end
69
- end
70
-
71
71
  it "blocks the original controller until the new call ends" do
72
72
  dial_in_thread
73
73
 
@@ -166,6 +166,21 @@ module Adhearsion
166
166
  end
167
167
  end
168
168
 
169
+ describe "when the caller has already hung up" do
170
+ before do
171
+ call << mock_end
172
+ end
173
+
174
+ it "should raise Call::Hangup" do
175
+ expect { subject.dial to, options }.to raise_error(Call::Hangup)
176
+ end
177
+
178
+ it "should not make any outbound calls" do
179
+ flexmock(OutboundCall).should_receive(:new).never
180
+ expect { subject.dial to, options }.to raise_error
181
+ end
182
+ end
183
+
169
184
  describe "with multiple third parties specified" do
170
185
  let(:options) { {} }
171
186
  let(:other_options) { options }
@@ -374,6 +389,86 @@ module Adhearsion
374
389
  status.result.should be == :timeout
375
390
  end
376
391
  end
392
+
393
+ describe "with a confirmation controller" do
394
+ let(:confirmation_controller) do
395
+ latch = confirmation_latch
396
+ Class.new(Adhearsion::CallController) do
397
+ @@confirmation_latch = latch
398
+
399
+ def run
400
+ @@confirmation_latch.countdown!
401
+ call['confirm'] || hangup
402
+ end
403
+ end
404
+ end
405
+
406
+ let(:confirmation_latch) { CountDownLatch.new 1 }
407
+
408
+ let(:options) { {:confirm => confirmation_controller} }
409
+
410
+ before do
411
+ flexmock(other_mock_call).should_receive(:dial).once
412
+ flexmock(OutboundCall).should_receive(:new).and_return other_mock_call
413
+ end
414
+
415
+ context "when an outbound call is answered" do
416
+ it "should execute the specified confirmation controller" do
417
+ flexmock(other_mock_call).should_receive(:hangup).twice.and_return do
418
+ other_mock_call << mock_end
419
+ end
420
+ other_mock_call['confirm'] = false
421
+
422
+ dial_in_thread
423
+
424
+ latch.wait(0.1).should be_false
425
+
426
+ other_mock_call << mock_answered
427
+
428
+ confirmation_latch.wait(1).should be_true
429
+ latch.wait(2).should be_true
430
+ end
431
+
432
+ it "should join the calls if the call is still active after execution of the call controller" do
433
+ flexmock(other_mock_call).should_receive(:hangup).once
434
+ other_mock_call['confirm'] = true
435
+ flexmock(other_mock_call).should_receive(:join).once.with(call)
436
+
437
+ t = dial_in_thread
438
+
439
+ latch.wait(1).should be_false
440
+
441
+ other_mock_call << mock_answered
442
+ other_mock_call << mock_end
443
+
444
+ latch.wait(1).should be_true
445
+
446
+ t.join
447
+ status = t.value
448
+ status.result.should be == :answer
449
+ end
450
+
451
+ it "should not join the calls if the call is not active after execution of the call controller" do
452
+ flexmock(other_mock_call).should_receive(:hangup).twice.and_return do
453
+ other_mock_call << mock_end
454
+ end
455
+ other_mock_call['confirm'] = false
456
+ flexmock(other_mock_call).should_receive(:join).never.with(call)
457
+
458
+ t = dial_in_thread
459
+
460
+ latch.wait(1).should be_false
461
+
462
+ other_mock_call << mock_answered
463
+
464
+ latch.wait(1).should be_true
465
+
466
+ t.join
467
+ status = t.value
468
+ status.result.should be == :unconfirmed
469
+ end
470
+ end
471
+ end
377
472
  end#describe #dial
378
473
  end
379
474
  end
@@ -164,7 +164,7 @@ module Adhearsion
164
164
 
165
165
  it "executes failure hook and returns :failure if menu fails" do
166
166
  menu_instance.should_receive(:should_continue?).and_return(false)
167
- menu_instance.should_receive(:execute_failure_hook)
167
+ menu_instance.should_receive(:execute_failure_hook).once
168
168
  result = subject.menu sound_files
169
169
  result.menu.should be menu_instance
170
170
  result.response.should be response
@@ -173,8 +173,8 @@ module Adhearsion
173
173
  it "executes invalid hook if input is invalid" do
174
174
  menu_instance.should_receive(:should_continue?).twice.and_return(true)
175
175
  menu_instance.should_receive(:continue).and_return(result_invalid, result_done)
176
- menu_instance.should_receive(:execute_invalid_hook)
177
- menu_instance.should_receive(:restart!)
176
+ menu_instance.should_receive(:execute_invalid_hook).once
177
+ menu_instance.should_receive(:restart!).once
178
178
  result = subject.menu sound_files
179
179
  result.menu.should be menu_instance
180
180
  result.response.should be response
@@ -184,8 +184,8 @@ module Adhearsion
184
184
  menu_instance.should_receive(:should_continue?).twice.and_return(true)
185
185
  menu_instance.should_receive(:continue).and_return(result_get_another_or_timeout, result_done)
186
186
  subject.should_receive(:play_sound_files_for_menu).with(menu_instance, sound_files).and_return(nil)
187
- menu_instance.should_receive(:execute_timeout_hook)
188
- menu_instance.should_receive(:restart!)
187
+ menu_instance.should_receive(:execute_timeout_hook).once
188
+ menu_instance.should_receive(:restart!).once
189
189
  subject.menu sound_files
190
190
  end
191
191
 
@@ -193,7 +193,7 @@ module Adhearsion
193
193
  menu_instance.should_receive(:should_continue?).twice.and_return(true)
194
194
  menu_instance.should_receive(:continue).and_return(result_get_another_or_timeout, result_done)
195
195
  subject.should_receive(:play_sound_files_for_menu).with(menu_instance, sound_files).and_return("1")
196
- menu_instance.should_receive(:<<).with("1")
196
+ menu_instance.should_receive(:<<).with("1").once
197
197
  subject.menu sound_files
198
198
  end
199
199
 
@@ -201,14 +201,14 @@ module Adhearsion
201
201
  menu_instance.should_receive(:should_continue?).and_return(true)
202
202
  menu_instance.should_receive(:continue).and_return(result_get_another_or_finish)
203
203
  subject.should_receive(:play_sound_files_for_menu).with(menu_instance, sound_files).and_return(nil)
204
- subject.should_receive(:jump_to).with(:match_object, :extension => :new_extension)
204
+ subject.should_receive(:jump_to).with(:match_object, :extension => :new_extension).once
205
205
  subject.menu sound_files
206
206
  end
207
207
 
208
208
  it "jumps to payload when result is found" do
209
209
  menu_instance.should_receive(:should_continue?).and_return(true)
210
210
  menu_instance.should_receive(:continue).and_return(result_found)
211
- subject.should_receive(:jump_to).with(:match_object, :extension => :new_extension)
211
+ subject.should_receive(:jump_to).with(:match_object, :extension => :new_extension).once
212
212
  result = subject.menu sound_files
213
213
  result.menu.should be menu_instance
214
214
  result.response.should be response
@@ -238,11 +238,8 @@ module Adhearsion
238
238
  let(:result_get_another_or_finish) { MenuDSL::Menu::MenuGetAnotherDigitOrFinish.new(:match_object, :new_extension) }
239
239
  let(:result_found) { MenuDSL::Menu::MenuResultFound.new(:match_object, :new_extension) }
240
240
 
241
- let(:status) { :foo }
242
- let(:response) { '1234' }
243
-
244
241
  before do
245
- flexmock menu_instance, :status => status, :result => response
242
+ flexmock menu_instance
246
243
  flexmock(MenuDSL::Menu).should_receive(:new).and_return(menu_instance)
247
244
  end
248
245
 
@@ -250,35 +247,36 @@ module Adhearsion
250
247
  menu_instance.should_receive(:continue).and_return(result_done)
251
248
  result = subject.ask sound_files
252
249
  result.menu.should be menu_instance
253
- result.response.should be response
250
+ result.response.should == ''
254
251
  end
255
252
 
256
253
  it "exits the function if MenuTerminated" do
257
254
  menu_instance.should_receive(:continue).and_return(result_terminated)
258
255
  result = subject.ask sound_files
259
256
  result.menu.should be menu_instance
260
- result.response.should be response
257
+ result.response.should == ''
261
258
  end
262
259
 
263
260
  it "exits the function if MenuLimitReached" do
264
261
  menu_instance.should_receive(:continue).and_return(result_limit_reached)
265
262
  result = subject.ask sound_files
266
263
  result.menu.should be menu_instance
267
- result.response.should be response
264
+ result.response.should == ''
268
265
  end
269
266
 
270
267
  it "plays audio, then executes timeout hook if input times out" do
271
268
  menu_instance.should_receive(:continue).and_return(result_get_another_or_timeout, result_done)
272
- subject.should_receive(:play_sound_files_for_menu).with(menu_instance, sound_files).and_return(nil)
273
- menu_instance.should_receive(:execute_timeout_hook)
274
- menu_instance.should_receive(:restart!)
275
- subject.ask sound_files
269
+ subject.should_receive(:play_sound_files_for_menu).once.with(menu_instance, sound_files).and_return(nil)
270
+ result = subject.ask sound_files
271
+ result.menu.should be menu_instance
272
+ result.response.should be == ''
273
+ result.status.should be :timeout
276
274
  end
277
275
 
278
276
  it "plays audio, then adds digit to digit buffer if input is received" do
279
277
  menu_instance.should_receive(:continue).and_return(result_get_another_or_timeout, result_done)
280
- subject.should_receive(:play_sound_files_for_menu).with(menu_instance, sound_files).and_return("1")
281
- menu_instance.should_receive(:<<).with("1")
278
+ subject.should_receive(:play_sound_files_for_menu).once.with(menu_instance, sound_files).and_return("1")
279
+ menu_instance.should_receive(:<<).with("1").once
282
280
  subject.ask sound_files
283
281
  end
284
282
  end
@@ -0,0 +1,67 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Adhearsion
6
+ class CallController
7
+ module Output
8
+ describe AsyncPlayer do
9
+ include CallControllerTestHelpers
10
+
11
+ let(:controller) { flexmock new_controller }
12
+
13
+ subject { AsyncPlayer.new controller }
14
+
15
+ describe "#output" do
16
+ let(:content) { RubySpeech::SSML.draw { string "BOO" } }
17
+
18
+ it "should execute an output component with the provided SSML content" do
19
+ component = Punchblock::Component::Output.new :ssml => content
20
+ expect_message_waiting_for_response component
21
+ subject.output content
22
+ end
23
+
24
+ it "should allow extra options to be passed to the output component" do
25
+ component = Punchblock::Component::Output.new :ssml => content, :start_paused => true
26
+ expect_message_waiting_for_response component
27
+ subject.output content, :start_paused => true
28
+ end
29
+
30
+ it "returns the component" do
31
+ component = Punchblock::Component::Output.new :ssml => content
32
+ expect_message_waiting_for_response component
33
+ subject.output(content).should be_a Punchblock::Component::Output
34
+ end
35
+
36
+ it "raises a PlaybackError if the component fails to start" do
37
+ expect_message_waiting_for_response Punchblock::Component::Output.new(:ssml => content), Punchblock::ProtocolError
38
+ lambda { subject.output content }.should raise_error(PlaybackError)
39
+ end
40
+
41
+ it "logs the complete event if it is an error" do
42
+ response = Punchblock::Event::Complete.new
43
+ response.reason = Punchblock::Event::Complete::Error.new
44
+ component = Punchblock::Component::Output.new(:ssml => content)
45
+ flexmock subject, :new_output => component
46
+ expect_message_waiting_for_response component
47
+ flexmock(controller.logger).should_receive(:error).once
48
+ subject.output content
49
+ component.request!
50
+ component.execute!
51
+ component.trigger_event_handler response
52
+ end
53
+ end
54
+
55
+ describe "#play_ssml" do
56
+ let(:ssml) { RubySpeech::SSML.draw { string "BOO" } }
57
+
58
+ it 'executes an Output with the correct ssml' do
59
+ component = Punchblock::Component::Output.new :ssml => ssml.to_s
60
+ expect_message_waiting_for_response component
61
+ subject.play_ssml ssml
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end