adzap-voice_form 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ == 0.1.1 2008-11-12
2
+ * refactored to auto include voice_form method in components, no more 'include VoiceForm'
3
+
4
+ == 0.1.0 2008-11-11
5
+ * Gemified using newgem
6
+
7
+ == 2008-11-11
8
+ * Added form field confirm callback to enable confirmation of input
9
+
10
+ == 2008-11-04
11
+ * Changed form.exit_form to form.exit
12
+
13
+ == 2008-11-03
14
+ * First commit
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Adam Meehan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,150 @@
1
+ #VoiceForm
2
+
3
+ A plugin for Adhearsion to add form functionality and flow, similar to VoiceXML style forms.
4
+
5
+ By Adam Meehan (adam.meehan@gmail.com, [http://duckpunching.com/](http://duckpunching.com/))
6
+
7
+ Released under the MIT license.
8
+
9
+ ##Introduction
10
+
11
+ After developing VoiceXML (VXML) apps for quite a while and then trying Adhearsion, I found I missed
12
+ the VXML form element flow when writing components. Given that most interactions with an IVR system
13
+ are simply prompt, input, validate and, reprompt or go the next field. This flow has been nicely
14
+ distilled into the VXML form element and its child elements. The problem with VXML is that you are
15
+ using XML in a programmatic way, yuck! Also you are not using Ruby, so you miss out on its awesomeness.
16
+
17
+ The plugin attempts to emulate some of the VXML form flow for use in your Adhearsion components.
18
+
19
+
20
+ ##Install
21
+
22
+ sudo gem install adzap-voice_form --source=http://gems.github.com/
23
+
24
+ At the bottom your projects startup.rb file put
25
+
26
+ require 'voice_form'
27
+
28
+ ##Example
29
+
30
+ I use the **speak** command in this example to give better context. The speak command is for TTS
31
+ and is currently disabled in Adhearsion. In your own application you can just use the **play**
32
+ command to play your sound files.
33
+
34
+ class MyComponent
35
+ include VoiceForm
36
+ add_call_context :as => :call_context
37
+
38
+ voice_form do
39
+
40
+ field(:age, :max_length => 3, :attempts => 3) do
41
+ prompt :speak => "Please enter your age", :timeout => 2
42
+ reprompt :speak => "Enter your age in years", :timeout => 2
43
+
44
+ setup do
45
+ @max_age = 110
46
+ end
47
+
48
+ timeout do
49
+ call_context.speak "You did not enter anything. Try again."
50
+ end
51
+
52
+ validate do
53
+ @age.to_i <= @max_age
54
+ end
55
+
56
+ invalid do
57
+ call_context.speak "Your age must be less than #{@max_age}. Try again."
58
+ end
59
+
60
+ success do
61
+ call_context.speak "You are #{@age} years old."
62
+ end
63
+
64
+ failure do
65
+ call_context.speak "You could not enter your age. Thats a bad sign."
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+
72
+ In your dialplan:
73
+
74
+ general {
75
+ my_component = new_my_component
76
+ my_component.start_voice_form
77
+ }
78
+
79
+ That covers most of the functionality, and hopefully it makes sense pretty much straight
80
+ away.
81
+
82
+ You don't have to start the form from the dialplan, but it makes it simple. You could start it from
83
+ within a component method.
84
+
85
+ All blocks (setup, validate, do_block etc.) are evaluated in the component scope so you can use
86
+ component methods and instance variables in them and they will work.
87
+
88
+ For a more complete example see the examples folder.
89
+
90
+ ### voice_form
91
+
92
+ The flow of the form works like a stack. So each field and do_block are executed in order until the
93
+ end of the form is reached. You can jump around the stack by using `form.goto :field_name` which
94
+ will move the *stack pointer* to the field after the current field is completed and move forward
95
+ through the form stack from that point, regardless whether a field has already been completed.
96
+
97
+ You can also use `form.restart` to start the form over from the beginning.
98
+
99
+ The form setup block is only run once and is not executed again, even with a `form.restart`.
100
+
101
+ The `voice_form` method takes only one option
102
+
103
+ - :call_context - to nominate the call context method if other than call context
104
+
105
+
106
+ ### field
107
+
108
+ This defines the field the with name given to collect on the form. The field method can be used
109
+ in a `voice_form` or on its own inside a component method.
110
+
111
+ The options available are:
112
+
113
+ - :length - the number of digits to accept
114
+ - :min_length - minimum number of digits to accept
115
+ - :max_length - maximum number of digits to accept
116
+ - :attempts - number of tries to get a valid input
117
+ - :call_context - the method name for the call context if other than 'call_context'
118
+
119
+ All fields defined get an accessor method defined of the same name in the component class.
120
+ This means you can access its value using the instance variable or the accessor method inside any of
121
+ the field callbacks and in other fields on a form.
122
+
123
+ The `prompt` and `reprompt` methods are a wrapper around the input command. And as such is always
124
+ interruptable or you can _bargein_ when you want to starting keying numbers. You pass in a
125
+ hash of options to control the prompt such as:
126
+
127
+ - :play - play one or more sound files
128
+ - :speak - play TTS text (needs my Adhearsion hack for speak in input command)
129
+ - :timeout - number of seconds to wait for input
130
+ - :repeats - number of attempts to use this prompt until the next one is used
131
+
132
+ The length expected for the input is taken from the options passed to the `field` method.
133
+
134
+ You can only use one of :play or :speak.
135
+
136
+ There can only be one `prompt` but you can have multiple `reprompt`s. When you add a reprompt it changes
137
+ what the prompt is if there is input the first time or the input is invalid.
138
+
139
+
140
+ TODO: Add specific info for callback and option.
141
+
142
+
143
+ TODO: More docs
144
+
145
+
146
+ ##Credits
147
+
148
+ Adam Meehan (adam.meehan@gmail.com, [http://duckpunching.com/](http://duckpunching.com/))
149
+
150
+ Also thanks to Jay Phillips for his brilliant work on Adhearsion ([http://adhearsion.com](http://adhearsion.com)).
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rubygems/specification'
4
+ require 'date'
5
+ require 'spec/rake/spectask'
6
+
7
+ GEM = "voice_form"
8
+ GEM_VERSION = "0.1.0"
9
+ AUTHOR = "Adam Meehan"
10
+ EMAIL = "adam.meehan@gmail.com"
11
+ HOMEPAGE = "http://github.com/adzap/voice_form"
12
+ SUMMARY = "A DSL for Adhearsion to create forms in the style of the VoiceXML form element."
13
+
14
+ spec = Gem::Specification.new do |s|
15
+ s.name = GEM
16
+ s.version = GEM_VERSION
17
+ s.platform = Gem::Platform::RUBY
18
+ s.has_rdoc = false
19
+ s.summary = SUMMARY
20
+ s.description = s.summary
21
+ s.author = AUTHOR
22
+ s.email = EMAIL
23
+ s.homepage = HOMEPAGE
24
+ s.extra_rdoc_files = ["History.txt"]
25
+ # Uncomment this to add a dependency
26
+ # s.add_dependency "foo"
27
+
28
+ s.require_path = 'lib'
29
+ s.autorequire = GEM
30
+ s.files = %w(MIT-LICENSE README.markdown Rakefile) + Dir.glob("{lib,spec,examples}/**/*")
31
+ end
32
+
33
+ task :default => :spec
34
+
35
+ desc "Run specs"
36
+ Spec::Rake::SpecTask.new do |t|
37
+ t.spec_files = FileList['spec/**/*_spec.rb']
38
+ t.spec_opts = %w(-fs --color)
39
+ end
40
+
41
+ Rake::GemPackageTask.new(spec) do |pkg|
42
+ pkg.gem_spec = spec
43
+ end
44
+
45
+ desc "install the gem locally"
46
+ task :install => [:package] do
47
+ sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
48
+ end
49
+
50
+ desc "create a gemspec file"
51
+ task :make_spec do
52
+ File.open("#{GEM}.gemspec", "w") do |file|
53
+ file.puts spec.to_ruby
54
+ end
55
+ end
@@ -0,0 +1,57 @@
1
+ class MyComponent
2
+ include VoiceForm
3
+ add_call_context :as => :call_context
4
+
5
+ voice_form do
6
+ field(:age, :max_length => 3, :attempts => 3) do
7
+ prompt :speak => "Please enter your age", :timeout => 2, :repeats => 2
8
+ reprompt :speak => "Enter your age in years", :timeout => 2
9
+
10
+ setup { @max_age = 110 }
11
+
12
+ validate { @age.to_i < @max_age }
13
+
14
+ invalid do
15
+ call_context.speak "Your age must be less than #{@max_age}. Try again."
16
+ end
17
+
18
+ success do
19
+ call_context.speak "You are #{@age} years old."
20
+ end
21
+
22
+ failure do
23
+ call_context.speak "You could not enter your age. Thats a bad sign."
24
+ end
25
+ end
26
+
27
+ do_block do
28
+ call_context.speak "Get ready for the next question."
29
+ end
30
+
31
+ field(:postcode, :length => 4, :attempts => 5) do
32
+ prompt :speak => "Please enter your 4 digit post code", :timeout => 3
33
+
34
+ validate { @postcode[0..0] != '0' }
35
+
36
+ invalid do
37
+ if @postcode.size < 4
38
+ call_context.speak "Your postcode must 4 digits."
39
+ else
40
+ call_context.speak "Your postcode cannot start with a 0."
41
+ end
42
+ end
43
+
44
+ success do
45
+ call_context.speak "Your postcode is #{@postcode.scan(/\d/).join(', ')}."
46
+ end
47
+
48
+ failure do
49
+ if @age.empty?
50
+ call_context.speak "Lets start over shall we."
51
+ form.restart
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,11 @@
1
+ require 'voice_form/form_methods'
2
+ require 'voice_form/form'
3
+ require 'voice_form/form_field'
4
+
5
+ Adhearsion::Components::Behavior.module_eval do
6
+ include VoiceForm::FormMethods
7
+ end
8
+
9
+ Adhearsion::Components::Behavior::ClassMethods.module_eval do
10
+ include VoiceForm::MacroMethods
11
+ end
@@ -0,0 +1,106 @@
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={})
10
+ @options = options
11
+
12
+ @form_stack = []
13
+ @stack_index = 0
14
+ end
15
+
16
+ def run(component)
17
+ @component = component
18
+
19
+ alias_call_context
20
+
21
+ add_field_accessors
22
+
23
+ run_setup
24
+
25
+ run_form_stack
26
+ end
27
+
28
+ def setup(&block)
29
+ @setup = block
30
+ end
31
+
32
+ def do_block(&block)
33
+ form_stack << block
34
+ end
35
+
36
+ def goto(name)
37
+ index = nil
38
+ form_stack.each_with_index {|slot, i|
39
+ index = i and break if form_field?(slot) && slot.name == name
40
+ }
41
+ raise "goto failed: No form field found with name '#{name}'." unless index
42
+ @stack_index = index
43
+ end
44
+
45
+ def restart
46
+ @stack_index = 0
47
+ end
48
+
49
+ def exit
50
+ @exit_form = true
51
+ end
52
+
53
+ private
54
+
55
+ def run_setup
56
+ @component.instance_eval(&@setup) if @setup
57
+ end
58
+
59
+ def run_form_stack
60
+ while @stack_index < form_stack.size do
61
+ slot = form_stack[@stack_index]
62
+ @stack_index += 1
63
+
64
+ if form_field?(slot)
65
+ @current_field = slot.name
66
+ slot.run(@component)
67
+ else
68
+ @current_field = nil
69
+ @component.instance_eval(&slot)
70
+ end
71
+
72
+ break if @exit_form
73
+ end
74
+ @stack_index = 0
75
+ @current_field = nil
76
+ end
77
+
78
+ def add_field_accessors
79
+ return if @accessors_added
80
+
81
+ form_stack.each do |field|
82
+ next unless form_field?(field)
83
+ @component.class.class_eval do
84
+ attr_accessor field.name
85
+ end
86
+ end
87
+
88
+ @accessors_added = true
89
+ end
90
+
91
+ def form_field?(slot)
92
+ slot.is_a?(VoiceForm::FormField)
93
+ end
94
+
95
+ def alias_call_context
96
+ # hack to avoid setting the call_context in each field for different context name
97
+ if context_name = @options[:call_context] && !@component.respond_to?(:call_context)
98
+ @component.class_eval do
99
+ alias_method :call_context, context_name
100
+ end
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,184 @@
1
+ module VoiceForm
2
+
3
+ class FormField
4
+ attr_reader :name
5
+ attr_accessor :prompts
6
+
7
+ def initialize(name, options, component)
8
+ @name, @options, @component = name, options, component
9
+ @options.reverse_merge!(:attempts => 5, :call_context => 'call_context')
10
+ @callbacks = {}
11
+ @prompts = []
12
+ end
13
+
14
+ def prompt(options)
15
+ add_prompts(options.reverse_merge(:timeout => 5))
16
+ end
17
+
18
+ def reprompt(options)
19
+ raise 'A reprompt can only be used after a prompt' if @prompts.empty?
20
+ add_prompts(options.reverse_merge(:timeout => 5))
21
+ end
22
+
23
+ def setup(&block)
24
+ @callbacks[:setup] = block
25
+ end
26
+
27
+ def validate(&block)
28
+ @callbacks[:validate] = block
29
+ end
30
+
31
+ def invalid(&block)
32
+ @callbacks[:invalid] = block
33
+ end
34
+
35
+ def timeout(&block)
36
+ @callbacks[:timeout] = block
37
+ end
38
+
39
+ def success(&block)
40
+ @callbacks[:success] = block
41
+ end
42
+
43
+ def failure(&block)
44
+ @callbacks[:failure] = block
45
+ end
46
+
47
+ def confirm(options={}, &block)
48
+ options.reverse_merge!(
49
+ :attempts => 3,
50
+ :accept => 1,
51
+ :reject => 2,
52
+ :timeout => 3
53
+ )
54
+ @confirmation_options = options.merge(:block => block)
55
+ end
56
+
57
+ def run(component=nil)
58
+ @component = component if component
59
+
60
+ set_component_value('')
61
+
62
+ run_callback(:setup)
63
+
64
+ result = 1.upto(@options[:attempts]) do |attempt|
65
+ if get_input(attempt).empty?
66
+ run_callback(:timeout)
67
+ next
68
+ end
69
+
70
+ set_component_value @value
71
+
72
+ if input_valid?
73
+ if value_confirmed?
74
+ break 0
75
+ else
76
+ next
77
+ end
78
+ else
79
+ run_callback(:invalid)
80
+ end
81
+ end
82
+ if result == 0
83
+ run_callback(:success)
84
+ else
85
+ run_callback(:failure)
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def prompt_for_attempt(attempt)
92
+ prompt = if attempt == 1 || @prompts.size == 1 then
93
+ @prompts.first
94
+ else
95
+ @prompts[attempt-1] || @prompts.last
96
+ end
97
+ evaluate_prompt(prompt)
98
+ end
99
+
100
+ def get_input(attempt)
101
+ input_options = @options.dup
102
+ input_options.merge!(prompt_for_attempt(attempt))
103
+ args = [ input_options ]
104
+ length = input_options.delete(:length) || input_options.delete(:max_length)
105
+ args.unshift(length) if length
106
+ @value = call_context.input(*args)
107
+ end
108
+
109
+ def input_valid?
110
+ @value.size >= minimum_length &&
111
+ @value.size <= maximum_length &&
112
+ run_callback(:validate)
113
+ end
114
+
115
+ def value_confirmed?
116
+ return true unless @confirmation_options
117
+ options = @confirmation_options.dup
118
+
119
+ if block = options.delete(:block)
120
+ message = @component.instance_eval(&block)
121
+ prompt = case message
122
+ when Array: {:play => message}
123
+ when String: {:speak => message}
124
+ end
125
+ prompt.merge!(options.slice(:timeout))
126
+ end
127
+ 1.upto(options[:attempts]) do |attempt|
128
+ value = call_context.input(1, prompt)
129
+ case value
130
+ when options[:accept].to_s
131
+ return true
132
+ when options[: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
+ result = @component.instance_eval(&block)
144
+ @value = get_component_value
145
+ result
146
+ else
147
+ true
148
+ end
149
+ end
150
+
151
+ def set_component_value(value)
152
+ @component.send("#{@name}=", @value)
153
+ end
154
+
155
+ def get_component_value
156
+ @component.send("#{@name}")
157
+ end
158
+
159
+ def minimum_length
160
+ @options[:min_length] || @options[:length] || 1
161
+ end
162
+
163
+ def maximum_length
164
+ @options[:max_length] || @options[:length] || @value.size
165
+ end
166
+
167
+ def call_context
168
+ @call_context ||= @component.send(@options[:call_context])
169
+ end
170
+
171
+ def add_prompts(options)
172
+ repeats = options[:repeats] || 1
173
+ @prompts += ([options] * repeats)
174
+ end
175
+
176
+ def evaluate_prompt(prompt)
177
+ key = prompt.has_key?(:play) ? :play : :speak
178
+ message = prompt[key]
179
+ message = @component.instance_eval(&message) if message.is_a?(Proc)
180
+ prompt.merge(key => message)
181
+ end
182
+ end
183
+
184
+ end
@@ -0,0 +1,63 @@
1
+ module VoiceForm
2
+
3
+ def self.included(base)
4
+ base.extend MacroMethods
5
+ base.class_eval do
6
+ include FormMethods
7
+ end
8
+ end
9
+
10
+ module MacroMethods
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
20
+ end
21
+
22
+ self.voice_form_options = [options, block]
23
+ end
24
+
25
+ end
26
+
27
+ module InstanceMethods
28
+
29
+ def start_voice_form
30
+ raise "No voice form defined" unless self.voice_form_options
31
+ self.form = VoiceForm::Form.new(self.class.voice_form_options[0])
32
+ self.form.instance_eval(&self.class.voice_form_options[1])
33
+ self.form.run(self)
34
+ end
35
+
36
+ end
37
+
38
+ module FormMethods
39
+
40
+ # Can be used in a form or stand-alone in a component method
41
+ def field(field_name, options={}, &block)
42
+ raise 'Field require a block' unless block_given?
43
+
44
+ form_field = VoiceForm::FormField.new(field_name, options, self)
45
+
46
+ form_field.instance_eval(&block)
47
+ raise 'At least one prompt is required' if form_field.prompts.empty?
48
+
49
+ if self.class == VoiceForm::Form
50
+ self.form_stack << form_field
51
+ else
52
+ unless self.respond_to?(field_name)
53
+ self.class.class_eval do
54
+ attr_accessor field_name
55
+ end
56
+ end
57
+ form_field.run
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,275 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe VoiceForm::FormField do
4
+ include VoiceForm::FormMethods
5
+
6
+ attr_accessor :call_context, :my_field
7
+
8
+ before do
9
+ @call_context = mock('CallContext', :play => nil, :speak => nil)
10
+ @call_context.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 :speak => 'test'
16
+ end
17
+ self.methods.include?(:my_field)
18
+ end
19
+
20
+ it "should raise error if no prompts defined" do
21
+ item = form_field(:my_field) do
22
+ end
23
+ lambda { item.run }.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 :speak => 'and 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 :speak => "first"
37
+ end
38
+ item.send(:prompt_for_attempt, 1)[:speak].should == 'first'
39
+ item.send(:prompt_for_attempt, 2)[:speak].should == 'first'
40
+ end
41
+
42
+ it "should return reprompt for subsequent prompts" do
43
+ item = form_field(:my_field) do
44
+ prompt :speak => "first"
45
+ reprompt :speak => 'next'
46
+ end
47
+ item.send(:prompt_for_attempt, 1)[:speak].should == 'first'
48
+ item.send(:prompt_for_attempt, 2)[:speak].should == 'next'
49
+ item.send(:prompt_for_attempt, 3)[:speak].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 :speak => "first", :repeats => 2
55
+ reprompt :speak => 'next'
56
+ end
57
+ item.send(:prompt_for_attempt, 1)[:speak].should == 'first'
58
+ item.send(:prompt_for_attempt, 2)[:speak].should == 'first'
59
+ item.send(:prompt_for_attempt, 3)[:speak].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 :speak => "first"
65
+ end
66
+ call_context.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 :speak => "first"
76
+ setup { call_me.call }
77
+ end
78
+ call_context.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 :speak => "first"
87
+ timeout { call_me.call }
88
+ end
89
+ call_context.should_receive(:input).and_return('')
90
+
91
+ item.run
92
+ end
93
+
94
+ it "should make all attempts to get valid input" do
95
+ item = form_field(:my_field) do
96
+ prompt :speak => "first"
97
+ end
98
+ call_context.should_receive(:input).exactly(3).times
99
+
100
+ item.run
101
+ end
102
+
103
+ it "should make one attempt if input is valid" do
104
+ item = form_field(:my_field) do
105
+ prompt :speak => "first"
106
+ end
107
+ item.stub!(:input_valid?).and_return(true)
108
+ call_context.should_receive(:input).once
109
+
110
+ item.run
111
+ end
112
+
113
+ it "should check if input_valid?" do
114
+ item = form_field(:my_field, :length => 3) do
115
+ prompt :speak => "first"
116
+ end
117
+ call_context.should_receive(:input).and_return('123')
118
+ item.should_receive(:input_valid?)
119
+
120
+ item.run
121
+ end
122
+
123
+ it "should run validation callback if defined" do
124
+ call_me = i_should_be_called
125
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
126
+ prompt :speak => "first"
127
+ validate { call_me.call }
128
+ end
129
+ call_context.stub!(:input).and_return('123')
130
+
131
+ item.run
132
+ end
133
+
134
+ it "should run confirm callback if defined" do
135
+ call_me = i_should_be_called
136
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
137
+ prompt :speak => "first"
138
+ confirm { call_me.call; [] }
139
+ end
140
+ call_context.stub!(:input).and_return('123')
141
+
142
+ item.run
143
+ end
144
+
145
+ describe "confirm callback" do
146
+
147
+ it "should not run if not valid input" do
148
+ dont_call_me = i_should_not_be_called
149
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
150
+ prompt :speak => "first"
151
+ validate { false }
152
+ confirm { dont_call_me.call }
153
+ end
154
+ call_context.stub!(:input).and_return('123')
155
+
156
+ item.run
157
+ end
158
+
159
+ it "should be run the number of attempts if no valid response" do
160
+ call_context.should_receive(:input).with(3, anything).and_return('123')
161
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
162
+ prompt :speak => "first"
163
+
164
+ confirm(:attempts => 3) { [] }
165
+ end
166
+ call_context.should_receive(:input).with(1, anything).exactly(3).times.and_return('')
167
+
168
+ item.run
169
+ end
170
+
171
+ it "should run success callback if accept value entered" do
172
+ call_me = i_should_be_called
173
+ call_context.should_receive(:input).with(3, anything).and_return('123')
174
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
175
+ prompt :speak => "first"
176
+
177
+ success { call_me.call }
178
+ confirm(:accept => '1', :reject => '2') { [] }
179
+ end
180
+ call_context.should_receive(:input).with(1, anything).and_return('1')
181
+
182
+ item.run
183
+ end
184
+
185
+ it "should run failure callback if reject value entered" do
186
+ call_me = i_should_be_called
187
+ call_context.should_receive(:input).with(3, anything).and_return('123')
188
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
189
+ prompt :speak => "first"
190
+
191
+ failure { call_me.call }
192
+ confirm(:accept => '1', :reject => '2') { [] }
193
+ end
194
+ call_context.should_receive(:input).with(1, anything).and_return('2')
195
+
196
+ item.run
197
+ end
198
+
199
+ it "should play confirmation input prompt if confirm block return value is array" do
200
+ call_me = i_should_be_called
201
+ call_context.should_receive(:input).with(3, anything).and_return('123')
202
+
203
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
204
+ prompt :speak => "first"
205
+
206
+ success { call_me.call }
207
+ confirm(:timeout => 3) { [] }
208
+ end
209
+ call_context.should_receive(:input).with(1, {:play => [], :timeout => 3}).and_return('1')
210
+
211
+ item.run
212
+ end
213
+
214
+ it "should speak confirmation input prompt if confirm block return value is string" do
215
+ call_me = i_should_be_called
216
+ call_context.should_receive(:input).with(3, anything).and_return('123')
217
+
218
+ item = form_field(:my_field, :length => 3, :attempts => 1) do
219
+ prompt :speak => "first"
220
+
221
+ success { call_me.call }
222
+ confirm(:timeout => 3) { '' }
223
+ end
224
+ call_context.should_receive(:input).with(1, {:speak => '', :timeout => 3}).and_return('1')
225
+
226
+ item.run
227
+ end
228
+ end
229
+
230
+ it "should run failure callback if no input" do
231
+ call_me = i_should_be_called
232
+ item = form_field(:my_field, :length => 3) do
233
+ prompt :speak => "first"
234
+ failure { call_me.call }
235
+ end
236
+ call_context.should_receive(:input).and_return('')
237
+
238
+ item.run
239
+ end
240
+
241
+ it "should run success callback if input valid length" do
242
+ call_me = i_should_be_called
243
+ item = form_field(:my_field, :length => 3) do
244
+ prompt :speak => "first"
245
+ success { call_me.call }
246
+ end
247
+ call_context.should_receive(:input).and_return('123')
248
+
249
+ item.run
250
+ end
251
+
252
+ it "should run success callback if input valid length and valid input" do
253
+ validate_me = i_should_be_called
254
+ call_me = i_should_be_called
255
+ item = form_field(:my_field, :length => 3) do
256
+ prompt :speak => "first"
257
+ validate do
258
+ validate_me.call
259
+ my_field.to_i > 100
260
+ end
261
+ success { call_me.call }
262
+ end
263
+ call_context.should_receive(:input).and_return('123')
264
+
265
+ item.run
266
+ end
267
+
268
+ def form_field(field, options={}, &block)
269
+ self.class.class_eval { attr_accessor field }
270
+ item = VoiceForm::FormField.new(field, {:attempts => 3}.merge(options), self )
271
+
272
+ item.instance_eval(&block)
273
+ item
274
+ end
275
+ end
@@ -0,0 +1,213 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe VoiceForm::Form do
4
+ include VoiceForm
5
+
6
+ attr_accessor :call_context
7
+
8
+ before do
9
+ new_voice_form
10
+ @call_context = mock('CallContext', :play => nil, :speak => nil)
11
+ @call_context.stub!(:input).with(any_args).and_return('')
12
+ end
13
+
14
+ it "should define form and run it" do
15
+ call_me = i_should_be_called
16
+
17
+ self.class.voice_form &call_me
18
+
19
+ start_voice_form
20
+ end
21
+
22
+ it "should call setup block" do
23
+ form.setup &i_should_be_called
24
+
25
+ run_form
26
+ end
27
+
28
+ it "should run single form field" do
29
+ call_me = i_should_be_called
30
+
31
+ form.field(:my_field) do
32
+ prompt :speak => 'enter value'
33
+ setup { call_me.call }
34
+ end
35
+
36
+ run_form
37
+ end
38
+
39
+ it "should run all form fields" do
40
+ first_call_me = i_should_be_called
41
+ second_call_me = i_should_be_called
42
+
43
+ form.field(:first_field) do
44
+ prompt :speak => 'enter value'
45
+ setup { first_call_me.call }
46
+ end
47
+ form.field(:second_field) do
48
+ prompt :speak => 'enter value'
49
+ setup { second_call_me.call }
50
+ end
51
+
52
+ run_form
53
+ end
54
+
55
+ it "should run do_blocks" do
56
+ do_block_call_me = i_should_be_called
57
+
58
+ form.do_block { do_block_call_me.call }
59
+
60
+ run_form
61
+ end
62
+
63
+ it "should run all fields and do_blocks" do
64
+ field_call_me = i_should_be_called
65
+ do_block_call_me = i_should_be_called
66
+
67
+ form.field(:first_field) do
68
+ prompt :speak => 'enter value'
69
+ setup { field_call_me.call }
70
+ end
71
+ form.do_block { do_block_call_me.call }
72
+
73
+ run_form
74
+ end
75
+
76
+ it "should jump forward in form stack to field in goto" do
77
+ first_call_me = i_should_be_called
78
+ do_block_call_me = i_should_not_be_called
79
+ second_call_me = i_should_be_called
80
+
81
+ form.field(:first_field, :attempts => 1) do
82
+ prompt :speak => 'enter value'
83
+ setup { first_call_me.call }
84
+ failure { form.goto :second_field }
85
+ end
86
+
87
+ form.do_block { do_block_call_me.call }
88
+
89
+ form.field(:second_field) do
90
+ prompt :speak => 'enter value'
91
+ setup { second_call_me.call }
92
+ end
93
+
94
+ run_form
95
+ end
96
+
97
+ it "should jump back in form stack to goto field and repeat form stack items" do
98
+ first_call_me = i_should_be_called(2)
99
+ do_block_call_me = i_should_be_called(2)
100
+ second_call_me = i_should_be_called(2)
101
+
102
+ form.field(:first_field, :attempts => 1) do
103
+ prompt :speak => 'enter value'
104
+ setup { first_call_me.call }
105
+ end
106
+
107
+ form.do_block { do_block_call_me.call }
108
+
109
+ form.field(:second_field) do
110
+ prompt :speak => 'enter value'
111
+ setup { second_call_me.call }
112
+ failure {
113
+ unless @once
114
+ @once = true
115
+ form.goto :first_field
116
+ end
117
+ }
118
+ end
119
+
120
+ run_form
121
+ end
122
+
123
+ it "should restart form and repeat all form stack items" do
124
+ first_call_me = i_should_be_called(2)
125
+ do_block_call_me = i_should_be_called(2)
126
+ second_call_me = i_should_be_called(2)
127
+
128
+ form.field(:first_field, :attempts => 1) do
129
+ prompt :speak => 'enter value'
130
+ setup { first_call_me.call }
131
+ end
132
+
133
+ form.do_block { do_block_call_me.call }
134
+
135
+ form.field(:second_field) do
136
+ prompt :speak => 'enter value'
137
+ setup { second_call_me.call }
138
+ failure {
139
+ unless @once
140
+ @once = true
141
+ form.restart
142
+ end
143
+ }
144
+ end
145
+
146
+ run_form
147
+ end
148
+
149
+ it "should exit form and not run subsequent fields" do
150
+ first_call_me = i_should_be_called
151
+ do_block_call_me = i_should_not_be_called
152
+ second_call_me = i_should_not_be_called
153
+
154
+ form.field(:first_field, :attempts => 1) do
155
+ prompt :speak => 'enter value'
156
+ setup { first_call_me.call }
157
+ failure { form.exit }
158
+ end
159
+
160
+ form.do_block { do_block_call_me.call }
161
+
162
+ form.field(:second_field) do
163
+ prompt :speak => 'enter value'
164
+ setup { second_call_me.call }
165
+ end
166
+
167
+ run_form
168
+ end
169
+
170
+ describe "current_field" do
171
+ before do
172
+ @field = nil
173
+ end
174
+
175
+ it "should be name of current field being run" do
176
+ form.field(:my_field) do
177
+ prompt :speak => 'enter value'
178
+ setup { @field = form.current_field }
179
+ end
180
+ run_form
181
+
182
+ @field.should == :my_field
183
+ end
184
+
185
+ it "should be nil in do_block" do
186
+ form.do_block do
187
+ @field = form.current_field
188
+ end
189
+ run_form
190
+
191
+ @field.should be_nil
192
+ end
193
+
194
+ it "should be nil after form is run" do
195
+ form.field(:my_field) do
196
+ prompt :speak => 'enter value'
197
+ end
198
+ run_form
199
+
200
+ form.current_field.should be_nil
201
+ end
202
+ end
203
+
204
+ def new_voice_form
205
+ self.class.voice_form { }
206
+ self.form = VoiceForm::Form.new(self.class.voice_form_options[0])
207
+ self.form.instance_eval(&self.class.voice_form_options[1])
208
+ end
209
+
210
+ def run_form
211
+ form.run(self)
212
+ end
213
+ end
@@ -0,0 +1,28 @@
1
+ $: << File.dirname(__FILE__) + '/../lib'
2
+ $: << File.dirname(__FILE__)
3
+
4
+ require 'rubygems'
5
+ require 'spec'
6
+ require 'active_support'
7
+
8
+ require 'voice_form/form_methods'
9
+ require 'voice_form/form'
10
+ require 'voice_form/form_field'
11
+
12
+ module SpecHelpers
13
+ def i_should_be_called(times=1, &block)
14
+ proc = mock('Proc should be called')
15
+ proc.should_receive(:call).exactly(times).times.instance_eval(&(block || Proc.new {}))
16
+ Proc.new { |*args| proc.call(*args) }
17
+ end
18
+
19
+ def i_should_not_be_called(&block)
20
+ proc = mock('Proc should be called')
21
+ proc.should_not_receive(:call).instance_eval(&(block || Proc.new {}))
22
+ Proc.new { |*args| proc.call(*args) }
23
+ end
24
+ end
25
+
26
+ Spec::Runner.configure do |config|
27
+ config.include SpecHelpers
28
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adzap-voice_form
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Adam Meehan
8
+ autorequire: voice_form
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-11-11 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A DSL for Adhearsion to create forms in the style of the VoiceXML form element.
17
+ email: adam.meehan@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - History.txt
24
+ files:
25
+ - MIT-LICENSE
26
+ - README.markdown
27
+ - Rakefile
28
+ - lib/voice_form.rb
29
+ - lib/voice_form
30
+ - lib/voice_form/form.rb
31
+ - lib/voice_form/form_field.rb
32
+ - lib/voice_form/form_methods.rb
33
+ - spec/form_field_spec.rb
34
+ - spec/spec_helper.rb
35
+ - spec/form_spec.rb
36
+ - examples/my_component.rb
37
+ - History.txt
38
+ has_rdoc: false
39
+ homepage: http://github.com/adzap/voice_form
40
+ post_install_message:
41
+ rdoc_options: []
42
+
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ requirements: []
58
+
59
+ rubyforge_project:
60
+ rubygems_version: 1.2.0
61
+ signing_key:
62
+ specification_version: 2
63
+ summary: A DSL for Adhearsion to create forms in the style of the VoiceXML form element.
64
+ test_files: []
65
+