adhearsion 2.0.1 → 2.1.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 (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