larynx 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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