adhearsion 2.1.3 → 2.2.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 (61) hide show
  1. data/CHANGELOG.md +87 -0
  2. data/Guardfile +1 -1
  3. data/Rakefile +1 -2
  4. data/adhearsion.gemspec +1 -1
  5. data/features/cli_create.feature +1 -0
  6. data/features/step_definitions/cli_steps.rb +0 -10
  7. data/lib/adhearsion.rb +11 -0
  8. data/lib/adhearsion/call.rb +27 -12
  9. data/lib/adhearsion/call_controller.rb +1 -1
  10. data/lib/adhearsion/call_controller/dial.rb +3 -1
  11. data/lib/adhearsion/call_controller/input.rb +4 -4
  12. data/lib/adhearsion/call_controller/menu_dsl.rb +1 -0
  13. data/lib/adhearsion/call_controller/menu_dsl/array_match_calculator.rb +26 -0
  14. data/lib/adhearsion/call_controller/menu_dsl/fixnum_match_calculator.rb +2 -14
  15. data/lib/adhearsion/call_controller/menu_dsl/string_match_calculator.rb +6 -6
  16. data/lib/adhearsion/call_controller/output.rb +20 -14
  17. data/lib/adhearsion/call_controller/output/abstract_player.rb +5 -5
  18. data/lib/adhearsion/call_controller/output/formatter.rb +68 -68
  19. data/lib/adhearsion/call_controller/record.rb +91 -28
  20. data/lib/adhearsion/call_controller/utility.rb +12 -5
  21. data/lib/adhearsion/calls.rb +1 -1
  22. data/lib/adhearsion/configuration.rb +4 -0
  23. data/lib/adhearsion/events.rb +2 -1
  24. data/lib/adhearsion/generators/app/app_generator.rb +2 -2
  25. data/lib/adhearsion/generators/app/templates/Gemfile.erb +4 -0
  26. data/lib/adhearsion/generators/app/templates/Rakefile +5 -0
  27. data/lib/adhearsion/generators/app/templates/lib/simon_game.rb +3 -0
  28. data/lib/adhearsion/generators/app/templates/rspec +2 -0
  29. data/lib/adhearsion/generators/app/templates/spec/call_controllers/simon_game_spec.rb +142 -0
  30. data/lib/adhearsion/generators/controller/templates/lib/controller.rb +1 -1
  31. data/lib/adhearsion/generators/controller/templates/spec/controller_spec.rb +2 -0
  32. data/lib/adhearsion/initializer.rb +3 -2
  33. data/lib/adhearsion/outbound_call.rb +5 -2
  34. data/lib/adhearsion/router/route.rb +13 -2
  35. data/lib/adhearsion/rspec.rb +1 -1
  36. data/lib/adhearsion/statistics.rb +138 -0
  37. data/lib/adhearsion/tasks/environment.rb +1 -1
  38. data/lib/adhearsion/version.rb +1 -1
  39. data/spec/adhearsion/call_controller/dial_spec.rb +26 -0
  40. data/spec/adhearsion/call_controller/input_spec.rb +13 -1
  41. data/spec/adhearsion/call_controller/menu_dsl/array_match_calculator_spec.rb +76 -0
  42. data/spec/adhearsion/call_controller/menu_dsl/fixnum_match_calculator_spec.rb +8 -6
  43. data/spec/adhearsion/call_controller/menu_dsl/range_match_calculator_spec.rb +4 -4
  44. data/spec/adhearsion/call_controller/menu_dsl/string_match_calculator_spec.rb +6 -6
  45. data/spec/adhearsion/call_controller/output/formatter_spec.rb +3 -5
  46. data/spec/adhearsion/call_controller/output_spec.rb +59 -25
  47. data/spec/adhearsion/call_controller/record_spec.rb +123 -2
  48. data/spec/adhearsion/call_controller/utility_spec.rb +31 -11
  49. data/spec/adhearsion/call_spec.rb +77 -36
  50. data/spec/adhearsion/calls_spec.rb +13 -0
  51. data/spec/adhearsion/initializer_spec.rb +7 -0
  52. data/spec/adhearsion/outbound_call_spec.rb +14 -0
  53. data/spec/adhearsion/punchblock_plugin_spec.rb +5 -2
  54. data/spec/adhearsion/router/openended_route_spec.rb +2 -1
  55. data/spec/adhearsion/router/route_spec.rb +9 -1
  56. data/spec/adhearsion/router/unaccepting_route_spec.rb +2 -1
  57. data/spec/adhearsion/statistics/dump_spec.rb +38 -0
  58. data/spec/adhearsion/statistics_spec.rb +61 -0
  59. data/spec/adhearsion_spec.rb +21 -1
  60. data/spec/spec_helper.rb +2 -0
  61. metadata +16 -7
@@ -3,4 +3,4 @@
3
3
  class <%= @controller_name.camelcase %> < Adhearsion::CallController
4
4
  def run
5
5
  end
6
- end
6
+ end
@@ -1,5 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'spec_helper'
4
+
3
5
  describe <%= @controller_name.camelcase %> do
4
6
 
5
7
  let(:mock_call) { mock 'Call', :to => '1112223333', :from => "2223334444" }
@@ -38,9 +38,10 @@ module Adhearsion
38
38
  end
39
39
 
40
40
  def start
41
+ Adhearsion.statistics
41
42
  resolve_pid_file_path
42
43
  load_lib_folder
43
- load_config
44
+ load_config_file
44
45
  initialize_log_paths
45
46
  daemonize! if should_daemonize?
46
47
  start_logging
@@ -165,7 +166,7 @@ module Adhearsion
165
166
  true
166
167
  end
167
168
 
168
- def load_config
169
+ def load_config_file
169
170
  require "#{Adhearsion.config.root}/config/adhearsion.rb"
170
171
  end
171
172
 
@@ -15,6 +15,7 @@ module Adhearsion
15
15
  # @param [String] to the URI of the party to dial
16
16
  # @param [Hash] opts modifier options
17
17
  # @option opts [Class] :controller the controller to execute when the call is answered
18
+ # @option opts [Hash] :controller_metadata key-value pairs of metadata to set on the controller
18
19
  # @yield Call controller routine in block form
19
20
  #
20
21
  # @return [OutboundCall] the ringing call
@@ -23,7 +24,7 @@ module Adhearsion
23
24
  #
24
25
  def originate(to, opts = {}, &controller_block)
25
26
  new.tap do |call|
26
- call.execute_controller_or_router_on_answer opts.delete(:controller), &controller_block
27
+ call.execute_controller_or_router_on_answer opts.delete(:controller), opts.delete(:controller_metadata), &controller_block
27
28
  call.dial to, opts
28
29
  end
29
30
  end
@@ -69,6 +70,7 @@ module Adhearsion
69
70
  write_and_await_response(Punchblock::Command::Dial.new(options), wait_timeout).tap do |dial_command|
70
71
  @dial_command = dial_command
71
72
  Adhearsion.active_calls << current_actor
73
+ Adhearsion::Events.trigger_immediately :call_dialed, current_actor
72
74
  end
73
75
  end
74
76
 
@@ -93,9 +95,10 @@ module Adhearsion
93
95
  end
94
96
  end
95
97
 
96
- def execute_controller_or_router_on_answer(controller, &controller_block)
98
+ def execute_controller_or_router_on_answer(controller, metadata = {}, &controller_block)
97
99
  if controller || controller_block
98
100
  route = Router::Route.new 'inbound', controller, &controller_block
101
+ route.controller_metadata = metadata
99
102
  on_answer { route.dispatch current_actor }
100
103
  else
101
104
  run_router_on_answer
@@ -24,10 +24,12 @@ module Adhearsion
24
24
  end
25
25
 
26
26
  def dispatch(call, callback = nil)
27
+ Adhearsion::Events.trigger_immediately :call_routed, call: call, route: self
28
+
27
29
  controller = if target.respond_to?(:call)
28
- CallController.new call, &target
30
+ CallController.new call, controller_metadata, &target
29
31
  else
30
- target.new call
32
+ target.new call, controller_metadata
31
33
  end
32
34
 
33
35
  call.accept if accepting?
@@ -45,6 +47,15 @@ module Adhearsion
45
47
  }
46
48
  end
47
49
 
50
+ def controller_metadata=(metadata)
51
+ @controller_metadata = metadata
52
+ end
53
+
54
+ def controller_metadata
55
+ return {} unless instance_variable_defined?(:@controller_metadata)
56
+ @controller_metadata
57
+ end
58
+
48
59
  def evented?
49
60
  false
50
61
  end
@@ -4,4 +4,4 @@ require 'adhearsion'
4
4
 
5
5
  initializer = Adhearsion::Initializer.new
6
6
  initializer.load_lib_folder
7
- initializer.load_config
7
+ initializer.load_config_file
@@ -0,0 +1,138 @@
1
+ # encoding: utf-8
2
+
3
+ module Adhearsion
4
+ class Statistics
5
+ include Celluloid
6
+
7
+ exclusive
8
+
9
+ def initialize
10
+ @calls_dialed = @calls_offered = @calls_routed = @calls_rejected = 0
11
+ @calls_by_route = Hash.new { |h,k| h[k] = 0 }
12
+ end
13
+
14
+ #
15
+ # Create a point-time dump of process statistics
16
+ #
17
+ # @return [Adhearsion::Statistics::Dump]
18
+ def dump
19
+ Dump.new timestamp: Time.now, call_counts: dump_call_counts, calls_by_route: dump_calls_by_route
20
+ end
21
+
22
+ # @private
23
+ def register_call_dialed
24
+ @calls_dialed += 1
25
+ end
26
+
27
+ # @private
28
+ def register_call_offered
29
+ @calls_offered += 1
30
+ end
31
+
32
+ # @private
33
+ def register_call_routed(data)
34
+ @calls_routed += 1
35
+ @calls_by_route[data[:route].name] += 1
36
+ end
37
+
38
+ # @private
39
+ def register_call_rejected
40
+ @calls_rejected += 1
41
+ end
42
+
43
+ # @private
44
+ def setup_event_handlers
45
+ stats = current_actor
46
+
47
+ Events.punchblock(Punchblock::Event::Offer) do
48
+ begin
49
+ stats.register_call_offered
50
+ rescue Celluloid::DeadActorError
51
+ end
52
+ throw :pass
53
+ end
54
+
55
+ Events.call_dialed do
56
+ begin
57
+ stats.register_call_dialed
58
+ rescue Celluloid::DeadActorError
59
+ end
60
+ throw :pass
61
+ end
62
+
63
+ Events.call_rejected do
64
+ begin
65
+ stats.register_call_rejected
66
+ rescue Celluloid::DeadActorError
67
+ end
68
+ throw :pass
69
+ end
70
+
71
+ Events.call_routed do |data|
72
+ begin
73
+ stats.register_call_routed data
74
+ rescue Celluloid::DeadActorError
75
+ end
76
+ throw :pass
77
+ end
78
+ end
79
+
80
+ def to_s
81
+ "#<#{self.class} dump=#{dump}>"
82
+ end
83
+ alias :inspect :to_s
84
+
85
+ private
86
+
87
+ def dump_call_counts
88
+ {dialed: @calls_dialed, offered: @calls_offered, routed: @calls_routed, rejected: @calls_rejected, active: Adhearsion.active_calls.count}
89
+ end
90
+
91
+ def dump_calls_by_route
92
+ @calls_by_route.tap do |index|
93
+ Adhearsion.router.routes.each do |route|
94
+ index[route.name]
95
+ end
96
+ end
97
+ end
98
+
99
+ #
100
+ # A point-time dump of process statistics
101
+ class Dump
102
+ include Comparable
103
+
104
+ #
105
+ # @attribute
106
+ # @return [Time] the time at which this dump was generated
107
+ attr_reader :timestamp
108
+
109
+ #
110
+ # @attribute
111
+ # @return [Hash] hash of call counts during the lifetime of the process.
112
+ attr_reader :call_counts
113
+
114
+ #
115
+ # @attribute
116
+ # @return [Hash] hash of call counts during the lifetime of the process, indexed by the route they matched.
117
+ attr_reader :calls_by_route
118
+
119
+ def initialize(opts = {})
120
+ @timestamp = opts[:timestamp]
121
+ @call_counts = opts[:call_counts]
122
+ @calls_by_route = opts[:calls_by_route]
123
+ end
124
+
125
+ def <=>(other)
126
+ timestamp <=> other.timestamp
127
+ end
128
+
129
+ def to_s
130
+ attrs = [:timestamp, :call_counts, :calls_by_route].map do |attr|
131
+ "#{attr}=#{send(attr).inspect}"
132
+ end
133
+ "#<#{self.class} #{attrs.join ', '}>"
134
+ end
135
+ alias :inspect :to_s
136
+ end
137
+ end
138
+ end
@@ -7,7 +7,7 @@ task :environment do
7
7
  Adhearsion.config # load default config vlaues
8
8
  initializer = Adhearsion::Initializer.new
9
9
  initializer.load_lib_folder
10
- initializer.load_config
10
+ initializer.load_config_file
11
11
  rescue Exception => ex
12
12
  STDERR.puts "\nError while loading application configuration file: #{ex}"
13
13
  end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Adhearsion
4
- VERSION = '2.1.3'
4
+ VERSION = '2.2.0'
5
5
  end
@@ -436,6 +436,10 @@ module Adhearsion
436
436
  @@confirmation_latch = latch
437
437
 
438
438
  def run
439
+ # Copy metadata onto call variables so we can assert it later. Ugly hack
440
+ metadata.each_pair do |key, value|
441
+ call[key] = value
442
+ end
439
443
  @@confirmation_latch.countdown!
440
444
  call['confirm'] || hangup
441
445
  end
@@ -451,6 +455,28 @@ module Adhearsion
451
455
  flexmock(OutboundCall).should_receive(:new).and_return other_mock_call
452
456
  end
453
457
 
458
+ context "with confirmation controller metadata specified" do
459
+ let(:options) { {:confirm => confirmation_controller, :confirm_metadata => {:foo => 'bar'}} }
460
+
461
+ it "should set the metadata on the controller" do
462
+ flexmock(other_mock_call).should_receive(:hangup).twice.and_return do
463
+ other_mock_call << mock_end
464
+ end
465
+ other_mock_call['confirm'] = false
466
+
467
+ dial_in_thread
468
+
469
+ latch.wait(0.1).should be_false
470
+
471
+ other_mock_call << mock_answered
472
+
473
+ confirmation_latch.wait(1).should be_true
474
+ latch.wait(2).should be_true
475
+
476
+ other_mock_call[:foo].should == 'bar'
477
+ end
478
+ end
479
+
454
480
  context "when an outbound call is answered" do
455
481
  it "should execute the specified confirmation controller" do
456
482
  flexmock(other_mock_call).should_receive(:hangup).twice.and_return do
@@ -63,9 +63,11 @@ module Adhearsion
63
63
  )
64
64
  }
65
65
 
66
+ let(:utterance) { 'dtmf-5' }
67
+
66
68
  def expect_component_complete_event
67
69
  complete_event = Punchblock::Event::Complete.new
68
- flexmock(complete_event).should_receive(:reason => flexmock(:interpretation => 'dtmf-5', :name => :input))
70
+ flexmock(complete_event).should_receive(:reason => flexmock(:utterance => utterance, :name => :input))
69
71
  flexmock(Punchblock::Component::Input).new_instances do |input|
70
72
  input.should_receive(:complete?).and_return(false)
71
73
  input.should_receive(:complete_event).and_return(complete_event)
@@ -84,6 +86,16 @@ module Adhearsion
84
86
  subject.wait_for_digit(timeout).should be == '5'
85
87
  end
86
88
 
89
+ context "when the utterance does not have dtmf- prefix" do
90
+ let(:utterance) { '5' }
91
+
92
+ it "returns the correct pressed digit" do
93
+ expect_component_complete_event
94
+ subject.should_receive(:execute_component_and_await_completion).once.with(Punchblock::Component::Input).and_return input_component
95
+ subject.wait_for_digit(timeout).should be == '5'
96
+ end
97
+ end
98
+
87
99
  context "with a nil timeout" do
88
100
  let(:timeout) { nil }
89
101
  let(:timeout_ms) { nil }
@@ -0,0 +1,76 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Adhearsion
6
+ class CallController
7
+ module MenuDSL
8
+ describe ArrayMatchCalculator do
9
+
10
+ let(:match_payload) { :doesnt_matter }
11
+
12
+ it "matching arrays with fixnums" do
13
+ calculator = ArrayMatchCalculator.new [11,5,14,115], match_payload
14
+ match_case = calculator.match '11'
15
+ match_case.should be_exact_match
16
+ match_case.should be_potential_match
17
+ match_case.exact_matches.should be == [11]
18
+ match_case.potential_matches.should be == [115]
19
+ end
20
+
21
+ it "matching arrays with strings with digits and special digits" do
22
+ calculator = ArrayMatchCalculator.new %w[*57 4 *54 115 ###], match_payload
23
+ match_case = calculator.match '*5'
24
+ match_case.should_not be_exact_match
25
+ match_case.should be_potential_match
26
+ match_case.potential_matches.should be == %w[*57 *54]
27
+
28
+ match_case = calculator.match '*57'
29
+ match_case.should be_exact_match
30
+ match_case.should_not be_potential_match
31
+ match_case.exact_matches.should be == %w[*57]
32
+ end
33
+
34
+ it "matching an array with a combination of Fixnums and Strings" do
35
+ calculator = ArrayMatchCalculator.new ['11',5,'14',115], match_payload
36
+ match_case = calculator.match '11'
37
+ match_case.should be_exact_match
38
+ match_case.should be_potential_match
39
+ match_case.exact_matches.should be == ['11']
40
+ match_case.potential_matches.should be == [115]
41
+ end
42
+
43
+ it "matching empty array should never match" do
44
+ calculator = ArrayMatchCalculator.new [], match_payload
45
+ match_case = calculator.match '98'
46
+ match_case.should_not be_exact_match
47
+ match_case.should_not be_potential_match
48
+ match_case.exact_matches.should be == []
49
+ match_case.potential_matches.should be == []
50
+
51
+ match_case = calculator.match '*2'
52
+ match_case.should_not be_exact_match
53
+ match_case.should_not be_potential_match
54
+ match_case.exact_matches.should be == []
55
+ match_case.potential_matches.should be == []
56
+ end
57
+
58
+ it "matching array with nil should skip nil field" do
59
+ pattern = [1,2,nil,5,10]
60
+ calculator = ArrayMatchCalculator.new pattern, match_payload
61
+ match_case = calculator.match '1'
62
+ match_case.should be_exact_match
63
+ match_case.should be_potential_match
64
+ match_case.exact_matches.should be == [1]
65
+ match_case.potential_matches.should be == [10]
66
+
67
+ match_case = calculator.match '99'
68
+ match_case.should_not be_exact_match
69
+ match_case.should_not be_potential_match
70
+
71
+ pattern.should == [1,2,nil,5,10]
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -10,26 +10,28 @@ module Adhearsion
10
10
 
11
11
  it "a potential match scenario" do
12
12
  calculator = FixnumMatchCalculator.new(444, match_payload)
13
- match = calculator.match 4
14
- match.potential_match?.should be true
15
- match.exact_match?.should_not be true
13
+ match = calculator.match '4'
14
+ match.should be_potential_match
15
+ match.should_not be_exact_match
16
16
  match.potential_matches.should be == [444]
17
17
  end
18
18
 
19
19
  it "a multi-digit exact match scenario" do
20
20
  calculator = FixnumMatchCalculator.new(5555, match_payload)
21
- calculator.match(5555).exact_match?.should be true
21
+ match = calculator.match '5555'
22
+ match.should be_exact_match
22
23
  end
23
24
 
24
25
  it "a single-digit exact match scenario" do
25
26
  calculator = FixnumMatchCalculator.new(1, match_payload)
26
- calculator.match(1).exact_match?.should be true
27
+ match = calculator.match '1'
28
+ match.should be_exact_match
27
29
  end
28
30
 
29
31
  it "the context name given to the calculator should be passed on the CalculatedMatch" do
30
32
  match_payload = :icanhascheezburger
31
33
  calculator = FixnumMatchCalculator.new(1337, match_payload)
32
- calculator.match(1337).match_payload.should be match_payload
34
+ calculator.match('1337').match_payload.should be match_payload
33
35
  end
34
36
  end
35
37
  end