larynx 0.1.1 → 0.1.2

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.
data/Rakefile CHANGED
@@ -25,6 +25,10 @@ spec = Gem::Specification.new do |s|
25
25
  s.require_path = 'lib'
26
26
  s.autorequire = GEM_NAME
27
27
  s.files = %w(MIT-LICENSE README.rdoc Rakefile) + Dir.glob("{lib,spec,examples}/**/*")
28
+ s.add_dependency "eventmachine", ">= 0.12.10"
29
+ s.add_dependency "active_support", ">= 2.3.5"
30
+ s.add_development_dependency "rspec", ">= 1.3.0"
31
+ s.add_development_dependency "em-spec", ">= 0.1.3"
28
32
  end
29
33
 
30
34
  desc 'Default: run specs.'
@@ -1,4 +1,3 @@
1
- # FIXME interrupted commands callback can be fired out of order if new command sent on break
2
1
  module Larynx
3
2
  class CallHandler < EventMachine::Protocols::HeaderAndContentProtocol
4
3
  include Observable
@@ -36,16 +35,18 @@ module Larynx
36
35
  @session[:caller_caller_id_number]
37
36
  end
38
37
 
39
- def interrupt_command
40
- if @state == :executing && current_command.interruptable?
41
- break!
42
- end
43
- end
44
-
45
38
  def clear_input
46
39
  @input = []
47
40
  end
48
41
 
42
+ def current_command
43
+ @queue.first
44
+ end
45
+
46
+ def next_command
47
+ @queue[1]
48
+ end
49
+
49
50
  def execute(command, immediately=false)
50
51
  log "Queued: #{command.name}"
51
52
  if immediately
@@ -59,10 +60,13 @@ module Larynx
59
60
 
60
61
  def timer(name, timeout, &block)
61
62
  @timers[name] = [RestartableTimer.new(timeout) {
62
- timer = @timers.delete(name)
63
- timer[1].call if timer[1]
64
- notify_observers :timed_out
65
- send_next_command if @state == :ready
63
+ if timer = @timers.delete(name)
64
+ timer[1].call if timer[1]
65
+ notify_observers :timed_out
66
+ send_next_command if @state == :ready
67
+ else
68
+ puts name
69
+ end
66
70
  }, block]
67
71
  end
68
72
 
@@ -91,17 +95,11 @@ module Larynx
91
95
  end
92
96
  end
93
97
 
94
- def cleanup
95
- break! if @state == :executing
96
- cancel_all_timers
97
- clear_observers!
98
- end
99
-
100
98
  def receive_request(header, content)
101
99
  @response = Response.new(header, content)
102
100
 
103
101
  case
104
- when @response.reply? && !current_command.is_a?(AppCommand)
102
+ when @response.reply? && current_command && !current_command.is_a?(AppCommand)
105
103
  log "Completed: #{current_command.name}"
106
104
  finalize_command
107
105
  @state = :ready
@@ -111,11 +109,13 @@ module Larynx
111
109
  run_command_setup
112
110
  @state = :executing
113
111
  when @response.executed? && current_command
112
+ this_command = current_command
114
113
  finalize_command
115
- unless interrupted?
116
- @state = :ready
117
- send_next_command
118
- end
114
+ unless this_command.interrupted?
115
+ current_command.fire_callback(:after) if this_command.command == 'break'
116
+ @state = :ready
117
+ send_next_command
118
+ end
119
119
  when @response.dtmf?
120
120
  log "Button pressed: #{@response.body[:dtmf_digit]}"
121
121
  handle_dtmf
@@ -136,16 +136,11 @@ module Larynx
136
136
  send_next_command if @state == :ready
137
137
  end
138
138
 
139
- def current_command
140
- @queue.first
141
- end
142
-
143
- def interrupting?
144
- current_command && current_command.command == 'break'
145
- end
146
-
147
- def interrupted?
148
- last_command && last_command.command == 'break'
139
+ def interrupt_command
140
+ if @state == :executing && current_command.interruptable?
141
+ current_command.interrupted = true
142
+ break!
143
+ end
149
144
  end
150
145
 
151
146
  def run_command_setup
@@ -154,18 +149,28 @@ module Larynx
154
149
 
155
150
  def finalize_command
156
151
  if command = @queue.shift
157
- command.fire_callback :after
152
+ command.fire_callback(:after) unless command.interrupted?
158
153
  @last_command = command
159
154
  end
160
155
  end
161
156
 
157
+ def command_to_send
158
+ current_command.try(:interrupted?) ? next_command : current_command
159
+ end
160
+
162
161
  def send_next_command
163
- if current_command
162
+ if command = command_to_send
164
163
  @state = :sending
165
- send_data current_command.to_s
164
+ send_data command.to_s
166
165
  end
167
166
  end
168
167
 
168
+ def cleanup
169
+ break! if @state == :executing
170
+ cancel_all_timers
171
+ clear_observers!
172
+ end
173
+
169
174
  def log(msg)
170
175
  LARYNX_LOGGER.info msg
171
176
  end
@@ -2,6 +2,7 @@ module Larynx
2
2
  class Command
3
3
  include Callbacks
4
4
  attr_reader :command
5
+ attr_accessor :interrupted
5
6
 
6
7
  define_callback :before, :after
7
8
 
@@ -21,6 +22,10 @@ module Larynx
21
22
  def interruptable?
22
23
  false
23
24
  end
25
+
26
+ def interrupted?
27
+ @interrupted
28
+ end
24
29
  end
25
30
 
26
31
  class CallCommand < Command
data/lib/larynx/fields.rb CHANGED
@@ -38,6 +38,46 @@ module Larynx
38
38
 
39
39
  end
40
40
 
41
+ module CallbacksWithMode
42
+
43
+ def setup(mode=:sync, &block)
44
+ block = app_scope(block)
45
+ callback = (mode == :async) ? lambda { EM.defer(block, lambda { send_next_command } ) } : block
46
+ super &callback
47
+ end
48
+
49
+ def success(mode=:sync, &block)
50
+ block = app_scope(block)
51
+ callback = (mode == :async) ? lambda { EM.defer(block, lambda { send_next_command } ) } : block
52
+ super &callback
53
+ end
54
+
55
+ def failure(mode=:sync, &block)
56
+ block = app_scope(block)
57
+ callback = (mode == :async) ? lambda { EM.defer(block, lambda { send_next_command } ) } : block
58
+ super &callback
59
+ end
60
+
61
+ def validate(mode=:sync, &block)
62
+ block = app_scope(block)
63
+ if mode == :async
64
+ super &lambda { EM.defer(block, lambda {|result| evaluate_validity(result) } ) }
65
+ else
66
+ super &lambda { evaluate_validity(block.call) }
67
+ end
68
+ end
69
+
70
+ def invalid(mode=:sync, &block)
71
+ block = app_scope(block)
72
+ if mode == :async
73
+ super &lambda { EM.defer(block, lambda {|result| invalid_input } ) }
74
+ else
75
+ super &lambda { block.call; invalid_input }
76
+ end
77
+ end
78
+
79
+ end
80
+
41
81
  class Field
42
82
  include Callbacks
43
83
 
@@ -45,6 +85,8 @@ module Larynx
45
85
  define_callback :setup, :validate, :invalid, :success, :failure
46
86
 
47
87
  def initialize(name, options, &block)
88
+ class_eval { include CallbacksWithMode }
89
+
48
90
  @name, @callbacks = name, {}
49
91
  @options = options.reverse_merge(:attempts => 3)
50
92
  @prompt_queue = []
@@ -82,38 +124,46 @@ module Larynx
82
124
 
83
125
  def execute_prompt
84
126
  call.execute current_prompt.command
127
+ send_next_command
85
128
  end
86
129
 
87
130
  def increment_attempts
88
131
  @attempt += 1
89
132
  end
90
133
 
91
- def fire_callback(callback)
92
- if block = @callbacks[callback]
93
- @app.instance_eval(&block)
134
+ def valid_length?
135
+ @value.size >= minimum_length
136
+ end
137
+
138
+ def evaluate_input
139
+ if valid_length?
140
+ fire_callback(:validate) { evaluate_validity(true) }
94
141
  else
95
- true
142
+ fire_callback(:invalid) { invalid_input }
96
143
  end
97
144
  end
98
145
 
99
- def valid?
100
- @value.size >= minimum_length && fire_callback(:validate)
146
+ def evaluate_validity(result)
147
+ if result
148
+ fire_callback(:success) { send_next_command }
149
+ else
150
+ fire_callback(:invalid) { invalid_input }
151
+ end
101
152
  end
102
153
 
103
- def evaluate_input
104
- if valid?
105
- fire_callback(:success)
154
+ def invalid_input
155
+ if @attempt < @options[:attempts]
156
+ increment_attempts
157
+ execute_prompt
106
158
  else
107
- fire_callback(:invalid)
108
- if @attempt < @options[:attempts]
109
- increment_attempts
110
- execute_prompt
111
- else
112
- fire_callback(:failure)
113
- end
159
+ fire_callback(:failure) { send_next_command }
114
160
  end
115
161
  end
116
162
 
163
+ def send_next_command
164
+ call.send_next_command if call.state == :ready
165
+ end
166
+
117
167
  def set_instance_variables(input)
118
168
  @value = input
119
169
  @app.send("#{@name}=", input)
@@ -137,6 +187,20 @@ module Larynx
137
187
  def call
138
188
  @app.call
139
189
  end
190
+
191
+ def app_scope(block)
192
+ lambda { @app.instance_eval(&block) }
193
+ end
194
+
195
+ # fire callback with a default block if callback not defined
196
+ def fire_callback(callback, *args, &block)
197
+ if @callbacks && @callbacks[callback]
198
+ @callbacks[callback].call(*args)
199
+ else
200
+ yield if block_given?
201
+ end
202
+ end
203
+
140
204
  end
141
205
 
142
206
  end
@@ -1,3 +1,3 @@
1
1
  module Larynx
2
- VERSION = '0.1.1'
2
+ VERSION = '0.1.2'
3
3
  end
@@ -0,0 +1,51 @@
1
+ RESPONSES[:break] = {
2
+ :header => [ "Content-Length: 1630", "Content-Type: text/event-plain" ],
3
+ :content => <<-DATA
4
+ Event-Name: CHANNEL_EXECUTE
5
+ Core-UUID: 8a4da428-37c0-11df-8380-0166d7747984
6
+ FreeSWITCH-Hostname: ubuntu
7
+ FreeSWITCH-IPv4: 172.16.150.131
8
+ FreeSWITCH-IPv6: %3A%3A1
9
+ Event-Date-Local: 2010-03-26%2018%3A11%3A13
10
+ Event-Date-GMT: Sat,%2027%20Mar%202010%2001%3A11%3A13%20GMT
11
+ Event-Date-Timestamp: 1269652273898632
12
+ Event-Calling-File: switch_core_session.c
13
+ Event-Calling-Function: switch_core_session_exec
14
+ Event-Calling-Line-Number: 1823
15
+ Channel-State: CS_EXECUTE
16
+ Channel-State-Number: 4
17
+ Channel-Name: sofia/internal/1000%40172.16.150.131
18
+ Unique-ID: 3e19d55a-394f-11df-8485-0166d7747984
19
+ Call-Direction: inbound
20
+ Presence-Call-Direction: inbound
21
+ Channel-Presence-ID: 1000%40172.16.150.131
22
+ Answer-State: answered
23
+ Channel-Read-Codec-Name: GSM
24
+ Channel-Read-Codec-Rate: 8000
25
+ Channel-Write-Codec-Name: GSM
26
+ Channel-Write-Codec-Rate: 8000
27
+ Caller-Username: 1000
28
+ Caller-Dialplan: XML
29
+ Caller-Caller-ID-Name: FreeSWITCH
30
+ Caller-Caller-ID-Number: 1000
31
+ Caller-Network-Addr: 172.16.150.1
32
+ Caller-ANI: 1000
33
+ Caller-Destination-Number: 502
34
+ Caller-Unique-ID: 3e19d55a-394f-11df-8485-0166d7747984
35
+ Caller-Source: mod_sofia
36
+ Caller-Context: default
37
+ Caller-Channel-Name: sofia/internal/1000%40172.16.150.131
38
+ Caller-Profile-Index: 1
39
+ Caller-Profile-Created-Time: 1269652267394234
40
+ Caller-Channel-Created-Time: 1269652267394234
41
+ Caller-Channel-Answered-Time: 1269652267509097
42
+ Caller-Channel-Progress-Time: 0
43
+ Caller-Channel-Progress-Media-Time: 0
44
+ Caller-Channel-Hangup-Time: 0
45
+ Caller-Channel-Transfer-Time: 0
46
+ Caller-Screen-Bit: true
47
+ Caller-Privacy-Hide-Name: false
48
+ Caller-Privacy-Hide-Number: false
49
+ Application: break
50
+ DATA
51
+ }
@@ -241,6 +241,50 @@ describe Larynx::CallHandler do
241
241
  end
242
242
  end
243
243
 
244
+ context "interrupting a command" do
245
+ before do
246
+ call.queue = []
247
+ @executing_command = call.speak('hello', :bargein => true) { call.speak 'next hello' }
248
+ call.send_next_command
249
+ call.send_response :execute
250
+ call.interrupt_command
251
+ end
252
+
253
+ it "should push break onto front of queue" do
254
+ call.queue[0].command.should == 'break'
255
+ call.queue[1].should == @executing_command
256
+ end
257
+
258
+ it "should execute break immediately" do
259
+ call.sent_data.should match(/break/)
260
+ end
261
+
262
+ it "should set executing command to interrupted" do
263
+ @executing_command.interrupted?.should be_true
264
+ end
265
+
266
+ it "should fire callback of interrupted command when break execute complete" do
267
+ @executing_command.should_receive(:fire_callback).with(:after).once
268
+ call.send_response :execute_complete
269
+ end
270
+
271
+ it "should leave executing command in queue" do
272
+ call.send_response :execute_complete
273
+ call.queue[0].should == @executing_command
274
+ end
275
+
276
+ it "should execute next command after interrupted command" do
277
+ call.send_response :execute_complete
278
+ call.sent_data.should match(/next hello/)
279
+ end
280
+
281
+ it "should not fire callback of interrupted command once execute complete" do
282
+ call.send_response :execute_complete
283
+ @executing_command.should_not_receive(:fire_callback).with(:after)
284
+ call.send_response :execute_complete
285
+ end
286
+ end
287
+
244
288
  context "timer" do
245
289
  it "should add EM timer with name and timeout" do
246
290
  Larynx::RestartableTimer.stub!(:new)
@@ -86,16 +86,18 @@ describe Larynx::Fields do
86
86
  fld.current_prompt.message.should == 'second'
87
87
  end
88
88
 
89
- context 'valid?' do
89
+ context 'valid_length?' do
90
90
  it 'should be false if input size less than minimum' do
91
91
  fld = field(:guess) do
92
92
  prompt :speak => 'first'
93
93
  end
94
94
  fld.run app
95
95
  fld.current_prompt.finalise
96
- fld.valid?.should be_false
96
+ fld.valid_length?.should be_false
97
97
  end
98
+ end
98
99
 
100
+ context 'input evaluation' do
99
101
  it 'should run validate callback if input minimum length' do
100
102
  call_me = should_be_called
101
103
  fld = field(:guess, :min_length => 1) do
@@ -106,9 +108,7 @@ describe Larynx::Fields do
106
108
  call.input << '1'
107
109
  fld.current_prompt.finalise
108
110
  end
109
- end
110
111
 
111
- context 'input evaluation' do
112
112
  it 'should run invalid callback if length not valid' do
113
113
  call_me = should_be_called
114
114
  fld = field(:guess) do
@@ -185,6 +185,20 @@ describe Larynx::Fields do
185
185
  end
186
186
  end
187
187
 
188
+ context "async callbacks" do
189
+ # it "should be run in thread" do
190
+ # em do
191
+ # fld = field(:guess) do
192
+ # prompt :speak => 'first'
193
+ # validate(:async) { sleep(0.25) }
194
+ # success { done }
195
+ # end.run(app)
196
+ # call.input << '1'
197
+ # end
198
+ # @callback.should be_nil
199
+ # end
200
+ end
201
+
188
202
  end
189
203
 
190
204
  def field(name, options={}, &block)
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 1
8
- - 1
9
- version: 0.1.1
8
+ - 2
9
+ version: 0.1.2
10
10
  platform: ruby
11
11
  authors:
12
12
  - Adam Meehan
@@ -14,10 +14,65 @@ autorequire: larynx
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-03-25 00:00:00 +11:00
17
+ date: 2010-03-31 00:00:00 +11:00
18
18
  default_executable:
19
- dependencies: []
20
-
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: eventmachine
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ - 12
30
+ - 10
31
+ version: 0.12.10
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: active_support
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 2
43
+ - 3
44
+ - 5
45
+ version: 2.3.5
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: rspec
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 1
57
+ - 3
58
+ - 0
59
+ version: 1.3.0
60
+ type: :development
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: em-spec
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ segments:
70
+ - 0
71
+ - 1
72
+ - 3
73
+ version: 0.1.3
74
+ type: :development
75
+ version_requirements: *id004
21
76
  description: ""
22
77
  email: adam.meehan@gmail.com
23
78
  executables:
@@ -46,6 +101,7 @@ files:
46
101
  - lib/larynx/version.rb
47
102
  - lib/larynx.rb
48
103
  - spec/fixtures/answer.rb
104
+ - spec/fixtures/break.rb
49
105
  - spec/fixtures/channel_data.rb
50
106
  - spec/fixtures/dtmf.rb
51
107
  - spec/fixtures/execute.rb