larynx 0.1.2 → 0.1.3
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/README.rdoc +60 -6
- data/Rakefile +1 -1
- data/lib/larynx/call_handler.rb +33 -34
- data/lib/larynx/callbacks_with_async.rb +60 -0
- data/lib/larynx/command.rb +8 -0
- data/lib/larynx/fields.rb +49 -92
- data/lib/larynx/prompt.rb +21 -3
- data/lib/larynx/response.rb +4 -0
- data/lib/larynx/version.rb +1 -1
- data/lib/larynx.rb +15 -5
- data/spec/fixtures/answered.rb +131 -0
- data/spec/larynx/call_handler_spec.rb +30 -30
- data/spec/larynx/fields_spec.rb +39 -24
- data/spec/larynx/prompt_spec.rb +26 -10
- data/spec/spec_helper.rb +23 -1
- metadata +7 -5
data/README.rdoc
CHANGED
@@ -104,7 +104,7 @@ A more sophisticated example using the Form class
|
|
104
104
|
|
105
105
|
Larynx.answer {|call| Guess.run(call) }
|
106
106
|
|
107
|
-
The Form class wraps up many handy conventions into a pleasant DSL in which allows you to control the
|
107
|
+
The Form class wraps up many handy conventions into a pleasant DSL in which allows you to control the caller
|
108
108
|
interaction more easily.
|
109
109
|
|
110
110
|
Save your app into file and run larynx comand to start the app server ready to receive calls.
|
@@ -167,10 +167,6 @@ The class method initialises some things for you and then calls <tt>run</tt> on
|
|
167
167
|
there its up to you. You can use all the commands directly rather than call them on the call
|
168
168
|
instance.
|
169
169
|
|
170
|
-
== Form Class
|
171
|
-
|
172
|
-
|
173
|
-
|
174
170
|
== Event Hooks
|
175
171
|
|
176
172
|
The Application and Form classes have a couple of useful event hook methods available which are
|
@@ -180,7 +176,7 @@ The Application and Form classes have a couple of useful event hook methods avai
|
|
180
176
|
end
|
181
177
|
|
182
178
|
def dtmf_received(input)
|
183
|
-
# input is the button the
|
179
|
+
# input is the button the caller just pushed
|
184
180
|
end
|
185
181
|
|
186
182
|
def hungup
|
@@ -188,4 +184,62 @@ The Application and Form classes have a couple of useful event hook methods avai
|
|
188
184
|
end
|
189
185
|
end
|
190
186
|
|
187
|
+
== Form Class
|
188
|
+
|
189
|
+
The Form class is a sublcass of the Application class with some added goodness. You can use the field
|
190
|
+
DSL to abstract away a lot of the reptitive work and more difficult asynchronous stuff you would have
|
191
|
+
to handle yourself.
|
192
|
+
|
193
|
+
When collection input from the caller you are usually needing to do one or more of a few things.
|
194
|
+
|
195
|
+
These are:
|
196
|
+
1. Set-up something before the caller is prompted for the information.
|
197
|
+
2. Repeat the prompt for input if the caller doesn't enter anything or does not enter enough digits.
|
198
|
+
3. Validate the input for constraints beyond length such as a range or finding a matching database record.
|
199
|
+
4. Perform some task, perhaps say something, if the input is invalid but before the prompt is repeated.
|
200
|
+
5. Handle the next action if the input is valid
|
201
|
+
6. Handle the next action if the caller fails enter valid input after a number of attempts.
|
202
|
+
|
203
|
+
You define these tasks as callbacks for each type of action, be it setup, validate, invalid, success or failure.
|
204
|
+
These callbacks are run in the scope of the form class instance each time so that they can access user
|
205
|
+
defined methods or instance variables you need.
|
206
|
+
|
207
|
+
Each field defined becomes an attribute on the form class instance which you can use at any time. The value
|
208
|
+
is set to the caller input after each prompt, just use the field attribute method to retrieve it. A form may
|
209
|
+
have as many fields defined you as need. Though the idea would be to group fields with a particular purpose
|
210
|
+
together and define another form for unrelated fields.
|
211
|
+
|
212
|
+
Lets look at a simple Form class with empty callbacks in place.
|
213
|
+
|
214
|
+
class MyApp < Larynx::Form
|
215
|
+
field(:my_field, :attempts => 3, :length => 1) do
|
216
|
+
prompt :speak => 'Please enter a value.'
|
217
|
+
|
218
|
+
setup do
|
219
|
+
# run once when the field is starts
|
220
|
+
end
|
221
|
+
|
222
|
+
validate do
|
223
|
+
# run when the input is of a valid length
|
224
|
+
end
|
225
|
+
|
226
|
+
invalid do
|
227
|
+
# run when the input is not a valid length or the validate block returns false
|
228
|
+
end
|
229
|
+
|
230
|
+
success do
|
231
|
+
# run when the input is a valid length the validate block, if defined, returns true
|
232
|
+
end
|
233
|
+
|
234
|
+
failure do
|
235
|
+
# run when the maximum attempts has been reached and valid input has not been entered
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
This form will do the following when run:
|
241
|
+
* Define a field attribute method called <tt>my_field</tt>
|
242
|
+
* Atempt to obtain the field value up to 3 times if the caller fails to successfully enter it
|
243
|
+
* Accept a single digit value as complete and valid intput
|
244
|
+
|
191
245
|
|
data/Rakefile
CHANGED
@@ -16,7 +16,7 @@ spec = Gem::Specification.new do |s|
|
|
16
16
|
s.has_rdoc = true
|
17
17
|
s.extra_rdoc_files = ["README.rdoc"]
|
18
18
|
s.executables = ["larynx"]
|
19
|
-
s.summary = ""
|
19
|
+
s.summary = "An evented application framework for the FreeSWITCH telephony platform"
|
20
20
|
s.description = s.summary
|
21
21
|
s.author = "Adam Meehan"
|
22
22
|
s.email = "adam.meehan@gmail.com"
|
data/lib/larynx/call_handler.rb
CHANGED
@@ -8,23 +8,16 @@ module Larynx
|
|
8
8
|
# EM hook which is run when call is received
|
9
9
|
def post_init
|
10
10
|
@queue, @input, @timers = [], [], {}
|
11
|
-
connect {
|
12
|
-
@session = Session.new(@response.header)
|
13
|
-
log "Call received from #{caller_id}"
|
14
|
-
Larynx.fire_callback(:connect, self)
|
15
|
-
start_session
|
16
|
-
}
|
11
|
+
connect { start_session }
|
17
12
|
send_next_command
|
18
13
|
end
|
19
14
|
|
20
15
|
def start_session
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
}
|
27
|
-
}
|
16
|
+
@session = Session.new(@response.header)
|
17
|
+
log "Call received from #{caller_id}"
|
18
|
+
myevents
|
19
|
+
linger
|
20
|
+
Larynx.fire_callback(:connect, self)
|
28
21
|
end
|
29
22
|
|
30
23
|
def called_number
|
@@ -58,33 +51,35 @@ module Larynx
|
|
58
51
|
command
|
59
52
|
end
|
60
53
|
|
61
|
-
def
|
54
|
+
def add_timer(name, timeout, &block)
|
62
55
|
@timers[name] = [RestartableTimer.new(timeout) {
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
else
|
68
|
-
puts name
|
69
|
-
end
|
56
|
+
timer = @timers.delete(name)
|
57
|
+
timer[1].call if timer[1]
|
58
|
+
notify_observers :timed_out
|
59
|
+
send_next_command if @state == :ready
|
70
60
|
}, block]
|
71
61
|
end
|
72
62
|
|
73
63
|
def cancel_timer(name)
|
74
|
-
if timer = @timers
|
64
|
+
if timer = @timers[name]
|
75
65
|
timer[0].cancel
|
66
|
+
@timers.delete(name)
|
76
67
|
send_next_command if @state == :ready
|
77
68
|
end
|
78
69
|
end
|
79
70
|
|
80
71
|
def cancel_all_timers
|
81
72
|
@timers.values.each {|t| t[0].cancel }
|
73
|
+
@timers = {}
|
82
74
|
end
|
83
75
|
|
84
76
|
def stop_timer(name)
|
85
|
-
if timer = @timers
|
86
|
-
|
87
|
-
timer[
|
77
|
+
if timer = @timers[name]
|
78
|
+
# only run callback if it was actually cancelled (i.e. returns false)
|
79
|
+
if timer[0].cancel == false && timer[1]
|
80
|
+
timer[1].call
|
81
|
+
end
|
82
|
+
@timers.delete(name)
|
88
83
|
send_next_command if @state == :ready
|
89
84
|
end
|
90
85
|
end
|
@@ -97,29 +92,37 @@ module Larynx
|
|
97
92
|
|
98
93
|
def receive_request(header, content)
|
99
94
|
@response = Response.new(header, content)
|
95
|
+
handle_request
|
96
|
+
end
|
100
97
|
|
98
|
+
def handle_request
|
101
99
|
case
|
102
|
-
when @response.reply? && current_command
|
100
|
+
when @response.reply? && current_command.is_a?(CallCommand)
|
103
101
|
log "Completed: #{current_command.name}"
|
104
102
|
finalize_command
|
105
103
|
@state = :ready
|
106
104
|
send_next_command
|
105
|
+
when @response.answered?
|
106
|
+
log 'Answered call'
|
107
|
+
finalize_command
|
108
|
+
@state = :ready
|
109
|
+
Larynx.fire_callback(:answer, self)
|
110
|
+
send_next_command
|
107
111
|
when @response.executing?
|
108
112
|
log "Executing: #{current_command.name}"
|
109
|
-
|
113
|
+
current_command.setup
|
110
114
|
@state = :executing
|
111
115
|
when @response.executed? && current_command
|
112
116
|
this_command = current_command
|
113
117
|
finalize_command
|
114
118
|
unless this_command.interrupted?
|
115
|
-
current_command.
|
119
|
+
current_command.finalize if this_command.command == 'break'
|
116
120
|
@state = :ready
|
117
121
|
send_next_command
|
118
122
|
end
|
119
123
|
when @response.dtmf?
|
120
124
|
log "Button pressed: #{@response.body[:dtmf_digit]}"
|
121
125
|
handle_dtmf
|
122
|
-
when @response.speech?
|
123
126
|
when @response.disconnect?
|
124
127
|
log "Disconnected."
|
125
128
|
cleanup
|
@@ -143,13 +146,9 @@ module Larynx
|
|
143
146
|
end
|
144
147
|
end
|
145
148
|
|
146
|
-
def run_command_setup
|
147
|
-
current_command.fire_callback :before
|
148
|
-
end
|
149
|
-
|
150
149
|
def finalize_command
|
151
150
|
if command = @queue.shift
|
152
|
-
command.
|
151
|
+
command.finalize unless command.interrupted?
|
153
152
|
@last_command = command
|
154
153
|
end
|
155
154
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Larynx
|
2
|
+
# Allows you to set the :async flag on the back to the callback in with
|
3
|
+
# EM.defer method. Its useful for long running tasks like database calls.
|
4
|
+
module CallbacksWithAsync
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
base.cattr_accessor :callback_options
|
8
|
+
base.callback_options = {}
|
9
|
+
base.class_eval do
|
10
|
+
include InstanceMethods
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def define_callback(*callbacks)
|
17
|
+
options = callbacks.extract_options!
|
18
|
+
callbacks.each do |callback|
|
19
|
+
self.callback_options[callback] = options
|
20
|
+
class_eval <<-DEF
|
21
|
+
def #{callback}(mode=:sync, &block)
|
22
|
+
@callbacks ||= {}
|
23
|
+
@callbacks[:#{callback}] = [block, mode]
|
24
|
+
self
|
25
|
+
end
|
26
|
+
DEF
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
module InstanceMethods
|
33
|
+
|
34
|
+
def fire_callback(callback)
|
35
|
+
if @callbacks && @callbacks[callback]
|
36
|
+
block, mode = *@callbacks[callback]
|
37
|
+
scope = self.class.callback_options[callback][:scope]
|
38
|
+
if mode == :async
|
39
|
+
EM.defer(scope_callback(block, scope), lambda {|result| callback_complete(callback, result) })
|
40
|
+
else
|
41
|
+
callback_complete(callback, scope_callback(block, scope).call)
|
42
|
+
end
|
43
|
+
else
|
44
|
+
callback_complete(callback, nil)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Scope takes the callback block and a method symbol which is used
|
49
|
+
# to return an object that scopes the block evaluation.
|
50
|
+
def scope_callback(block, scope=nil)
|
51
|
+
scope ? lambda { send(scope).instance_eval(&block) } : block
|
52
|
+
end
|
53
|
+
|
54
|
+
def callback_complete(callback, result)
|
55
|
+
# Override in class to handle post callback result
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
data/lib/larynx/command.rb
CHANGED
data/lib/larynx/fields.rb
CHANGED
@@ -5,15 +5,18 @@ module Larynx
|
|
5
5
|
|
6
6
|
def self.included(base)
|
7
7
|
base.extend ClassMethods
|
8
|
-
base.
|
8
|
+
base.class_eval do
|
9
|
+
include InstanceMethods
|
10
|
+
class_inheritable_accessor :field_definitions
|
11
|
+
self.field_definitions = []
|
12
|
+
attr_accessor :fields
|
13
|
+
end
|
9
14
|
end
|
10
15
|
|
11
16
|
module ClassMethods
|
12
|
-
attr_accessor :fields
|
13
17
|
|
14
18
|
def field(name, options={}, &block)
|
15
|
-
|
16
|
-
@fields << Field.new(name, options, &block)
|
19
|
+
self.field_definitions << {:name => name, :options => options, :block => block}
|
17
20
|
attr_accessor name
|
18
21
|
end
|
19
22
|
|
@@ -21,73 +24,36 @@ module Larynx
|
|
21
24
|
|
22
25
|
module InstanceMethods
|
23
26
|
|
27
|
+
def initialize(*args, &block)
|
28
|
+
@fields = self.class.field_definitions.map {|field| Field.new(field[:name], field[:options], &field[:block]) }
|
29
|
+
@current_field = 0
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
24
33
|
def next_field(field_name=nil)
|
25
|
-
@current_field
|
26
|
-
|
27
|
-
if field = self.class.fields[@current_field]
|
34
|
+
@current_field = field_index(field_name) if field_name
|
35
|
+
if field = @fields[@current_field]
|
28
36
|
field.run(self)
|
29
37
|
@current_field += 1
|
30
38
|
field
|
31
39
|
end
|
32
40
|
end
|
33
41
|
|
34
|
-
def
|
35
|
-
field =
|
36
|
-
|
37
|
-
end
|
38
|
-
|
39
|
-
end
|
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
|
42
|
+
def field_index(name)
|
43
|
+
field = @fields.find {|f| f.name == name }
|
44
|
+
@fields.index(field)
|
77
45
|
end
|
78
46
|
|
79
47
|
end
|
80
48
|
|
81
49
|
class Field
|
82
|
-
include
|
50
|
+
include CallbacksWithAsync
|
83
51
|
|
84
|
-
attr_reader :name
|
85
|
-
define_callback :setup, :validate, :invalid, :success, :failure
|
52
|
+
attr_reader :name, :app
|
53
|
+
define_callback :setup, :validate, :invalid, :success, :failure, :scope => :app
|
86
54
|
|
87
55
|
def initialize(name, options, &block)
|
88
|
-
|
89
|
-
|
90
|
-
@name, @callbacks = name, {}
|
56
|
+
@name = name
|
91
57
|
@options = options.reverse_merge(:attempts => 3)
|
92
58
|
@prompt_queue = []
|
93
59
|
|
@@ -105,19 +71,20 @@ module Larynx
|
|
105
71
|
end
|
106
72
|
|
107
73
|
def add_prompt(options)
|
74
|
+
options.assert_valid_keys(:play, :speak, :phrase, :bargein, :repeats, :interdigit_timeout, :timeout)
|
108
75
|
repeats = options.delete(:repeats) || 1
|
109
|
-
options.merge!(@options.slice(:length, :min_length, :max_length))
|
76
|
+
options.merge!(@options.slice(:length, :min_length, :max_length, :interdigit_timeout, :timeout))
|
110
77
|
@prompt_queue += ([options] * repeats)
|
111
78
|
end
|
112
79
|
|
113
80
|
def current_prompt
|
114
81
|
options = (@prompt_queue[@attempt-1] || @prompt_queue.last).dup
|
115
|
-
method = (
|
82
|
+
method = command_from_options(options)
|
116
83
|
message = options[method].is_a?(Symbol) ? @app.send(options[method]) : options[method]
|
117
84
|
options[method] = message
|
118
85
|
|
119
|
-
Prompt.new(call, options) {|input|
|
120
|
-
set_instance_variables(input)
|
86
|
+
Prompt.new(call, options) {|input, result|
|
87
|
+
set_instance_variables(input, result)
|
121
88
|
evaluate_input
|
122
89
|
}
|
123
90
|
end
|
@@ -131,24 +98,25 @@ module Larynx
|
|
131
98
|
@attempt += 1
|
132
99
|
end
|
133
100
|
|
134
|
-
|
135
|
-
|
101
|
+
# hook called when callback is complete
|
102
|
+
def callback_complete(callback, result)
|
103
|
+
case callback
|
104
|
+
when :validate
|
105
|
+
result = result.nil? ? true : result
|
106
|
+
evaluate_validity(result)
|
107
|
+
when :invalid
|
108
|
+
invalid_input
|
109
|
+
when :success, :failure
|
110
|
+
finalize
|
111
|
+
end
|
136
112
|
end
|
137
113
|
|
138
114
|
def evaluate_input
|
139
|
-
|
140
|
-
fire_callback(:validate) { evaluate_validity(true) }
|
141
|
-
else
|
142
|
-
fire_callback(:invalid) { invalid_input }
|
143
|
-
end
|
115
|
+
@valid_length ? fire_callback(:validate) : fire_callback(:invalid)
|
144
116
|
end
|
145
117
|
|
146
118
|
def evaluate_validity(result)
|
147
|
-
|
148
|
-
fire_callback(:success) { send_next_command }
|
149
|
-
else
|
150
|
-
fire_callback(:invalid) { invalid_input }
|
151
|
-
end
|
119
|
+
result ? fire_callback(:success) : fire_callback(:invalid)
|
152
120
|
end
|
153
121
|
|
154
122
|
def invalid_input
|
@@ -156,7 +124,7 @@ module Larynx
|
|
156
124
|
increment_attempts
|
157
125
|
execute_prompt
|
158
126
|
else
|
159
|
-
fire_callback(:failure)
|
127
|
+
fire_callback(:failure)
|
160
128
|
end
|
161
129
|
end
|
162
130
|
|
@@ -164,22 +132,19 @@ module Larynx
|
|
164
132
|
call.send_next_command if call.state == :ready
|
165
133
|
end
|
166
134
|
|
167
|
-
def set_instance_variables(input)
|
168
|
-
@value = input
|
135
|
+
def set_instance_variables(input, result)
|
136
|
+
@value, @valid_length = input, result
|
169
137
|
@app.send("#{@name}=", input)
|
170
138
|
end
|
171
139
|
|
172
|
-
def
|
173
|
-
|
174
|
-
end
|
175
|
-
|
176
|
-
def minimum_length
|
177
|
-
@options[:min_length] || @options[:length] || 1
|
140
|
+
def command_from_options(options)
|
141
|
+
([:play, :speak, :phrase] & options.keys).first
|
178
142
|
end
|
179
143
|
|
180
144
|
def run(app)
|
181
145
|
@app = app
|
182
146
|
@attempt = 1
|
147
|
+
call.add_observer self
|
183
148
|
fire_callback(:setup)
|
184
149
|
execute_prompt
|
185
150
|
end
|
@@ -188,17 +153,9 @@ module Larynx
|
|
188
153
|
@app.call
|
189
154
|
end
|
190
155
|
|
191
|
-
def
|
192
|
-
|
193
|
-
|
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
|
156
|
+
def finalize
|
157
|
+
call.remove_observer self
|
158
|
+
send_next_command
|
202
159
|
end
|
203
160
|
|
204
161
|
end
|
data/lib/larynx/prompt.rb
CHANGED
@@ -45,10 +45,22 @@ module Larynx
|
|
45
45
|
@options[:termchar]
|
46
46
|
end
|
47
47
|
|
48
|
+
def minimum_length
|
49
|
+
@options[:min_length] || @options[:length] || 1
|
50
|
+
end
|
51
|
+
|
48
52
|
def maximum_length
|
49
53
|
@options[:max_length] || @options[:length]
|
50
54
|
end
|
51
55
|
|
56
|
+
def interdigit_timeout
|
57
|
+
@options[:interdigit_timeout]
|
58
|
+
end
|
59
|
+
|
60
|
+
def timeout
|
61
|
+
@options[:timeout]
|
62
|
+
end
|
63
|
+
|
52
64
|
def command_name
|
53
65
|
([:play, :speak, :phrase] & @options.keys).first.to_s
|
54
66
|
end
|
@@ -57,9 +69,15 @@ module Larynx
|
|
57
69
|
@options[command_name.to_sym]
|
58
70
|
end
|
59
71
|
|
72
|
+
def valid_length?
|
73
|
+
length = input.size
|
74
|
+
length >= minimum_length && length <= (maximum_length || length)
|
75
|
+
end
|
76
|
+
|
60
77
|
def finalise
|
61
78
|
call.remove_observer self
|
62
|
-
@block.call(input)
|
79
|
+
@block.arity == 2 ? @block.call(input, valid_length?) : @block.call(input)
|
80
|
+
call.clear_input
|
63
81
|
end
|
64
82
|
|
65
83
|
def dtmf_received(digit)
|
@@ -72,14 +90,14 @@ module Larynx
|
|
72
90
|
end
|
73
91
|
|
74
92
|
def add_digit_timer
|
75
|
-
call.
|
93
|
+
call.add_timer(:digit, interdigit_timeout) {
|
76
94
|
call.cancel_timer :input
|
77
95
|
finalise
|
78
96
|
}
|
79
97
|
end
|
80
98
|
|
81
99
|
def add_input_timer
|
82
|
-
call.
|
100
|
+
call.add_timer(:input, timeout) {
|
83
101
|
call.cancel_timer :digit
|
84
102
|
finalise
|
85
103
|
}
|
data/lib/larynx/response.rb
CHANGED
data/lib/larynx/version.rb
CHANGED
data/lib/larynx.rb
CHANGED
@@ -8,6 +8,7 @@ require 'larynx/version'
|
|
8
8
|
require 'larynx/logger'
|
9
9
|
require 'larynx/observable'
|
10
10
|
require 'larynx/callbacks'
|
11
|
+
require 'larynx/callbacks_with_async'
|
11
12
|
require 'larynx/session'
|
12
13
|
require 'larynx/response'
|
13
14
|
require 'larynx/command'
|
@@ -29,13 +30,13 @@ module Larynx
|
|
29
30
|
@options = {
|
30
31
|
:ip => "0.0.0.0",
|
31
32
|
:port => 8084,
|
32
|
-
:pid_file =>
|
33
|
-
:log_file =>
|
33
|
+
:pid_file => "./larynx.pid",
|
34
|
+
:log_file => "./larynx.log"
|
34
35
|
}
|
35
36
|
opts = OptionParser.new
|
36
37
|
opts.banner = "Usage: larynx [options]"
|
37
38
|
opts.separator ''
|
38
|
-
opts.separator "Larynx is a
|
39
|
+
opts.separator "Larynx is a framework to develop FreeSWITCH IVR applications in Ruby."
|
39
40
|
opts.on('-i', '--ip IP', 'Listen for connections on this IP') {|ip| @options[:ip] = ip }
|
40
41
|
opts.on('-p', '--port PORT', 'Listen on this port', Integer) {|port| @options[:port] = port }
|
41
42
|
opts.on('-d', '--daemonize', 'Run as daemon') { @options[:daemonize] = true }
|
@@ -53,7 +54,10 @@ module Larynx
|
|
53
54
|
end
|
54
55
|
|
55
56
|
def graceful_exit
|
56
|
-
|
57
|
+
msg = "Shutting down Larynx"
|
58
|
+
$stderr.puts msg unless @options[:daemon]
|
59
|
+
LARYNX_LOGGER.info msg
|
60
|
+
|
57
61
|
EM.stop_server @em_signature
|
58
62
|
@em_signature = nil
|
59
63
|
remove_pid_file if @options[:daemonize]
|
@@ -85,7 +89,10 @@ module Larynx
|
|
85
89
|
end
|
86
90
|
|
87
91
|
def start_server
|
88
|
-
|
92
|
+
msg = "Larynx starting up on #{@options[:ip]}:#{@options[:port]}"
|
93
|
+
$stderr.puts msg unless @options[:daemon]
|
94
|
+
LARYNX_LOGGER.info msg
|
95
|
+
|
89
96
|
EM::run {
|
90
97
|
@em_signature = EM::start_server @options[:ip], @options[:port], Larynx::CallHandler
|
91
98
|
}
|
@@ -104,6 +111,9 @@ module Larynx
|
|
104
111
|
!@em_signature.nil?
|
105
112
|
end
|
106
113
|
end
|
114
|
+
|
115
|
+
# Default connect callback is to answer call
|
116
|
+
connect {|call| call.answer }
|
107
117
|
end
|
108
118
|
|
109
119
|
Larynx.run unless defined?(TEST)
|
@@ -0,0 +1,131 @@
|
|
1
|
+
RESPONSES[:answered] = {
|
2
|
+
:header => [ "Content-Length: 5793", "Content-Type: text/event-plain" ],
|
3
|
+
:content => <<-DATA
|
4
|
+
Event-Name: CHANNEL_EXECUTE_COMPLETE
|
5
|
+
Core-UUID: 0a3bd7a4-2a47-11df-8f89-ddea2631ab78
|
6
|
+
FreeSWITCH-Hostname: ubuntu
|
7
|
+
FreeSWITCH-IPv4: 172.16.150.131
|
8
|
+
FreeSWITCH-IPv6: %3A%3A1
|
9
|
+
Event-Date-Local: 2010-03-07%2016%3A39%3A02
|
10
|
+
Event-Date-GMT: Mon,%2008%20Mar%202010%2000%3A39%3A02%20GMT
|
11
|
+
Event-Date-Timestamp: 1268008742244659
|
12
|
+
Event-Calling-File: switch_core_session.c
|
13
|
+
Event-Calling-Function: switch_core_session_exec
|
14
|
+
Event-Calling-Line-Number: 1824
|
15
|
+
Channel-State: CS_EXECUTE
|
16
|
+
Channel-State-Number: 4
|
17
|
+
Channel-Name: sofia/internal/1000%40172.16.150.131
|
18
|
+
Unique-ID: f81a3fbc-2a4a-11df-8f9d-ddea2631ab78
|
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: f81a3fbc-2a4a-11df-8f9d-ddea2631ab78
|
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: 1268008732274835
|
40
|
+
Caller-Channel-Created-Time: 1268008732274835
|
41
|
+
Caller-Channel-Answered-Time: 1268008732325458
|
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
|
+
variable_uuid: f81a3fbc-2a4a-11df-8f9d-ddea2631ab78
|
50
|
+
variable_sip_network_ip: 172.16.150.1
|
51
|
+
variable_sip_network_port: 61230
|
52
|
+
variable_sip_received_ip: 172.16.150.1
|
53
|
+
variable_sip_received_port: 61230
|
54
|
+
variable_sip_via_protocol: udp
|
55
|
+
variable_sip_authorized: true
|
56
|
+
variable_sip_number_alias: 1000
|
57
|
+
variable_sip_auth_username: 1000
|
58
|
+
variable_sip_auth_realm: 172.16.150.131
|
59
|
+
variable_number_alias: 1000
|
60
|
+
variable_user_name: 1000
|
61
|
+
variable_domain_name: 172.16.150.131
|
62
|
+
variable_toll_allow: domestic,international,local
|
63
|
+
variable_accountcode: 1000
|
64
|
+
variable_user_context: default
|
65
|
+
variable_effective_caller_id_name: Extension%201000
|
66
|
+
variable_effective_caller_id_number: 1000
|
67
|
+
variable_outbound_caller_id_name: FreeSWITCH
|
68
|
+
variable_outbound_caller_id_number: 0000000000
|
69
|
+
variable_callgroup: techsupport
|
70
|
+
variable_record_stereo: true
|
71
|
+
variable_default_gateway: example.com
|
72
|
+
variable_default_areacode: 918
|
73
|
+
variable_transfer_fallback_extension: operator
|
74
|
+
variable_sip_from_user: 1000
|
75
|
+
variable_sip_from_uri: 1000%40172.16.150.131
|
76
|
+
variable_sip_from_host: 172.16.150.131
|
77
|
+
variable_sip_from_user_stripped: 1000
|
78
|
+
variable_sofia_profile_name: internal
|
79
|
+
variable_sip_full_via: SIP/2.0/UDP%20192.168.20.2%3A61230%3Brport%3D61230%3Bbranch%3Dz9hG4bKPjywNOvUq.sdNaFl0hFfl2j92kst3ChkRT%3Breceived%3D172.16.150.1
|
80
|
+
variable_sip_req_user: 502
|
81
|
+
variable_sip_req_uri: 502%40172.16.150.131
|
82
|
+
variable_sip_req_host: 172.16.150.131
|
83
|
+
variable_sip_to_user: 502
|
84
|
+
variable_sip_to_uri: 502%40172.16.150.131
|
85
|
+
variable_sip_to_host: 172.16.150.131
|
86
|
+
variable_sip_contact_user: 1000
|
87
|
+
variable_sip_contact_port: 61230
|
88
|
+
variable_sip_contact_uri: 1000%40192.168.20.2%3A61230
|
89
|
+
variable_sip_contact_host: 192.168.20.2
|
90
|
+
variable_channel_name: sofia/internal/1000%40172.16.150.131
|
91
|
+
variable_sip_user_agent: Telephone%200.14.3
|
92
|
+
variable_sip_via_host: 192.168.20.2
|
93
|
+
variable_sip_via_port: 61230
|
94
|
+
variable_sip_via_rport: 61230
|
95
|
+
variable_max_forwards: 70
|
96
|
+
variable_presence_id: 1000%40172.16.150.131
|
97
|
+
variable_switch_r_sdp: v%3D0%0D%0Ao%3D-%203476997537%203476997537%20IN%20IP4%20192.168.20.2%0D%0As%3Dpjmedia%0D%0Ac%3DIN%20IP4%20192.168.20.2%0D%0At%3D0%200%0D%0Aa%3DX-nat%3A0%0D%0Am%3Daudio%204010%20RTP/AVP%20103%20102%20104%20117%203%200%208%209%20101%0D%0Aa%3Drtpmap%3A103%20speex/16000%0D%0Aa%3Drtpmap%3A102%20speex/8000%0D%0Aa%3Drtpmap%3A104%20speex/32000%0D%0Aa%3Drtpmap%3A117%20iLBC/8000%0D%0Aa%3Dfmtp%3A117%20mode%3D30%0D%0Aa%3Drtpmap%3A3%20GSM/8000%0D%0Aa%3Drtpmap%3A0%20PCMU/8000%0D%0Aa%3Drtpmap%3A8%20PCMA/8000%0D%0Aa%3Drtpmap%3A9%20G722/8000%0D%0Aa%3Drtpmap%3A101%20telephone-event/8000%0D%0Aa%3Dfmtp%3A101%200-15%0D%0Aa%3Drtcp%3A4011%20IN%20IP4%20192.168.20.2%0D%0A
|
98
|
+
variable_sip_use_codec_name: GSM
|
99
|
+
variable_sip_use_codec_rate: 8000
|
100
|
+
variable_sip_use_codec_ptime: 20
|
101
|
+
variable_read_codec: GSM
|
102
|
+
variable_read_rate: 8000
|
103
|
+
variable_write_codec: GSM
|
104
|
+
variable_write_rate: 8000
|
105
|
+
variable_tts_engine: cepstral
|
106
|
+
variable_tts_voice: allison
|
107
|
+
variable_socket_host: 172.16.150.1
|
108
|
+
variable_sip_local_sdp_str: v%3D0%0Ao%3DFreeSWITCH%201267985798%201267985799%20IN%20IP4%20172.16.150.131%0As%3DFreeSWITCH%0Ac%3DIN%20IP4%20172.16.150.131%0At%3D0%200%0Am%3Daudio%2022934%20RTP/AVP%203%20101%0Aa%3Drtpmap%3A3%20GSM/8000%0Aa%3Drtpmap%3A101%20telephone-event/8000%0Aa%3Dfmtp%3A101%200-16%0Aa%3DsilenceSupp%3Aoff%20-%20-%20-%20-%0Aa%3Dptime%3A20%0Aa%3Dsendrecv%0A
|
109
|
+
variable_local_media_ip: 172.16.150.131
|
110
|
+
variable_local_media_port: 22934
|
111
|
+
variable_sip_use_pt: 3
|
112
|
+
variable_rtp_use_ssrc: 2898819412
|
113
|
+
variable_sip_nat_detected: true
|
114
|
+
variable_endpoint_disposition: ANSWER
|
115
|
+
variable_sip_to_tag: gDK3D6pXtFm3e
|
116
|
+
variable_sip_from_tag: elWUmu5et-auCRPijiEAiLkCgci1M67D
|
117
|
+
variable_sip_cseq: 14587
|
118
|
+
variable_sip_call_id: bjGKd9gAHJVCxd1uVy9knG.iFWn-n2h4
|
119
|
+
variable_sip_from_display: FreeSWITCH
|
120
|
+
variable_sip_full_from: %22FreeSWITCH%22%20%3Csip%3A1000%40172.16.150.131%3E%3Btag%3DelWUmu5et-auCRPijiEAiLkCgci1M67D
|
121
|
+
variable_sip_full_to: %3Csip%3A502%40172.16.150.131%3E%3Btag%3DgDK3D6pXtFm3e
|
122
|
+
variable_remote_media_ip_reported: 192.168.20.2
|
123
|
+
variable_remote_media_ip: 172.16.150.1
|
124
|
+
variable_remote_media_port_reported: 4010
|
125
|
+
variable_remote_media_port: 4010
|
126
|
+
variable_rtp_auto_adjust: true
|
127
|
+
variable_current_application: answer
|
128
|
+
Application: answer
|
129
|
+
Application-Response: _none_
|
130
|
+
DATA
|
131
|
+
}
|
@@ -64,9 +64,8 @@ describe Larynx::CallHandler do
|
|
64
64
|
end
|
65
65
|
|
66
66
|
it "should run app command before callback" do
|
67
|
-
call.speak('hello world').before
|
67
|
+
call.speak('hello world').before &should_be_called
|
68
68
|
call.send_response :execute
|
69
|
-
@callback.should be_true
|
70
69
|
end
|
71
70
|
|
72
71
|
it "should change state to executing" do
|
@@ -132,9 +131,19 @@ describe Larynx::CallHandler do
|
|
132
131
|
end
|
133
132
|
|
134
133
|
it "should fire global connect callback" do
|
135
|
-
|
134
|
+
with_global_callback(:connect, should_be_called) do
|
135
|
+
connect_call
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should subscribe to myevents" do
|
140
|
+
call.should_receive(:myevents)
|
141
|
+
connect_call
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should set event lingering on after filter events" do
|
145
|
+
call.should_receive(:linger)
|
136
146
|
connect_call
|
137
|
-
@callback.should be_true
|
138
147
|
end
|
139
148
|
|
140
149
|
it "should start the call session" do
|
@@ -146,39 +155,36 @@ describe Larynx::CallHandler do
|
|
146
155
|
call.send_response :channel_data
|
147
156
|
end
|
148
157
|
|
149
|
-
after do
|
150
|
-
Larynx.connect {|call|}
|
151
|
-
end
|
152
158
|
end
|
153
159
|
|
154
|
-
context "
|
160
|
+
context "#session_start" do
|
155
161
|
before do
|
156
162
|
call.queue = []
|
157
163
|
call.session = mock('session', :unique_id => '123')
|
164
|
+
call.response = mock('response', :header => {})
|
158
165
|
end
|
159
166
|
|
160
|
-
it "should subscribe to
|
167
|
+
it "should subscribe to events" do
|
161
168
|
call.should_receive(:myevents)
|
162
169
|
call.start_session
|
163
170
|
end
|
164
171
|
|
165
|
-
it "should
|
172
|
+
it "should execute linger command" do
|
166
173
|
call.should_receive(:linger)
|
167
174
|
call.start_session
|
168
|
-
call.send_response :reply_ok
|
169
175
|
end
|
170
176
|
|
171
|
-
it "should answer
|
177
|
+
it "should run answer command in connect callback be default" do
|
172
178
|
call.should_receive(:answer)
|
173
179
|
call.start_session
|
174
|
-
call.send_response :reply_ok
|
175
180
|
end
|
176
181
|
end
|
177
182
|
|
178
183
|
context "on answer" do
|
179
184
|
before do
|
180
|
-
call.queue
|
181
|
-
call.session
|
185
|
+
call.queue = []
|
186
|
+
call.session = mock('session', :unique_id => '123')
|
187
|
+
call.response = mock('response', :header => {})
|
182
188
|
end
|
183
189
|
|
184
190
|
it "should change state to ready" do
|
@@ -187,17 +193,13 @@ describe Larynx::CallHandler do
|
|
187
193
|
end
|
188
194
|
|
189
195
|
it "should fire global answer callback" do
|
190
|
-
|
191
|
-
|
192
|
-
|
196
|
+
with_global_callback(:answer, should_be_called) do
|
197
|
+
answer_call
|
198
|
+
end
|
193
199
|
end
|
194
200
|
|
195
201
|
def answer_call
|
196
|
-
call.
|
197
|
-
call.send_response :reply_ok
|
198
|
-
call.send_response :reply_ok
|
199
|
-
call.send_response :reply_ok
|
200
|
-
call.send_response :execute_complete
|
202
|
+
call.send_response :answered
|
201
203
|
end
|
202
204
|
end
|
203
205
|
|
@@ -288,13 +290,13 @@ describe Larynx::CallHandler do
|
|
288
290
|
context "timer" do
|
289
291
|
it "should add EM timer with name and timeout" do
|
290
292
|
Larynx::RestartableTimer.stub!(:new)
|
291
|
-
call.
|
293
|
+
call.add_timer(:test, 0.1)
|
292
294
|
call.timers[:test].should_not be_nil
|
293
295
|
end
|
294
296
|
|
295
297
|
it "should run callback on timeout" do
|
296
298
|
em do
|
297
|
-
call.
|
299
|
+
call.add_timer(:test, 0.2) { @callback = true; done }
|
298
300
|
end
|
299
301
|
@callback.should be_true
|
300
302
|
end
|
@@ -303,20 +305,18 @@ describe Larynx::CallHandler do
|
|
303
305
|
context "stop_timer" do
|
304
306
|
it "should run callback on timeout" do
|
305
307
|
em do
|
306
|
-
call.
|
308
|
+
call.add_timer(:test, 1, &should_be_called)
|
307
309
|
EM::Timer.new(0.1) { call.stop_timer :test; done }
|
308
310
|
end
|
309
|
-
@callback.should be_true
|
310
311
|
end
|
311
312
|
end
|
312
313
|
|
313
314
|
context "cancel_timer" do
|
314
315
|
it "should not run callback" do
|
315
316
|
em do
|
316
|
-
call.
|
317
|
+
call.add_timer(:test, 0.2, &should_not_be_called)
|
317
318
|
EM::Timer.new(0.1) { call.cancel_timer :test; done }
|
318
319
|
end
|
319
|
-
@callback.should_not be_true
|
320
320
|
end
|
321
321
|
end
|
322
322
|
|
@@ -325,7 +325,7 @@ describe Larynx::CallHandler do
|
|
325
325
|
start = Time.now
|
326
326
|
em do
|
327
327
|
EM::Timer.new(0.5) { call.restart_timer :test }
|
328
|
-
call.
|
328
|
+
call.add_timer(:test, 1) { done }
|
329
329
|
end
|
330
330
|
(Time.now-start).should be_close(1.5, 0.2)
|
331
331
|
end
|
data/spec/larynx/fields_spec.rb
CHANGED
@@ -1,40 +1,47 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
2
|
|
3
|
+
class TestApp < Larynx::Application; end
|
4
|
+
|
3
5
|
describe Larynx::Fields do
|
4
6
|
attr_reader :call, :app
|
5
7
|
|
6
8
|
before do
|
7
9
|
@call = TestCallHandler.new(1)
|
8
|
-
@app
|
10
|
+
@app = define_app.new(@call)
|
9
11
|
end
|
10
12
|
|
11
13
|
context 'module' do
|
12
|
-
|
14
|
+
before do
|
15
|
+
@app_class = define_app
|
16
|
+
end
|
13
17
|
|
14
18
|
it 'should add field class method' do
|
15
|
-
|
19
|
+
@app_class.should respond_to(:field)
|
16
20
|
end
|
17
21
|
|
18
22
|
it 'should add instance accessor for field name' do
|
19
|
-
|
20
|
-
|
23
|
+
@app_class.field(:guess) { prompt :speak => 'hello' }
|
24
|
+
@app_class.methods.include?(:guess)
|
21
25
|
end
|
22
26
|
end
|
23
27
|
|
24
28
|
context 'next_field' do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
+
before do
|
30
|
+
@app = define_app do
|
31
|
+
field(:field1) { prompt :speak => 'hello' }
|
32
|
+
field(:field2) { prompt :speak => 'hello' }
|
33
|
+
field(:field3) { prompt :speak => 'hello' }
|
34
|
+
end.new(call)
|
35
|
+
end
|
29
36
|
|
30
37
|
it 'should iterate over defined fields' do
|
31
|
-
next_field.name.should == :field1
|
32
|
-
next_field.name.should == :field2
|
33
|
-
next_field.name.should == :field3
|
38
|
+
app.next_field.name.should == :field1
|
39
|
+
app.next_field.name.should == :field2
|
40
|
+
app.next_field.name.should == :field3
|
34
41
|
end
|
35
42
|
|
36
43
|
it 'should jump to field name if supplied' do
|
37
|
-
next_field(:field2).name.should == :field2
|
44
|
+
app.next_field(:field2).name.should == :field2
|
38
45
|
end
|
39
46
|
end
|
40
47
|
|
@@ -52,6 +59,18 @@ describe Larynx::Fields do
|
|
52
59
|
fld.run app
|
53
60
|
end
|
54
61
|
|
62
|
+
it 'should pass timeout and length options to the prompt object' do
|
63
|
+
fld = field(:guess, :length => 1, :min_length => 1, :max_length => 2, :interdigit_timeout => 1, :timeout => 2) do
|
64
|
+
prompt :speak => 'first'
|
65
|
+
end
|
66
|
+
fld.run(app)
|
67
|
+
prompt = fld.current_prompt
|
68
|
+
prompt.interdigit_timeout.should == 1
|
69
|
+
prompt.timeout.should == 2
|
70
|
+
prompt.minimum_length.should == 1
|
71
|
+
prompt.maximum_length.should == 2
|
72
|
+
end
|
73
|
+
|
55
74
|
it 'should return same prompt all attempts if single prompt' do
|
56
75
|
fld = field(:guess) do
|
57
76
|
prompt :speak => 'first'
|
@@ -86,17 +105,6 @@ describe Larynx::Fields do
|
|
86
105
|
fld.current_prompt.message.should == 'second'
|
87
106
|
end
|
88
107
|
|
89
|
-
context 'valid_length?' do
|
90
|
-
it 'should be false if input size less than minimum' do
|
91
|
-
fld = field(:guess) do
|
92
|
-
prompt :speak => 'first'
|
93
|
-
end
|
94
|
-
fld.run app
|
95
|
-
fld.current_prompt.finalise
|
96
|
-
fld.valid_length?.should be_false
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
108
|
context 'input evaluation' do
|
101
109
|
it 'should run validate callback if input minimum length' do
|
102
110
|
call_me = should_be_called
|
@@ -205,4 +213,11 @@ describe Larynx::Fields do
|
|
205
213
|
@app.class.class_eval { attr_accessor name }
|
206
214
|
Larynx::Fields::Field.new(name, options, &block)
|
207
215
|
end
|
216
|
+
|
217
|
+
def define_app(&block)
|
218
|
+
reset_class(TestApp) do
|
219
|
+
include Larynx::Fields
|
220
|
+
instance_eval &block if block_given?
|
221
|
+
end
|
222
|
+
end
|
208
223
|
end
|
data/spec/larynx/prompt_spec.rb
CHANGED
@@ -85,17 +85,18 @@ describe Larynx::Prompt do
|
|
85
85
|
context "after callback" do
|
86
86
|
context "input completed" do
|
87
87
|
it "should not add timers if reached length" do
|
88
|
+
prompt = new_prompt
|
89
|
+
prompt.should_not_receive(:add_digit_timer)
|
90
|
+
prompt.should_not_receive(:add_input_timer)
|
88
91
|
call.input << '1'
|
89
|
-
|
90
|
-
call.should_not_receive(:timer).with(:input, anything())
|
91
|
-
after_callback new_prompt
|
92
|
+
after_callback prompt
|
92
93
|
end
|
93
94
|
|
94
95
|
it "should not add timers if termchar input" do
|
95
96
|
prompt = new_prompt(:speak => 'hello', :length => 2, :termchar => '#')
|
97
|
+
prompt.should_not_receive(:add_digit_timer)
|
98
|
+
prompt.should_not_receive(:add_input_timer)
|
96
99
|
call.input << '#'
|
97
|
-
call.should_not_receive(:timer).with(:digit, anything())
|
98
|
-
call.should_not_receive(:timer).with(:input, anything())
|
99
100
|
after_callback prompt
|
100
101
|
end
|
101
102
|
|
@@ -109,14 +110,15 @@ describe Larynx::Prompt do
|
|
109
110
|
|
110
111
|
context "input not completed" do
|
111
112
|
it "should add timers" do
|
112
|
-
|
113
|
-
|
114
|
-
|
113
|
+
prompt = new_prompt
|
114
|
+
prompt.should_receive(:add_digit_timer)
|
115
|
+
prompt.should_receive(:add_input_timer)
|
116
|
+
after_callback prompt
|
115
117
|
end
|
116
118
|
|
117
119
|
it "should add itself as call observer" do
|
118
120
|
prompt = new_prompt
|
119
|
-
call.stub!(:
|
121
|
+
call.stub!(:add_timer)
|
120
122
|
after_callback prompt
|
121
123
|
call.observers.should include(prompt)
|
122
124
|
end
|
@@ -142,15 +144,29 @@ describe Larynx::Prompt do
|
|
142
144
|
call.should_receive(:remove_observer).with(prompt)
|
143
145
|
prompt.finalise
|
144
146
|
end
|
147
|
+
|
148
|
+
it "should clear input" do
|
149
|
+
prompt = new_prompt
|
150
|
+
call.should_receive(:clear_input)
|
151
|
+
prompt.finalise
|
152
|
+
end
|
145
153
|
end
|
146
154
|
|
147
155
|
context "user callback" do
|
148
156
|
it "should be passed input argument equal to call input" do
|
149
|
-
prompt = new_prompt {|input| @callback =
|
157
|
+
prompt = new_prompt {|input| @callback = input }
|
150
158
|
call.input << '1'
|
151
159
|
after_callback prompt
|
152
160
|
@callback.should == '1'
|
153
161
|
end
|
162
|
+
|
163
|
+
it "should be passed input and result arguments if block arity is 2" do
|
164
|
+
prompt = new_prompt(:speak => '', :length => 1) {|input, result| @input = input; @result = result }
|
165
|
+
call.input << '1'
|
166
|
+
after_callback prompt
|
167
|
+
@input.should == '1'
|
168
|
+
@result.should be_true
|
169
|
+
end
|
154
170
|
end
|
155
171
|
|
156
172
|
context "interdigit timeout" do
|
data/spec/spec_helper.rb
CHANGED
@@ -11,8 +11,14 @@ LARYNX_LOGGER = Logger.new(STDOUT)
|
|
11
11
|
RESPONSES = {}
|
12
12
|
Dir['spec/fixtures/*.rb'].each {|file| require file }
|
13
13
|
|
14
|
+
Larynx.module_eval do
|
15
|
+
def self.callbacks
|
16
|
+
@callbacks
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
14
20
|
class TestCallHandler < Larynx::CallHandler
|
15
|
-
attr_accessor :sent_data, :session, :state, :queue, :input, :timers
|
21
|
+
attr_accessor :sent_data, :session, :response, :state, :queue, :input, :timers
|
16
22
|
|
17
23
|
def send_data(msg)
|
18
24
|
@sent_data = msg
|
@@ -40,6 +46,22 @@ module SpecHelper
|
|
40
46
|
proc.should_not_receive(:call).instance_eval(&(block || lambda {}))
|
41
47
|
lambda { |*args| proc.call(*args) }
|
42
48
|
end
|
49
|
+
|
50
|
+
def with_global_callback(name, test_callback)
|
51
|
+
default = Larynx.callbacks[name]
|
52
|
+
Larynx.send(name, &test_callback)
|
53
|
+
yield
|
54
|
+
Larynx.send(name, &default)
|
55
|
+
end
|
56
|
+
|
57
|
+
def reset_class(klass, &block)
|
58
|
+
name = klass.name.to_sym
|
59
|
+
Object.send(:remove_const, name)
|
60
|
+
eval "class #{klass}#{' < ' + klass.superclass.to_s if klass.superclass != Class}; end", TOPLEVEL_BINDING
|
61
|
+
new_klass = Object.const_get(name)
|
62
|
+
new_klass.class_eval &block if block_given?
|
63
|
+
new_klass
|
64
|
+
end
|
43
65
|
end
|
44
66
|
|
45
67
|
Spec::Runner.configure do |config|
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 1
|
8
|
-
-
|
9
|
-
version: 0.1.
|
8
|
+
- 3
|
9
|
+
version: 0.1.3
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Adam Meehan
|
@@ -14,7 +14,7 @@ autorequire: larynx
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-04-29 00:00:00 +10:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -73,7 +73,7 @@ dependencies:
|
|
73
73
|
version: 0.1.3
|
74
74
|
type: :development
|
75
75
|
version_requirements: *id004
|
76
|
-
description:
|
76
|
+
description: An evented application framework for the FreeSWITCH telephony platform
|
77
77
|
email: adam.meehan@gmail.com
|
78
78
|
executables:
|
79
79
|
- larynx
|
@@ -88,6 +88,7 @@ files:
|
|
88
88
|
- lib/larynx/application.rb
|
89
89
|
- lib/larynx/call_handler.rb
|
90
90
|
- lib/larynx/callbacks.rb
|
91
|
+
- lib/larynx/callbacks_with_async.rb
|
91
92
|
- lib/larynx/command.rb
|
92
93
|
- lib/larynx/commands.rb
|
93
94
|
- lib/larynx/fields.rb
|
@@ -101,6 +102,7 @@ files:
|
|
101
102
|
- lib/larynx/version.rb
|
102
103
|
- lib/larynx.rb
|
103
104
|
- spec/fixtures/answer.rb
|
105
|
+
- spec/fixtures/answered.rb
|
104
106
|
- spec/fixtures/break.rb
|
105
107
|
- spec/fixtures/channel_data.rb
|
106
108
|
- spec/fixtures/dtmf.rb
|
@@ -147,6 +149,6 @@ rubyforge_project: larynx
|
|
147
149
|
rubygems_version: 1.3.6
|
148
150
|
signing_key:
|
149
151
|
specification_version: 3
|
150
|
-
summary:
|
152
|
+
summary: An evented application framework for the FreeSWITCH telephony platform
|
151
153
|
test_files: []
|
152
154
|
|