adzap-voice_form 0.1.1

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,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
+