voice_form 0.3.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.
@@ -0,0 +1,3 @@
1
+ require 'voice_form/form_methods'
2
+ require 'voice_form/form'
3
+ require 'voice_form/form_field'
@@ -0,0 +1,101 @@
1
+ module VoiceForm
2
+
3
+ class Form
4
+ include VoiceForm::FormMethods
5
+
6
+ attr_accessor :form_stack
7
+ attr_reader :current_field
8
+
9
+ def initialize(options={}, &block)
10
+ @options = options
11
+ @form_stack = []
12
+ @stack_index = 0
13
+
14
+ instance_eval(&block)
15
+ raise 'A form requires at least one field defined' if fields.empty?
16
+ end
17
+
18
+ def run(component)
19
+ @component = component
20
+
21
+ add_field_accessors
22
+ run_setup
23
+ run_form_stack
24
+ end
25
+
26
+ def setup(&block)
27
+ @setup = block
28
+ end
29
+
30
+ def do_block(&block)
31
+ form_stack << block
32
+ end
33
+
34
+ def goto(name)
35
+ index = field_index(name)
36
+ raise "goto failed: No form field found with name '#{name}'." unless index
37
+ @stack_index = index
38
+ end
39
+
40
+ def restart
41
+ @stack_index = 0
42
+ end
43
+
44
+ def exit
45
+ @exit = true
46
+ end
47
+
48
+ private
49
+
50
+ def run_setup
51
+ @component.instance_eval(&@setup) if @setup
52
+ end
53
+
54
+ def run_form_stack
55
+ while @stack_index < form_stack.size && !@exit do
56
+ slot = form_stack[@stack_index]
57
+ @stack_index += 1
58
+
59
+ if form_field?(slot)
60
+ @current_field = slot.name
61
+ slot.run(@component)
62
+ else
63
+ @current_field = nil
64
+ @component.instance_eval(&slot)
65
+ end
66
+ end
67
+ @stack_index = 0
68
+ @current_field = nil
69
+ end
70
+
71
+ def add_field_accessors
72
+ return if @accessors_added
73
+
74
+ fields.keys.each do |field_name|
75
+ @component.class.class_eval do
76
+ attr_accessor field_name
77
+ end
78
+ end
79
+
80
+ @accessors_added = true
81
+ end
82
+
83
+ def form_field?(slot)
84
+ slot.is_a?(VoiceForm::FormField)
85
+ end
86
+
87
+ def fields
88
+ @fields ||= form_stack.inject({}) do |flds,s|
89
+ flds[s.name] = s if form_field?(s)
90
+ flds
91
+ end
92
+ end
93
+
94
+ def field_index(field)
95
+ form_stack.each_with_index {|slot, i|
96
+ return i if form_field?(slot) && slot.name == field.to_sym
97
+ }
98
+ end
99
+ end
100
+
101
+ end
@@ -0,0 +1,209 @@
1
+ module VoiceForm
2
+
3
+ class FormField
4
+ cattr_accessor :default_prompt_options
5
+ attr_reader :name
6
+
7
+ self.default_prompt_options = { :bargein => true, :timeout => 5 }
8
+
9
+ def initialize(name, options, component, &block)
10
+ @name, @options, @component = name, options, component
11
+ @options.reverse_merge!(:attempts => 5, :call => 'call')
12
+ @callbacks = {}
13
+ @prompt_queue = []
14
+
15
+ instance_eval(&block)
16
+ raise 'A field requires a prompt to be defined' if @prompt_queue.empty?
17
+ end
18
+
19
+ def prompt(options)
20
+ add_prompt(options.reverse_merge(self.class.default_prompt_options))
21
+ end
22
+
23
+ def reprompt(options)
24
+ raise 'A reprompt can only be used after a prompt' if @prompt_queue.empty?
25
+ add_prompt(options.reverse_merge(self.class.default_prompt_options))
26
+ end
27
+
28
+ def setup(&block)
29
+ @callbacks[:setup] = block
30
+ end
31
+
32
+ def validate(&block)
33
+ @callbacks[:validate] = block
34
+ end
35
+
36
+ def invalid(&block)
37
+ @callbacks[:invalid] = block
38
+ end
39
+
40
+ def timeout(&block)
41
+ @callbacks[:timeout] = block
42
+ end
43
+
44
+ def success(&block)
45
+ @callbacks[:success] = block
46
+ end
47
+
48
+ def failure(&block)
49
+ @callbacks[:failure] = block
50
+ end
51
+
52
+ def confirm(options={}, &block)
53
+ options.reverse_merge!(
54
+ self.class.default_prompt_options.merge(
55
+ :attempts => 3,
56
+ :accept => 1,
57
+ :reject => 2
58
+ )
59
+ )
60
+ @confirmation_options = options.merge(:message => block)
61
+ end
62
+
63
+ def run(component=nil)
64
+ @component = component if component
65
+
66
+ run_callback(:setup)
67
+
68
+ result = 1.upto(@options[:attempts]) do |attempt|
69
+ @value = get_input(prompt_for_attempt(attempt))
70
+
71
+ unless valid_length?
72
+ run_callback(:timeout)
73
+ next
74
+ end
75
+
76
+ if input_valid?
77
+ if value_confirmed?
78
+ break 0
79
+ else
80
+ next
81
+ end
82
+ else
83
+ run_callback(:invalid)
84
+ end
85
+ end
86
+ if result == 0
87
+ run_callback(:success)
88
+ else
89
+ run_callback(:failure)
90
+ end
91
+ set_component_value @value
92
+ end
93
+
94
+ private
95
+
96
+ def get_input(prompt)
97
+ method = prompt[:method]
98
+ message = prompt[:message]
99
+
100
+ if prompt[:bargein]
101
+ prompt[method] = message
102
+ else
103
+ call.send(method, message)
104
+ end
105
+
106
+ args = [ prompt.slice(method, :timeout, :accept_key) ]
107
+ args.unshift(prompt[:length]) if prompt[:length]
108
+ call.input(*args)
109
+ end
110
+
111
+ def input_valid?
112
+ run_callback(:validate)
113
+ end
114
+
115
+ def valid_length?
116
+ !@value.empty? &&
117
+ @value.size >= minimum_length &&
118
+ @value.size <= maximum_length
119
+ end
120
+
121
+ def value_confirmed?
122
+ return true unless @confirmation_options
123
+
124
+ prompt = evaluate_prompt(@confirmation_options)
125
+ prompt[:method] = prompt[:message].is_a?(Array) ? :play : :speak
126
+ prompt[:length] = [ prompt[:accept].to_s.size, prompt[:reject].to_s.size ].max
127
+
128
+ 1.upto(prompt[:attempts]) do |attempt|
129
+ case get_input(prompt)
130
+ when prompt[:accept].to_s
131
+ return true
132
+ when prompt[:reject].to_s
133
+ return false
134
+ else
135
+ next
136
+ end
137
+ end
138
+ false
139
+ end
140
+
141
+ def run_callback(callback)
142
+ if block = @callbacks[callback]
143
+ set_component_value @value
144
+ result = @component.instance_eval(&block)
145
+ @value = get_component_value
146
+ result
147
+ else
148
+ true
149
+ end
150
+ end
151
+
152
+ def set_component_value(value)
153
+ @component.send("#{@name}=", @value)
154
+ end
155
+
156
+ def get_component_value
157
+ @component.send(@name)
158
+ end
159
+
160
+ def minimum_length
161
+ @options[:min_length] || @options[:length] || 1
162
+ end
163
+
164
+ def maximum_length
165
+ @options[:max_length] || @options[:length] || @value.size
166
+ end
167
+
168
+ def call
169
+ @call ||= @component.send(@options[:call])
170
+ end
171
+
172
+ def add_prompt(options)
173
+ method = options.has_key?(:play) ? :play : :speak
174
+ options[:message] = options.delete(method)
175
+ options[:method] = method
176
+ options[:length] = @options[:length] || @options[:max_length]
177
+
178
+ repeats = options[:repeats] || 1
179
+ @prompt_queue += ([options] * repeats)
180
+ end
181
+
182
+ def prompt_for_attempt(attempt)
183
+ prompt = if attempt == 1 || @prompt_queue.size == 1 then
184
+ @prompt_queue.first
185
+ else
186
+ @prompt_queue[attempt-1] || @prompt_queue.last
187
+ end
188
+ evaluate_prompt(prompt)
189
+ end
190
+
191
+ def evaluate_prompt(prompt)
192
+ options = prompt.dup
193
+ message = options[:message]
194
+
195
+ message = case message
196
+ when String, Array
197
+ message
198
+ when Symbol
199
+ @component.send(message)
200
+ when Proc
201
+ @component.instance_eval(&message)
202
+ end
203
+
204
+ options[:message] = message
205
+ options
206
+ end
207
+ end
208
+
209
+ end
@@ -0,0 +1,69 @@
1
+ module VoiceForm
2
+
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.class_eval do
6
+ include FormMethods
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ def voice_form(options={}, &block)
13
+ raise "Voice form requires block" unless block_given?
14
+
15
+ self.class_eval do
16
+ include InstanceMethods
17
+
18
+ cattr_accessor :voice_form_options
19
+ attr_accessor :form, :call
20
+ end
21
+
22
+ self.voice_form_options = [options, block]
23
+ end
24
+
25
+ def start_voice_form(call)
26
+ raise "No voice form defined" unless voice_form_options
27
+ self.new.start_voice_form(call)
28
+ end
29
+
30
+ end
31
+
32
+ module InstanceMethods
33
+
34
+ def start_voice_form(call)
35
+ raise "No voice form defined" unless self.class.voice_form_options
36
+ options, block = *self.class.voice_form_options
37
+ @call = call
38
+ self.form = VoiceForm::Form.new(options, &block)
39
+ self.form.run(self)
40
+ end
41
+
42
+ def as_digits(string)
43
+ string.scan(/\d/).map {|v| v.to_i }
44
+ end
45
+ end
46
+
47
+ module FormMethods
48
+
49
+ # Can be used in a form or stand-alone in a component method
50
+ def field(field_name, options={}, &block)
51
+ raise "A field requires a block" unless block_given?
52
+
53
+ form_field = VoiceForm::FormField.new(field_name, options, self, &block)
54
+
55
+ if self.is_a?(VoiceForm::Form)
56
+ self.form_stack << form_field
57
+ else
58
+ unless self.respond_to?(field_name)
59
+ self.class.class_eval do
60
+ attr_accessor field_name
61
+ end
62
+ end
63
+ form_field.run
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ end
@@ -0,0 +1,303 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe VoiceForm::FormField do
4
+ include VoiceForm::FormMethods
5
+
6
+ attr_accessor :call, :my_field
7
+
8
+ before do
9
+ @call = mock('Call', :play => nil, :speak => nil)
10
+ @call.stub!(:input).with(any_args).and_return('')
11
+ end
12
+
13
+ it "should define accessor for field in component" do
14
+ field(:my_field) do
15
+ prompt :play => 'test'
16
+ end
17
+ self.methods.include?(:my_field)
18
+ end
19
+
20
+ it "should raise error if no prompts defined" do
21
+ lambda {
22
+ form_field(:my_field) {}
23
+ }.should raise_error
24
+ end
25
+
26
+ it "should raise error if reprompt defined before prompt" do
27
+ lambda {
28
+ form_field(:my_field) do
29
+ reprompt :play => 'again'
30
+ end
31
+ }.should raise_error
32
+ end
33
+
34
+ it "should return same prompt for for all attempts if single prompt" do
35
+ item = form_field(:my_field) do
36
+ prompt :play => "first"
37
+ end
38
+ item.send(:prompt_for_attempt, 1)[:message].should == 'first'
39
+ item.send(:prompt_for_attempt, 2)[:message].should == 'first'
40
+ end
41
+
42
+ it "should return reprompt for subsequent prompts" do
43
+ item = form_field(:my_field) do
44
+ prompt :play => "first"
45
+ reprompt :play => 'next'
46
+ end
47
+ item.send(:prompt_for_attempt, 1)[:message].should == 'first'
48
+ item.send(:prompt_for_attempt, 2)[:message].should == 'next'
49
+ item.send(:prompt_for_attempt, 3)[:message].should == 'next'
50
+ end
51
+
52
+ it "should return prompt for given number of repeats before subsequent prompts" do
53
+ item = form_field(:my_field) do
54
+ prompt :play => "first", :repeats => 2
55
+ reprompt :play => 'next'
56
+ end
57
+ item.send(:prompt_for_attempt, 1)[:message].should == 'first'
58
+ item.send(:prompt_for_attempt, 2)[:message].should == 'first'
59
+ item.send(:prompt_for_attempt, 3)[:message].should == 'next'
60
+ end
61
+
62
+ it "should set input value in component" do
63
+ item = form_field(:my_field, :length => 3) do
64
+ prompt :play => "first"
65
+ end
66
+ call.stub!(:input).and_return('123')
67
+ item.run
68
+
69
+ my_field.should == '123'
70
+ end
71
+
72
+ it "should run setup callback once" do
73
+ call_me = i_should_be_called
74
+ item = form_field(:my_field, :attempts => 3) do
75
+ prompt :play => "first"
76
+ setup { call_me.call }
77
+ end
78
+ call.should_receive(:input).and_return('')
79
+
80
+ item.run
81
+ end
82
+
83
+ it "should run timeout callback if no input" do
84
+ call_me = i_should_be_called
85
+ item = form_field(:my_field, :attempts => 1) do
86
+ prompt :play => "first"
87
+ timeout { call_me.call }
88
+ end
89
+ call.should_receive(:input).and_return('')
90
+
91
+ item.run
92
+ end
93
+
94
+ it "should run timeout callback if input not valid length" do
95
+ call_me = i_should_be_called
96
+ item = form_field(:my_field, :attempts => 1, :length => 3) do
97
+ prompt :play => "first"
98
+ timeout { call_me.call }
99
+ end
100
+ call.should_receive(:input).and_return('12')
101
+
102
+ item.run
103
+ end
104
+
105
+ it "should make all attempts to get valid input" do
106
+ item = form_field(:my_field) do
107
+ prompt :play => "first"
108
+ end
109
+ call.should_receive(:input).exactly(3).times
110
+
111
+ item.run
112
+ end
113
+
114
+ it "should make one attempt if input is valid" do
115
+ item = form_field(:my_field) do
116
+ prompt :play => "first"
117
+ end
118
+ item.stub!(:input_valid?).and_return(true)
119
+ call.should_receive(:input).once
120
+
121
+ item.run
122
+ end
123
+
124
+ it "should check if input_valid?" do
125
+ item = form_field(:my_field, :length => 3) do
126
+ prompt :play => "first"
127
+ end
128
+ call.should_receive(:input).and_return('123')
129
+ item.should_receive(:input_valid?)
130
+
131
+ item.run
132
+ end
133
+
134
+ it "should run validation callback if defined" do
135
+ call_me = i_should_be_called
136
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
137
+ prompt :play => "first"
138
+ validate { call_me.call }
139
+ end
140
+ call.stub!(:input).and_return('123')
141
+
142
+ item.run
143
+ end
144
+
145
+ it "should run confirm callback if defined" do
146
+ call_me = i_should_be_called
147
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
148
+ prompt :play => "first"
149
+ confirm { call_me.call; [] }
150
+ end
151
+ call.stub!(:input).and_return('123')
152
+
153
+ item.run
154
+ end
155
+
156
+ describe "confirm callback" do
157
+
158
+ it "should not run if not valid input" do
159
+ dont_call_me = i_should_not_be_called
160
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
161
+ prompt :play => "first"
162
+ validate { false }
163
+ confirm { dont_call_me.call }
164
+ end
165
+ call.stub!(:input).and_return('123')
166
+
167
+ item.run
168
+ end
169
+
170
+ it "should be run the number of attempts if no valid response" do
171
+ call.should_receive(:input).with(3, anything).and_return('123')
172
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
173
+ prompt :play => "first"
174
+
175
+ confirm(:attempts => 3) { [] }
176
+ end
177
+ call.should_receive(:input).with(1, anything).exactly(3).times.and_return('')
178
+
179
+ item.run
180
+ end
181
+
182
+ it "should run success callback if accept value entered" do
183
+ call_me = i_should_be_called
184
+ call.should_receive(:input).with(3, anything).and_return('123')
185
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
186
+ prompt :play => "first"
187
+
188
+ success { call_me.call }
189
+ confirm(:accept => '1', :reject => '2') { [] }
190
+ end
191
+ call.should_receive(:input).with(1, anything).and_return('1')
192
+
193
+ item.run
194
+ end
195
+
196
+ it "should run failure callback if reject value entered" do
197
+ call_me = i_should_be_called
198
+ call.should_receive(:input).with(3, anything).and_return('123')
199
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
200
+ prompt :play => "first"
201
+
202
+ failure { call_me.call }
203
+ confirm(:accept => '1', :reject => '2') { [] }
204
+ end
205
+ call.should_receive(:input).with(1, anything).and_return('2')
206
+
207
+ item.run
208
+ end
209
+
210
+ it "should play confirmation input prompt if confirm block return value is array" do
211
+ call_me = i_should_be_called
212
+ call.should_receive(:input).with(3, anything).and_return('123')
213
+
214
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
215
+ prompt :play => "first"
216
+
217
+ success { call_me.call }
218
+ confirm(:timeout => 3) { [] }
219
+ end
220
+ call.should_receive(:input).with(1, {:play => [], :timeout => 3}).and_return('1')
221
+
222
+ item.run
223
+ end
224
+
225
+ it "should speak confirmation input prompt if confirm block return value is string" do
226
+ call_me = i_should_be_called
227
+ call.should_receive(:input).with(3, anything).and_return('123')
228
+
229
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
230
+ prompt :play => "first"
231
+
232
+ success { call_me.call }
233
+ confirm(:timeout => 3) { 'speak me' }
234
+ end
235
+ call.should_receive(:input).with(1, {:speak => 'speak me', :timeout => 3}).and_return('1')
236
+
237
+ item.run
238
+ end
239
+ end
240
+
241
+ it "should run failure callback if no input" do
242
+ call_me = i_should_be_called
243
+ item = form_field(:my_field, :length => 3) do
244
+ prompt :play => "first"
245
+ failure { call_me.call }
246
+ end
247
+ call.should_receive(:input).and_return('')
248
+
249
+ item.run
250
+ end
251
+
252
+ it "should run success callback if input valid length" do
253
+ call_me = i_should_be_called
254
+ item = form_field(:my_field, :length => 3) do
255
+ prompt :play => "first"
256
+ success { call_me.call }
257
+ end
258
+ call.should_receive(:input).and_return('123')
259
+
260
+ item.run
261
+ end
262
+
263
+ it "should run success callback if input valid length and valid input" do
264
+ validate_me = i_should_be_called
265
+ call_me = i_should_be_called
266
+ item = form_field(:my_field, :length => 3) do
267
+ prompt :play => "first"
268
+ validate do
269
+ validate_me.call
270
+ my_field.to_i > 100
271
+ end
272
+ success { call_me.call }
273
+ end
274
+ call.should_receive(:input).and_return('123')
275
+
276
+ item.run
277
+ end
278
+
279
+ it "should execute input with message when bargein is true" do
280
+ item = form_field(:my_field, :length => 3) do
281
+ prompt :play => "first", :bargein => true
282
+ end
283
+ call.should_not_receive(:play)
284
+ call.should_receive(:input).with(3, :play => "first", :timeout => 5)
285
+
286
+ item.run
287
+ end
288
+
289
+ it "should execute playback method with message and input without message when bargein is false" do
290
+ item = form_field(:my_field, :length => 3) do
291
+ prompt :play => "first", :bargein => false
292
+ end
293
+ call.should_receive(:play).with('first')
294
+ call.should_receive(:input).with(3, :timeout => 5)
295
+
296
+ item.run
297
+ end
298
+
299
+ def form_field(field, options={}, &block)
300
+ self.class.class_eval { attr_accessor field }
301
+ VoiceForm::FormField.new(field, {:attempts => 3}.merge(options), self, &block)
302
+ end
303
+ end